feat(client): premium UI overhaul — Iris design system + single-instance lock

Production-grade visual upgrade. Pulls the violet→blue gradient from
the H glass app icon up into a coherent design system: brand color,
focus rings, button gradients, sidebar accents, selection highlight,
shadow tints all derive from the same palette. Result: login surface
no longer clashes with the app icon, post-login UI feels like a
single product instead of a Tauri shell.

Changes
-------

src/theme/globals.css — Iris design tokens:
  - --color-primary #C5A572 (gold) → #7B6BE3 (violet, from logo)
  - --color-secondary slate → #6B7CE0 (blue from logo)
  - --color-tertiary gold → #9A8DEC (lighter violet)
  - Surface stack rebased to slight cool/blue undertone (#13131A,
    #181820, …) so violet accents pop without clashing
  - Borders softened: --color-border 0.08 → 0.06, separator 0.06 → 0.04
  - Radii loosened: sm 4 → 6, md 8 → 10, lg 12 → 14, xl 16 → 18
  - --gradient-btn-primary: 3-stop violet→primary→blue (replaces flat
    gold gradient); CTA glow shadow tinted violet
  - --gradient-brand-wordmark exposed as a token so wordmark / avatar
    gradient stays consistent across surfaces
  - --shadow-dropdown: 3-stop layered, dark stop tinted violet so
    floating menus feel of-a-piece with brand
  - --shadow-focus-ring: violet 55% so input focus is visible
  - --color-selection-bg: violet 32%
  - Sidebar item hover/active: rgba violet base instead of plain white
  - Sidebar panel backdrop: 2-stop radial (violet top-left + blue
    bottom-right) replacing plain gold radial
  - Type scale tokens: --text-xxs … --text-display
  - Body font-feature-settings: 'kern','liga','cv11' +
    text-rendering: optimizeLegibility for crisp Inter / Manrope
  - .tabular-nums utility for status counters

src/components/login/HeicodeLoginPage.tsx — login redesign:
  - Embeds /app-icon.png (the H glass logo) above wordmark
  - Drop-shadow violet-tinted so icon feels continuous with backdrop
  - Layered backdrop: violet halo top, blue glow bottom, faint SVG
    grain texture for tactile premium feel
  - Wordmark + tagline use --gradient-brand-wordmark token
  - Card max-width tightened to sm (was md) for taller, more focused
    composition (Linear / Vercel / Raycast desktop login style)
  - Chrome strip: blurred translucent surface

src/components/login/ProviderLoginCard.tsx:
  - Card gets faint violet→neutral surface gradient (lifts off backdrop
    without heavy 1px border)
  - Top hairline accent (centered violet line) at card top edge
  - CTA button uses --gradient-btn-primary; hover glow stronger;
    active scale 0.99 microinteraction
  - Removed hardcoded rgba(197,165,114,…) gold artifact

src/components/layout/TitleBar.tsx:
  - UserPill avatar gradient now uses --gradient-brand-wordmark token
    (was inline hardcoded violet) so future palette tweaks propagate

src-tauri (Cargo.toml + src/lib.rs):
  - Add tauri-plugin-single-instance and register it as the FIRST
    plugin in the Builder chain. Subsequent Heicode launches focus
    the existing window via show_main_window() instead of spawning
    a new sidecar / new server port. Fixes "every shortcut click
    starts a new instance" behavior.

Verification
------------
  - bunx tsc -b --noEmit (desktop)         clean
  - cargo check (src-tauri)                 clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 17:38:31 +08:00
co-authored by Claude Opus 4.7
parent 6cc981d366
commit 463f9be2db
7 changed files with 624 additions and 85 deletions
+401
View File
@@ -56,6 +56,137 @@ dependencies = [
"derive_arbitrary",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"pin-project-lite",
"slab",
]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener",
"futures-lite",
"rustix",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "async-signal"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "atk"
version = "0.18.2"
@@ -151,6 +282,19 @@ dependencies = [
"objc2",
]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "brotli"
version = "8.0.2"
@@ -337,6 +481,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -738,6 +891,33 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "endi"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -765,6 +945,27 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.4.1"
@@ -924,6 +1125,19 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-lite"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.32"
@@ -1322,9 +1536,16 @@ dependencies = [
"tauri-plugin-dialog",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
]
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.4.3"
@@ -2291,6 +2512,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "os_pipe"
version = "1.2.3"
@@ -2340,6 +2571,12 @@ dependencies = [
"system-deps",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -2568,6 +2805,17 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "pkg-config"
version = "0.3.33"
@@ -2606,6 +2854,20 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "polling"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "portable-pty"
version = "0.9.0"
@@ -4019,6 +4281,21 @@ dependencies = [
"tokio",
]
[[package]]
name = "tauri-plugin-single-instance"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af"
dependencies = [
"serde",
"serde_json",
"tauri",
"thiserror 2.0.18",
"tracing",
"windows-sys 0.60.2",
"zbus",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.1"
@@ -4476,9 +4753,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
@@ -4528,6 +4817,17 @@ version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
[[package]]
name = "uds_windows"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
]
[[package]]
name = "unic-char-property"
version = "0.9.0"
@@ -5611,6 +5911,67 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zbus"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1"
dependencies = [
"async-broadcast",
"async-executor",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"libc",
"ordered-stream",
"rustix",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow 1.0.2",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
dependencies = [
"serde",
"winnow 1.0.2",
"zvariant",
]
[[package]]
name = "zerocopy"
version = "0.8.48"
@@ -5708,3 +6069,43 @@ name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zvariant"
version = "5.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow 1.0.2",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.117",
"winnow 1.0.2",
]
+1
View File
@@ -16,6 +16,7 @@ tauri-plugin-shell = "2"
tauri-plugin-dialog = "2"
tauri-plugin-process = "2"
tauri-plugin-updater = "2"
tauri-plugin-single-instance = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1.0.102"
+9
View File
@@ -963,6 +963,15 @@ mod tests {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let builder = tauri::Builder::default()
// Single-instance lock (must be the FIRST plugin registered per
// Tauri docs — it intercepts the second-process startup before
// anything else runs and forwards the launch arguments to the
// first process via this callback). Subsequent app icon clicks /
// shortcut launches will focus the existing window instead of
// spawning a new sidecar + new server port.
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
show_main_window(app);
}))
.manage(ServerState::default())
.manage(AdapterState::default())
.manage(TerminalState::default())
@@ -94,9 +94,7 @@ function UserPill() {
>
<span
className="flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-bold text-white"
style={{
backgroundImage: 'linear-gradient(135deg, #B888E5 0%, #6B7CE0 100%)',
}}
style={{ backgroundImage: 'var(--gradient-brand-wordmark)' }}
>
{initial}
</span>
@@ -1,10 +1,20 @@
// desktop/src/components/login/HeicodeLoginPage.tsx
//
// 全屏登录页 — 当 Heicode 检测到尚未登录时由 AppShell 渲染。
// 仅 1 个登录入口:TaijiAICloud。
// Production-grade login surface. Layout follows
// docs/product-package/11-product-prototype-wireframes.md §1
// (brand wordmark + tagline + single sign-in card) but the visual
// system was tightened up:
//
// 顶部自绘 36px 高的 chrome(drag region + WindowControls),因为
// tauri.conf.json 已设 decorations:false 以摆脱 Windows 系统灰边框。
// - 36px chrome strip with drag region + window controls
// - Layered gradient backdrop (violet/blue glow at top, vignette at
// bottom) so the login surface feels like a hero, not a form
// - Centered logo image + wordmark above tagline; both pull the
// same gradient as --gradient-brand-wordmark so the login screen
// matches the app icon you see in the title bar / taskbar / dock
// - Single CTA card on a generous max-w-sm canvas
// - Footer copy lives at the very bottom, not floating in the middle
//
// Visual references: Linear / Vercel / Raycast desktop login screens.
import { useEffect } from 'react'
import { useHeicodeAuthStore } from '../../stores/heicodeAuthStore'
@@ -34,18 +44,18 @@ export function HeicodeLoginPage() {
return (
<div
className="relative flex h-screen w-screen flex-col bg-[var(--color-background)]"
className="relative flex h-screen w-screen flex-col overflow-hidden bg-[var(--color-background)]"
data-testid="heicode-login-page"
>
{/* Chrome strip — drag region + window controls */}
{/* ─── Chrome strip ────────────────────────────────────── */}
<div
className="flex h-9 w-full items-center justify-between border-b border-[var(--color-border)] bg-[var(--color-surface)]"
className="relative z-20 flex h-9 w-full items-center justify-between border-b border-[var(--color-border-separator)] bg-[var(--color-surface)]/70 backdrop-blur-md"
data-tauri-drag-region
>
<div className="flex items-center gap-2 px-4" data-tauri-drag-region>
<span
className="text-[10px] font-bold tracking-[0.22em] uppercase bg-clip-text text-transparent"
style={{ backgroundImage: 'linear-gradient(135deg, #B888E5 0%, #6B7CE0 100%)' }}
className="bg-clip-text text-[10px] font-bold uppercase tracking-[0.22em] text-transparent"
style={{ backgroundImage: 'var(--gradient-brand-wordmark)' }}
>
Heicode
</span>
@@ -53,30 +63,64 @@ export function HeicodeLoginPage() {
<WindowControls />
</div>
{/* Subtle radial backdrop for premium feel */}
{/* ─── Layered backdrop ────────────────────────────────── */}
{/* Top violet halo */}
<div
aria-hidden
className="pointer-events-none absolute inset-0"
className="pointer-events-none absolute inset-x-0 top-0 z-0 h-[60vh]"
style={{
backgroundImage:
'radial-gradient(circle at 50% 30%, rgba(123,107,227,0.08), transparent 55%)',
'radial-gradient(ellipse 70% 50% at 50% 0%, rgba(123,107,227,0.18), transparent 60%)',
}}
/>
{/* Lower blue glow */}
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 z-0 h-[40vh]"
style={{
backgroundImage:
'radial-gradient(ellipse 60% 60% at 50% 100%, rgba(107,124,224,0.10), transparent 60%)',
}}
/>
{/* Subtle grain — adds the "real product" tactility */}
<div
aria-hidden
className="pointer-events-none absolute inset-0 z-0 opacity-[0.025]"
style={{
backgroundImage:
"url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>\")",
}}
/>
{/* Content */}
<div className="relative flex flex-1 flex-col items-center justify-center overflow-auto px-6 py-10">
<div className="mb-10 text-center">
{/* Wordmark uses the same purple→blue gradient as the H glass logo
so the login surface visually matches the app icon (window
tray icon, taskbar, dock). */}
{/* ─── Content ─────────────────────────────────────────── */}
<div className="relative z-10 flex flex-1 flex-col items-center justify-center overflow-auto px-6 py-10">
<div className="mb-10 flex flex-col items-center text-center">
{/* App icon — the H glass. Drop-shadow tinted with violet so
the icon reads as continuous with the gradient backdrop. */}
<img
src="/app-icon.png"
alt=""
aria-hidden
className="h-[88px] w-[88px] select-none"
style={{
filter:
'drop-shadow(0 8px 24px rgba(123,107,227,0.30)) drop-shadow(0 2px 4px rgba(0,0,0,0.24))',
}}
draggable={false}
/>
<h1
className="text-5xl font-bold tracking-[0.18em] uppercase bg-clip-text text-transparent"
style={{ backgroundImage: 'linear-gradient(135deg, #B888E5 0%, #6B7CE0 100%)' }}
className="mt-6 bg-clip-text text-4xl font-bold uppercase tracking-[0.16em] text-transparent"
style={{
backgroundImage: 'var(--gradient-brand-wordmark)',
fontFamily: 'var(--font-headline)',
}}
>
Heicode
</h1>
<p
className="mt-5 text-sm text-[var(--color-text-secondary)]"
className="mt-4 max-w-sm text-[13px] leading-relaxed text-[var(--color-text-secondary)]"
style={{ fontFamily: 'var(--font-headline)' }}
>
{t('login.tagline')}
@@ -90,13 +134,13 @@ export function HeicodeLoginPage() {
) : null}
{hasFetched && providers.length === 0 ? (
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] p-4 text-sm text-[var(--color-error)]">
<div className="max-w-sm rounded-[var(--radius-md)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)] p-4 text-sm text-[var(--color-error)]">
{t('login.errors.noProviders')}
</div>
) : null}
{hasFetched && providers.length > 0 ? (
<div className="grid w-full max-w-md grid-cols-1 gap-5">
<div className="grid w-full max-w-sm grid-cols-1 gap-4">
{providers.map((provider) => (
<ProviderLoginCard key={provider.id} provider={provider} />
))}
@@ -104,12 +148,12 @@ export function HeicodeLoginPage() {
) : null}
{error ? (
<div className="mt-6 max-w-md rounded-[var(--radius-md)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)] px-4 py-3 text-sm text-[var(--color-error)]">
<div className="mt-6 max-w-sm rounded-[var(--radius-md)] border border-[var(--color-error)]/30 bg-[var(--color-error-container)] px-4 py-3 text-xs text-[var(--color-error)]">
{error}
</div>
) : null}
<p className="mt-10 max-w-md text-center text-xs leading-relaxed text-[var(--color-text-tertiary)]">
<p className="mt-12 max-w-sm text-center text-[11px] leading-relaxed text-[var(--color-text-tertiary)]">
{t('login.footer.heicodeProvidesModels')}
</p>
</div>
@@ -80,12 +80,32 @@ export function ProviderLoginCard({ provider }: Props) {
const buttonDisabled = phase === 'opening' || phase === 'waiting'
return (
<div className="relative flex flex-col gap-5 rounded-[var(--radius-lg)] border border-[var(--color-border)] bg-[var(--color-surface-container)] p-7 shadow-[var(--shadow-dropdown)] backdrop-blur-sm transition-colors duration-200 hover:border-[var(--color-border-hi,rgba(255,255,255,0.14))]">
<div
className="relative flex flex-col gap-5 rounded-[var(--radius-xl)] border border-[var(--color-border)] p-7 shadow-[var(--shadow-dropdown)] backdrop-blur-md transition-all duration-200 hover:border-[var(--color-border-hi)]"
style={{
// Card surface is a subtle violet→neutral gradient over the
// base container, so the card "lifts" off the backdrop without
// a heavy 1px border. Looks closer to Linear's command-palette.
backgroundImage:
'linear-gradient(180deg, rgba(123,107,227,0.04) 0%, rgba(255,255,255,0) 60%), linear-gradient(180deg, var(--color-surface-container), var(--color-surface-container))',
}}
>
{/* Top hairline accent — barely-there violet line at the very top
edge of the card; classic premium app affordance. */}
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 h-px rounded-t-[var(--radius-xl)]"
style={{
backgroundImage:
'linear-gradient(90deg, transparent 0%, rgba(123,107,227,0.55) 50%, transparent 100%)',
}}
/>
<div className="text-center">
<p className="text-[10px] font-medium uppercase tracking-[0.2em] text-[var(--color-text-tertiary)]">
{t('login.signInTarget')}
</p>
<p className="mt-1 font-mono text-xs text-[var(--color-text-secondary)] break-all">
<p className="mt-1.5 font-mono text-[12px] text-[var(--color-text-secondary)] break-all">
{signInHost}
</p>
</div>
@@ -94,7 +114,17 @@ export function ProviderLoginCard({ provider }: Props) {
type="button"
onClick={handleBrowserLogin}
disabled={buttonDisabled}
className="rounded-[var(--radius-md)] border border-[var(--color-primary)]/40 bg-[var(--color-primary)] px-4 py-3 text-sm font-medium text-[var(--color-on-primary)] shadow-[var(--shadow-button-primary)] transition-all duration-200 hover:bg-[var(--color-primary-fixed-dim)] hover:shadow-[0_8px_24px_rgba(197,165,114,0.28)] disabled:cursor-not-allowed disabled:border-[var(--color-border)] disabled:bg-[var(--color-surface-container-high)] disabled:text-[var(--color-text-tertiary)] disabled:shadow-none"
className="rounded-[var(--radius-md)] px-4 py-3 text-sm font-semibold text-[var(--color-on-primary)] shadow-[var(--shadow-button-primary)] transition-all duration-200 hover:shadow-[0_10px_32px_rgba(123,107,227,0.40)] active:scale-[0.99] disabled:cursor-not-allowed disabled:bg-[var(--color-surface-container-high)] disabled:text-[var(--color-text-tertiary)] disabled:shadow-none"
style={{
// Use the brand-wordmark gradient for the CTA so the login card's
// single action visually echoes the H logo + the wordmark above
// it. Remove default border to let the gradient extend cleanly.
backgroundImage: buttonDisabled
? undefined
: 'var(--gradient-btn-primary)',
// Letter spacing pulled in slightly — premium-feeling tight tracking.
letterSpacing: '0.01em',
}}
>
{buttonLabel}
</button>
+110 -54
View File
@@ -108,46 +108,56 @@
outline: none;
}
/* ─── Tailwind Theme Override — Heicode Premium Black ─── */
/* ─── Tailwind Theme Override — Heicode Iris (premium violet) ─── */
/*
* Brand palette derived from the H glass logo's purple→blue gradient.
* Goal: production-grade dark UI that reads as confident + modern,
* with chromatic accents tied to the app icon for instant brand
* recognition. Surface stack stays near-black with a faint cool tint
* (so violet accents pop without clashing with warm content like
* code highlighting strings).
*/
@theme {
/* Brand accent — restrained warm gold, used sparingly */
--color-primary: #C5A572;
--color-primary-container: #A38247;
--color-primary-fixed: #E0C896;
--color-primary-fixed-dim: #C5A572;
/* Brand accent — violet from logo gradient */
--color-primary: #7B6BE3;
--color-primary-container: #5B4FB8;
--color-primary-fixed: #9A8DEC;
--color-primary-fixed-dim: #7B6BE3;
--color-on-primary: #1A1410;
--color-on-primary: #FFFFFF;
--color-on-surface: #ECECEE;
--color-on-surface-variant: #B0B0B5;
/* Black surface stack — near-black with slight warm tint, low-contrast layering */
--color-surface: #141418;
--color-surface-bright: #1F1F25;
--color-surface-dim: #0A0A0C;
--color-surface-container: #1A1A1F;
--color-surface-container-low: #16161A;
--color-surface-container-high: #1F1F25;
--color-surface-container-highest: #26262E;
--color-surface-container-lowest: #0A0A0C;
--color-surface-variant: #1F1F25;
/* Surface stack — near-black with a cool (blue-leaning) undertone
so violet accents read as natural, not garish. */
--color-surface: #13131A;
--color-surface-bright: #1E1E27;
--color-surface-dim: #0A0A0F;
--color-surface-container: #181820;
--color-surface-container-low: #15151C;
--color-surface-container-high: #1E1E27;
--color-surface-container-highest: #262631;
--color-surface-container-lowest: #0A0A0F;
--color-surface-variant: #1E1E27;
--color-background: #0A0A0C;
--color-background: #0A0A0F;
--color-outline: #5A5A62;
--color-outline-variant: #2A2A30;
--color-outline: #5A5A66;
--color-outline-variant: #26262E;
--color-secondary: #8B95A8;
--color-secondary-container: #2A2F3A;
--color-tertiary: #C5A572;
--color-tertiary-container: #2A241A;
/* Secondary = blue from logo (companion accent for tags / tertiary state) */
--color-secondary: #6B7CE0;
--color-secondary-container: #1E2238;
--color-tertiary: #9A8DEC;
--color-tertiary-container: #1E1A30;
--color-error: #E5484D;
--color-error-container: #3A1517;
--color-on-error-container: #FECDCD;
--color-inverse-surface: #ECECEE;
--color-inverse-on-surface: #14140C;
--color-inverse-primary: #8E7547;
--color-inverse-on-surface: #13131A;
--color-inverse-primary: #5B4FB8;
--color-success: #2EA86E;
--color-warning: #E5A23E;
@@ -172,28 +182,30 @@
--motion-sidebar-easing: cubic-bezier(0.22, 1, 0.36, 1);
/* Surface aliases */
--color-surface-sidebar: #101014;
--color-surface-sidebar: #0F0F15;
--color-surface-hover: var(--color-surface-container-high);
--color-surface-selected: var(--color-surface-container-highest);
--color-model-option-selected-bg: rgba(197, 165, 114, 0.10);
--color-model-option-selected-border: rgba(197, 165, 114, 0.32);
--color-model-option-selected-bg: rgba(123, 107, 227, 0.12);
--color-model-option-selected-border: rgba(123, 107, 227, 0.32);
--color-sidebar-filter-bg: rgba(255, 255, 255, 0.02);
--color-sidebar-filter-border: rgba(255, 255, 255, 0.06);
--color-sidebar-filter-border: rgba(255, 255, 255, 0.05);
--color-sidebar-filter-icon-bg: transparent;
--color-sidebar-search-bg: rgba(255, 255, 255, 0.03);
--color-sidebar-search-border: rgba(255, 255, 255, 0.06);
--color-sidebar-item-hover: rgba(255, 255, 255, 0.04);
--color-sidebar-item-active: rgba(255, 255, 255, 0.07);
--color-sidebar-item-active-border: rgba(197, 165, 114, 0.18);
--color-sidebar-search-border: rgba(255, 255, 255, 0.05);
--color-sidebar-item-hover: rgba(123, 107, 227, 0.06);
--color-sidebar-item-active: rgba(123, 107, 227, 0.10);
--color-sidebar-item-active-border: rgba(123, 107, 227, 0.24);
--sidebar-panel-bg-image:
radial-gradient(circle at top left, rgba(197, 165, 114, 0.04), transparent 38%),
linear-gradient(180deg, rgba(255, 255, 255, 0.015), rgba(255, 255, 255, 0));
radial-gradient(circle at top left, rgba(123, 107, 227, 0.07), transparent 42%),
radial-gradient(circle at bottom right, rgba(107, 124, 224, 0.04), transparent 50%),
linear-gradient(180deg, rgba(255, 255, 255, 0.012), rgba(255, 255, 255, 0));
/* Border aliases */
--color-border: rgba(255, 255, 255, 0.08);
--color-border-hi: rgba(255, 255, 255, 0.14);
/* Border aliases — softer than before so cards lean on elevation,
not contour lines, for separation. */
--color-border: rgba(255, 255, 255, 0.06);
--color-border-hi: rgba(255, 255, 255, 0.12);
--color-border-focus: var(--color-primary);
--color-border-separator: rgba(255, 255, 255, 0.06);
--color-border-separator: rgba(255, 255, 255, 0.04);
/* Text aliases */
--color-text-primary: var(--color-on-surface);
@@ -207,17 +219,33 @@
--color-brand: var(--color-primary);
--color-text-accent: var(--color-primary);
/* Shadows — soft ambient, deeper for floaty cards */
--shadow-dropdown: 0 8px 24px rgba(0, 0, 0, 0.40), 0 24px 60px rgba(0, 0, 0, 0.50);
/* Shadows — layered ambient + a faint violet tint on dropdowns
so floating surfaces feel of-a-piece with the brand instead of
dropping a generic black shadow. Tuned for premium dark UIs:
two stops, deeper second stop catches eyes on >40px popovers. */
--shadow-dropdown:
0 1px 2px rgba(0, 0, 0, 0.30),
0 8px 24px rgba(0, 0, 0, 0.40),
0 24px 60px rgba(10, 8, 24, 0.55);
--shadow-sidebar-filter: none;
--shadow-focus-ring: 0 0 0 1px rgba(197, 165, 114, 0.40);
--shadow-focus-ring: 0 0 0 1px rgba(123, 107, 227, 0.55);
--shadow-error-ring: 0 0 0 1px rgba(229, 72, 77, 0.32);
--shadow-button-primary: 0 6px 20px rgba(197, 165, 114, 0.18);
/* Primary button glow — violet halo so the CTA reads as alive on hover */
--shadow-button-primary:
0 1px 2px rgba(0, 0, 0, 0.20),
0 8px 24px rgba(123, 107, 227, 0.28);
/* Button colors — solid gold, no garish gradient */
/* Button colors — gradient drives the iridescent look matching the
logo. Direction (135deg) keeps highlight at top-left for a
subtle "lit-from-above" feel. */
--color-btn-primary-fg: var(--color-on-primary);
--gradient-btn-primary: linear-gradient(180deg, var(--color-primary), var(--color-primary-container));
--gradient-btn-primary-hover: linear-gradient(180deg, var(--color-primary-fixed), var(--color-primary));
--gradient-btn-primary:
linear-gradient(135deg, var(--color-primary-fixed) 0%, var(--color-primary) 50%, var(--color-secondary) 100%);
--gradient-btn-primary-hover:
linear-gradient(135deg, #B0A4F2 0%, var(--color-primary-fixed) 50%, #8595E5 100%);
/* Brand wordmark / icon gradient — used by login + user pill avatar */
--gradient-brand-wordmark:
linear-gradient(135deg, #B888E5 0%, #6B7CE0 100%);
/* User message bubble */
--color-surface-user-msg: var(--color-surface-container-high);
@@ -268,15 +296,30 @@
/* Misc */
--color-window-close-hover: #C63B44;
--color-selection-bg: rgba(197, 165, 114, 0.28);
--color-selection-fg: #ECECEE;
--color-selection-bg: rgba(123, 107, 227, 0.32);
--color-selection-fg: #FFFFFF;
/* Radii */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
/* Radii — slightly tighter on small elements so chips/inputs feel
crisp; cards keep generous radius. */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 18px;
--radius-full: 9999px;
/* Type scale — explicit tokens prevent mixed font sizes leaking
in via ad-hoc Tailwind classes. Components should reach for
these via inline style or extend later via Tailwind plugin. */
--text-xxs: 10px;
--text-xs: 11px;
--text-sm: 13px;
--text-base: 14px;
--text-md: 15px;
--text-lg: 17px;
--text-xl: 20px;
--text-2xl: 26px;
--text-3xl: 34px;
--text-display: 48px;
}
/* ─── Optional Light Theme Override ─────────────────────────────── */
@@ -436,12 +479,25 @@ html, body, #root {
margin: 0;
padding: 0;
font-family: var(--font-body);
font-size: var(--text-base);
line-height: 1.5;
color: var(--color-on-surface);
background-color: var(--color-background);
/* Premium typography polish: kerning, contextual alternates,
stylistic features that Inter / Manrope ship with. Cleaner
letterforms in both Latin and CJK contexts. */
font-feature-settings: 'kern', 'liga', 'cv11';
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Tighter tabular numbers for status counters / token counts so digits
line up cleanly when they animate. */
.tabular-nums {
font-variant-numeric: tabular-nums;
}
::selection {
background: var(--color-selection-bg);
color: var(--color-selection-fg);