mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
style(web): polish the session sidebar (#1519)
* feat(web): use sidebar fold/unfold icons for sidebar toggle * feat(web): move settings entry to a sidebar footer row * feat(web): fully collapse sidebar with animated width transition * feat(web): redesign sidebar colors, spacing and macos desktop chrome * feat(desktop): center traffic lights on the 48px header row * fix(web): restore webkit thin scrollbars and unify sidebar icon sizes * feat(web): add Kbd keycap component and justify sidebar search shortcut * style(web): rework sidebar palette and pin a resident sidebar toggle * fix(desktop): sync window appearance with web UI theme so dimmed traffic lights stay visible * feat(web): adopt Kimi design icons in the sidebar via a local icon collection * style(web): mute workspace group title color in the sidebar * style(web): refine sidebar typography, unify shortcut keycaps, float workspace row actions * style(web): cap sidebar draggable width at 480px * style(web): derive sidebar row height from type and padding, float the kebab * chore: add changeset for sidebar UI polish * fix(nix): update pnpmDeps hash * style(web): put the sidebar collapse button inside the header on non-mac * fix(nix): update pnpmDeps hash
This commit is contained in:
parent
1bf2c9afee
commit
170ae44205
28 changed files with 758 additions and 413 deletions
5
.changeset/polish-sidebar-ui.md
Normal file
5
.changeset/polish-sidebar-ui.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"@moonshot-ai/kimi-code": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
web: Polish the session sidebar layout, colors, icons, and typography.
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { dirname, join } from 'node:path';
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
import { app, BrowserWindow, Menu, shell } from 'electron';
|
import { app, BrowserWindow, Menu, nativeTheme, shell } from 'electron';
|
||||||
import type { MenuItemConstructorOptions } from 'electron';
|
import type { MenuItemConstructorOptions } from 'electron';
|
||||||
|
|
||||||
import { ensureServer, kimiHome, serverLogPath } from './ensure-server';
|
import { ensureServer, kimiHome, serverLogPath } from './ensure-server';
|
||||||
|
|
@ -152,8 +152,13 @@ function createWindow(): void {
|
||||||
title: 'Kimi Code Desktop',
|
title: 'Kimi Code Desktop',
|
||||||
// macOS: hide the native title bar and float the traffic lights over the
|
// macOS: hide the native title bar and float the traffic lights over the
|
||||||
// content; the web UI reserves a draggable strip at the top to clear them.
|
// content; the web UI reserves a draggable strip at the top to clear them.
|
||||||
|
// 'hidden' (not 'hiddenInset') so trafficLightPosition can pin the lights
|
||||||
|
// to the vertical center of the web UI's 48px header row (y 18 + 12px
|
||||||
|
// button height / 2 = 24 = the header's midline — same line as the
|
||||||
|
// sidebar-expand button and the conversation title).
|
||||||
// 'default' on other platforms (they keep their native title bar).
|
// 'default' on other platforms (they keep their native title bar).
|
||||||
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
|
titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default',
|
||||||
|
trafficLightPosition: { x: 16, y: 18 },
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
|
|
@ -165,6 +170,63 @@ function createWindow(): void {
|
||||||
win.webContents.on('page-title-updated', (event) => {
|
win.webContents.on('page-title-updated', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
});
|
});
|
||||||
|
// macOS traffic lights.
|
||||||
|
//
|
||||||
|
// 1) Visibility across transitions: with titleBarStyle 'hidden' + a custom
|
||||||
|
// trafficLightPosition, the buttons can vanish (or lose their custom
|
||||||
|
// position) after a full-screen round-trip or on re-focus. Re-assert both
|
||||||
|
// on those transitions (observed on Electron 33; belt-and-braces).
|
||||||
|
//
|
||||||
|
// 2) Blur is NOT such a case: unfocused traffic lights are merely DIMMED by
|
||||||
|
// AppKit, and the dimmed color follows the WINDOW appearance, not the
|
||||||
|
// page (electron#27295) — with the OS in dark mode but the web UI on a
|
||||||
|
// light theme, the light-gray dimmed dots become invisible against the
|
||||||
|
// light sidebar. That is fixed by the theme sync below, which keeps the
|
||||||
|
// window appearance aligned with the web UI's <html data-color-scheme>.
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const showTrafficLights = (): void => {
|
||||||
|
if (win.isDestroyed()) return;
|
||||||
|
win.setWindowButtonPosition({ x: 16, y: 18 });
|
||||||
|
win.setWindowButtonVisibility(true);
|
||||||
|
};
|
||||||
|
win.on('enter-full-screen', showTrafficLights);
|
||||||
|
win.on('leave-full-screen', showTrafficLights);
|
||||||
|
win.on('focus', showTrafficLights);
|
||||||
|
|
||||||
|
// Theme sync: no preload/IPC channel exists, so inject a tiny observer
|
||||||
|
// that reports <html data-color-scheme> ('light' | 'dark' | 'system')
|
||||||
|
// through a tagged console message, and mirror it into
|
||||||
|
// nativeTheme.themeSource (same three states). The startup/error screens
|
||||||
|
// (data: URLs) have no such attribute and harmlessly report 'system'.
|
||||||
|
const THEME_TAG = '__kimi_desktop_theme__:';
|
||||||
|
win.webContents.on('console-message', (_event, _level, message) => {
|
||||||
|
if (!message.startsWith(THEME_TAG)) return;
|
||||||
|
const scheme = message.slice(THEME_TAG.length);
|
||||||
|
if (scheme === 'light' || scheme === 'dark' || scheme === 'system') {
|
||||||
|
nativeTheme.themeSource = scheme;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
win.webContents.on('did-finish-load', () => {
|
||||||
|
win.webContents
|
||||||
|
.executeJavaScript(
|
||||||
|
`(() => {
|
||||||
|
const report = () => {
|
||||||
|
const v = document.documentElement.dataset.colorScheme;
|
||||||
|
console.info(${JSON.stringify(THEME_TAG)} + (v === 'light' || v === 'dark' ? v : 'system'));
|
||||||
|
};
|
||||||
|
new MutationObserver(report).observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-color-scheme'],
|
||||||
|
});
|
||||||
|
report();
|
||||||
|
})();`,
|
||||||
|
)
|
||||||
|
.catch(() => {
|
||||||
|
// Navigation can tear the page down mid-injection; theme sync is
|
||||||
|
// cosmetic, so ignore.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
win.on('close', () => {
|
win.on('close', () => {
|
||||||
saveBounds(win);
|
saveBounds(win);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/ri": "^1.2.10",
|
"@iconify-json/ri": "^1.2.10",
|
||||||
|
"@iconify-json/tabler": "^1.2.35",
|
||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"typescript": "6.0.2",
|
"typescript": "6.0.2",
|
||||||
"unplugin-icons": "^23.0.0",
|
"unplugin-icons": "^23.0.0",
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ import { stripSkillPrefix } from './lib/slashCommands';
|
||||||
import Button from './components/ui/Button.vue';
|
import Button from './components/ui/Button.vue';
|
||||||
import IconButton from './components/ui/IconButton.vue';
|
import IconButton from './components/ui/IconButton.vue';
|
||||||
import Icon from './components/ui/Icon.vue';
|
import Icon from './components/ui/Icon.vue';
|
||||||
|
import InternalBuildBanner from './components/InternalBuildBanner.vue';
|
||||||
|
import { isMacosDesktop } from './lib/desktopFlag';
|
||||||
|
|
||||||
// Hydrate the server-transport credential (fragment token or sessionStorage)
|
// Hydrate the server-transport credential (fragment token or sessionStorage)
|
||||||
// BEFORE the client connects, so the first REST/WS calls already carry it.
|
// BEFORE the client connects, so the first REST/WS calls already carry it.
|
||||||
|
|
@ -213,6 +215,7 @@ const {
|
||||||
sidebarMax,
|
sidebarMax,
|
||||||
sessionColWidth,
|
sessionColWidth,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
|
sidebarDragging,
|
||||||
sideWidth,
|
sideWidth,
|
||||||
loadSidebarCollapsed,
|
loadSidebarCollapsed,
|
||||||
toggleSidebarCollapse,
|
toggleSidebarCollapse,
|
||||||
|
|
@ -645,13 +648,18 @@ function openPr(url: string): void {
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="app"
|
class="app"
|
||||||
:class="{ mobile: isMobile, 'sidebar-collapsed': sidebarCollapsed && !isMobile }"
|
:class="{
|
||||||
:style="{ '--side-w': sideWidth + 'px', '--preview-w': previewPanelWidth + 'px' }"
|
mobile: isMobile,
|
||||||
|
'sidebar-collapsed': sidebarCollapsed && !isMobile,
|
||||||
|
'macos-desktop': isMacosDesktop,
|
||||||
|
}"
|
||||||
|
:style="{ '--preview-w': previewPanelWidth + 'px' }"
|
||||||
>
|
>
|
||||||
<!-- Desktop navigation: workspace rail + resizable session column. -->
|
<!-- Desktop navigation: workspace rail + resizable session column. -->
|
||||||
<template v-if="!isMobile">
|
<template v-if="!isMobile">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
v-show="!sidebarCollapsed"
|
:collapsed="sidebarCollapsed"
|
||||||
|
:dragging="sidebarDragging"
|
||||||
:col-width="sideWidth"
|
:col-width="sideWidth"
|
||||||
:active-workspace="client.visibleWorkspace.value"
|
:active-workspace="client.visibleWorkspace.value"
|
||||||
:active-workspace-id="client.activeWorkspaceId.value"
|
:active-workspace-id="client.activeWorkspaceId.value"
|
||||||
|
|
@ -681,21 +689,14 @@ function openPr(url: string): void {
|
||||||
/>
|
/>
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
v-show="!sidebarCollapsed"
|
v-show="!sidebarCollapsed"
|
||||||
|
class="side-handle"
|
||||||
:storage-key="SIDEBAR_WIDTH_KEY"
|
:storage-key="SIDEBAR_WIDTH_KEY"
|
||||||
:default-width="SIDEBAR_DEFAULT"
|
:default-width="SIDEBAR_DEFAULT"
|
||||||
:min="SIDEBAR_MIN"
|
:min="SIDEBAR_MIN"
|
||||||
:max="sidebarMax"
|
:max="sidebarMax"
|
||||||
@update:width="sessionColWidth = $event"
|
@update:width="sessionColWidth = $event"
|
||||||
|
@update:dragging="sidebarDragging = $event"
|
||||||
/>
|
/>
|
||||||
<div v-if="sidebarCollapsed" class="sidebar-rail">
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
:label="t('sidebar.expandSidebar')"
|
|
||||||
@click="toggleSidebarCollapse"
|
|
||||||
>
|
|
||||||
<Icon name="panel-expand" size="sm" />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Mobile navigation: slim top bar (switcher + settings sheets). -->
|
<!-- Mobile navigation: slim top bar (switcher + settings sheets). -->
|
||||||
|
|
@ -792,8 +793,29 @@ function openPr(url: string): void {
|
||||||
@edit-message="handleEditMessage"
|
@edit-message="handleEditMessage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Sidebar toggle — floating only when the in-header control can't serve:
|
||||||
|
on macOS desktop it's RESIDENT (always rendered beside the traffic
|
||||||
|
lights, the sidebar slides underneath and only the glyph swaps, so it
|
||||||
|
never moves or flashes); on Windows/web the collapse button lives
|
||||||
|
inside the sidebar header, so this floating button only appears while
|
||||||
|
COLLAPSED (to re-expand the sidebar). It must come AFTER
|
||||||
|
ConversationPane in the DOM: Electron computes the window-drag region
|
||||||
|
in tree order (drag rects union, no-drag rects subtract), so a no-drag
|
||||||
|
element placed before the ChatHeader drag region would have its hole
|
||||||
|
painted back over — making the button an inert drag area. -->
|
||||||
|
<IconButton
|
||||||
|
v-if="!isMobile && (isMacosDesktop || sidebarCollapsed)"
|
||||||
|
class="sidebar-toggle-btn"
|
||||||
|
size="sm"
|
||||||
|
:label="sidebarCollapsed ? t('sidebar.expandSidebar') : t('sidebar.collapseSidebar')"
|
||||||
|
@click="toggleSidebarCollapse"
|
||||||
|
>
|
||||||
|
<Icon :name="sidebarCollapsed ? 'panel-expand' : 'panel-collapse'" />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
<ResizeHandle
|
<ResizeHandle
|
||||||
v-if="sidePanelVisible && !isMobile"
|
v-if="sidePanelVisible && !isMobile"
|
||||||
|
class="preview-handle"
|
||||||
:storage-key="PREVIEW_WIDTH_KEY"
|
:storage-key="PREVIEW_WIDTH_KEY"
|
||||||
:default-width="previewDefaultWidth"
|
:default-width="previewDefaultWidth"
|
||||||
:min="PREVIEW_MIN"
|
:min="PREVIEW_MIN"
|
||||||
|
|
@ -875,6 +897,11 @@ function openPr(url: string): void {
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
|
<!-- Internal-build tag — pinned to the app's bottom-right corner, above
|
||||||
|
whatever pane happens to be there. Purely informational: pointer
|
||||||
|
events pass through so it never blocks clicks. -->
|
||||||
|
<InternalBuildBanner class="internal-build-fab" />
|
||||||
|
|
||||||
<!-- Model Picker overlay -->
|
<!-- Model Picker overlay -->
|
||||||
<ModelPicker
|
<ModelPicker
|
||||||
v-if="showModelPicker"
|
v-if="showModelPicker"
|
||||||
|
|
@ -1101,18 +1128,20 @@ function openPr(url: string): void {
|
||||||
color: var(--dim);
|
color: var(--dim);
|
||||||
}
|
}
|
||||||
.app {
|
.app {
|
||||||
--side-w: 248px;
|
|
||||||
--preview-w: 460px;
|
--preview-w: 460px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
/* sidebar (rail + resizable session column) | 0-width handle | conversation.
|
/* sidebar | 0-width handle | conversation | 0-width handle | right panel.
|
||||||
The 4px ResizeHandle overflows its zero-width track via negative margins so
|
The 4px ResizeHandles overflow their zero-width tracks via negative margins
|
||||||
the whole strip is grabbable without consuming layout space. */
|
so the whole strip is grabbable without consuming layout space. */
|
||||||
/* The right-panel track is PERMANENT (auto = follows the aside's width, 0
|
/* Both side tracks are PERMANENT (auto = follows the aside's width, 0 when
|
||||||
when closed) — opening animates the aside's width, so the conversation
|
closed/collapsed) — opening or collapsing animates the aside's width, so
|
||||||
column is squeezed over smoothly instead of snapping to a new template. */
|
the conversation column is squeezed over smoothly instead of snapping to a
|
||||||
grid-template-columns: var(--side-w) 0 minmax(0, 1fr) 0 auto;
|
new template. Every column is pinned explicitly (grid-column 1–5) so a
|
||||||
|
display:none handle can't shift auto-placement. */
|
||||||
|
grid-template-columns: auto 0 minmax(0, 1fr) 0 auto;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -1126,20 +1155,50 @@ function openPr(url: string): void {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collapsed sidebar rail: keeps a slim, dedicated grid track so the expand
|
/* Pin every desktop grid child to its track so auto-placement can never
|
||||||
button never overlaps the conversation header or squeezes the main pane. */
|
reshuffle columns when a handle is display:none (v-show/v-if). */
|
||||||
.sidebar-rail {
|
.app > .side { grid-column: 1; }
|
||||||
grid-column: 1;
|
.side-handle { grid-column: 2; }
|
||||||
display: flex;
|
.app:not(.mobile) > .con { grid-column: 3; }
|
||||||
justify-content: center;
|
.preview-handle { grid-column: 4; }
|
||||||
padding-top: 8px;
|
|
||||||
background: var(--panel);
|
/* Sidebar toggle — floating button pinned to the top-left corner. On macOS
|
||||||
border-right: 1px solid var(--line);
|
desktop it is resident (rendered in both states beside the traffic lights);
|
||||||
|
on Windows/web it only appears while the sidebar is collapsed (the collapse
|
||||||
|
button lives inside the sidebar header). While collapsed the conversation
|
||||||
|
header pads left so its content clears the button (global block below). */
|
||||||
|
.sidebar-toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
/* Vertically centered in the 48px conversation header. */
|
||||||
|
top: 11px;
|
||||||
|
left: 16px;
|
||||||
|
z-index: var(--z-sticky);
|
||||||
|
/* Fade in on appearance (Windows/web: only rendered while collapsed, so
|
||||||
|
this plays as the sidebar finishes sliding away). macOS disables it. */
|
||||||
|
animation: sidebar-toggle-btn-in 0.18s var(--ease-out) 0.12s backwards;
|
||||||
|
/* Floats over the macOS-desktop window-drag header; keep it clickable. */
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
/* The collapsed rail occupies track 1; keep the main pane pinned to the
|
/* macOS desktop (hidden title bar): resident beside the floating traffic
|
||||||
conversation track even though the sidebar/handle are display:none. */
|
lights (green light's right edge ≈ 68px; 72 keeps a gap that matches the
|
||||||
.app.sidebar-collapsed > .con {
|
lights' own 8px rhythm); no entrance animation since it never appears. */
|
||||||
grid-column: 3;
|
.app.macos-desktop .sidebar-toggle-btn {
|
||||||
|
left: 72px;
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
@keyframes sidebar-toggle-btn-in {
|
||||||
|
from { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Internal-build tag pinned to the app's bottom-right corner (desktop app
|
||||||
|
only — the component renders nothing elsewhere). Informational: never
|
||||||
|
intercepts pointer input. */
|
||||||
|
.internal-build-fab {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--space-3);
|
||||||
|
bottom: var(--space-3);
|
||||||
|
z-index: var(--z-sticky);
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile single-column shell: slim top bar (auto) over the full-width
|
/* Mobile single-column shell: slim top bar (auto) over the full-width
|
||||||
|
|
@ -1208,4 +1267,19 @@ function openPr(url: string): void {
|
||||||
one continuous line across the layout. */
|
one continuous line across the layout. */
|
||||||
--panel-head-h: 48px;
|
--panel-head-h: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar collapsed (desktop): the conversation header pads left so its
|
||||||
|
content clears the floating sidebar toggle (.sidebar-toggle-btn) — and the
|
||||||
|
macOS traffic lights on desktop builds. Animated in step with the sidebar
|
||||||
|
width transition. Cross-component rule (ChatHeader renders the header), so
|
||||||
|
it lives in this global block. */
|
||||||
|
.app:not(.mobile) .chat-header {
|
||||||
|
transition: padding-left 0.28s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
.app.sidebar-collapsed .chat-header {
|
||||||
|
padding-left: 52px;
|
||||||
|
}
|
||||||
|
.app.sidebar-collapsed.macos-desktop .chat-header {
|
||||||
|
padding-left: 108px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import { isDesktop } from '../lib/desktopFlag';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
// True only inside the Kimi Desktop app (see desktopFlag.ts). Renders an inline
|
// True only inside the Kimi Desktop app (see desktopFlag.ts). Renders a small
|
||||||
// tag meant to sit next to the "Kimi Code" brand in the sidebar header.
|
// tag pinned to the app's bottom-right corner (positioned by App.vue).
|
||||||
const show = isDesktop;
|
const show = isDesktop;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,7 @@ defineExpose({ closeMenu });
|
||||||
:label="t('sidebar.options')"
|
:label="t('sidebar.options')"
|
||||||
@click.stop="toggleMenu($event)"
|
@click.stop="toggleMenu($event)"
|
||||||
>
|
>
|
||||||
<Icon name="dots-horizontal" size="sm" />
|
<Icon name="dots-horizontal" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -287,22 +287,24 @@ defineExpose({ closeMenu });
|
||||||
.se {
|
.se {
|
||||||
/* --sb-* vars come from .side in Sidebar.vue: the title starts at
|
/* --sb-* vars come from .side in Sidebar.vue: the title starts at
|
||||||
--sb-pad-x + --sb-gutter + --sb-gap, exactly under the workspace name.
|
--sb-pad-x + --sb-gutter + --sb-gap, exactly under the workspace name.
|
||||||
The row is an inset pill: a 6px horizontal margin + 10px padding lands the
|
The row is an inset pill: the .sessions container's --sb-inset padding +
|
||||||
leading icon at --sb-pad-x (16px), aligned with the workspace header. */
|
the row's own padding land the leading slot at --sb-pad-x, aligned with
|
||||||
|
the workspace header. */
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--space-1) var(--space-2);
|
padding: 8px var(--space-2);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-sm);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.se:hover { background: var(--color-surface-sunken); color: var(--color-text); }
|
.se:hover { background: var(--sb-hover, var(--color-surface-sunken)); color: var(--color-text); }
|
||||||
|
/* Selected: neutral fill (NOT accent-tinted — selection reads as "where I
|
||||||
|
am", the accent stays reserved for actions and status). */
|
||||||
.se.on {
|
.se.on {
|
||||||
background: var(--color-accent-soft);
|
background: var(--color-selected);
|
||||||
color: var(--color-accent-hover);
|
color: var(--color-text);
|
||||||
box-shadow: inset 0 0 0 1px var(--color-accent-bd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
|
|
@ -310,9 +312,9 @@ defineExpose({ closeMenu });
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--sb-gap, 6px);
|
gap: var(--sb-gap, 6px);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
/* Floor the row at the hover-kebab height (IconButton sm = 26px) so swapping
|
/* Row height is font-driven: title line-height (13×1.25≈16px) + 2×5px
|
||||||
the timestamp for the kebab on hover doesn't grow the row. */
|
.se padding ≈ 26px. The hover kebab is absolutely positioned (see .act)
|
||||||
min-height: 26px;
|
so it never contributes to row height and can't cause hover jitter. */
|
||||||
}
|
}
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
|
|
@ -341,7 +343,7 @@ defineExpose({ closeMenu });
|
||||||
|
|
||||||
.t {
|
.t {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-size: var(--text-base);
|
font-size: var(--ui-font-size-sm);
|
||||||
font-weight: 450;
|
font-weight: 450;
|
||||||
line-height: var(--leading-tight);
|
line-height: var(--leading-tight);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
@ -356,30 +358,37 @@ defineExpose({ closeMenu });
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-weight: 475;
|
font-weight: 475;
|
||||||
|
line-height: var(--leading-tight);
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Trailing action slot: time and kebab share one grid cell (grid-area:1/1).
|
/* Trailing action slot: the relative time (in flow) sets the slot size; the
|
||||||
Both stay in the layout and swap via `visibility` (never display:none), so
|
kebab is absolutely positioned over it and swapped via `visibility`, so it
|
||||||
the slot width = max(time width, IconButton sm 26px) is identical in hover
|
contributes neither height (the row stays font-driven) nor width changes
|
||||||
and rest — the badges and title don't reflow, eliminating hover jitter.
|
(min-width reserves the kebab's footprint, the title doesn't reflow). */
|
||||||
`.act .kebab` out-specificities IconButton's own display so the hidden
|
|
||||||
default wins. */
|
|
||||||
.act {
|
.act {
|
||||||
display: inline-grid;
|
position: relative;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-items: end;
|
justify-content: flex-end;
|
||||||
|
/* Reserve the kebab's width so the trailing slot (and thus the title) never
|
||||||
|
shifts between the time and the kebab, even for short times like "2m". */
|
||||||
|
min-width: 26px;
|
||||||
|
}
|
||||||
|
.act .kebab {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
.act .ts,
|
|
||||||
.act .kebab { grid-area: 1 / 1; }
|
|
||||||
.act .kebab { visibility: hidden; }
|
|
||||||
.se:hover .act .kebab,
|
.se:hover .act .kebab,
|
||||||
.act:has(.kebab.open) .kebab { visibility: visible; }
|
.act:has(.kebab.open) .kebab { visibility: visible; }
|
||||||
.se:hover .act .ts,
|
.se:hover .act .ts,
|
||||||
.act:has(.kebab.open) .ts { visibility: hidden; }
|
.act:has(.kebab.open) .ts { visibility: hidden; }
|
||||||
.kebab.open { color: var(--color-text); background: var(--color-surface-sunken); }
|
.kebab.open { color: var(--color-text); background: var(--sb-hover, var(--color-surface-sunken)); }
|
||||||
|
|
||||||
/* Fixed + anchored to the ⋯ button via inline style (see positionMenu); the menu
|
/* Fixed + anchored to the ⋯ button via inline style (see positionMenu); the menu
|
||||||
is teleported to <body> so the collapsing list's `overflow: hidden` can't clip it. */
|
is teleported to <body> so the collapsing list's `overflow: hidden` can't clip it. */
|
||||||
|
|
@ -413,15 +422,10 @@ defineExpose({ closeMenu });
|
||||||
|
|
||||||
.sessions .se {
|
.sessions .se {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-sm);
|
||||||
/* Trim the row padding by the inset margin so the title still starts at the
|
/* Trim the row padding by the container inset so the title still starts at
|
||||||
same x as the workspace name (whose header has no inset). */
|
the same x as the workspace name (whose header has no inset). */
|
||||||
padding: var(--space-1) calc(var(--sb-pad-x, 12px) - var(--space-2));
|
padding: 8px calc(var(--sb-pad-x, 20px) - var(--sb-inset, 12px));
|
||||||
}
|
|
||||||
.sessions .se:hover { background: var(--panel2); }
|
|
||||||
.sessions .se.on {
|
|
||||||
background: var(--color-accent-soft);
|
|
||||||
box-shadow: inset 0 0 0 1px var(--color-accent-bd);
|
|
||||||
}
|
}
|
||||||
.sessions .se .rename-input { border-radius: var(--radius-sm); font-family: var(--sans); }
|
.sessions .se .rename-input { border-radius: var(--radius-sm); font-family: var(--sans); }
|
||||||
.sessions .se .kebab { border-radius: var(--radius-sm); }
|
.sessions .se .kebab { border-radius: var(--radius-sm); }
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,18 @@ import { serverEndpointLabel } from '../api/config';
|
||||||
import { copyTextToClipboard } from '../lib/clipboard';
|
import { copyTextToClipboard } from '../lib/clipboard';
|
||||||
import {
|
import {
|
||||||
loadCollapsedWorkspaces,
|
loadCollapsedWorkspaces,
|
||||||
loadShowWorkspacePaths,
|
|
||||||
saveCollapsedWorkspaces,
|
saveCollapsedWorkspaces,
|
||||||
saveShowWorkspacePaths,
|
|
||||||
} from '../lib/storage';
|
} from '../lib/storage';
|
||||||
import { moveInOrder, type DropPosition, type WorkspaceSortMode } from '../lib/workspaceOrder';
|
import { moveInOrder, type DropPosition, type WorkspaceSortMode } from '../lib/workspaceOrder';
|
||||||
import type { Session, WorkspaceGroup as WorkspaceGroupType, WorkspaceView } from '../types';
|
import type { Session, WorkspaceGroup as WorkspaceGroupType, WorkspaceView } from '../types';
|
||||||
import SearchSessionsDialog from './dialogs/SearchSessionsDialog.vue';
|
import SearchSessionsDialog from './dialogs/SearchSessionsDialog.vue';
|
||||||
import WorkspaceGroup from './WorkspaceGroup.vue';
|
import WorkspaceGroup from './WorkspaceGroup.vue';
|
||||||
import InternalBuildBanner from './InternalBuildBanner.vue';
|
|
||||||
import { isMacosDesktop } from '../lib/desktopFlag';
|
import { isMacosDesktop } from '../lib/desktopFlag';
|
||||||
import IconButton from './ui/IconButton.vue';
|
import IconButton from './ui/IconButton.vue';
|
||||||
import Icon from './ui/Icon.vue';
|
import Icon from './ui/Icon.vue';
|
||||||
|
import Kbd from './ui/Kbd.vue';
|
||||||
import Menu from './ui/Menu.vue';
|
import Menu from './ui/Menu.vue';
|
||||||
import MenuItem from './ui/MenuItem.vue';
|
import MenuItem from './ui/MenuItem.vue';
|
||||||
import ShortcutKey from './ui/ShortcutKey.vue';
|
|
||||||
import { useConfirmDialog } from '../composables/useConfirmDialog';
|
import { useConfirmDialog } from '../composables/useConfirmDialog';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -50,6 +47,12 @@ const props = withDefaults(
|
||||||
unreadBySession?: Record<string, boolean>;
|
unreadBySession?: Record<string, boolean>;
|
||||||
/** Width (px) of the session column, driven by the App resize handle. */
|
/** Width (px) of the session column, driven by the App resize handle. */
|
||||||
colWidth?: number;
|
colWidth?: number;
|
||||||
|
/** True when the sidebar is collapsed: the container animates to width 0
|
||||||
|
* (content keeps `colWidth` and is clipped), then hides itself. */
|
||||||
|
collapsed?: boolean;
|
||||||
|
/** True while the resize handle is dragged — disables the width transition
|
||||||
|
* so the sidebar follows the pointer 1:1. */
|
||||||
|
dragging?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
activeWorkspace: null,
|
activeWorkspace: null,
|
||||||
|
|
@ -58,6 +61,8 @@ const props = withDefaults(
|
||||||
pendingBySession: () => ({}),
|
pendingBySession: () => ({}),
|
||||||
unreadBySession: () => ({}),
|
unreadBySession: () => ({}),
|
||||||
colWidth: 220,
|
colWidth: 220,
|
||||||
|
collapsed: false,
|
||||||
|
dragging: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -84,7 +89,7 @@ const emit = defineEmits<{
|
||||||
// Session search dialog (Spotlight-style; filters title + last prompt)
|
// Session search dialog (Spotlight-style; filters title + last prompt)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const showSearch = ref(false);
|
const showSearch = ref(false);
|
||||||
const sessionSearchShortcut = isAppleShortcutPlatform() ? '⌘K' : 'Ctrl K';
|
const sessionSearchKeys = isAppleShortcutPlatform() ? ['⌘', 'K'] : ['Ctrl', 'K'];
|
||||||
|
|
||||||
function openSearch(): void {
|
function openSearch(): void {
|
||||||
// Sessions are loaded per-workspace (first page only); lazily drain the rest
|
// Sessions are loaded per-workspace (first page only); lazily drain the rest
|
||||||
|
|
@ -111,7 +116,7 @@ function isAppleShortcutPlatform(): boolean {
|
||||||
return userAgentData?.platform === 'macOS' || userAgentData?.platform === 'iOS';
|
return userAgentData?.platform === 'macOS' || userAgentData?.platform === 'iOS';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll-linked header seam: the .btn-wrap bottom border/shadow only appears
|
// Scroll-linked header seam: the .search-wrap bottom border/shadow only appears
|
||||||
// once the session list has actually scrolled, so an unscrolled list shows no
|
// once the session list has actually scrolled, so an unscrolled list shows no
|
||||||
// abrupt boundary.
|
// abrupt boundary.
|
||||||
const sessionsScrolled = ref(false);
|
const sessionsScrolled = ref(false);
|
||||||
|
|
@ -186,18 +191,6 @@ function onLoadMore(id: string): void {
|
||||||
emit('loadMoreSessions', id);
|
emit('loadMoreSessions', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Workspace path display (toggle in the Workspaces section header)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Off by default so the list stays compact; turning it on reveals every
|
|
||||||
// workspace's root path as a stable subtitle (no hover-induced layout shift).
|
|
||||||
const showWorkspacePaths = ref<boolean>(loadShowWorkspacePaths());
|
|
||||||
|
|
||||||
function toggleShowWorkspacePaths(): void {
|
|
||||||
showWorkspacePaths.value = !showWorkspacePaths.value;
|
|
||||||
saveShowWorkspacePaths(showWorkspacePaths.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Workspace drag-to-reorder
|
// Workspace drag-to-reorder
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
@ -487,11 +480,6 @@ function chooseSortMode(mode: WorkspaceSortMode): void {
|
||||||
closeSectionMenu();
|
closeSectionMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleShowWorkspacePathsFromMenu(): void {
|
|
||||||
toggleShowWorkspacePaths();
|
|
||||||
closeSectionMenu();
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('mousedown', onGhMenuDocClick, true);
|
document.removeEventListener('mousedown', onGhMenuDocClick, true);
|
||||||
document.removeEventListener('mousedown', onWsMenuDocClick);
|
document.removeEventListener('mousedown', onWsMenuDocClick);
|
||||||
|
|
@ -560,54 +548,49 @@ onBeforeUnmount(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside class="side" :class="{ 'macos-desktop': isMacosDesktop }">
|
<aside
|
||||||
|
class="side"
|
||||||
|
:class="{ 'macos-desktop': isMacosDesktop, collapsed, 'no-anim': dragging }"
|
||||||
|
:style="{ width: collapsed ? '0px' : colWidth + 'px' }"
|
||||||
|
>
|
||||||
<!-- Session column -->
|
<!-- Session column -->
|
||||||
<div class="col" :style="{ width: colWidth + 'px' }">
|
<div class="col" :style="{ width: colWidth + 'px' }">
|
||||||
<!-- Header: logo + settings (no hard border — flows into workspace list) -->
|
<!-- Header: brand + collapse. The collapse button lives INSIDE the header
|
||||||
|
on non-mac platforms (right-aligned); on macOS desktop the brand is
|
||||||
|
hidden (traffic lights own that corner) and the header is just a
|
||||||
|
window-drag strip — there the toggle is App.vue's resident floating
|
||||||
|
button beside the traffic lights. -->
|
||||||
<div class="ch">
|
<div class="ch">
|
||||||
<div class="ch-brand">
|
<div class="ch-brand">
|
||||||
<svg ref="logoRef" class="ch-logo" :class="{ 'is-dev': isDev }" viewBox="0 0 32 22" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Kimi Code" @click="onLogoClick" @pointerdown="onLogoPointerDown" @pointerup="onLogoPointerUp" @pointercancel="onLogoPointerUp">
|
<template v-if="!isMacosDesktop">
|
||||||
<defs>
|
<svg ref="logoRef" class="ch-logo" :class="{ 'is-dev': isDev }" viewBox="0 0 32 22" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Kimi Code" @click="onLogoClick" @pointerdown="onLogoPointerDown" @pointerup="onLogoPointerUp" @pointercancel="onLogoPointerUp">
|
||||||
<mask id="kimiEyes" maskUnits="userSpaceOnUse">
|
<defs>
|
||||||
<rect x="0" y="0" width="32" height="22" fill="#fff" />
|
<mask id="kimiEyes" maskUnits="userSpaceOnUse">
|
||||||
<g class="ch-eyes" fill="#000">
|
<rect x="0" y="0" width="32" height="22" fill="#fff" />
|
||||||
<rect class="ch-eye" x="11.8" y="7" width="2.8" height="8" rx="1.4" />
|
<g class="ch-eyes" fill="#000">
|
||||||
<rect class="ch-eye" x="17.4" y="7" width="2.8" height="8" rx="1.4" />
|
<rect class="ch-eye" x="11.8" y="7" width="2.8" height="8" rx="1.4" />
|
||||||
</g>
|
<rect class="ch-eye" x="17.4" y="7" width="2.8" height="8" rx="1.4" />
|
||||||
</mask>
|
</g>
|
||||||
</defs>
|
</mask>
|
||||||
<rect x="1" y="1" width="30" height="20" rx="6" fill="var(--logo)" mask="url(#kimiEyes)" />
|
</defs>
|
||||||
</svg>
|
<rect x="1" y="1" width="30" height="20" rx="6" fill="var(--logo)" mask="url(#kimiEyes)" />
|
||||||
<span class="ch-name">Kimi Code<span v-if="isDev" class="ch-endpoint"> · {{ endpoint }}</span></span>
|
</svg>
|
||||||
<InternalBuildBanner />
|
<span class="ch-name">Kimi Code<span v-if="isDev" class="ch-endpoint"> · {{ endpoint }}</span></span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
v-if="!isMacosDesktop"
|
||||||
|
class="ch-collapse"
|
||||||
size="sm"
|
size="sm"
|
||||||
:label="t('sidebar.collapseSidebar')"
|
:label="t('sidebar.collapseSidebar')"
|
||||||
@click.stop="emit('collapse')"
|
@click.stop="emit('collapse')"
|
||||||
>
|
>
|
||||||
<Icon name="panel-collapse" />
|
<Icon name="panel-collapse" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
:label="t('settings.title')"
|
|
||||||
@click.stop="emit('openSettings')"
|
|
||||||
>
|
|
||||||
<Icon name="settings" />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Session search — opens the Spotlight-style search dialog -->
|
|
||||||
<button class="search" type="button" @click="openSearch">
|
|
||||||
<Icon class="search-icon" name="search" />
|
|
||||||
<span class="search-input">
|
|
||||||
<span class="search-label">{{ t('sidebar.searchPlaceholder') }}</span>
|
|
||||||
<ShortcutKey>{{ sessionSearchShortcut }}</ShortcutKey>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- New chat + new workspace buttons -->
|
<!-- New chat + new workspace buttons -->
|
||||||
<div class="btn-wrap" :class="{ 'btn-wrap--scrolled': sessionsScrolled }">
|
<div class="btn-wrap">
|
||||||
<button class="btn-new-chat" type="button" @click.stop="emit('create')">
|
<button class="btn-new-chat" type="button" @click.stop="emit('create')">
|
||||||
<Icon name="chat-new" />
|
<Icon name="chat-new" />
|
||||||
<span>{{ t('sidebar.newChat') }}</span>
|
<span>{{ t('sidebar.newChat') }}</span>
|
||||||
|
|
@ -622,6 +605,16 @@ onBeforeUnmount(() => {
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Session search — opens the Spotlight-style search dialog. Last fixed
|
||||||
|
row above the list, so it carries the scroll-linked seam. -->
|
||||||
|
<div class="search-wrap" :class="{ 'search-wrap--scrolled': sessionsScrolled }">
|
||||||
|
<button class="search" type="button" @click="openSearch">
|
||||||
|
<Icon class="search-icon" name="search" />
|
||||||
|
<span class="search-input">{{ t('sidebar.search') }}</span>
|
||||||
|
<Kbd :keys="sessionSearchKeys" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Session list — grouped by workspace -->
|
<!-- Session list — grouped by workspace -->
|
||||||
<div class="sessions" @scroll="onSessionsScroll">
|
<div class="sessions" @scroll="onSessionsScroll">
|
||||||
<!-- Empty state — only when no workspace is registered at all; empty
|
<!-- Empty state — only when no workspace is registered at all; empty
|
||||||
|
|
@ -679,7 +672,6 @@ onBeforeUnmount(() => {
|
||||||
:dragging="draggingWsId === g.workspace.id"
|
:dragging="draggingWsId === g.workspace.id"
|
||||||
:is-collapsed="isCollapsed"
|
:is-collapsed="isCollapsed"
|
||||||
:is-expanded="isExpanded"
|
:is-expanded="isExpanded"
|
||||||
:show-path="showWorkspacePaths"
|
|
||||||
@group-click="handleGhClick"
|
@group-click="handleGhClick"
|
||||||
@group-contextmenu="openGhMenu"
|
@group-contextmenu="openGhMenu"
|
||||||
@toggle-ws-menu="toggleWsMenu"
|
@toggle-ws-menu="toggleWsMenu"
|
||||||
|
|
@ -699,6 +691,14 @@ onBeforeUnmount(() => {
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer: settings entry pinned under the session list -->
|
||||||
|
<div class="side-footer">
|
||||||
|
<button class="btn-settings" type="button" @click.stop="emit('openSettings')">
|
||||||
|
<Icon name="settings" />
|
||||||
|
<span>{{ t('settings.title') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Workspace right-click menu (position:fixed) -->
|
<!-- Workspace right-click menu (position:fixed) -->
|
||||||
|
|
@ -749,13 +749,6 @@ onBeforeUnmount(() => {
|
||||||
</span>
|
</span>
|
||||||
{{ t('sidebar.sortRecent') }}
|
{{ t('sidebar.sortRecent') }}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem separator />
|
|
||||||
<MenuItem @click="toggleShowWorkspacePathsFromMenu()">
|
|
||||||
<span class="section-menu-check">
|
|
||||||
<Icon v-if="showWorkspacePaths" name="check" size="sm" />
|
|
||||||
</span>
|
|
||||||
{{ t('sidebar.showWorkspacePaths') }}
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
</Menu>
|
||||||
<!-- Session search dialog (Cmd/Ctrl+K) -->
|
<!-- Session search dialog (Cmd/Ctrl+K) -->
|
||||||
<SearchSessionsDialog
|
<SearchSessionsDialog
|
||||||
|
|
@ -776,24 +769,48 @@ onBeforeUnmount(() => {
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.side {
|
.side {
|
||||||
border-right: 1px solid var(--line);
|
/* Sidebar sits on its own surface (--color-sidebar-bg, one step off --bg);
|
||||||
background: var(--panel);
|
the 1px hairline on .col still separates it from the conversation pane. */
|
||||||
|
background: var(--color-sidebar-bg);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
/* Anchor content to the right edge: while the container width animates to 0
|
||||||
|
the fixed-width column slides out to the left and is clipped, instead of
|
||||||
|
reflowing. Mirrors the right-side preview panel (App.vue .global-preview). */
|
||||||
|
justify-content: flex-end;
|
||||||
|
overflow: hidden;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
/* Alignment contract, inherited by SessionRow and the de-terminalization
|
transition:
|
||||||
rules in style.css: text in the workspace header, the path line and session
|
width 0.28s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
rows all starts at --sb-pad-x + --sb-gutter + --sb-gap from the sidebar edge. */
|
visibility 0.28s;
|
||||||
--sb-pad-x: var(--space-4); /* row horizontal padding */
|
/* Alignment contract, inherited by SessionRow and WorkspaceGroup:
|
||||||
--sb-gutter: 20px; /* leading icon slot (14px folder icon + 6px margin) */
|
- row boxes (hover/selected pills) sit --sb-inset from the sidebar edges;
|
||||||
|
- text/icons start at --sb-pad-x = --sb-inset + 8px row padding;
|
||||||
|
- row titles start at --sb-pad-x + --sb-gutter + --sb-gap. */
|
||||||
|
--sb-inset: var(--space-3); /* row box inset from the sidebar edge */
|
||||||
|
--sb-pad-x: var(--space-5); /* content start x (inset + row padding) */
|
||||||
|
--sb-gutter: 16px; /* leading icon slot (matches the 16px folder icon, so the session title aligns under the workspace name) */
|
||||||
--sb-gap: var(--space-2); /* gap between the icon slot and the text */
|
--sb-gap: var(--space-2); /* gap between the icon slot and the text */
|
||||||
/* Sidebar stays one step above compact UI chrome, but still follows the
|
/* Row hover wash — global --color-hover (lighter than the selected fill;
|
||||||
user-controlled font-size preference. */
|
both translucent, so they sit on any surface). */
|
||||||
--ui-font-size: var(--sidebar-ui-font-size);
|
--sb-hover: var(--color-hover);
|
||||||
|
}
|
||||||
|
/* While dragging the resize handle, follow the pointer 1:1 (same pattern as
|
||||||
|
.global-preview.no-anim in App.vue). */
|
||||||
|
.side.no-anim {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
/* Fully collapsed: width 0 (animated), then drop out of hit-testing / tab
|
||||||
|
order once the transition ends (visibility interpolates to hidden at the
|
||||||
|
end when collapsing, and back to visible immediately when expanding). */
|
||||||
|
.side.collapsed {
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Session column. Width is set inline from the App resize handle. */
|
/* Session column. Width is set inline from the App resize handle; it stays
|
||||||
|
fixed while the collapsing container clips it. Carries the sidebar's right
|
||||||
|
hairline so the border is clipped away together with the content. */
|
||||||
.col {
|
.col {
|
||||||
flex: none;
|
flex: none;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -801,35 +818,38 @@ onBeforeUnmount(() => {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
container-name: sidebar-col;
|
container-name: sidebar-col;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header: logo + settings (no border — flows into the workspace list). */
|
/* Header: brand strip (no border — flows into the workspace list). On non-mac
|
||||||
|
platforms the brand sits on the left and the collapse button on the right
|
||||||
|
(justify-content: space-between); on macOS desktop the brand is hidden and
|
||||||
|
the header is a window-drag strip (see below). min-height keeps the 26px
|
||||||
|
control row (50px total with padding) so the list below starts at a stable
|
||||||
|
y. */
|
||||||
.ch {
|
.ch {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: var(--space-3) var(--space-3) var(--space-2);
|
padding: var(--space-3);
|
||||||
|
min-height: calc(26px + 2 * var(--space-3));
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
/* macOS desktop: the window uses a hidden title bar, so the traffic lights float
|
/* macOS desktop: the window uses a hidden title bar, so the traffic lights
|
||||||
over the top-left of the sidebar. Push the header content right to clear them,
|
float over the top-left of the sidebar and the resident toggle sits beside
|
||||||
and turn the whole header into the window-drag region — matching the chat
|
them. The header renders no content here (brand hidden) — it is purely a
|
||||||
header. The action buttons and the logo opt out with no-drag so they stay
|
window-drag strip. */
|
||||||
clickable: this is the same no-drag-inside-drag pattern ChatHeader.vue relies
|
|
||||||
on (the previous "drag only the brand area" approach still captured the
|
|
||||||
sibling buttons, because Electron treats a flex-grown drag item's hit area as
|
|
||||||
covering the whole flex line). */
|
|
||||||
.side.macos-desktop .ch {
|
.side.macos-desktop .ch {
|
||||||
padding-left: 80px;
|
padding-left: 80px;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
}
|
}
|
||||||
.side.macos-desktop .ch button,
|
.side.macos-desktop .ch-brand {
|
||||||
.side.macos-desktop .ch-logo {
|
display: none;
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
}
|
||||||
.ch-logo {
|
.ch-logo {
|
||||||
height: 22px;
|
height: 22px;
|
||||||
|
|
@ -884,22 +904,14 @@ onBeforeUnmount(() => {
|
||||||
.ch-name { display: none; }
|
.ch-name { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Action buttons */
|
/* Action buttons — first row of the actions group (New chat + search): rows
|
||||||
.btn-wrap {
|
inside the group stack flush (0 gap, same rhythm as the session list rows);
|
||||||
|
the group's bottom gap lives on .search-wrap. */
|
||||||
|
.btn-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 0 var(--space-2) var(--space-2);
|
padding: 0 var(--sb-inset);
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
background: var(--panel);
|
|
||||||
border-bottom: 1px solid transparent;
|
|
||||||
transition: border-color var(--duration-base) var(--ease-out),
|
|
||||||
box-shadow var(--duration-base) var(--ease-out);
|
|
||||||
}
|
|
||||||
.btn-wrap--scrolled {
|
|
||||||
border-bottom-color: var(--line);
|
|
||||||
box-shadow: var(--shadow-sm);
|
|
||||||
}
|
}
|
||||||
.btn-new-chat {
|
.btn-new-chat {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -907,46 +919,61 @@ onBeforeUnmount(() => {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 26px;
|
padding: 8px calc(var(--sb-pad-x) - var(--sb-inset));
|
||||||
padding: var(--space-1) calc(var(--sb-pad-x) - var(--space-2));
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-sm);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-size: var(--ui-font-size);
|
font-size: var(--ui-font-size-sm);
|
||||||
font-weight: var(--weight-medium);
|
line-height: var(--leading-tight);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.btn-new-chat:hover { background: var(--color-surface-sunken); }
|
.btn-new-chat:hover { background: var(--sb-hover); }
|
||||||
.btn-new-chat:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
|
.btn-new-chat:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
|
||||||
.btn-new-chat svg { flex: none; width: 16px; height: 16px; }
|
.btn-new-chat svg { flex: none; }
|
||||||
.btn-new-chat span {
|
.btn-new-chat span {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Session search */
|
/* Session search — the wrapper is the last fixed row above the list and
|
||||||
|
carries the scroll-linked seam: its bottom border/shadow only appear once
|
||||||
|
the session list has actually scrolled, so an unscrolled list shows no
|
||||||
|
abrupt boundary. */
|
||||||
|
.search-wrap {
|
||||||
|
padding: 0 var(--sb-inset);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--color-sidebar-bg);
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
transition: border-color var(--duration-base) var(--ease-out),
|
||||||
|
box-shadow var(--duration-base) var(--ease-out);
|
||||||
|
}
|
||||||
|
.search-wrap--scrolled {
|
||||||
|
border-bottom-color: var(--line);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
.search {
|
.search {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-height: 26px;
|
width: 100%;
|
||||||
margin: 0 var(--space-2) var(--space-2);
|
margin: 0;
|
||||||
padding: var(--space-1) calc(var(--sb-pad-x) - var(--space-2));
|
padding: 8px calc(var(--sb-pad-x) - var(--sb-inset));
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-sm);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.search:hover { background: var(--color-surface-sunken); }
|
.search:hover { background: var(--sb-hover); }
|
||||||
.search:focus-visible {
|
.search:focus-visible {
|
||||||
background: var(--color-surface-sunken);
|
background: var(--sb-hover);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
outline: 2px solid var(--color-accent-bd);
|
outline: 2px solid var(--color-accent-bd);
|
||||||
outline-offset: -2px;
|
outline-offset: -2px;
|
||||||
|
|
@ -955,42 +982,71 @@ onBeforeUnmount(() => {
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
.search-input {
|
.search-input {
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-1);
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-size: var(--ui-font-size);
|
font-size: var(--ui-font-size-sm);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.search-input > span {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
.search-label {
|
|
||||||
font-weight: var(--weight-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sessions */
|
/* Sessions — owns the vertical padding around the list (the 12px gap to the
|
||||||
|
search row above and the bottom breathing room). Scrolled content passes
|
||||||
|
through the top padding and clips at the .search-wrap seam. Scrollbar: the
|
||||||
|
4px ::-webkit-scrollbar below; standard scrollbar-width would kill it on
|
||||||
|
Chromium (see the global scrollbar block in style.css). */
|
||||||
.sessions {
|
.sessions {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 var(--space-2) var(--space-2);
|
padding: var(--space-3) var(--sb-inset);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--line) transparent;
|
|
||||||
}
|
}
|
||||||
.sessions::-webkit-scrollbar { width: 4px; }
|
.sessions::-webkit-scrollbar { width: 4px; }
|
||||||
.sessions::-webkit-scrollbar-track { background: transparent; }
|
.sessions::-webkit-scrollbar-track { background: transparent; }
|
||||||
.sessions::-webkit-scrollbar-thumb {
|
.sessions::-webkit-scrollbar-thumb {
|
||||||
background: var(--line);
|
/* Neutral, text-derived translucency — adapts to both schemes and sits
|
||||||
border-radius: var(--radius-xs);
|
quietly on the sidebar surface (no accent tint on hover). */
|
||||||
|
background: color-mix(in srgb, var(--color-text) 12%, transparent);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
}
|
||||||
|
.sessions::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--color-text) 25%, transparent); }
|
||||||
|
|
||||||
|
/* Footer — settings entry pinned under the session list. Same list-style
|
||||||
|
control family as search / New chat (full-width, left-aligned, hover
|
||||||
|
sunken — not a Button). */
|
||||||
|
.side-footer {
|
||||||
|
flex: none;
|
||||||
|
padding: var(--space-2) var(--sb-inset);
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.btn-settings {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px calc(var(--sb-pad-x) - var(--sb-inset));
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--ui-font-size-sm);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.btn-settings:hover { background: var(--sb-hover); }
|
||||||
|
.btn-settings:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
|
||||||
|
.btn-settings svg { flex: none; }
|
||||||
|
.btn-settings span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.sessions::-webkit-scrollbar-thumb:hover { background: var(--color-accent-bd); }
|
|
||||||
|
|
||||||
/* Section label — heads the workspace list below the action buttons. Aligns
|
/* Section label — heads the workspace list below the action buttons. Aligns
|
||||||
with the rows' leading inset (--sb-pad-x) so it reads as the list's title. */
|
with the rows' leading inset (--sb-pad-x) so it reads as the list's title. */
|
||||||
|
|
@ -1000,9 +1056,9 @@ onBeforeUnmount(() => {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 0 var(--space-3) var(--space-1) var(--space-2);
|
padding: 0 var(--space-3) var(--space-1) var(--space-2);
|
||||||
font-size: var(--text-sm);
|
font-family: var(--font-ui);
|
||||||
font-weight: 500;
|
font-size: var(--text-xs);
|
||||||
letter-spacing: .08em;
|
font-weight: var(--weight-regular);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,6 @@ const props = defineProps<{
|
||||||
wsMenuOpenId: string | null;
|
wsMenuOpenId: string | null;
|
||||||
/** True while this group is the active drag source (drag-to-reorder). */
|
/** True while this group is the active drag source (drag-to-reorder). */
|
||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
/** When true, render the workspace root path as a stable subtitle line. */
|
|
||||||
showPath: boolean;
|
|
||||||
isCollapsed: (id: string) => boolean;
|
isCollapsed: (id: string) => boolean;
|
||||||
/** When true, render all loaded sessions; otherwise only the first page
|
/** When true, render all loaded sessions; otherwise only the first page
|
||||||
* (`group.initialCount`). Drives the in-group show-more / show-less toggle. */
|
* (`group.initialCount`). Drives the in-group show-more / show-less toggle. */
|
||||||
|
|
@ -109,7 +107,7 @@ function onHeaderDragStart(event: DragEvent): void {
|
||||||
<div class="group" :class="{ dragging }">
|
<div class="group" :class="{ dragging }">
|
||||||
<div
|
<div
|
||||||
class="gh"
|
class="gh"
|
||||||
:class="{ on: group.workspace.id === activeWorkspaceId, collapsed: isCollapsed(group.workspace.id), 'show-path': showPath }"
|
:class="{ on: group.workspace.id === activeWorkspaceId, collapsed: isCollapsed(group.workspace.id) }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@click.stop="emit('groupClick', group.workspace.id, $event)"
|
@click.stop="emit('groupClick', group.workspace.id, $event)"
|
||||||
@contextmenu="emit('groupContextmenu', group.workspace, $event)"
|
@contextmenu="emit('groupContextmenu', group.workspace, $event)"
|
||||||
|
|
@ -118,14 +116,13 @@ function onHeaderDragStart(event: DragEvent): void {
|
||||||
>
|
>
|
||||||
<div class="gh-top">
|
<div class="gh-top">
|
||||||
<!-- Folder icon -->
|
<!-- Folder icon -->
|
||||||
<Icon v-if="isCollapsed(group.workspace.id)" class="gh-folder" name="folder-closed" size="sm" />
|
<Icon v-if="isCollapsed(group.workspace.id)" class="gh-folder" name="folder-closed" />
|
||||||
<Icon v-else class="gh-folder" name="folder" size="sm" />
|
<Icon v-else class="gh-folder" name="folder" />
|
||||||
|
|
||||||
<!-- Workspace name -->
|
<!-- Workspace name — hover reveals the full root path -->
|
||||||
<span
|
<Tooltip v-if="renamingId !== group.workspace.id" :text="group.workspace.root">
|
||||||
v-if="renamingId !== group.workspace.id"
|
<span class="gh-name">{{ group.workspace.name }}</span>
|
||||||
class="gh-name"
|
</Tooltip>
|
||||||
>{{ group.workspace.name }}</span>
|
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
:ref="setRenameInputRef"
|
:ref="setRenameInputRef"
|
||||||
|
|
@ -138,31 +135,36 @@ function onHeaderDragStart(event: DragEvent): void {
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IconButton
|
<!-- Hover actions — float over the row's right edge (no reserved
|
||||||
class="gh-more"
|
layout space, the name gets the full row width when idle). Hidden
|
||||||
|
while renaming so the floating buttons can't cover the input. -->
|
||||||
|
<div
|
||||||
|
v-if="renamingId !== group.workspace.id"
|
||||||
|
class="gh-actions"
|
||||||
:class="{ open: wsMenuOpenId === group.workspace.id }"
|
:class="{ open: wsMenuOpenId === group.workspace.id }"
|
||||||
size="sm"
|
|
||||||
:label="t('sidebar.options')"
|
|
||||||
aria-haspopup="menu"
|
|
||||||
:aria-expanded="wsMenuOpenId === group.workspace.id"
|
|
||||||
@click.stop="emit('toggleWsMenu', group.workspace, $event)"
|
|
||||||
>
|
>
|
||||||
<Icon name="dots-horizontal" size="sm" />
|
<IconButton
|
||||||
</IconButton>
|
class="gh-more"
|
||||||
|
:class="{ open: wsMenuOpenId === group.workspace.id }"
|
||||||
|
size="sm"
|
||||||
|
:label="t('sidebar.options')"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
:aria-expanded="wsMenuOpenId === group.workspace.id"
|
||||||
|
@click.stop="emit('toggleWsMenu', group.workspace, $event)"
|
||||||
|
>
|
||||||
|
<Icon name="dots-horizontal" />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
class="gh-add"
|
class="gh-add"
|
||||||
size="sm"
|
size="sm"
|
||||||
:label="t('workspace.newInGroup')"
|
:label="t('workspace.newInGroup')"
|
||||||
@click.stop="emit('createInWorkspace', group.workspace.id)"
|
@click.stop="emit('createInWorkspace', group.workspace.id)"
|
||||||
>
|
>
|
||||||
<Icon name="chat-new" />
|
<Icon name="chat-new" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tooltip :text="group.workspace.root">
|
|
||||||
<div class="gh-path">{{ group.workspace.shortPath || group.workspace.root }}</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="group-sessions"
|
class="group-sessions"
|
||||||
|
|
@ -212,8 +214,8 @@ function onHeaderDragStart(event: DragEvent): void {
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Workspace group. The --sb-* custom properties are inherited from .side in
|
/* Workspace group. The --sb-* custom properties are inherited from .side in
|
||||||
Sidebar.vue, so they don't need to be redeclared here. */
|
Sidebar.vue, so they don't need to be redeclared here. Groups stack flush —
|
||||||
.group { padding-bottom: var(--space-2); }
|
no bottom gap. */
|
||||||
.group.dragging { opacity: 0.45; }
|
.group.dragging { opacity: 0.45; }
|
||||||
|
|
||||||
/* Session list: collapses/expands via a height transition. `interpolate-size:
|
/* Session list: collapses/expands via a height transition. `interpolate-size:
|
||||||
|
|
@ -230,15 +232,14 @@ function onHeaderDragStart(event: DragEvent): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Workspace header — an inset rounded row that mirrors the session-row inset
|
/* Workspace header — an inset rounded row that mirrors the session-row inset
|
||||||
(6px margin + 10px padding), so the folder icon lands at --sb-pad-x and the
|
(container --sb-inset + row padding), so the folder icon lands at --sb-pad-x
|
||||||
name lines up with the session titles below. Hover washes the whole header
|
and the name lines up with the session titles below. Hover washes the whole
|
||||||
(name row + path) in the sunken surface. */
|
header in the row hover fill. */
|
||||||
.gh {
|
.gh {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--space-1) var(--space-2);
|
padding: 8px calc(var(--sb-pad-x) - var(--sb-inset));
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
|
|
@ -249,24 +250,28 @@ function onHeaderDragStart(event: DragEvent): void {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
.gh:active { cursor: grabbing; }
|
.gh:active { cursor: grabbing; }
|
||||||
.gh:hover { background: var(--color-surface-sunken); }
|
.gh:hover { background: var(--sb-hover, var(--color-surface-sunken)); }
|
||||||
.gh-top {
|
.gh-top {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--sb-gap);
|
gap: var(--sb-gap);
|
||||||
|
/* Header height is font-driven: name line-height (13×1.25≈16px) + 2×5px
|
||||||
|
.gh padding ≈ 26px. The floating .gh-actions never contribute to height. */
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-folder {
|
.gh-folder {
|
||||||
flex: none;
|
flex: none;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
/* 14px icon + margin fills the --sb-gutter icon slot */
|
|
||||||
margin-right: calc(var(--sb-gutter) - 14px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Group title — quiet by design: regular weight (no bold), muted color (one
|
||||||
|
step lighter than the session titles), so group heads read as grouping
|
||||||
|
labels rather than list content. */
|
||||||
.gh-name {
|
.gh-name {
|
||||||
font-size: var(--ui-font-size-lg);
|
font-size: var(--ui-font-size-sm);
|
||||||
font-weight: 550;
|
line-height: var(--leading-tight);
|
||||||
color: var(--color-text);
|
color: var(--color-text-muted);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -274,51 +279,59 @@ function onHeaderDragStart(event: DragEvent): void {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.gh-path {
|
|
||||||
color: var(--color-text-faint);
|
|
||||||
font-weight: 425;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding-left: calc(var(--sb-gutter) + var(--sb-gap));
|
|
||||||
font-size: var(--ui-font-size-xs);
|
|
||||||
max-height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
transition: max-height var(--duration-base) var(--ease-out),
|
|
||||||
opacity var(--duration-base) var(--ease-out);
|
|
||||||
}
|
|
||||||
/* Path subtitle — revealed only when the section-header "show paths" toggle is
|
|
||||||
on (`.show-path`) or the group is collapsed. Driven by explicit toggle state
|
|
||||||
rather than hover/focus, so the header height never shifts under the pointer
|
|
||||||
(a11y: stable layout) and the path stays reachable for touch/keyboard users. */
|
|
||||||
.gh.show-path .gh-path,
|
|
||||||
.gh.collapsed .gh-path {
|
|
||||||
max-height: 1.4em;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* More + add buttons — hidden until hover (or while the more menu is open /
|
/* More + add buttons — float over the row's right edge instead of reserving
|
||||||
focused). `.gh .gh-more` / `.gh .gh-add` out-specificity IconButton's display
|
layout space, so the name can use the full row width when idle (no
|
||||||
so the hidden default wins. */
|
truncation caused by invisible buttons). Revealed on hover / keyboard focus
|
||||||
.gh .gh-more,
|
/ while the more menu is open; the backing stacks the row hover wash on the
|
||||||
.gh .gh-add {
|
sidebar surface so the overlapped title tail doesn't bleed through. */
|
||||||
|
.gh-actions {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding-left: var(--space-1);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
isolation: isolate;
|
||||||
|
/* Opaque sidebar surface — hides the overlapped name tail. The ::after
|
||||||
|
hover wash sits above this (still behind the buttons) so the layer reads
|
||||||
|
seamless with the row. */
|
||||||
|
background: var(--color-sidebar-bg);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.gh:hover .gh-more,
|
/* Row hover wash — only while the row is actually hovered. Painted above the
|
||||||
.gh:hover .gh-add,
|
element background (z-index 0) but below the buttons (z-index 1). */
|
||||||
.gh:focus-within .gh-more,
|
.gh-actions::after {
|
||||||
.gh:focus-within .gh-add,
|
content: '';
|
||||||
.gh-more.open,
|
position: absolute;
|
||||||
.gh-more:focus-visible,
|
inset: 0;
|
||||||
.gh-add:focus-visible {
|
z-index: 0;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.gh:hover .gh-actions::after {
|
||||||
|
background: var(--sb-hover, var(--color-surface-sunken));
|
||||||
|
}
|
||||||
|
.gh-actions > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.gh:hover .gh-actions,
|
||||||
|
.gh:focus-within .gh-actions,
|
||||||
|
.gh-actions.open {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.gh-more.open { color: var(--color-text); background: var(--color-line); }
|
.gh-more.open { color: var(--color-text); background: var(--color-line); }
|
||||||
|
|
||||||
.group-empty {
|
.group-empty {
|
||||||
padding: var(--space-1) var(--space-2) var(--space-1) calc(var(--sb-pad-x) - var(--space-2) + var(--sb-gutter) + var(--sb-gap));
|
/* Left padding lands the text at the same x as session titles / the
|
||||||
|
show-more label: (pad-x − inset) row padding + gutter + gap. */
|
||||||
|
padding: var(--space-1) var(--space-2) var(--space-1) calc(var(--sb-pad-x) - var(--sb-inset) + var(--sb-gutter) + var(--sb-gap));
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
color: var(--color-text-faint);
|
color: var(--color-text-faint);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
|
|
@ -326,26 +339,26 @@ function onHeaderDragStart(event: DragEvent): void {
|
||||||
/* Show-more / show-less — a session-row-shaped compact list control (§07). The
|
/* Show-more / show-less — a session-row-shaped compact list control (§07). The
|
||||||
empty lead slot mirrors a session row's status gutter, so the label text lands
|
empty lead slot mirrors a session row's status gutter, so the label text lands
|
||||||
at the exact same x as the session titles (--sb-pad-x + --sb-gutter + --sb-gap
|
at the exact same x as the session titles (--sb-pad-x + --sb-gutter + --sb-gap
|
||||||
from the sidebar edge). Hover washes the row in the sunken surface, matching
|
from the sidebar edge). Hover washes the row in the shared row hover fill,
|
||||||
New chat / session rows; no text recolor. */
|
matching New chat / session rows; no text recolor. */
|
||||||
.show-more {
|
.show-more {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--sb-gap);
|
gap: var(--sb-gap);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 26px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: var(--space-1) calc(var(--sb-pad-x) - var(--space-2));
|
padding: 8px calc(var(--sb-pad-x) - var(--sb-inset));
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-sm);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
|
line-height: var(--leading-tight);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.show-more:hover { background: var(--color-surface-sunken); }
|
.show-more:hover { background: var(--sb-hover, var(--color-surface-sunken)); }
|
||||||
.show-more:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
|
.show-more:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
|
||||||
.show-more-lead { width: var(--sb-gutter); flex: none; }
|
.show-more-lead { width: var(--sb-gutter); flex: none; }
|
||||||
.show-more-label {
|
.show-more-label {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import Badge from '../ui/Badge.vue';
|
||||||
import Button from '../ui/Button.vue';
|
import Button from '../ui/Button.vue';
|
||||||
import IconButton from '../ui/IconButton.vue';
|
import IconButton from '../ui/IconButton.vue';
|
||||||
import Icon from '../ui/Icon.vue';
|
import Icon from '../ui/Icon.vue';
|
||||||
import ShortcutKey from '../ui/ShortcutKey.vue';
|
import Kbd from '../ui/Kbd.vue';
|
||||||
import Tooltip from '../ui/Tooltip.vue';
|
import Tooltip from '../ui/Tooltip.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -315,20 +315,20 @@ onUnmounted(() => document.removeEventListener('keydown', handleKeydown));
|
||||||
:loading="pendingAction === `option:${opt.label}`"
|
:loading="pendingAction === `option:${opt.label}`"
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
@click="approveOption(opt.label)"
|
@click="approveOption(opt.label)"
|
||||||
>{{ opt.label }}<ShortcutKey class="k">[{{ i + 1 }}]</ShortcutKey></Button>
|
>{{ opt.label }}<Kbd class="k" :keys="[String(i + 1)]" /></Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
<Button v-else class="kbtn" size="sm" variant="primary" :loading="pendingAction === 'approvePlan'" :disabled="busy" @click="approvePlan">{{ t('approval.approvePlan') }}<ShortcutKey class="k">[1]</ShortcutKey></Button>
|
<Button v-else class="kbtn" size="sm" variant="primary" :loading="pendingAction === 'approvePlan'" :disabled="busy" @click="approvePlan">{{ t('approval.approvePlan') }}<Kbd class="k" :keys="['1']" /></Button>
|
||||||
<Button class="kbtn" size="sm" variant="secondary" :disabled="busy" @click="revisePlan">{{ t('approval.revise') }}<ShortcutKey v-if="planReview.options.length === 0" class="k">[2]</ShortcutKey></Button>
|
<Button class="kbtn" size="sm" variant="secondary" :disabled="busy" @click="revisePlan">{{ t('approval.revise') }}<Kbd v-if="planReview.options.length === 0" class="k" :keys="['2']" /></Button>
|
||||||
<Button class="kbtn" size="sm" variant="danger-soft" :loading="pendingAction === 'rejectAndExit'" :disabled="busy" @click="rejectAndExitPlan">{{ t('approval.rejectAndExit') }}<ShortcutKey v-if="planReview.options.length === 0" class="k">[3]</ShortcutKey></Button>
|
<Button class="kbtn" size="sm" variant="danger-soft" :loading="pendingAction === 'rejectAndExit'" :disabled="busy" @click="rejectAndExitPlan">{{ t('approval.rejectAndExit') }}<Kbd v-if="planReview.options.length === 0" class="k" :keys="['3']" /></Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- default actions row -->
|
<!-- default actions row -->
|
||||||
<div v-else class="abtn">
|
<div v-else class="abtn">
|
||||||
<Button class="kbtn" size="sm" variant="primary" :loading="pendingAction === 'approve'" :disabled="busy" @click="approve">{{ t('approval.approve') }}<ShortcutKey class="k">[1]</ShortcutKey></Button>
|
<Button class="kbtn" size="sm" variant="primary" :loading="pendingAction === 'approve'" :disabled="busy" @click="approve">{{ t('approval.approve') }}<Kbd class="k" :keys="['1']" /></Button>
|
||||||
<Button class="kbtn" size="sm" variant="secondary" :loading="pendingAction === 'approveSession'" :disabled="busy" @click="approveSession">{{ t('approval.approveSession') }}<ShortcutKey class="k">[2]</ShortcutKey></Button>
|
<Button class="kbtn" size="sm" variant="secondary" :loading="pendingAction === 'approveSession'" :disabled="busy" @click="approveSession">{{ t('approval.approveSession') }}<Kbd class="k" :keys="['2']" /></Button>
|
||||||
<Button class="kbtn" size="sm" variant="secondary" :loading="pendingAction === 'reject'" :disabled="busy" @click="reject">{{ t('approval.reject') }}<ShortcutKey class="k">[3]</ShortcutKey></Button>
|
<Button class="kbtn" size="sm" variant="secondary" :loading="pendingAction === 'reject'" :disabled="busy" @click="reject">{{ t('approval.reject') }}<Kbd class="k" :keys="['3']" /></Button>
|
||||||
<Button class="kbtn" size="sm" variant="secondary" :disabled="busy" @click="openFeedback">{{ t('approval.feedback') }}<ShortcutKey class="k">[4]</ShortcutKey></Button>
|
<Button class="kbtn" size="sm" variant="secondary" :disabled="busy" @click="openFeedback">{{ t('approval.feedback') }}<Kbd class="k" :keys="['4']" /></Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,11 @@ defineExpose({ el });
|
||||||
transition: background var(--duration-base) var(--ease-out),
|
transition: background var(--duration-base) var(--ease-out),
|
||||||
color var(--duration-base) var(--ease-out);
|
color var(--duration-base) var(--ease-out);
|
||||||
}
|
}
|
||||||
.ui-icon-button:hover:not(:disabled) { background: var(--color-surface-sunken); color: var(--color-text); }
|
/* Translucent text-mix instead of the sunken surface: stays visible on ANY
|
||||||
|
backdrop — the sunken token equals the page bg in dark mode, which made
|
||||||
|
hover feedback vanish for icon buttons sitting directly on --color-bg
|
||||||
|
(chat header, flat sidebar). */
|
||||||
|
.ui-icon-button:hover:not(:disabled) { background: color-mix(in srgb, var(--color-text) 8%, transparent); color: var(--color-text); }
|
||||||
.ui-icon-button:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
|
.ui-icon-button:focus-visible { outline: none; box-shadow: var(--p-focus-ring); }
|
||||||
.ui-icon-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
.ui-icon-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
|
|
||||||
40
apps/kimi-web/src/components/ui/Kbd.vue
Normal file
40
apps/kimi-web/src/components/ui/Kbd.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<!-- apps/kimi-web/src/components/ui/Kbd.vue -->
|
||||||
|
<!-- Design-system §03 Kbd: keyboard shortcut rendered as keycaps — one <kbd>
|
||||||
|
block per key (e.g. ['⌘', 'K'] renders two caps). Keycap look: sunken
|
||||||
|
surface + 2px bottom border, 18px tall to match Badge sm. -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps<{
|
||||||
|
keys: string[];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span class="ui-kbd">
|
||||||
|
<kbd v-for="key in keys" :key="key" class="ui-kbd__key">{{ key }}</kbd>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ui-kbd {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.ui-kbd__key {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border: 1px solid var(--color-line);
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
background: var(--color-surface-sunken);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
<!-- Token-styled keyboard shortcut. Keeps the semantic <kbd> element reusable
|
|
||||||
across compact UI surfaces. -->
|
|
||||||
<template>
|
|
||||||
<kbd class="shortcut-key">
|
|
||||||
<slot />
|
|
||||||
</kbd>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.shortcut-key {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex: none;
|
|
||||||
padding: 0 var(--space-1);
|
|
||||||
border: 1px solid var(--color-line);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
background: var(--color-surface-raised);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-family: var(--font-ui);
|
|
||||||
font-size: var(--ui-font-size-xs);
|
|
||||||
line-height: var(--leading-tight);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -11,7 +11,9 @@ const SIDEBAR_WIDTH_KEY = STORAGE_KEYS.sidebarWidth;
|
||||||
const SIDEBAR_COLLAPSED_KEY = STORAGE_KEYS.sidebarCollapsed;
|
const SIDEBAR_COLLAPSED_KEY = STORAGE_KEYS.sidebarCollapsed;
|
||||||
const SIDEBAR_DEFAULT = 270;
|
const SIDEBAR_DEFAULT = 270;
|
||||||
const SIDEBAR_MIN = 170;
|
const SIDEBAR_MIN = 170;
|
||||||
const SIDEBAR_COLLAPSED_WIDTH = 36;
|
// Hard cap on how wide the sidebar can be dragged, regardless of viewport.
|
||||||
|
// Below this, the conversation-reserve rule still wins (narrow windows).
|
||||||
|
const SIDEBAR_MAX = 480;
|
||||||
// Minimum width kept for the conversation pane. The sidebar is capped so the
|
// Minimum width kept for the conversation pane. The sidebar is capped so the
|
||||||
// conversation keeps at least this much room, which also guarantees the sidebar
|
// conversation keeps at least this much room, which also guarantees the sidebar
|
||||||
// resize handle and collapse button stay inside the viewport even when a width
|
// resize handle and collapse button stay inside the viewport even when a width
|
||||||
|
|
@ -28,19 +30,25 @@ export function useSidebarLayout(options: UseSidebarLayoutOptions = {}) {
|
||||||
const { viewportWidth } = useViewportWidth();
|
const { viewportWidth } = useViewportWidth();
|
||||||
const sessionColWidth = ref(SIDEBAR_DEFAULT);
|
const sessionColWidth = ref(SIDEBAR_DEFAULT);
|
||||||
const sidebarCollapsed = ref(false);
|
const sidebarCollapsed = ref(false);
|
||||||
|
// True while the sidebar ResizeHandle is being dragged — the sidebar disables
|
||||||
|
// its width transition so it follows the pointer 1:1 (mirrors panelDragging
|
||||||
|
// in useDetailPanel).
|
||||||
|
const sidebarDragging = ref(false);
|
||||||
|
|
||||||
// Largest sidebar width that still leaves the conversation pane usable. When
|
// Largest sidebar width that still leaves the conversation pane usable, then
|
||||||
// the right-side panel is open, also reserves its minimum width so the
|
// clamped to SIDEBAR_MAX so it can never be dragged absurdly wide on large
|
||||||
// conversation column can never be squeezed to nothing.
|
// displays. When the right-side panel is open, also reserves its minimum
|
||||||
|
// width so the conversation column can never be squeezed to nothing.
|
||||||
const sidebarMax = computed(() => {
|
const sidebarMax = computed(() => {
|
||||||
const reserve = CONVERSATION_MIN + (toValue(options.previewOpen) ? PREVIEW_MIN : 0);
|
const reserve = CONVERSATION_MIN + (toValue(options.previewOpen) ? PREVIEW_MIN : 0);
|
||||||
return panelMaxWidth(viewportWidth.value, SIDEBAR_MIN, reserve);
|
return Math.min(SIDEBAR_MAX, panelMaxWidth(viewportWidth.value, SIDEBAR_MIN, reserve));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Expanded width of the sidebar. Collapsing does NOT change this value: the
|
||||||
|
// sidebar keeps its content at this fixed width and animates its container
|
||||||
|
// width to 0 (clip, not reflow), mirroring the right-side preview panel.
|
||||||
const sideWidth = computed(() =>
|
const sideWidth = computed(() =>
|
||||||
sidebarCollapsed.value
|
clampPanelWidth(sessionColWidth.value, SIDEBAR_MIN, sidebarMax.value),
|
||||||
? SIDEBAR_COLLAPSED_WIDTH
|
|
||||||
: clampPanelWidth(sessionColWidth.value, SIDEBAR_MIN, sidebarMax.value),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function loadSidebarCollapsed(): void {
|
function loadSidebarCollapsed(): void {
|
||||||
|
|
@ -71,6 +79,7 @@ export function useSidebarLayout(options: UseSidebarLayoutOptions = {}) {
|
||||||
sidebarMax,
|
sidebarMax,
|
||||||
sessionColWidth,
|
sessionColWidth,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
|
sidebarDragging,
|
||||||
sideWidth,
|
sideWidth,
|
||||||
loadSidebarCollapsed,
|
loadSidebarCollapsed,
|
||||||
toggleSidebarCollapse,
|
toggleSidebarCollapse,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ export default {
|
||||||
sortRecent: 'Last edited',
|
sortRecent: 'Last edited',
|
||||||
collapseAll: 'Collapse all workspaces',
|
collapseAll: 'Collapse all workspaces',
|
||||||
expandAll: 'Expand all workspaces',
|
expandAll: 'Expand all workspaces',
|
||||||
showWorkspacePaths: 'Show workspace paths',
|
|
||||||
newSession: 'New Session',
|
newSession: 'New Session',
|
||||||
newChat: 'New Chat',
|
newChat: 'New Chat',
|
||||||
newWorkspace: 'New Workspace',
|
newWorkspace: 'New Workspace',
|
||||||
|
|
@ -38,7 +37,7 @@ export default {
|
||||||
collapseSidebar: 'Collapse sidebar',
|
collapseSidebar: 'Collapse sidebar',
|
||||||
expandSidebar: 'Expand sidebar',
|
expandSidebar: 'Expand sidebar',
|
||||||
searchPlaceholder: 'Search sessions',
|
searchPlaceholder: 'Search sessions',
|
||||||
searchShortcut: 'Search sessions ({shortcut})',
|
search: 'Search',
|
||||||
searchHint: '↑↓ navigate · ↵ open · Esc close',
|
searchHint: '↑↓ navigate · ↵ open · Esc close',
|
||||||
searchNoResults: 'No matching sessions',
|
searchNoResults: 'No matching sessions',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ export default {
|
||||||
sortRecent: '按最后编辑时间',
|
sortRecent: '按最后编辑时间',
|
||||||
collapseAll: '折叠全部工作区',
|
collapseAll: '折叠全部工作区',
|
||||||
expandAll: '展开全部工作区',
|
expandAll: '展开全部工作区',
|
||||||
showWorkspacePaths: '显示工作区路径',
|
|
||||||
newSession: '新建会话',
|
newSession: '新建会话',
|
||||||
newChat: '新建对话',
|
newChat: '新建对话',
|
||||||
newWorkspace: '新建工作区',
|
newWorkspace: '新建工作区',
|
||||||
|
|
@ -38,7 +37,7 @@ export default {
|
||||||
collapseSidebar: '收起侧边栏',
|
collapseSidebar: '收起侧边栏',
|
||||||
expandSidebar: '展开侧边栏',
|
expandSidebar: '展开侧边栏',
|
||||||
searchPlaceholder: '搜索会话',
|
searchPlaceholder: '搜索会话',
|
||||||
searchShortcut: '搜索会话 ({shortcut})',
|
search: '搜索',
|
||||||
searchHint: '↑↓ 选择 · ↵ 打开 · Esc 关闭',
|
searchHint: '↑↓ 选择 · ↵ 打开 · Esc 关闭',
|
||||||
searchNoResults: '没有匹配的会话',
|
searchNoResults: '没有匹配的会话',
|
||||||
};
|
};
|
||||||
|
|
|
||||||
4
apps/kimi-web/src/icons/kimi/add-conversation.svg
Normal file
4
apps/kimi-web/src/icons/kimi/add-conversation.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.1 12.9001V15.0909C11.1 15.593 11.5029 16 12 16C12.4971 16 12.9 15.593 12.9 15.0909V12.9001H15.0909C15.593 12.9001 16 12.4972 16 12.0001C16 11.5031 15.593 11.1001 15.0909 11.1001H12.9V8.90909C12.9 8.40701 12.4971 8 12 8C11.5029 8 11.1 8.40701 11.1 8.90909V11.1001H8.90909C8.40701 11.1001 8 11.5031 8 12.0001C8 12.4972 8.40701 12.9001 8.90909 12.9001H11.1Z" fill="currentColor"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.9996 2.1001C6.53199 2.1001 2.09961 6.53248 2.09961 12.0001C2.09961 13.9226 2.64847 15.7192 3.59804 17.2391L2.517 19.8207C2.10313 20.8091 2.82908 21.9001 3.90059 21.9001H11.9996C17.4672 21.9001 21.8996 17.4677 21.8996 12.0001C21.8996 6.53248 17.4672 2.1001 11.9996 2.1001ZM3.89961 12.0001C3.89961 7.52659 7.5261 3.9001 11.9996 3.9001C16.4731 3.9001 20.0996 7.52659 20.0996 12.0001C20.0996 16.4736 16.4724 20.1001 11.9989 20.1001H4.35146L5.63494 17.0351L5.35165 16.6291C4.43632 15.3172 3.89961 13.7227 3.89961 12.0001Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
10
apps/kimi-web/src/icons/kimi/folder-open.svg
Normal file
10
apps/kimi-web/src/icons/kimi/folder-open.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_4626_2033)">
|
||||||
|
<path d="M18.3623 9.99976L18.209 8.48999C18.2031 8.43161 18.2004 8.37289 18.2002 8.31421C18.1988 8.31196 18.1956 8.30842 18.1904 8.30347C18.1718 8.28559 18.1302 8.26245 18.0713 8.26245H11C10.261 8.26245 9.59753 7.81016 9.32617 7.1228L8.9082 6.06421C8.88101 5.9953 8.85737 5.92501 8.83887 5.85327C8.83778 5.85099 8.833 5.84268 8.81836 5.83179C8.79454 5.81475 8.7549 5.79939 8.70605 5.80054H3.92871C3.86986 5.80054 3.82825 5.82368 3.80957 5.84155C3.80816 5.8429 3.80675 5.84428 3.80566 5.84546L4.47559 14.0955L3.62109 17.5154L5.12109 11.5154C5.34367 10.6251 6.1438 9.99977 7.06152 9.99976H18.3623ZM7.06152 11.7996C6.96976 11.7996 6.88944 11.8629 6.86719 11.9519L5.36719 17.9519C5.33598 18.078 5.43158 18.1999 5.56152 18.2H19.4385C19.5302 18.1999 19.6106 18.1376 19.6328 18.0486L21.1328 12.0486C21.1644 11.9224 21.0686 11.7996 20.9385 11.7996H7.06152ZM20.9385 9.99976C22.2396 9.99977 23.1945 11.2228 22.8789 12.4851L21.3789 18.4851C21.1563 19.3754 20.3562 19.9997 19.4385 19.9998H4.92871C4.41722 19.9998 3.92613 19.8059 3.56445 19.4597C3.20281 19.1135 3.00004 18.6436 3 18.1541L2 5.84644C2.00006 5.35711 2.20311 4.88786 2.56445 4.54175C2.92613 4.19554 3.41722 4.00073 3.92871 4.00073H8.66406C9.10133 3.99051 9.5296 4.1225 9.87793 4.37573C10.2285 4.63118 10.4767 4.99457 10.582 5.40405L11 6.46167H18.0713C18.5828 6.46167 19.0739 6.65648 19.4355 7.00269C19.7971 7.34888 20 7.81883 20 8.30835L20.1719 9.99976H20.9385Z" fill="currentColor"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_4626_2033">
|
||||||
|
<rect width="24" height="24" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
3
apps/kimi-web/src/icons/kimi/folder.svg
Normal file
3
apps/kimi-web/src/icons/kimi/folder.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.2373 3.7002C10.4169 3.7002 11.5297 4.24779 12.249 5.18262L12.4424 5.43359H18C20.0987 5.43359 21.7998 7.13472 21.7998 9.2334V16.5C21.7998 18.5987 20.0987 20.2998 18 20.2998H6C3.90132 20.2998 2.2002 18.5987 2.2002 16.5V7.5C2.2002 5.40132 3.90132 3.7002 6 3.7002H9.2373ZM6 5.5C4.89543 5.5 4 6.39543 4 7.5V16.5C4 17.6046 4.89543 18.5 6 18.5H18C19.0357 18.5 19.887 17.7128 19.9893 16.7041L20 16.5V9.2334C20 8.19775 19.2128 7.34641 18.2041 7.24414L18 7.2334H12.0479L11.9326 7.22656C11.666 7.19561 11.4205 7.05812 11.2549 6.84277L10.8223 6.28027C10.4437 5.78834 9.85808 5.5 9.2373 5.5H6ZM16 9.59961C16.4971 9.59961 16.9004 10.0029 16.9004 10.5C16.9004 10.9971 16.4971 11.4004 16 11.4004H8C7.50294 11.4004 7.09961 10.9971 7.09961 10.5C7.09961 10.0029 7.50294 9.59961 8 9.59961H16Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 911 B |
5
apps/kimi-web/src/icons/kimi/more.svg
Normal file
5
apps/kimi-web/src/icons/kimi/more.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 12C6 12.8283 5.32834 13.5 4.5 13.5C3.67166 13.5 3 12.8283 3 12C3 11.1717 3.67166 10.5 4.5 10.5C5.32834 10.5 6 11.1717 6 12Z" fill="currentColor"/>
|
||||||
|
<path d="M13.5 12C13.5 12.8283 12.8283 13.5 12 13.5C11.1717 13.5 10.5 12.8283 10.5 12C10.5 11.1717 11.1717 10.5 12 10.5C12.8283 10.5 13.5 11.1717 13.5 12Z" fill="currentColor"/>
|
||||||
|
<path d="M19.5002 13.5C20.3287 13.5 21 12.8287 21 12.0002C21 11.1718 20.3287 10.5 19.5002 10.5C18.6718 10.5 18 11.1718 18 12.0002C18 12.8287 18.6718 13.5 19.5002 13.5Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 631 B |
3
apps/kimi-web/src/icons/kimi/search.svg
Normal file
3
apps/kimi-web/src/icons/kimi/search.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M11.5 3C16.1944 3 20 6.80558 20 11.5C20 13.523 19.2933 15.381 18.1132 16.8404L21.1364 19.8636C21.4879 20.2151 21.4879 20.7849 21.1364 21.1364C20.7849 21.4879 20.2151 21.4879 19.8636 21.1364L16.8404 18.1132C15.381 19.2933 13.523 20 11.5 20C6.80558 20 3 16.1944 3 11.5C3 6.80558 6.80558 3 11.5 3ZM11.5 18.2C15.2003 18.2 18.2 15.2003 18.2 11.5C18.2 7.79969 15.2003 4.8 11.5 4.8C7.79969 4.8 4.8 7.79969 4.8 11.5C4.8 15.2003 7.79969 18.2 11.5 18.2Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 579 B |
4
apps/kimi-web/src/icons/kimi/setting.svg
Normal file
4
apps/kimi-web/src/icons/kimi/setting.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.0404 12C16.0404 9.76874 14.2313 7.9596 12.0001 7.9596C9.76883 7.9596 7.95972 9.76874 7.95972 12C7.95972 14.2313 9.76883 16.0404 12.0001 16.0404C14.2313 16.0404 16.0404 14.2313 16.0404 12ZM14.2222 12C14.2222 13.2271 13.2271 14.2222 12 14.2222C10.7729 14.2222 9.77783 13.2271 9.77783 12C9.77783 10.7729 10.7729 9.77778 12 9.77778C13.2271 9.77778 14.2222 10.7729 14.2222 12Z" fill="currentColor"/>
|
||||||
|
<path d="M9.91145 21.8009C9.29001 21.6797 8.76914 21.2612 8.50632 20.6922L8.07372 19.7556C7.88838 19.3544 7.43553 19.1048 6.95371 19.1549L5.89572 19.2647C5.2733 19.3293 4.64823 19.114 4.22298 18.6611C3.74343 18.1504 3.32454 17.6037 2.97033 17.0181C2.61571 16.4318 2.32839 15.8106 2.10407 15.1566C1.89769 14.5549 2.02148 13.8954 2.4089 13.3902L3.0376 12.5704C3.30043 12.2277 3.30042 11.7722 3.03758 11.4295L2.40413 10.6035C2.01474 10.0958 1.891 9.43198 2.10208 8.82826C2.55037 7.54612 3.27017 6.35997 4.22 5.34259C4.64518 4.8872 5.27275 4.67067 5.89701 4.73544L6.95383 4.84514C7.43561 4.89515 7.88844 4.6456 8.07377 4.24441L8.50266 3.31593C8.76494 2.74818 9.28448 2.33019 9.90423 2.20761C11.2916 1.9332 12.7148 1.93127 14.0885 2.19913C14.7099 2.32029 15.2308 2.73881 15.4937 3.3078L15.9263 4.24441C16.1116 4.6456 16.5644 4.89514 17.0462 4.84514L18.1043 4.73532C18.7267 4.67072 19.3518 4.88603 19.777 5.33886C20.2566 5.84953 20.6755 6.3963 21.0297 6.98193C21.3843 7.56823 21.6716 8.18942 21.8959 8.84339C22.1023 9.44509 21.9785 10.1046 21.5911 10.6098L20.9624 11.4295C20.6996 11.7722 20.6996 12.2278 20.9624 12.5705L21.5959 13.3964C21.9853 13.9042 22.109 14.568 21.8979 15.1717C21.4497 16.4538 20.7299 17.6399 19.7801 18.6573C19.3549 19.1128 18.7273 19.3294 18.103 19.2646L17.0462 19.1549C16.5645 19.1049 16.1116 19.3544 15.9263 19.7556L15.4974 20.6841C15.2351 21.2518 14.7156 21.6698 14.0958 21.7924C12.7083 22.0668 11.2852 22.0687 9.91145 21.8009ZM13.7432 20.0088C13.7844 20.0006 13.8259 19.9673 13.847 19.9216L14.2758 18.9931C14.7915 17.8768 15.9886 17.2171 17.2341 17.3464L18.2909 17.4561C18.3649 17.4638 18.4272 17.4423 18.4512 17.4166C19.2296 16.5828 19.8171 15.6146 20.1817 14.5716C20.1845 14.5636 20.1796 14.5373 20.1532 14.5029L19.5198 13.677C18.7564 12.6815 18.7564 11.3185 19.5198 10.323L20.1485 9.5033C20.1746 9.46927 20.1795 9.4429 20.1762 9.43327C19.9932 8.89965 19.7603 8.39623 19.4741 7.92293C19.1873 7.4489 18.846 7.00333 18.4517 6.58351C18.4272 6.55739 18.3656 6.53616 18.2921 6.54378L17.234 6.65361C15.9886 6.78287 14.7915 6.12317 14.2758 5.00689L13.8432 4.07027C13.822 4.02448 13.7811 3.9916 13.7406 3.98371C12.5983 3.76097 11.4132 3.76258 10.2571 3.99124C10.2158 3.99941 10.1744 4.03271 10.1533 4.07842L9.72441 5.00689C9.20875 6.12317 8.01164 6.7829 6.76619 6.6536L5.70942 6.54391C5.63535 6.53623 5.573 6.55774 5.54905 6.5834C4.77067 7.41713 4.18312 8.38534 3.81845 9.42835C3.81564 9.43637 3.82054 9.46265 3.84693 9.49706L4.48038 10.323C5.24381 11.3185 5.24383 12.6815 4.48041 13.6769L3.85171 14.4967C3.82561 14.5307 3.82066 14.5571 3.82396 14.5667C4.00701 15.1004 4.23986 15.6038 4.52613 16.0771C4.81284 16.5511 5.15421 16.9967 5.54845 17.4165C5.57298 17.4426 5.63461 17.4638 5.70811 17.4562L6.76608 17.3464C8.01157 17.2171 9.20871 17.8768 9.72438 18.9932L10.157 19.9297C10.1781 19.9755 10.2191 20.0084 10.2595 20.0163C11.4018 20.239 12.587 20.2374 13.7432 20.0088Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
|
|
@ -1,13 +1,22 @@
|
||||||
// apps/kimi-web/src/lib/icons.ts
|
// apps/kimi-web/src/lib/icons.ts
|
||||||
// Single source of truth for apps/kimi-web icons (design-system §02).
|
// Single source of truth for apps/kimi-web icons (design-system §02).
|
||||||
//
|
//
|
||||||
// Icons come from Remix Icon (https://remixicon.com/, Apache-2.0) and are
|
// Icons come from three collections, all bundled by unplugin-icons at build
|
||||||
// bundled by unplugin-icons at build time — only the icons listed below end up
|
// time — only the icons listed below end up in the production bundle:
|
||||||
// in the production bundle. Each icon is imported twice: once as a Vue
|
// - `~icons/kimi/*` — Kimi Design System icons (24×24 outlined,
|
||||||
// component (for <Icon name=... />) and once as a `?raw` SVG string (for
|
// fill="currentColor"), local SVGs under src/icons/kimi/ registered as a
|
||||||
// iconSvg() in v-html contexts such as lib/toolMeta.ts).
|
// custom collection in vite.config.ts. Preferred when a Kimi icon exists
|
||||||
|
// for the intent.
|
||||||
|
// - `~icons/tabler/*` — Tabler Icons (https://tabler.io/icons, MIT),
|
||||||
|
// 24×24 stroke-based (stroke="currentColor"); used for the sidebar
|
||||||
|
// panel toggle, which neither pack above covers well.
|
||||||
|
// - `~icons/ri/*` — Remix Icon (https://remixicon.com/, Apache-2.0) for
|
||||||
|
// the remaining intents.
|
||||||
|
// Each icon is imported twice: once as a Vue component (for <Icon name=... />)
|
||||||
|
// and once as a `?raw` SVG string (for iconSvg() in v-html contexts such as
|
||||||
|
// lib/toolMeta.ts).
|
||||||
//
|
//
|
||||||
// Remix icons are fill-based (fill="currentColor") on a 24x24 source grid; the
|
// All collections share the 24x24 source grid and follow currentColor; the
|
||||||
// rendered size comes from the size token prop. Colour follows text.
|
// rendered size comes from the size token prop. Colour follows text.
|
||||||
//
|
//
|
||||||
// Two consumers share this registry:
|
// Two consumers share this registry:
|
||||||
|
|
@ -16,7 +25,19 @@
|
||||||
|
|
||||||
import type { Component } from 'vue';
|
import type { Component } from 'vue';
|
||||||
|
|
||||||
// Components (Vue) ---------------------------------------------------------
|
// Components (Kimi collection) ----------------------------------------------
|
||||||
|
import KimiAddConversation from '~icons/kimi/add-conversation';
|
||||||
|
import KimiFolder from '~icons/kimi/folder';
|
||||||
|
import KimiFolderOpen from '~icons/kimi/folder-open';
|
||||||
|
import KimiMore from '~icons/kimi/more';
|
||||||
|
import KimiSearch from '~icons/kimi/search';
|
||||||
|
import KimiSetting from '~icons/kimi/setting';
|
||||||
|
|
||||||
|
// Components (Tabler) ---------------------------------------------------------
|
||||||
|
import TablerSidebarLeftCollapse from '~icons/tabler/layout-sidebar-left-collapse';
|
||||||
|
import TablerSidebarLeftExpand from '~icons/tabler/layout-sidebar-left-expand';
|
||||||
|
|
||||||
|
// Components (Remix) ---------------------------------------------------------
|
||||||
import RiAddLine from '~icons/ri/add-line';
|
import RiAddLine from '~icons/ri/add-line';
|
||||||
import RiAlertLine from '~icons/ri/alert-line';
|
import RiAlertLine from '~icons/ri/alert-line';
|
||||||
import RiArrowDownLine from '~icons/ri/arrow-down-line';
|
import RiArrowDownLine from '~icons/ri/arrow-down-line';
|
||||||
|
|
@ -29,17 +50,14 @@ import RiBracesLine from '~icons/ri/braces-line';
|
||||||
import RiCalendarCloseLine from '~icons/ri/calendar-close-line';
|
import RiCalendarCloseLine from '~icons/ri/calendar-close-line';
|
||||||
import RiCalendarScheduleLine from '~icons/ri/calendar-schedule-line';
|
import RiCalendarScheduleLine from '~icons/ri/calendar-schedule-line';
|
||||||
import RiCalendarTodoLine from '~icons/ri/calendar-todo-line';
|
import RiCalendarTodoLine from '~icons/ri/calendar-todo-line';
|
||||||
import RiChatNewLine from '~icons/ri/chat-new-line';
|
|
||||||
import RiCheckLine from '~icons/ri/check-line';
|
import RiCheckLine from '~icons/ri/check-line';
|
||||||
import RiCloseLine from '~icons/ri/close-line';
|
import RiCloseLine from '~icons/ri/close-line';
|
||||||
import RiCodeLine from '~icons/ri/code-line';
|
import RiCodeLine from '~icons/ri/code-line';
|
||||||
import RiCollapseDiagonalLine from '~icons/ri/collapse-diagonal-line';
|
import RiCollapseDiagonalLine from '~icons/ri/collapse-diagonal-line';
|
||||||
import RiContractLeftLine from '~icons/ri/contract-left-line';
|
|
||||||
import RiDownloadLine from '~icons/ri/download-line';
|
import RiDownloadLine from '~icons/ri/download-line';
|
||||||
import RiDraggable from '~icons/ri/draggable';
|
import RiDraggable from '~icons/ri/draggable';
|
||||||
import RiEqualizerLine from '~icons/ri/equalizer-line';
|
import RiEqualizerLine from '~icons/ri/equalizer-line';
|
||||||
import RiExpandDiagonalLine from '~icons/ri/expand-diagonal-line';
|
import RiExpandDiagonalLine from '~icons/ri/expand-diagonal-line';
|
||||||
import RiExpandRightLine from '~icons/ri/expand-right-line';
|
|
||||||
import RiExternalLinkLine from '~icons/ri/external-link-line';
|
import RiExternalLinkLine from '~icons/ri/external-link-line';
|
||||||
import RiFileAddLine from '~icons/ri/file-add-line';
|
import RiFileAddLine from '~icons/ri/file-add-line';
|
||||||
import RiFileCopyLine from '~icons/ri/file-copy-line';
|
import RiFileCopyLine from '~icons/ri/file-copy-line';
|
||||||
|
|
@ -49,8 +67,6 @@ import RiFileTextLine from '~icons/ri/file-text-line';
|
||||||
import RiFlashlightLine from '~icons/ri/flashlight-line';
|
import RiFlashlightLine from '~icons/ri/flashlight-line';
|
||||||
import RiFolderAddLine from '~icons/ri/folder-add-line';
|
import RiFolderAddLine from '~icons/ri/folder-add-line';
|
||||||
import RiFolderFill from '~icons/ri/folder-fill';
|
import RiFolderFill from '~icons/ri/folder-fill';
|
||||||
import RiFolderLine from '~icons/ri/folder-line';
|
|
||||||
import RiFolderOpenLine from '~icons/ri/folder-open-line';
|
|
||||||
import RiGitPullRequestLine from '~icons/ri/git-pull-request-line';
|
import RiGitPullRequestLine from '~icons/ri/git-pull-request-line';
|
||||||
import RiGlobalLine from '~icons/ri/global-line';
|
import RiGlobalLine from '~icons/ri/global-line';
|
||||||
import RiImageLine from '~icons/ri/image-line';
|
import RiImageLine from '~icons/ri/image-line';
|
||||||
|
|
@ -61,13 +77,10 @@ import RiListUnordered from '~icons/ri/list-unordered';
|
||||||
import RiLoginBoxLine from '~icons/ri/login-box-line';
|
import RiLoginBoxLine from '~icons/ri/login-box-line';
|
||||||
import RiMailLine from '~icons/ri/mail-line';
|
import RiMailLine from '~icons/ri/mail-line';
|
||||||
import RiMessageLine from '~icons/ri/message-line';
|
import RiMessageLine from '~icons/ri/message-line';
|
||||||
import RiMoreLine from '~icons/ri/more-line';
|
|
||||||
import RiPauseFill from '~icons/ri/pause-fill';
|
import RiPauseFill from '~icons/ri/pause-fill';
|
||||||
import RiPencilLine from '~icons/ri/pencil-line';
|
import RiPencilLine from '~icons/ri/pencil-line';
|
||||||
import RiPlayFill from '~icons/ri/play-fill';
|
import RiPlayFill from '~icons/ri/play-fill';
|
||||||
import RiQuestionLine from '~icons/ri/question-line';
|
import RiQuestionLine from '~icons/ri/question-line';
|
||||||
import RiSearchLine from '~icons/ri/search-line';
|
|
||||||
import RiSettings3Line from '~icons/ri/settings-3-line';
|
|
||||||
import RiSortDesc from '~icons/ri/sort-desc';
|
import RiSortDesc from '~icons/ri/sort-desc';
|
||||||
import RiSparklingLine from '~icons/ri/sparkling-line';
|
import RiSparklingLine from '~icons/ri/sparkling-line';
|
||||||
import RiStarFill from '~icons/ri/star-fill';
|
import RiStarFill from '~icons/ri/star-fill';
|
||||||
|
|
@ -80,7 +93,19 @@ import RiTimeLine from '~icons/ri/time-line';
|
||||||
import RiToolsLine from '~icons/ri/tools-line';
|
import RiToolsLine from '~icons/ri/tools-line';
|
||||||
import RiUserLine from '~icons/ri/user-line';
|
import RiUserLine from '~icons/ri/user-line';
|
||||||
|
|
||||||
// Raw SVG strings ----------------------------------------------------------
|
// Raw SVG strings (Kimi collection) -----------------------------------------
|
||||||
|
import RawKimiAddConversation from '~icons/kimi/add-conversation?raw';
|
||||||
|
import RawKimiFolder from '~icons/kimi/folder?raw';
|
||||||
|
import RawKimiFolderOpen from '~icons/kimi/folder-open?raw';
|
||||||
|
import RawKimiMore from '~icons/kimi/more?raw';
|
||||||
|
import RawKimiSearch from '~icons/kimi/search?raw';
|
||||||
|
import RawKimiSetting from '~icons/kimi/setting?raw';
|
||||||
|
|
||||||
|
// Raw SVG strings (Tabler) ----------------------------------------------------
|
||||||
|
import RawTablerSidebarLeftCollapse from '~icons/tabler/layout-sidebar-left-collapse?raw';
|
||||||
|
import RawTablerSidebarLeftExpand from '~icons/tabler/layout-sidebar-left-expand?raw';
|
||||||
|
|
||||||
|
// Raw SVG strings (Remix) ----------------------------------------------------
|
||||||
import RawAddLine from '~icons/ri/add-line?raw';
|
import RawAddLine from '~icons/ri/add-line?raw';
|
||||||
import RawAlertLine from '~icons/ri/alert-line?raw';
|
import RawAlertLine from '~icons/ri/alert-line?raw';
|
||||||
import RawArrowDownLine from '~icons/ri/arrow-down-line?raw';
|
import RawArrowDownLine from '~icons/ri/arrow-down-line?raw';
|
||||||
|
|
@ -93,17 +118,14 @@ import RawBracesLine from '~icons/ri/braces-line?raw';
|
||||||
import RawCalendarCloseLine from '~icons/ri/calendar-close-line?raw';
|
import RawCalendarCloseLine from '~icons/ri/calendar-close-line?raw';
|
||||||
import RawCalendarScheduleLine from '~icons/ri/calendar-schedule-line?raw';
|
import RawCalendarScheduleLine from '~icons/ri/calendar-schedule-line?raw';
|
||||||
import RawCalendarTodoLine from '~icons/ri/calendar-todo-line?raw';
|
import RawCalendarTodoLine from '~icons/ri/calendar-todo-line?raw';
|
||||||
import RawChatNewLine from '~icons/ri/chat-new-line?raw';
|
|
||||||
import RawCheckLine from '~icons/ri/check-line?raw';
|
import RawCheckLine from '~icons/ri/check-line?raw';
|
||||||
import RawCloseLine from '~icons/ri/close-line?raw';
|
import RawCloseLine from '~icons/ri/close-line?raw';
|
||||||
import RawCodeLine from '~icons/ri/code-line?raw';
|
import RawCodeLine from '~icons/ri/code-line?raw';
|
||||||
import RawCollapseDiagonalLine from '~icons/ri/collapse-diagonal-line?raw';
|
import RawCollapseDiagonalLine from '~icons/ri/collapse-diagonal-line?raw';
|
||||||
import RawContractLeftLine from '~icons/ri/contract-left-line?raw';
|
|
||||||
import RawDownloadLine from '~icons/ri/download-line?raw';
|
import RawDownloadLine from '~icons/ri/download-line?raw';
|
||||||
import RawDraggable from '~icons/ri/draggable?raw';
|
import RawDraggable from '~icons/ri/draggable?raw';
|
||||||
import RawEqualizerLine from '~icons/ri/equalizer-line?raw';
|
import RawEqualizerLine from '~icons/ri/equalizer-line?raw';
|
||||||
import RawExpandDiagonalLine from '~icons/ri/expand-diagonal-line?raw';
|
import RawExpandDiagonalLine from '~icons/ri/expand-diagonal-line?raw';
|
||||||
import RawExpandRightLine from '~icons/ri/expand-right-line?raw';
|
|
||||||
import RawExternalLinkLine from '~icons/ri/external-link-line?raw';
|
import RawExternalLinkLine from '~icons/ri/external-link-line?raw';
|
||||||
import RawFileAddLine from '~icons/ri/file-add-line?raw';
|
import RawFileAddLine from '~icons/ri/file-add-line?raw';
|
||||||
import RawFileCopyLine from '~icons/ri/file-copy-line?raw';
|
import RawFileCopyLine from '~icons/ri/file-copy-line?raw';
|
||||||
|
|
@ -113,8 +135,6 @@ import RawFileTextLine from '~icons/ri/file-text-line?raw';
|
||||||
import RawFlashlightLine from '~icons/ri/flashlight-line?raw';
|
import RawFlashlightLine from '~icons/ri/flashlight-line?raw';
|
||||||
import RawFolderAddLine from '~icons/ri/folder-add-line?raw';
|
import RawFolderAddLine from '~icons/ri/folder-add-line?raw';
|
||||||
import RawFolderFill from '~icons/ri/folder-fill?raw';
|
import RawFolderFill from '~icons/ri/folder-fill?raw';
|
||||||
import RawFolderLine from '~icons/ri/folder-line?raw';
|
|
||||||
import RawFolderOpenLine from '~icons/ri/folder-open-line?raw';
|
|
||||||
import RawGitPullRequestLine from '~icons/ri/git-pull-request-line?raw';
|
import RawGitPullRequestLine from '~icons/ri/git-pull-request-line?raw';
|
||||||
import RawGlobalLine from '~icons/ri/global-line?raw';
|
import RawGlobalLine from '~icons/ri/global-line?raw';
|
||||||
import RawImageLine from '~icons/ri/image-line?raw';
|
import RawImageLine from '~icons/ri/image-line?raw';
|
||||||
|
|
@ -125,13 +145,10 @@ import RawListUnordered from '~icons/ri/list-unordered?raw';
|
||||||
import RawLoginBoxLine from '~icons/ri/login-box-line?raw';
|
import RawLoginBoxLine from '~icons/ri/login-box-line?raw';
|
||||||
import RawMailLine from '~icons/ri/mail-line?raw';
|
import RawMailLine from '~icons/ri/mail-line?raw';
|
||||||
import RawMessageLine from '~icons/ri/message-line?raw';
|
import RawMessageLine from '~icons/ri/message-line?raw';
|
||||||
import RawMoreLine from '~icons/ri/more-line?raw';
|
|
||||||
import RawPauseFill from '~icons/ri/pause-fill?raw';
|
import RawPauseFill from '~icons/ri/pause-fill?raw';
|
||||||
import RawPencilLine from '~icons/ri/pencil-line?raw';
|
import RawPencilLine from '~icons/ri/pencil-line?raw';
|
||||||
import RawPlayFill from '~icons/ri/play-fill?raw';
|
import RawPlayFill from '~icons/ri/play-fill?raw';
|
||||||
import RawQuestionLine from '~icons/ri/question-line?raw';
|
import RawQuestionLine from '~icons/ri/question-line?raw';
|
||||||
import RawSearchLine from '~icons/ri/search-line?raw';
|
|
||||||
import RawSettings3Line from '~icons/ri/settings-3-line?raw';
|
|
||||||
import RawSortDesc from '~icons/ri/sort-desc?raw';
|
import RawSortDesc from '~icons/ri/sort-desc?raw';
|
||||||
import RawSparklingLine from '~icons/ri/sparkling-line?raw';
|
import RawSparklingLine from '~icons/ri/sparkling-line?raw';
|
||||||
import RawStarFill from '~icons/ri/star-fill?raw';
|
import RawStarFill from '~icons/ri/star-fill?raw';
|
||||||
|
|
@ -229,13 +246,13 @@ function entry(component: Component, svg: string): IconEntry {
|
||||||
|
|
||||||
export const ICONS: Record<IconName, IconEntry> = {
|
export const ICONS: Record<IconName, IconEntry> = {
|
||||||
plus: entry(RiAddLine, RawAddLine),
|
plus: entry(RiAddLine, RawAddLine),
|
||||||
'chat-new': entry(RiChatNewLine, RawChatNewLine),
|
'chat-new': entry(KimiAddConversation, RawKimiAddConversation),
|
||||||
'calendar-close': entry(RiCalendarCloseLine, RawCalendarCloseLine),
|
'calendar-close': entry(RiCalendarCloseLine, RawCalendarCloseLine),
|
||||||
'calendar-schedule': entry(RiCalendarScheduleLine, RawCalendarScheduleLine),
|
'calendar-schedule': entry(RiCalendarScheduleLine, RawCalendarScheduleLine),
|
||||||
'calendar-todo': entry(RiCalendarTodoLine, RawCalendarTodoLine),
|
'calendar-todo': entry(RiCalendarTodoLine, RawCalendarTodoLine),
|
||||||
close: entry(RiCloseLine, RawCloseLine),
|
close: entry(RiCloseLine, RawCloseLine),
|
||||||
check: entry(RiCheckLine, RawCheckLine),
|
check: entry(RiCheckLine, RawCheckLine),
|
||||||
search: entry(RiSearchLine, RawSearchLine),
|
search: entry(KimiSearch, RawKimiSearch),
|
||||||
copy: entry(RiFileCopyLine, RawFileCopyLine),
|
copy: entry(RiFileCopyLine, RawFileCopyLine),
|
||||||
link: entry(RiLinksLine, RawLinksLine),
|
link: entry(RiLinksLine, RawLinksLine),
|
||||||
'external-link': entry(RiExternalLinkLine, RawExternalLinkLine),
|
'external-link': entry(RiExternalLinkLine, RawExternalLinkLine),
|
||||||
|
|
@ -243,7 +260,7 @@ export const ICONS: Record<IconName, IconEntry> = {
|
||||||
undo: entry(RiArrowGoBackLine, RawArrowGoBackLine),
|
undo: entry(RiArrowGoBackLine, RawArrowGoBackLine),
|
||||||
send: entry(RiArrowUpLine, RawArrowUpLine),
|
send: entry(RiArrowUpLine, RawArrowUpLine),
|
||||||
image: entry(RiImageLine, RawImageLine),
|
image: entry(RiImageLine, RawImageLine),
|
||||||
settings: entry(RiSettings3Line, RawSettings3Line),
|
settings: entry(KimiSetting, RawKimiSetting),
|
||||||
sliders: entry(RiEqualizerLine, RawEqualizerLine),
|
sliders: entry(RiEqualizerLine, RawEqualizerLine),
|
||||||
'log-in': entry(RiLoginBoxLine, RawLoginBoxLine),
|
'log-in': entry(RiLoginBoxLine, RawLoginBoxLine),
|
||||||
'chevron-down': entry(RiArrowDownSLine, RawArrowDownSLine),
|
'chevron-down': entry(RiArrowDownSLine, RawArrowDownSLine),
|
||||||
|
|
@ -252,15 +269,15 @@ export const ICONS: Record<IconName, IconEntry> = {
|
||||||
'arrow-down': entry(RiArrowDownLine, RawArrowDownLine),
|
'arrow-down': entry(RiArrowDownLine, RawArrowDownLine),
|
||||||
'arrow-right': entry(RiArrowRightLine, RawArrowRightLine),
|
'arrow-right': entry(RiArrowRightLine, RawArrowRightLine),
|
||||||
minus: entry(RiSubtractLine, RawSubtractLine),
|
minus: entry(RiSubtractLine, RawSubtractLine),
|
||||||
'panel-collapse': entry(RiContractLeftLine, RawContractLeftLine),
|
'panel-collapse': entry(TablerSidebarLeftCollapse, RawTablerSidebarLeftCollapse),
|
||||||
'panel-expand': entry(RiExpandRightLine, RawExpandRightLine),
|
'panel-expand': entry(TablerSidebarLeftExpand, RawTablerSidebarLeftExpand),
|
||||||
expand: entry(RiExpandDiagonalLine, RawExpandDiagonalLine),
|
expand: entry(RiExpandDiagonalLine, RawExpandDiagonalLine),
|
||||||
collapse: entry(RiCollapseDiagonalLine, RawCollapseDiagonalLine),
|
collapse: entry(RiCollapseDiagonalLine, RawCollapseDiagonalLine),
|
||||||
list: entry(RiListUnordered, RawListUnordered),
|
list: entry(RiListUnordered, RawListUnordered),
|
||||||
sort: entry(RiSortDesc, RawSortDesc),
|
sort: entry(RiSortDesc, RawSortDesc),
|
||||||
grip: entry(RiDraggable, RawDraggable),
|
grip: entry(RiDraggable, RawDraggable),
|
||||||
folder: entry(RiFolderOpenLine, RawFolderOpenLine),
|
folder: entry(KimiFolderOpen, RawKimiFolderOpen),
|
||||||
'folder-closed': entry(RiFolderLine, RawFolderLine),
|
'folder-closed': entry(KimiFolder, RawKimiFolder),
|
||||||
'folder-plus': entry(RiFolderAddLine, RawFolderAddLine),
|
'folder-plus': entry(RiFolderAddLine, RawFolderAddLine),
|
||||||
'folder-solid': entry(RiFolderFill, RawFolderFill),
|
'folder-solid': entry(RiFolderFill, RawFolderFill),
|
||||||
file: entry(RiFileLine, RawFileLine),
|
file: entry(RiFileLine, RawFileLine),
|
||||||
|
|
@ -292,7 +309,7 @@ export const ICONS: Record<IconName, IconEntry> = {
|
||||||
stop: entry(RiStopFill, RawStopFill),
|
stop: entry(RiStopFill, RawStopFill),
|
||||||
star: entry(RiStarFill, RawStarFill),
|
star: entry(RiStarFill, RawStarFill),
|
||||||
'star-outline': entry(RiStarLine, RawStarLine),
|
'star-outline': entry(RiStarLine, RawStarLine),
|
||||||
'dots-horizontal': entry(RiMoreLine, RawMoreLine),
|
'dots-horizontal': entry(KimiMore, RawKimiMore),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getIcon(name: IconName): IconEntry {
|
export function getIcon(name: IconName): IconEntry {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ export const STORAGE_KEYS = {
|
||||||
colorScheme: 'kimi-web.color-scheme',
|
colorScheme: 'kimi-web.color-scheme',
|
||||||
hiddenWorkspaces: 'kimi-web.hidden-workspaces',
|
hiddenWorkspaces: 'kimi-web.hidden-workspaces',
|
||||||
collapsedWorkspaces: 'kimi-web.collapsed-workspaces',
|
collapsedWorkspaces: 'kimi-web.collapsed-workspaces',
|
||||||
showWorkspacePaths: 'kimi-web.show-workspace-paths',
|
|
||||||
workspaceOrder: 'kimi-web.workspace-order',
|
workspaceOrder: 'kimi-web.workspace-order',
|
||||||
workspaceNameOverrides: 'kimi-web.workspace-name-overrides',
|
workspaceNameOverrides: 'kimi-web.workspace-name-overrides',
|
||||||
workspaceSort: 'kimi-web.workspace-sort',
|
workspaceSort: 'kimi-web.workspace-sort',
|
||||||
|
|
@ -149,20 +148,6 @@ export function saveCollapsedWorkspaces(ids: Iterable<string>): void {
|
||||||
safeSetJson(STORAGE_KEYS.collapsedWorkspaces, Array.from(ids));
|
safeSetJson(STORAGE_KEYS.collapsedWorkspaces, Array.from(ids));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the sidebar shows each workspace's root path under its name. Off by
|
|
||||||
* default to keep the list compact; toggled from the Workspaces section header.
|
|
||||||
* Stored as a JSON boolean; any missing/unset value reads as false. UI-only
|
|
||||||
* state with no server-side source of truth.
|
|
||||||
*/
|
|
||||||
export function loadShowWorkspacePaths(): boolean {
|
|
||||||
return safeGetJson<boolean>(STORAGE_KEYS.showWorkspacePaths) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveShowWorkspacePaths(show: boolean): void {
|
|
||||||
safeSetJson(STORAGE_KEYS.showWorkspacePaths, show);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display order of workspace ids in the sidebar. Persisted as a JSON array so
|
* Display order of workspace ids in the sidebar. Persisted as a JSON array so
|
||||||
* the user can drag workspaces into a custom order that survives a page
|
* the user can drag workspaces into a custom order that survives a page
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,6 @@ summary {
|
||||||
--ui-font-size-lg: calc(var(--ui-font-size) + 1px);
|
--ui-font-size-lg: calc(var(--ui-font-size) + 1px);
|
||||||
--ui-font-size-xl: calc(var(--ui-font-size) + 2px);
|
--ui-font-size-xl: calc(var(--ui-font-size) + 2px);
|
||||||
--content-font-size: calc(var(--base-ui-font-size) + 1px);
|
--content-font-size: calc(var(--base-ui-font-size) + 1px);
|
||||||
--sidebar-ui-font-size: calc(var(--base-ui-font-size) + 1px);
|
|
||||||
--mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
--mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
|
||||||
/* Body/UI font follows the design-system canonical token. Mirrors --font-ui
|
/* Body/UI font follows the design-system canonical token. Mirrors --font-ui
|
||||||
so the legacy alias can never drift from the current text face. */
|
so the legacy alias can never drift from the current text face. */
|
||||||
|
|
@ -387,6 +386,15 @@ html[data-color-scheme="dark"][data-accent="mono"] {
|
||||||
--color-text-on-accent: #ffffff;
|
--color-text-on-accent: #ffffff;
|
||||||
--color-line: #e7eaee;
|
--color-line: #e7eaee;
|
||||||
--color-line-strong: #d4d9e0;
|
--color-line-strong: #d4d9e0;
|
||||||
|
/* Neutral selected fill (sidebar rows, list pickers) — deliberately NOT
|
||||||
|
accent-tinted, so selection reads as "where I am", not as an action. */
|
||||||
|
--color-selected: #00000014;
|
||||||
|
/* Row hover wash — lighter than the selected fill (hover < selected); both
|
||||||
|
are translucent black so they sit naturally on any light surface. */
|
||||||
|
--color-hover: #0000000d;
|
||||||
|
/* Sidebar surface — one step off --color-bg (warm off-white / near-black)
|
||||||
|
so the session column reads as its own plane. */
|
||||||
|
--color-sidebar-bg: #fbfaf9;
|
||||||
--color-accent: var(--accent-primary);
|
--color-accent: var(--accent-primary);
|
||||||
--color-accent-hover: #0f6fe0;
|
--color-accent-hover: #0f6fe0;
|
||||||
--color-accent-soft: #e8f3ff;
|
--color-accent-soft: #e8f3ff;
|
||||||
|
|
@ -511,6 +519,9 @@ html[data-color-scheme="dark"] {
|
||||||
--color-text-faint: #6b7280;
|
--color-text-faint: #6b7280;
|
||||||
--color-line: #2d333b;
|
--color-line: #2d333b;
|
||||||
--color-line-strong: #3d444d;
|
--color-line-strong: #3d444d;
|
||||||
|
--color-selected: #ffffff14;
|
||||||
|
--color-hover: #ffffff0d;
|
||||||
|
--color-sidebar-bg: #181817;
|
||||||
--color-accent: #58a6ff;
|
--color-accent: #58a6ff;
|
||||||
--color-accent-hover: #79b8ff;
|
--color-accent-hover: #79b8ff;
|
||||||
--color-accent-soft: rgba(88, 166, 255, 0.14);
|
--color-accent-soft: rgba(88, 166, 255, 0.14);
|
||||||
|
|
@ -550,6 +561,9 @@ html[data-color-scheme="dark"] {
|
||||||
--color-text-faint: #6b7280;
|
--color-text-faint: #6b7280;
|
||||||
--color-line: #2d333b;
|
--color-line: #2d333b;
|
||||||
--color-line-strong: #3d444d;
|
--color-line-strong: #3d444d;
|
||||||
|
--color-selected: #ffffff14;
|
||||||
|
--color-hover: #ffffff0d;
|
||||||
|
--color-sidebar-bg: #181817;
|
||||||
--color-accent: #58a6ff;
|
--color-accent: #58a6ff;
|
||||||
--color-accent-hover: #79b8ff;
|
--color-accent-hover: #79b8ff;
|
||||||
--color-accent-soft: rgba(88, 166, 255, 0.14);
|
--color-accent-soft: rgba(88, 166, 255, 0.14);
|
||||||
|
|
@ -614,9 +628,16 @@ body {
|
||||||
A single mid-gray works on either background, so no per-theme tokens needed.
|
A single mid-gray works on either background, so no per-theme tokens needed.
|
||||||
Components may still override locally (e.g. the sidebar's even-thinner rule).
|
Components may still override locally (e.g. the sidebar's even-thinner rule).
|
||||||
--------------------------------------------------------------------------- */
|
--------------------------------------------------------------------------- */
|
||||||
* {
|
/* Firefox-only fallback: the standard scrollbar properties DISABLE the whole
|
||||||
scrollbar-width: thin;
|
::-webkit-scrollbar customisation in Chromium 121+ (any non-auto value wins
|
||||||
scrollbar-color: rgba(128, 128, 128, 0.3) transparent;
|
over the pseudo-element styles), silently replacing the 6px/4px custom bars
|
||||||
|
with the ~11px native thin gutter. Scope them to engines that don't know
|
||||||
|
the webkit pseudo-element instead. */
|
||||||
|
@supports not selector(::-webkit-scrollbar) {
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(128, 128, 128, 0.3) transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ onUnmounted(() => {
|
||||||
<div class="color-card"><div class="color-chip" style="background:#ffffff"></div><div class="color-meta"><div class="cn">bg</div><div class="cv">#ffffff / #0d1117</div></div></div>
|
<div class="color-card"><div class="color-chip" style="background:#ffffff"></div><div class="color-meta"><div class="cn">bg</div><div class="cv">#ffffff / #0d1117</div></div></div>
|
||||||
<div class="color-card"><div class="color-chip" style="background:#fafbfc"></div><div class="color-meta"><div class="cn">surface</div><div class="cv">#fafbfc / #161b22</div></div></div>
|
<div class="color-card"><div class="color-chip" style="background:#fafbfc"></div><div class="color-meta"><div class="cn">surface</div><div class="cv">#fafbfc / #161b22</div></div></div>
|
||||||
<div class="color-card"><div class="color-chip" style="background:#f3f5f8"></div><div class="color-meta"><div class="cn">surface-sunken</div><div class="cv">#f3f5f8 / #0d1117</div></div></div>
|
<div class="color-card"><div class="color-chip" style="background:#f3f5f8"></div><div class="color-meta"><div class="cn">surface-sunken</div><div class="cv">#f3f5f8 / #0d1117</div></div></div>
|
||||||
|
<div class="color-card"><div class="color-chip" style="background:#eceff3"></div><div class="color-meta"><div class="cn">selected</div><div class="cv">#eceff3 / #2d333b</div></div></div>
|
||||||
<div class="color-card"><div class="color-chip" style="background:#14171c"></div><div class="color-meta"><div class="cn">fg</div><div class="cv">#14171c / #e8eaed</div></div></div>
|
<div class="color-card"><div class="color-chip" style="background:#14171c"></div><div class="color-meta"><div class="cn">fg</div><div class="cv">#14171c / #e8eaed</div></div></div>
|
||||||
<div class="color-card"><div class="color-chip" style="background:#6b7280"></div><div class="color-meta"><div class="cn">fg-muted</div><div class="cv">#6b7280 / #9aa0a8</div></div></div>
|
<div class="color-card"><div class="color-chip" style="background:#6b7280"></div><div class="color-meta"><div class="cn">fg-muted</div><div class="cv">#6b7280 / #9aa0a8</div></div></div>
|
||||||
<div class="color-card"><div class="color-chip" style="background:#e7eaee"></div><div class="color-meta"><div class="cn">line</div><div class="cv">#e7eaee / #2d333b</div></div></div>
|
<div class="color-card"><div class="color-chip" style="background:#e7eaee"></div><div class="color-meta"><div class="cn">line</div><div class="cv">#e7eaee / #2d333b</div></div></div>
|
||||||
|
|
@ -191,6 +192,9 @@ onUnmounted(() => {
|
||||||
<tr><td class="tk">--color-text</td><td class="val"><span class="swatch" style="background:#14171c"></span>#14171c</td><td class="val"><span class="swatch" style="background:#e8eaed"></span>#e8eaed</td><td>Body text / headings</td></tr>
|
<tr><td class="tk">--color-text</td><td class="val"><span class="swatch" style="background:#14171c"></span>#14171c</td><td class="val"><span class="swatch" style="background:#e8eaed"></span>#e8eaed</td><td>Body text / headings</td></tr>
|
||||||
<tr><td class="tk">--color-text-muted</td><td class="val"><span class="swatch" style="background:#6b7280"></span>#6b7280</td><td class="val"><span class="swatch" style="background:#9aa0a8"></span>#9aa0a8</td><td>Secondary text / placeholder</td></tr>
|
<tr><td class="tk">--color-text-muted</td><td class="val"><span class="swatch" style="background:#6b7280"></span>#6b7280</td><td class="val"><span class="swatch" style="background:#9aa0a8"></span>#9aa0a8</td><td>Secondary text / placeholder</td></tr>
|
||||||
<tr><td class="tk">--color-line</td><td class="val"><span class="swatch" style="background:#e7eaee"></span>#e7eaee</td><td class="val"><span class="swatch" style="background:#2d333b"></span>#2d333b</td><td>Divider / card border</td></tr>
|
<tr><td class="tk">--color-line</td><td class="val"><span class="swatch" style="background:#e7eaee"></span>#e7eaee</td><td class="val"><span class="swatch" style="background:#2d333b"></span>#2d333b</td><td>Divider / card border</td></tr>
|
||||||
|
<tr><td class="tk">--color-selected</td><td class="val"><span class="swatch" style="background:#00000014"></span>#00000014</td><td class="val"><span class="swatch" style="background:#ffffff14"></span>#ffffff14</td><td>Neutral selected fill (sidebar rows, list pickers) — translucent, never accent-tinted</td></tr>
|
||||||
|
<tr><td class="tk">--color-hover</td><td class="val"><span class="swatch" style="background:#0000000d"></span>#0000000d</td><td class="val"><span class="swatch" style="background:#ffffff0d"></span>#ffffff0d</td><td>Row hover wash — lighter than the selected fill (hover < selected); translucent, sits on any surface</td></tr>
|
||||||
|
<tr><td class="tk">--color-sidebar-bg</td><td class="val"><span class="swatch" style="background:#fbfaf9"></span>#fbfaf9</td><td class="val"><span class="swatch" style="background:#181817"></span>#181817</td><td>Sidebar surface — one step off <code>--color-bg</code> so the session column reads as its own plane</td></tr>
|
||||||
<tr><td class="tk">--color-accent</td><td class="val"><span class="swatch" style="background:#1783ff"></span>#1783ff</td><td class="val"><span class="swatch" style="background:#58a6ff"></span>#58a6ff</td><td>Primary action / link / focus</td></tr>
|
<tr><td class="tk">--color-accent</td><td class="val"><span class="swatch" style="background:#1783ff"></span>#1783ff</td><td class="val"><span class="swatch" style="background:#58a6ff"></span>#58a6ff</td><td>Primary action / link / focus</td></tr>
|
||||||
<tr><td class="tk">--color-success</td><td class="val"><span class="swatch" style="background:#0e7a38"></span>#0e7a38</td><td class="val"><span class="swatch" style="background:#3fb950"></span>#3fb950</td><td>Success / pass</td></tr>
|
<tr><td class="tk">--color-success</td><td class="val"><span class="swatch" style="background:#0e7a38"></span>#0e7a38</td><td class="val"><span class="swatch" style="background:#3fb950"></span>#3fb950</td><td>Success / pass</td></tr>
|
||||||
<tr><td class="tk">--color-warning</td><td class="val"><span class="swatch" style="background:#a9610a"></span>#a9610a</td><td class="val"><span class="swatch" style="background:#d29922"></span>#d29922</td><td>Warning / pending</td></tr>
|
<tr><td class="tk">--color-warning</td><td class="val"><span class="swatch" style="background:#a9610a"></span>#a9610a</td><td class="val"><span class="swatch" style="background:#d29922"></span>#d29922</td><td>Warning / pending</td></tr>
|
||||||
|
|
@ -269,7 +273,7 @@ onUnmounted(() => {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 class="sub">Type scale & weight</h3>
|
<h3 class="sub">Type scale & weight</h3>
|
||||||
<p>The user font-size preference writes <code>--base-ui-font-size</code>. Compact UI chrome follows it through <code>--ui-font-size</code>, while chat reading surfaces and the sidebar derive one readable step above it through <code>--content-font-size</code> and <code>--sidebar-ui-font-size</code>.</p>
|
<p>The user font-size preference writes <code>--base-ui-font-size</code>. Compact UI chrome and the sidebar follow it through <code>--ui-font-size</code>, while chat reading surfaces derive one readable step above it through <code>--content-font-size</code>.</p>
|
||||||
<p>The fixed product type tokens still define component defaults: <b>UI controls / buttons / forms</b> use <code>--text-base</code> (14px); <b>reading body — including chat Markdown, message bubbles, etc.</b> stays one step larger than compact chrome for readability; the <b>sidebar session list</b> follows that same readable step while keeping list density.
|
<p>The fixed product type tokens still define component defaults: <b>UI controls / buttons / forms</b> use <code>--text-base</code> (14px); <b>reading body — including chat Markdown, message bubbles, etc.</b> stays one step larger than compact chrome for readability; the <b>sidebar session list</b> follows that same readable step while keeping list density.
|
||||||
Drop stray <code>font-weight: 650 / 750</code>; converge on two weights, 400 / 500 (regular / emphasis).</p>
|
Drop stray <code>font-weight: 650 / 750</code>; converge on two weights, 400 / 500 (regular / emphasis).</p>
|
||||||
<div class="panel panel-pad" style="margin:16px 0">
|
<div class="panel panel-pad" style="margin:16px 0">
|
||||||
|
|
@ -287,7 +291,6 @@ onUnmounted(() => {
|
||||||
<tr><td class="tk">--font-mono</td><td class="val">JetBrains Mono…</td><td>code, tool names, line numbers, diffs</td></tr>
|
<tr><td class="tk">--font-mono</td><td class="val">JetBrains Mono…</td><td>code, tool names, line numbers, diffs</td></tr>
|
||||||
<tr><td class="tk">--base-ui-font-size</td><td class="val">14px user preference</td><td>root setting that drives UI, reading body, and sidebar font sizes</td></tr>
|
<tr><td class="tk">--base-ui-font-size</td><td class="val">14px user preference</td><td>root setting that drives UI, reading body, and sidebar font sizes</td></tr>
|
||||||
<tr><td class="tk">--content-font-size</td><td class="val">calc(base + 1px)</td><td>chat Markdown, message bubbles, composer</td></tr>
|
<tr><td class="tk">--content-font-size</td><td class="val">calc(base + 1px)</td><td>chat Markdown, message bubbles, composer</td></tr>
|
||||||
<tr><td class="tk">--sidebar-ui-font-size</td><td class="val">calc(base + 1px)</td><td>sidebar brand, search, workspace and session rows</td></tr>
|
|
||||||
<tr><td class="tk">--leading-tight/normal/relaxed</td><td class="val">1.25 / 1.5 / 1.7</td><td>headings / UI / long text</td></tr>
|
<tr><td class="tk">--leading-tight/normal/relaxed</td><td class="val">1.25 / 1.5 / 1.7</td><td>headings / UI / long text</td></tr>
|
||||||
<tr><td class="tk">--weight-regular/medium</td><td class="val">400 / 500</td><td>body / emphasis</td></tr>
|
<tr><td class="tk">--weight-regular/medium</td><td class="val">400 / 500</td><td>body / emphasis</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
@ -567,6 +570,18 @@ onUnmounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== Kbd ===== -->
|
||||||
|
<h3 class="sub">Kbd · keyboard shortcut</h3>
|
||||||
|
<p><b>Kbd</b> renders a shortcut as keycaps — one block per key, never inline text like <code>(⌘K)</code>. Caps are 18px tall (Badge sm rhythm): sunken surface, 1px border with a 2px bottom edge, 11px UI font, muted text. Typical placement: pushed to the row's trailing edge, opposite the label (e.g. the sidebar search row).</p>
|
||||||
|
<div class="stage-wrap">
|
||||||
|
<div class="stage-bar"><span class="st">Kbd · keycaps</span></div>
|
||||||
|
<div class="stage p">
|
||||||
|
<span class="p-kbd"><kbd>⌘</kbd><kbd>K</kbd></span>
|
||||||
|
<span class="p-kbd"><kbd>Ctrl</kbd><kbd>K</kbd></span>
|
||||||
|
<span class="p-kbd"><kbd>⌘</kbd><kbd>⇧</kbd><kbd>P</kbd></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ===== Card / Surface ===== -->
|
<!-- ===== Card / Surface ===== -->
|
||||||
<h3 class="sub">Card / Surface</h3>
|
<h3 class="sub">Card / Surface</h3>
|
||||||
<p>All cards across the site share <b>one shell</b>: flat, <code>1px</code> border, <code>--radius-md</code> radius, <b>no shadow</b>. The structure is split into three parts — <code>head / body / foot</code>. Cards differ <b>only in the head</b> — in two tiers by visual weight, while the shell stays consistent:</p>
|
<p>All cards across the site share <b>one shell</b>: flat, <code>1px</code> border, <code>--radius-md</code> radius, <b>no shadow</b>. The structure is split into three parts — <code>head / body / foot</code>. Cards differ <b>only in the head</b> — in two tiers by visual weight, while the shell stays consistent:</p>
|
||||||
|
|
@ -1338,13 +1353,13 @@ onUnmounted(() => {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h3 class="sub">Layout grid</h3>
|
<h3 class="sub">Layout grid</h3>
|
||||||
<p>On desktop it is a single-row 5-track grid: the sidebar and the right panel each occupy a permanent track, with the conversation column in the middle; two 0-width tracks are for the ResizeHandles.</p>
|
<p>On desktop it is a single-row 5-track grid: the sidebar and the right panel each occupy a permanent <code>auto</code> track, with the conversation column in the middle; two 0-width tracks are for the ResizeHandles.</p>
|
||||||
<div class="code"><div class="code-bar"><span class="d"></span><span class="d"></span><span class="d"></span><span class="fn">App.vue · .app</span></div><pre>grid-template-columns: var(--side-w) 0 minmax(0, 1fr) 0 auto;
|
<div class="code"><div class="code-bar"><span class="d"></span><span class="d"></span><span class="d"></span><span class="fn">App.vue · .app</span></div><pre>grid-template-columns: auto 0 minmax(0, 1fr) 0 auto;
|
||||||
/* sidebar ↑ ↑handle ↑conversation ↑handle ↑right panel (auto) */</pre></div>
|
/* sidebar ↑ ↑handle ↑conversation ↑handle ↑right panel (auto) */</pre></div>
|
||||||
<table class="dt">
|
<table class="dt">
|
||||||
<thead><tr><th>Token</th><th>Value</th><th>Usage</th></tr></thead>
|
<thead><tr><th>Token</th><th>Value</th><th>Usage</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="tk">--side-w</td><td class="val">248px (adjustable)</td><td>left conversation column width, changed by dragging the ResizeHandle; should approach §02's <code>--p-sidebar-w</code> (264px)</td></tr>
|
<tr><td class="tk">sidebar width</td><td class="val">270px default (adjustable)</td><td>expanded sidebar width, changed by dragging the ResizeHandle; should approach §02's <code>--p-sidebar-w</code> (264px)</td></tr>
|
||||||
<tr><td class="tk">--preview-w</td><td class="val">460px</td><td>width of the right preview panel when open</td></tr>
|
<tr><td class="tk">--preview-w</td><td class="val">460px</td><td>width of the right preview panel when open</td></tr>
|
||||||
<tr><td class="tk">--panel-head-h</td><td class="val">48px</td><td>unified height for all right panel heads + the conversation column head, so the hairline runs as one line</td></tr>
|
<tr><td class="tk">--panel-head-h</td><td class="val">48px</td><td>unified height for all right panel heads + the conversation column head, so the hairline runs as one line</td></tr>
|
||||||
<tr><td class="tk">--p-bp-sm</td><td class="val">640px</td><td>≤640 switches to a mobile single column (top bar + conversation), no sidebar / handle / right panel</td></tr>
|
<tr><td class="tk">--p-bp-sm</td><td class="val">640px</td><td>≤640 switches to a mobile single column (top bar + conversation), no sidebar / handle / right panel</td></tr>
|
||||||
|
|
@ -1352,17 +1367,18 @@ onUnmounted(() => {
|
||||||
</table>
|
</table>
|
||||||
<ul class="clean">
|
<ul class="clean">
|
||||||
<li>The right panel track exists permanently, with its width transitioning between <code>0 ↔ var(--preview-w)</code> (when open it squeezes the conversation column, rather than switching templates).</li>
|
<li>The right panel track exists permanently, with its width transitioning between <code>0 ↔ var(--preview-w)</code> (when open it squeezes the conversation column, rather than switching templates).</li>
|
||||||
<li>When the sidebar is collapsed, track 1 becomes a thin "rail" holding only an expand IconButton, avoiding crushing the conversation column head.</li>
|
<li>The sidebar collapses SYMMETRICALLY to the right panel: its container width animates to 0 while the content keeps its fixed width anchored to the right edge (clipped, sliding out left — no reflow, hairline stays on the clipped content). No rail remains. The collapse control differs by platform: on <b>macOS desktop</b> the toggle is a single resident floating IconButton pinned beside the traffic lights (rendered in both states, only the glyph swaps — the sidebar slides underneath it, never moves or flashes); on <b>Windows / web</b> the collapse button lives inside the sidebar header (right-aligned), and a floating expand button appears at the top-left only while collapsed. The conversation header pads left in step with the transition while collapsed.</li>
|
||||||
<li>All grid children must have <code>min-height:0; min-width:0</code>, so only the inner scroll containers scroll and the page itself does not scroll.</li>
|
<li>All grid children must have <code>min-height:0; min-width:0</code>, so only the inner scroll containers scroll and the page itself does not scroll.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 class="sub">Sidebar alignment system (<code>--sb-*</code>)</h3>
|
<h3 class="sub">Sidebar alignment system (<code>--sb-*</code>)</h3>
|
||||||
<p>All sidebar rows (group head, session row, New chat button) share 3 custom properties, so the "session title" aligns precisely under the "workspace name".</p>
|
<p>All sidebar rows (group head, session row, New chat button) share 4 custom properties, so the "session title" aligns precisely under the "workspace name".</p>
|
||||||
<table class="dt">
|
<table class="dt">
|
||||||
<thead><tr><th>Token</th><th>Value</th><th>Usage</th></tr></thead>
|
<thead><tr><th>Token</th><th>Value</th><th>Usage</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="tk">--sb-pad-x</td><td class="val">16px</td><td>row horizontal padding</td></tr>
|
<tr><td class="tk">--sb-inset</td><td class="val">12px</td><td>row box (hover/selected pill) inset from the sidebar edges — matches the brand header's 12px padding</td></tr>
|
||||||
<tr><td class="tk">--sb-gutter</td><td class="val">20px</td><td>leading icon slot width (14px icon + 6px whitespace)</td></tr>
|
<tr><td class="tk">--sb-pad-x</td><td class="val">20px</td><td>content start x (= --sb-inset + 8px row padding)</td></tr>
|
||||||
|
<tr><td class="tk">--sb-gutter</td><td class="val">16px</td><td>leading icon slot width — matches the workspace folder icon so the session title aligns under the workspace name</td></tr>
|
||||||
<tr><td class="tk">--sb-gap</td><td class="val">6px</td><td>gap between the icon slot and the text</td></tr>
|
<tr><td class="tk">--sb-gap</td><td class="val">6px</td><td>gap between the icon slot and the text</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
@ -1371,15 +1387,16 @@ onUnmounted(() => {
|
||||||
</div></div>
|
</div></div>
|
||||||
|
|
||||||
<h3 class="sub">Sidebar structure</h3>
|
<h3 class="sub">Sidebar structure</h3>
|
||||||
<p>The sidebar from top to bottom: brand header → search → New chat → grouped list (workspace head + session rows). Controls reuse the §03 primitives as much as possible.</p>
|
<p>The sidebar from top to bottom: brand header → New chat → search → grouped list (workspace head + session rows) → settings footer. Controls reuse the §03 primitives as much as possible. The sidebar sits on <code>--color-sidebar-bg</code> (one step off <code>--color-bg</code>: warm off-white in light, near-black in dark — the session column reads as its own plane; the hairline still separates it from the conversation pane). Vertical rhythm: the brand header keeps 12px padding (on macOS desktop the left padding grows to 80px to clear the traffic lights); rows inside the actions group (New chat + search) stack flush (0 gap, same rhythm as the list rows); adjacent groups are separated by 12px. Row hover uses <code>--sb-hover</code> (= the global <code>--color-hover</code> wash); the selected row uses <code>--color-selected</code> — neutral, never the accent.</p>
|
||||||
<table class="dt">
|
<table class="dt">
|
||||||
<thead><tr><th>Block</th><th>Use</th><th>Note</th></tr></thead>
|
<thead><tr><th>Block</th><th>Use</th><th>Note</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Brand header</td><td>logo + name + IconButton</td><td>collapse / settings use IconButton sm; the logo is animated (a blinking eye)</td></tr>
|
<tr><td>Brand header</td><td>logo + name + collapse IconButton (right-aligned)</td><td>on Windows / web the brand is left and the collapse IconButton sm is right-aligned inside the header; the logo is animated (a blinking eye). On macOS desktop the header is a bare drag strip (brand hidden, traffic lights + resident floating toggle over it)</td></tr>
|
||||||
<tr><td>Search</td><td>bare search row (custom)</td><td>no border, hover/focus shows a sunken background; icon + input + clear IconButton. <b>Do not</b> use Input (the 38px bordered version is too heavy)</td></tr>
|
<tr><td>New chat</td><td>full-width left-aligned button (custom)</td><td>same rhythm as the session rows in the list (left-aligned, hover = <code>--sb-hover</code>). <b>Do not</b> use Button (centered, breaks the rhythm)</td></tr>
|
||||||
<tr><td>New chat</td><td>full-width left-aligned button (custom)</td><td>same rhythm as the session rows in the list (left-aligned, hover sunken). <b>Do not</b> use Button (centered, breaks the rhythm)</td></tr>
|
<tr><td>Search</td><td>bare search row (custom)</td><td>no border, hover/focus shows a sunken background; icon + label, with the <code>Kbd</code> keycaps (⌘K / Ctrl K) pushed to the trailing edge — label and shortcut are justified apart. <b>Do not</b> use Input (the 38px bordered version is too heavy). Last fixed row above the list — its wrapper carries the scroll-linked seam</td></tr>
|
||||||
<tr><td>Section label</td><td><code>.p-section-label</code></td><td>uppercase muted small titles like "Workspaces"</td></tr>
|
<tr><td>Section label</td><td><code>.p-section-label</code></td><td>uppercase muted small titles like "Workspaces"</td></tr>
|
||||||
<tr><td>Workspace head / session row</td><td>see next two sections</td><td>share <code>--sb-*</code> alignment</td></tr>
|
<tr><td>Workspace head / session row</td><td>see next two sections</td><td>share <code>--sb-*</code> alignment</td></tr>
|
||||||
|
<tr><td>Settings footer</td><td>full-width left-aligned button (custom)</td><td>pinned row under the session list, separated by a 1px <code>--line</code> top border; icon + label, same list-style family as New chat</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="callout warn"><span class="ico">!</span><div>
|
<div class="callout warn"><span class="ico">!</span><div>
|
||||||
|
|
@ -1391,7 +1408,7 @@ onUnmounted(() => {
|
||||||
<table class="dt">
|
<table class="dt">
|
||||||
<thead><tr><th>Part</th><th>Rule</th></tr></thead>
|
<thead><tr><th>Part</th><th>Rule</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Container</td><td><code>margin: 1px 6px; padding: 7px 10px; radius-md</code>; hover = <code>surface-sunken</code>; active = <code>accent-soft</code> + <code>inset 0 0 0 1px accent-bd</code></td></tr>
|
<tr><td>Container</td><td><code>padding: 8px 8px</code> inside the list's <code>--sb-inset</code> gutter, <code>radius-sm</code>; <b>no fixed/min height</b> — row height is font-driven (title <code>line-height: --leading-tight</code>, ≈16px) → ≈32px total, the sidebar-wide row rhythm. The hover kebab is absolutely positioned so it never forces the row taller (no hover jitter). hover = <code>--sb-hover</code> (the global <code>--color-hover</code> wash); active = <code>--color-selected</code> — neutral, no accent tint, no border, no weight change</td></tr>
|
||||||
<tr><td>Status slot (lead)</td><td>fixed <code>--sb-gutter</code> width; running = <code>Spinner</code> sm, otherwise unread = 7px accent dot</td></tr>
|
<tr><td>Status slot (lead)</td><td>fixed <code>--sb-gutter</code> width; running = <code>Spinner</code> sm, otherwise unread = 7px accent dot</td></tr>
|
||||||
<tr><td>Title</td><td>flex:1 with truncation; double-click enters inline rename (compact input, not Input)</td></tr>
|
<tr><td>Title</td><td>flex:1 with truncation; double-click enters inline rename (compact input, not Input)</td></tr>
|
||||||
<tr><td>Time</td><td>mono xs, <code>fg-faint</code>; yields to the kebab on hover</td></tr>
|
<tr><td>Time</td><td>mono xs, <code>fg-faint</code>; yields to the kebab on hover</td></tr>
|
||||||
|
|
@ -1402,11 +1419,11 @@ onUnmounted(() => {
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h3 class="sub">Workspace group</h3>
|
<h3 class="sub">Workspace group</h3>
|
||||||
<p>The group head and session rows share <code>--sb-*</code>: folder icon (open/closed) → name → path subtitle, with the kebab and "+" revealed on hover.</p>
|
<p>The group head and session rows share <code>--sb-*</code>: folder icon (open/closed) → name, with the kebab and "+" revealed on hover.</p>
|
||||||
<ul class="clean">
|
<ul class="clean">
|
||||||
<li>The folder icon sits in the <code>--sb-gutter</code> slot, switching icons between open and closed states.</li>
|
<li>The folder icon leads the row (switching icons between open and closed states) with the plain <code>--sb-gap</code> before the name — it does not pad out the <code>--sb-gutter</code> slot.</li>
|
||||||
<li>A small <code>fg-muted</code> path line sits below the name.</li>
|
<li>The name is quiet by design — regular weight, muted color (<code>--color-text-muted</code>, one step lighter than session titles), so group heads read as grouping labels. No path subtitle; hovering the name shows the full root path in a <code>Tooltip</code>.</li>
|
||||||
<li>The kebab (menu) and "+" (new chat in this workspace) both use <code>IconButton</code> sm, shown on hover or keyboard focus (when not hovered they stay in the tab order via <code>opacity:0</code>, keeping them keyboard-reachable).</li>
|
<li>The kebab (menu) and "+" (new chat in this workspace) both use <code>IconButton</code> sm inside a floating actions layer anchored to the row's right edge — no reserved layout space, so the name uses the full row width when idle. Shown on hover, keyboard focus, or while the menu is open; the layer backs itself with the sidebar surface (container background) plus the row hover wash (an <code>::after</code> shown only while the row is hovered), so its color exactly equals the row's current background and the overlapped name tail doesn't bleed through (hidden via <code>opacity:0</code>, staying in the tab order).</li>
|
||||||
<li>The group is collapsible; when collapsed its session list is hidden.</li>
|
<li>The group is collapsible; when collapsed its session list is hidden.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
@ -1415,7 +1432,7 @@ onUnmounted(() => {
|
||||||
<table class="dt">
|
<table class="dt">
|
||||||
<thead><tr><th>Part</th><th>Rule</th></tr></thead>
|
<thead><tr><th>Part</th><th>Rule</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td class="tk">Container</td><td>session-row pill: <code>display:flex; gap:--sb-gap; min-height:26px</code>, same padding as a session row, <code>radius-md</code>; hover = <code>surface-sunken</code> (no text recolor); <code>:focus-visible</code> uses <code>--p-focus-ring</code></td></tr>
|
<tr><td class="tk">Container</td><td>session-row pill: <code>display:flex; gap:--sb-gap; padding:8px …</code>, <b>no fixed/min height</b> (font-driven, ≈32px like a session row), same padding as a session row, <code>radius-sm</code>; hover = <code>--sb-hover</code> (no text recolor); <code>:focus-visible</code> uses <code>--p-focus-ring</code></td></tr>
|
||||||
<tr><td class="tk">Lead slot</td><td>empty, <code>--sb-gutter</code> wide, so the label's start x aligns with the session titles (<code>--sb-pad-x + --sb-gutter + --sb-gap</code>)</td></tr>
|
<tr><td class="tk">Lead slot</td><td>empty, <code>--sb-gutter</code> wide, so the label's start x aligns with the session titles (<code>--sb-pad-x + --sb-gutter + --sb-gap</code>)</td></tr>
|
||||||
<tr><td class="tk">Label</td><td><code>font-ui</code>, <code>text-xs</code>, <code>--color-text</code>; flex:1, truncated</td></tr>
|
<tr><td class="tk">Label</td><td><code>font-ui</code>, <code>text-xs</code>, <code>--color-text</code>; flex:1, truncated</td></tr>
|
||||||
<tr><td class="tk">Behavior</td><td>"Load more" fetches the next page and auto-expands; once more than the first page is loaded, "Show less" appears and collapses back to the first page (view-layer trim — data is kept, no refetch); "Show all" re-expands</td></tr>
|
<tr><td class="tk">Behavior</td><td>"Load more" fetches the next page and auto-expands; once more than the first page is loaded, "Show less" appears and collapses back to the first page (view-layer trim — data is kept, no refetch); "Show all" re-expands</td></tr>
|
||||||
|
|
@ -1444,7 +1461,7 @@ onUnmounted(() => {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="callout info"><span class="ico">i</span><div>
|
<div class="callout info"><span class="ico">i</span><div>
|
||||||
<b>One-sentence principle:</b> the sidebar / shell is a "list + grid" skeleton that reuses the §02 tokens and §03 primitives (Button / IconButton / Badge / Menu / Spinner / PanelHeader); compact list controls that don't fit a primitive (search, New chat, inline rename, show-more) keep their custom form, governed by this section.
|
<b>One-sentence principle:</b> the sidebar / shell is a "list + grid" skeleton that reuses the §02 tokens and §03 primitives (Button / IconButton / Badge / Kbd / Menu / Spinner / PanelHeader); compact list controls that don't fit a primitive (search, New chat, inline rename, show-more) keep their custom form, governed by this section.
|
||||||
</div></div>
|
</div></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -1602,7 +1619,7 @@ onUnmounted(() => {
|
||||||
.brand-name { font-weight: 700; font-size: 15px; letter-spacing: -.01em; }
|
.brand-name { font-weight: 700; font-size: 15px; letter-spacing: -.01em; }
|
||||||
.brand-sub { font-size: 12px; color: var(--d-fg-faint); margin-bottom: 26px; padding-left: 36px; }
|
.brand-sub { font-size: 12px; color: var(--d-fg-faint); margin-bottom: 26px; padding-left: 36px; }
|
||||||
.nav-group { margin: 22px 0 8px; font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--d-fg-faint); }
|
.nav-group { margin: 22px 0 8px; font-size: 11px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--d-fg-faint); }
|
||||||
.p-section-label { font-size: 13px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--d-fg-faint); }
|
.p-section-label { font-size: 12px; font-weight: 400; text-transform: uppercase; color: var(--d-fg-faint); }
|
||||||
.nav a {
|
.nav a {
|
||||||
display: flex; align-items: center; gap: 9px; padding: 7px 10px; border-radius: 7px;
|
display: flex; align-items: center; gap: 9px; padding: 7px 10px; border-radius: 7px;
|
||||||
font-size: 13.5px; font-weight: 500; color: var(--d-fg-soft); margin: 1px 0;
|
font-size: 13.5px; font-weight: 500; color: var(--d-fg-soft); margin: 1px 0;
|
||||||
|
|
@ -1934,6 +1951,16 @@ onUnmounted(() => {
|
||||||
.p-badge.solid { background: var(--p-text); color: var(--p-bg); border-color: var(--p-text); }
|
.p-badge.solid { background: var(--p-text); color: var(--p-bg); border-color: var(--p-text); }
|
||||||
.p-badge .p-ic { width: 12px; height: 12px; }
|
.p-badge .p-ic { width: 12px; height: 12px; }
|
||||||
|
|
||||||
|
/* Kbd — shortcut keycaps (one <kbd> block per key) */
|
||||||
|
.p-kbd { display: inline-flex; align-items: center; gap: 3px; }
|
||||||
|
.p-kbd kbd {
|
||||||
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
|
min-width: 18px; height: 18px; padding: 0 5px;
|
||||||
|
border: 1px solid var(--p-line); border-bottom-width: 2px; border-radius: var(--p-r-xs);
|
||||||
|
background: var(--p-surface-sunken); color: var(--p-text-muted);
|
||||||
|
font-family: var(--p-font-sans); font-size: 11px; line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* model / mode pill (composer toolbar) */
|
/* model / mode pill (composer toolbar) */
|
||||||
.p-pill {
|
.p-pill {
|
||||||
display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px;
|
display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import Icons from 'unplugin-icons/vite';
|
import Icons from 'unplugin-icons/vite';
|
||||||
|
import { FileSystemIconLoader } from 'unplugin-icons/loaders';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
const webPort = Number(process.env.WEB_PORT) || 5175;
|
const webPort = Number(process.env.WEB_PORT) || 5175;
|
||||||
// Where the dev proxy forwards server traffic. Defaults to the local server
|
// Where the dev proxy forwards server traffic. Defaults to the local server
|
||||||
|
|
@ -12,7 +14,19 @@ const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue(), Icons({ compiler: 'vue3' })],
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
Icons({
|
||||||
|
compiler: 'vue3',
|
||||||
|
// Local Kimi Design System icons (24×24 outlined, fill="currentColor"),
|
||||||
|
// copied from the design-system icon pack into src/icons/kimi/ and
|
||||||
|
// imported as `~icons/kimi/<file-name>` (plus `?raw`), same as the ri
|
||||||
|
// collection. Registered in src/lib/icons.ts only.
|
||||||
|
customCollections: {
|
||||||
|
kimi: FileSystemIconLoader(fileURLToPath(new URL('./src/icons/kimi', import.meta.url))),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
// Expose the dev proxy's upstream server target to the client so the UI can
|
// Expose the dev proxy's upstream server target to the client so the UI can
|
||||||
// show which server it is connected to (the browser otherwise only sees its
|
// show which server it is connected to (the browser otherwise only sees its
|
||||||
// own same-origin URL). Unused by the same-origin production build.
|
// own same-origin URL). Unused by the same-origin production build.
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
inherit (finalAttrs) pname version src pnpmWorkspaces;
|
inherit (finalAttrs) pname version src pnpmWorkspaces;
|
||||||
inherit pnpm;
|
inherit pnpm;
|
||||||
fetcherVersion = 3;
|
fetcherVersion = 3;
|
||||||
hash = "sha256-i3T30dxNQ7DTWOmUX47kmqpIVtDYby0buPo58+Jhhmw=";
|
hash = "sha256-iBk+TV+rIhmd7bYnVFbW3kTGltojJl3pL2hhmsGO+Fk=";
|
||||||
};
|
};
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
|
|
|
||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -206,6 +206,9 @@ importers:
|
||||||
'@iconify-json/ri':
|
'@iconify-json/ri':
|
||||||
specifier: ^1.2.10
|
specifier: ^1.2.10
|
||||||
version: 1.2.10
|
version: 1.2.10
|
||||||
|
'@iconify-json/tabler':
|
||||||
|
specifier: ^1.2.35
|
||||||
|
version: 1.2.35
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^5.2.4
|
specifier: ^5.2.4
|
||||||
version: 5.2.4(vite@6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.35(typescript@6.0.2))
|
version: 5.2.4(vite@6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.35(typescript@6.0.2))
|
||||||
|
|
@ -1627,6 +1630,9 @@ packages:
|
||||||
'@iconify-json/simple-icons@1.2.84':
|
'@iconify-json/simple-icons@1.2.84':
|
||||||
resolution: {integrity: sha512-v4JVu6xIewGoETD4mm2k6UAdFAbTlY1duw5ZNSxYORfs2yFsHDhoU9Omn/BgrV0nR/ptWkF3ZIr/ZHoYXI/6Jw==}
|
resolution: {integrity: sha512-v4JVu6xIewGoETD4mm2k6UAdFAbTlY1duw5ZNSxYORfs2yFsHDhoU9Omn/BgrV0nR/ptWkF3ZIr/ZHoYXI/6Jw==}
|
||||||
|
|
||||||
|
'@iconify-json/tabler@1.2.35':
|
||||||
|
resolution: {integrity: sha512-/sJMqHvh5ZWrEERVfDCT5NjVDeKJdhosFtKjJofAVl+P/3AzLiryOQw7WvrfDF25Xa5N/eoOQ15Y1jnhYXxBoQ==}
|
||||||
|
|
||||||
'@iconify/types@2.0.0':
|
'@iconify/types@2.0.0':
|
||||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||||
|
|
||||||
|
|
@ -8563,6 +8569,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@iconify/types': 2.0.0
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
|
'@iconify-json/tabler@1.2.35':
|
||||||
|
dependencies:
|
||||||
|
'@iconify/types': 2.0.0
|
||||||
|
|
||||||
'@iconify/types@2.0.0': {}
|
'@iconify/types@2.0.0': {}
|
||||||
|
|
||||||
'@iconify/utils@3.1.3':
|
'@iconify/utils@3.1.3':
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue