Merge pull request #605 from Ltdaaa/view
feat: introduce a theme-aware frosted-glass visual system for OpenLess frontend
This commit is contained in:
26 files changed
+1677
-234
No files matched your search
@@ -0,0 +1,541 @@
|
||||
# OpenLess Aura Skin Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Apply an Aura-inspired visual skin to OpenLess by upgrading the shell, glass treatment, radii, typography, and overview surfaces while preserving the current information architecture and page density.
|
||||
|
||||
**Architecture:** The implementation stays shallow and visual-only. It starts by adding a contract test that defines the new skin hooks, then updates global tokens and chrome, then reskins the main shell and settings modal, and finally aligns shared atoms and the overview page to the new surface system.
|
||||
|
||||
**Tech Stack:** React 18, TypeScript, Vite 5, inline-style component patterns, CSS token files, Node-based contract checks
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify: `openless-all/app/package.json`
|
||||
Purpose: register a repeatable Aura skin contract check command.
|
||||
- Create: `openless-all/app/scripts/aura-skin-contract.test.mjs`
|
||||
Purpose: assert the new skin tokens and shell hooks exist before visual work is considered complete.
|
||||
- Modify: `openless-all/app/src/styles/tokens.css`
|
||||
Purpose: replace the current light-glass token ladder with the new Aura-inspired radii, surfaces, shadows, and typography.
|
||||
- Modify: `openless-all/app/src/styles/global.css`
|
||||
Purpose: add the static layered background and shared shell/panel helper classes without introducing animation.
|
||||
- Modify: `openless-all/app/src/components/FloatingShell.tsx`
|
||||
Purpose: reskin the app shell, sidebar, version zone, and main console wrapper.
|
||||
- Modify: `openless-all/app/src/components/SettingsModal.tsx`
|
||||
Purpose: align the settings modal shell and left rail with the new skin system.
|
||||
- Modify: `openless-all/app/src/pages/_atoms.tsx`
|
||||
Purpose: make cards, buttons, pills, and page headers inherit the new radius and surface vocabulary.
|
||||
- Modify: `openless-all/app/src/pages/Overview.tsx`
|
||||
Purpose: make the overview page visibly match the new shell without changing its data structure.
|
||||
|
||||
### Task 1: Add Aura Skin Contract Check
|
||||
|
||||
**Files:**
|
||||
- Create: `openless-all/app/scripts/aura-skin-contract.test.mjs`
|
||||
- Modify: `openless-all/app/package.json`
|
||||
- Test: `openless-all/app/scripts/aura-skin-contract.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
Create `openless-all/app/scripts/aura-skin-contract.test.mjs`:
|
||||
|
||||
```js
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const root = new URL('../', import.meta.url);
|
||||
|
||||
async function read(relPath) {
|
||||
return readFile(new URL(relPath, root), 'utf8');
|
||||
}
|
||||
|
||||
const [tokens, globalCss, shell, settingsModal, overview] = await Promise.all([
|
||||
read('src/styles/tokens.css'),
|
||||
read('src/styles/global.css'),
|
||||
read('src/components/FloatingShell.tsx'),
|
||||
read('src/components/SettingsModal.tsx'),
|
||||
read('src/pages/Overview.tsx'),
|
||||
]);
|
||||
|
||||
assert.match(tokens, /--ol-shell-radius:/, 'tokens.css must define --ol-shell-radius');
|
||||
assert.match(tokens, /--ol-panel-radius:/, 'tokens.css must define --ol-panel-radius');
|
||||
assert.match(tokens, /--ol-aura-shadow:/, 'tokens.css must define --ol-aura-shadow');
|
||||
assert.match(tokens, /--ol-font-display:/, 'tokens.css must define --ol-font-display');
|
||||
|
||||
assert.match(globalCss, /\.ol-app-shell-bg\b/, 'global.css must expose .ol-app-shell-bg');
|
||||
assert.match(globalCss, /\.ol-aura-panel\b/, 'global.css must expose .ol-aura-panel');
|
||||
assert.doesNotMatch(globalCss, /@keyframes ol-aura-halo/, 'global.css must not add an animated halo');
|
||||
|
||||
assert.match(shell, /ol-app-shell-bg/, 'FloatingShell must use the app shell background class');
|
||||
assert.match(shell, /ol-aura-sidebar/, 'FloatingShell must expose an Aura sidebar hook');
|
||||
assert.match(shell, /ol-aura-panel/, 'FloatingShell must expose an Aura panel hook');
|
||||
|
||||
assert.match(settingsModal, /ol-aura-settings/, 'SettingsModal must expose an Aura settings wrapper');
|
||||
assert.match(overview, /ol-overview-hero/, 'Overview must expose a high-visibility overview surface hook');
|
||||
|
||||
console.log('Aura skin contract OK');
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Register the check in `package.json`**
|
||||
|
||||
Modify `openless-all/app/package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"check:aura-skin": "node scripts/aura-skin-contract.test.mjs",
|
||||
"check:macos-capsule-spaces": "node scripts/macos-capsule-spaces-contract.test.mjs",
|
||||
"check:hotkey-injection": "node scripts/check-hotkey-injection.mjs"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the test to verify it fails**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run check:aura-skin
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
AssertionError [ERR_ASSERTION]: tokens.css must define --ol-shell-radius
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit the failing test harness**
|
||||
|
||||
```bash
|
||||
git add openless-all/app/package.json openless-all/app/scripts/aura-skin-contract.test.mjs
|
||||
git commit -m "test: add aura skin contract"
|
||||
```
|
||||
|
||||
### Task 2: Replace Global Tokens and App Chrome
|
||||
|
||||
**Files:**
|
||||
- Modify: `openless-all/app/src/styles/tokens.css`
|
||||
- Modify: `openless-all/app/src/styles/global.css`
|
||||
- Test: `openless-all/app/scripts/aura-skin-contract.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Update the token ladder**
|
||||
|
||||
Replace the current token center in `openless-all/app/src/styles/tokens.css` with the Aura skin ladder:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--ol-bg-base: #f3f4f6;
|
||||
--ol-bg-elevated: #fbfbfc;
|
||||
--ol-surface: rgba(255, 255, 255, 0.72);
|
||||
--ol-surface-2: rgba(255, 255, 255, 0.88);
|
||||
--ol-surface-solid: #ffffff;
|
||||
|
||||
--ol-line: rgba(15, 23, 42, 0.08);
|
||||
--ol-line-strong: rgba(15, 23, 42, 0.14);
|
||||
--ol-line-soft: rgba(255, 255, 255, 0.55);
|
||||
|
||||
--ol-ink: #10131a;
|
||||
--ol-ink-2: #222938;
|
||||
--ol-ink-3: rgba(16, 19, 26, 0.64);
|
||||
--ol-ink-4: rgba(16, 19, 26, 0.42);
|
||||
--ol-ink-5: rgba(16, 19, 26, 0.24);
|
||||
|
||||
--ol-blue: #2f6df6;
|
||||
--ol-blue-hover: #2458c8;
|
||||
--ol-blue-soft: rgba(47, 109, 246, 0.10);
|
||||
--ol-blue-ring: rgba(47, 109, 246, 0.22);
|
||||
|
||||
--ol-shell-radius: 32px;
|
||||
--ol-panel-radius: 28px;
|
||||
--ol-card-radius: 22px;
|
||||
--ol-pill-radius: 999px;
|
||||
|
||||
--ol-aura-shadow: 0 24px 80px -32px rgba(15, 23, 42, 0.24), 0 12px 36px -18px rgba(15, 23, 42, 0.12);
|
||||
--ol-aura-shadow-soft: 0 10px 30px -18px rgba(15, 23, 42, 0.16), 0 0 0 0.5px rgba(255, 255, 255, 0.55) inset;
|
||||
|
||||
--ol-font-display: "Aptos", "Segoe UI Variable Display", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
--ol-font-sans: "Aptos", "Segoe UI Variable Text", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
--ol-font-mono: "JetBrains Mono", "SF Mono", "Cascadia Code", Consolas, monospace;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add static Aura shell helpers**
|
||||
|
||||
Append shared helpers to `openless-all/app/src/styles/global.css`:
|
||||
|
||||
```css
|
||||
body {
|
||||
background:
|
||||
radial-gradient(circle at 18% 12%, rgba(255, 255, 255, 0.92), rgba(255, 255, 255, 0) 34%),
|
||||
radial-gradient(circle at 86% 18%, rgba(47, 109, 246, 0.08), rgba(47, 109, 246, 0) 30%),
|
||||
linear-gradient(180deg, #f6f7fa 0%, #eef1f6 100%);
|
||||
color: var(--ol-ink);
|
||||
}
|
||||
|
||||
.ol-app-shell-bg {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.68), rgba(255, 255, 255, 0) 38%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.68), rgba(245, 247, 251, 0.58));
|
||||
}
|
||||
|
||||
.ol-aura-panel {
|
||||
background: var(--ol-surface);
|
||||
backdrop-filter: blur(24px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(150%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.58);
|
||||
box-shadow: var(--ol-aura-shadow);
|
||||
}
|
||||
|
||||
.ol-aura-card {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(252, 252, 253, 0.82));
|
||||
border: 1px solid rgba(255, 255, 255, 0.74);
|
||||
box-shadow: var(--ol-aura-shadow-soft);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the contract check**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run check:aura-skin
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
AssertionError [ERR_ASSERTION]: FloatingShell must use the app shell background class
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit the token/chrome layer**
|
||||
|
||||
```bash
|
||||
git add openless-all/app/src/styles/tokens.css openless-all/app/src/styles/global.css
|
||||
git commit -m "feat: add aura skin tokens and chrome"
|
||||
```
|
||||
|
||||
### Task 3: Reskin the Shell and Settings Modal
|
||||
|
||||
**Files:**
|
||||
- Modify: `openless-all/app/src/components/FloatingShell.tsx`
|
||||
- Modify: `openless-all/app/src/components/SettingsModal.tsx`
|
||||
- Test: `openless-all/app/scripts/aura-skin-contract.test.mjs`
|
||||
|
||||
- [ ] **Step 1: Apply Aura shell hooks in `FloatingShell.tsx`**
|
||||
|
||||
Update the shell wrappers in `openless-all/app/src/components/FloatingShell.tsx`:
|
||||
|
||||
```tsx
|
||||
<div
|
||||
className="ol-app-shell-bg"
|
||||
style={{
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
paddingTop: os === 'mac' ? 28 : 0,
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
Update the sidebar and main panel wrappers:
|
||||
|
||||
```tsx
|
||||
<aside
|
||||
className="ol-aura-sidebar"
|
||||
style={{
|
||||
width: 196,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '14px 12px 14px',
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
```tsx
|
||||
<main
|
||||
className="ol-console-main ol-aura-panel"
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
borderRadius: 'var(--ol-panel-radius)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rework the sidebar visual details**
|
||||
|
||||
Update the brand zone and footer chip in `openless-all/app/src/components/FloatingShell.tsx`:
|
||||
|
||||
```tsx
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 10px 16px' }}>
|
||||
<img
|
||||
src="AppIcon.png"
|
||||
alt="OpenLess"
|
||||
style={{
|
||||
width: 26,
|
||||
height: 26,
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 10px 24px -16px rgba(47, 109, 246, 0.45)',
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, fontFamily: 'var(--ol-font-display)' }}>OpenLess</div>
|
||||
<div style={{ fontSize: 10.5, color: 'var(--ol-ink-4)', fontFamily: 'var(--ol-font-mono)', letterSpacing: '.08em' }}>
|
||||
VOICE CONSOLE
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Align the settings modal shell**
|
||||
|
||||
Update `openless-all/app/src/components/SettingsModal.tsx`:
|
||||
|
||||
```tsx
|
||||
<div
|
||||
className="ol-aura-settings"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxWidth: 920,
|
||||
height: '100%',
|
||||
maxHeight: 620,
|
||||
background: 'var(--ol-surface)',
|
||||
borderRadius: 'var(--ol-shell-radius)',
|
||||
border: '1px solid rgba(255,255,255,0.62)',
|
||||
boxShadow: 'var(--ol-aura-shadow)',
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
Reskin the left rail:
|
||||
|
||||
```tsx
|
||||
<aside
|
||||
style={{
|
||||
width: 214,
|
||||
flexShrink: 0,
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.58), rgba(246,248,252,0.72))',
|
||||
borderRight: '1px solid rgba(255,255,255,0.52)',
|
||||
padding: '20px 14px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the contract check**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run check:aura-skin
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
AssertionError [ERR_ASSERTION]: Overview must expose a high-visibility overview surface hook
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit the shell layer**
|
||||
|
||||
```bash
|
||||
git add openless-all/app/src/components/FloatingShell.tsx openless-all/app/src/components/SettingsModal.tsx
|
||||
git commit -m "feat: reskin aura shell and settings modal"
|
||||
```
|
||||
|
||||
### Task 4: Align Shared Atoms and the Overview Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `openless-all/app/src/pages/_atoms.tsx`
|
||||
- Modify: `openless-all/app/src/pages/Overview.tsx`
|
||||
- Test: `openless-all/app/scripts/aura-skin-contract.test.mjs`
|
||||
- Test: `openless-all/app/package.json`
|
||||
|
||||
- [ ] **Step 1: Upgrade the shared atoms**
|
||||
|
||||
Modify `openless-all/app/src/pages/_atoms.tsx`:
|
||||
|
||||
```tsx
|
||||
export function PageHeader({ kicker, title, desc, right, titleRight }: PageHeaderProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24, marginBottom: 28 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
{kicker && (
|
||||
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.14em', textTransform: 'uppercase', color: 'var(--ol-ink-4)', marginBottom: 10, fontFamily: 'var(--ol-font-mono)' }}>
|
||||
{kicker}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<h1 style={{ margin: 0, fontSize: 34, fontWeight: 600, letterSpacing: '-0.04em', color: 'var(--ol-ink)', fontFamily: 'var(--ol-font-display)' }}>
|
||||
{title}
|
||||
</h1>
|
||||
{titleRight}
|
||||
</div>
|
||||
{desc && <p style={{ margin: '10px 0 0', fontSize: 13.5, color: 'var(--ol-ink-3)', maxWidth: 680, lineHeight: 1.6 }}>{desc}</p>}
|
||||
</div>
|
||||
{right}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Update `Card` to use the new helper class and radius:
|
||||
|
||||
```tsx
|
||||
export function Card({ children, style, padding = 18, glassy = false, className }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={['ol-aura-card', className].filter(Boolean).join(' ')}
|
||||
style={{
|
||||
background: glassy ? 'var(--ol-surface)' : 'var(--ol-surface-solid)',
|
||||
border: '1px solid rgba(255,255,255,0.74)',
|
||||
borderRadius: 'var(--ol-card-radius)',
|
||||
padding,
|
||||
boxShadow: 'var(--ol-aura-shadow-soft)',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Make the overview page the first complete showcase**
|
||||
|
||||
Modify `openless-all/app/src/pages/Overview.tsx`:
|
||||
|
||||
```tsx
|
||||
<PageHeader
|
||||
kicker="AURA OVERVIEW"
|
||||
title={t('overview.title')}
|
||||
desc={t('overview.metricTotalTrend')}
|
||||
/>
|
||||
```
|
||||
|
||||
Add the overview showcase hooks and higher-visibility providers row:
|
||||
|
||||
```tsx
|
||||
<div
|
||||
className="ol-overview-hero"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: 14,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
```
|
||||
|
||||
Elevate the bottom containers:
|
||||
|
||||
```tsx
|
||||
<Card className="ol-overview-hero" padding={20} style={{ display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the Aura contract check**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run check:aura-skin
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
Aura skin contract OK
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the production build**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
vite v5.x building for production...
|
||||
✓ built in
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Commit the overview alignment**
|
||||
|
||||
```bash
|
||||
git add openless-all/app/src/pages/_atoms.tsx openless-all/app/src/pages/Overview.tsx
|
||||
git commit -m "feat: apply aura styling to overview surfaces"
|
||||
```
|
||||
|
||||
### Task 5: Final Verification and Manual Review
|
||||
|
||||
**Files:**
|
||||
- Test: `openless-all/app/scripts/aura-skin-contract.test.mjs`
|
||||
- Test: local dev preview at `http://127.0.0.1:1420`
|
||||
|
||||
- [ ] **Step 1: Start the dev server**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run dev -- --host 127.0.0.1
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
Local: http://127.0.0.1:1420/
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Manually verify the shell and overview**
|
||||
|
||||
Check these exact visual outcomes:
|
||||
|
||||
```text
|
||||
1. No animated light halo or moving glow exists anywhere in the app shell.
|
||||
2. The app background shows a static layered light field.
|
||||
3. Sidebar, main panel, and settings modal all share the same Aura glass language.
|
||||
4. Overview cards use larger radii and stronger hierarchy than the old skin.
|
||||
5. The product still reads as light, calm, and easy to scan.
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Re-run automated verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run check:aura-skin && npm run build
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```text
|
||||
Aura skin contract OK
|
||||
...build succeeds...
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Final commit**
|
||||
|
||||
```bash
|
||||
git add openless-all/app
|
||||
git commit -m "feat: ship openless aura skin"
|
||||
```
|
||||
@@ -0,0 +1,254 @@
|
||||
# OpenLess Aura Skin Design
|
||||
|
||||
## Goal
|
||||
|
||||
Refresh the OpenLess desktop UI by borrowing the visual language of the reference HTML at `iframe-export-1779865710530(1).html` while preserving OpenLess's current information architecture, page density, and reading comfort.
|
||||
|
||||
This is a skin redesign, not a product redesign.
|
||||
|
||||
## Design Intent
|
||||
|
||||
The target look should feel more premium, sculpted, and technical without becoming dark, noisy, or visually tiring.
|
||||
|
||||
We will reuse these reference qualities:
|
||||
|
||||
- larger radii and softer geometry
|
||||
- frosted glass layering
|
||||
- stronger typography hierarchy
|
||||
- restrained monochrome palette with blue reserved for accents
|
||||
- clearer separation between shell, panel, card, and utility surfaces
|
||||
|
||||
We will explicitly not reuse these reference qualities:
|
||||
|
||||
- animated halo or WebGL background
|
||||
- dark-theme-first presentation
|
||||
- login-page composition
|
||||
- decorative technical labels that compete with product content
|
||||
- large glow effects that reduce legibility
|
||||
|
||||
## Scope
|
||||
|
||||
This work changes visual skin only.
|
||||
|
||||
Included:
|
||||
|
||||
- app background treatment
|
||||
- main shell and console framing
|
||||
- sidebar navigation skin
|
||||
- overview cards and list containers
|
||||
- high-visibility buttons, chips, badges, and status treatments
|
||||
- settings modal shell and section styling
|
||||
- global tokens for radii, surface color, glass, shadows, and typography
|
||||
|
||||
Excluded:
|
||||
|
||||
- navigation structure
|
||||
- page IA and feature grouping
|
||||
- product logic, IPC, or state flow
|
||||
- major layout rewrites inside functional pages
|
||||
- dark mode implementation
|
||||
- animations beyond subtle existing transitions
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
Use a light-theme skin migration that concentrates changes in design tokens, the app shell, and the most visible shared surfaces.
|
||||
|
||||
This approach is preferred because it:
|
||||
|
||||
- gives the fastest visible transformation
|
||||
- keeps implementation risk low
|
||||
- preserves the current UX model
|
||||
- creates a reusable foundation for later page-by-page polish
|
||||
|
||||
## Visual Direction
|
||||
|
||||
### Background
|
||||
|
||||
Replace the current mostly flat neutral app backdrop with a quiet layered field:
|
||||
|
||||
- soft warm-gray to cool-gray base
|
||||
- one or two static radial highlights for depth
|
||||
- no motion, glow sweep, halo, or particle treatment
|
||||
|
||||
The background should support the interface rather than become the interface.
|
||||
|
||||
### Shell
|
||||
|
||||
The top-level OpenLess window should read as a frosted control console:
|
||||
|
||||
- larger outer radii than today
|
||||
- stronger edge softness
|
||||
- slightly deeper shadow stack
|
||||
- subtle translucent panel feel
|
||||
- clearer distinction between sidebar zone and content zone while keeping them visually related
|
||||
|
||||
The shell should feel continuous and premium, not like separate floating white boxes.
|
||||
|
||||
### Surfaces
|
||||
|
||||
Introduce a more intentional surface ladder:
|
||||
|
||||
1. app backdrop
|
||||
2. shell glass
|
||||
3. main console panel
|
||||
4. elevated cards and grouped containers
|
||||
5. utility pills, chips, and compact controls
|
||||
|
||||
Each level should differ through opacity, blur, border contrast, and shadow depth rather than through aggressive color changes.
|
||||
|
||||
### Typography
|
||||
|
||||
Adopt the reference's typographic mood without sacrificing Chinese readability.
|
||||
|
||||
- primary UI font: modern sans with better personality than the current system-default feel
|
||||
- mono font: reserved for version labels, technical badges, and small metadata
|
||||
- page titles: larger, more deliberate, more architectural
|
||||
- secondary labels: quieter and cleaner
|
||||
|
||||
If external web fonts are not already part of the app, prefer safe local or bundled-friendly fallbacks rather than adding fragile runtime font dependencies.
|
||||
|
||||
### Color Hierarchy
|
||||
|
||||
Keep OpenLess light and readable.
|
||||
|
||||
- main palette remains white, off-white, graphite, and soft gray
|
||||
- blue remains the only expressive accent
|
||||
- blue is used for active states, focus, selected indicators, and important actions
|
||||
- avoid broad saturated areas
|
||||
- avoid neon glow treatments
|
||||
|
||||
## Component Strategy
|
||||
|
||||
### Sidebar Navigation
|
||||
|
||||
Keep the same navigation items and ordering, but restyle them to match the new shell:
|
||||
|
||||
- calmer inactive state
|
||||
- more tactile hover state
|
||||
- stronger active pill treatment
|
||||
- cleaner icon and label alignment
|
||||
- better-integrated version and beta chip zone
|
||||
|
||||
The sidebar should feel like part of the same industrial-glass instrument rather than a separate admin rail.
|
||||
|
||||
### Overview Page
|
||||
|
||||
Do not reorganize the overview information model.
|
||||
|
||||
Instead:
|
||||
|
||||
- give provider cards more presence
|
||||
- unify stats cards into the same radius and surface system
|
||||
- style charts and recent-record containers as elevated glass/solid hybrids
|
||||
- improve whitespace rhythm and title hierarchy
|
||||
|
||||
### Buttons, Chips, and Status
|
||||
|
||||
Unify small UI affordances into one design language:
|
||||
|
||||
- pill-like compact chips
|
||||
- more sculpted primary buttons
|
||||
- restrained secondary buttons
|
||||
- status accents that rely on color and contrast, not glow
|
||||
|
||||
### Settings Modal
|
||||
|
||||
Retain the current settings structure and content, but reskin:
|
||||
|
||||
- modal container
|
||||
- section tabs or navigation surfaces
|
||||
- row grouping
|
||||
- toggles, chips, and button styles
|
||||
- footer actions
|
||||
|
||||
The modal should feel like it belongs to the same skin system as the main shell.
|
||||
|
||||
## Technical Plan
|
||||
|
||||
Apply the redesign through three layers.
|
||||
|
||||
### 1. Global Tokens
|
||||
|
||||
Update `src/styles/tokens.css` to define:
|
||||
|
||||
- revised neutral palette
|
||||
- revised accent ladder
|
||||
- larger radius system
|
||||
- updated shadow tokens
|
||||
- shell and card translucency tokens
|
||||
- font stack updates
|
||||
|
||||
### 2. Global Chrome
|
||||
|
||||
Update `src/styles/global.css` and closely related shell-level styling to define:
|
||||
|
||||
- background gradients
|
||||
- shell glass helpers
|
||||
- optional subtle grain
|
||||
- scrollbar polish
|
||||
- global focus and interaction refinement
|
||||
|
||||
### 3. High-Visibility Containers
|
||||
|
||||
Update the shell and visible page wrappers first:
|
||||
|
||||
- `src/components/FloatingShell.tsx`
|
||||
- settings modal shell
|
||||
- overview high-visibility containers
|
||||
- shared visual wrappers where existing pages already inherit common surfaces
|
||||
|
||||
This keeps the redesign cohesive while avoiding a broad page-by-page rewrite.
|
||||
|
||||
## Risks and Guardrails
|
||||
|
||||
### Risk: Readability Regression
|
||||
|
||||
Glass and translucency can reduce legibility, especially with Chinese text.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- keep text surfaces more opaque than the reference
|
||||
- use blur as support, not as the primary effect
|
||||
- preserve strong text contrast
|
||||
|
||||
### Risk: Incomplete Visual Consistency
|
||||
|
||||
If only the shell changes, some inner surfaces may look older than the new frame.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- prioritize the overview page and settings modal after tokens
|
||||
- rely on shared tokens so untouched pages still improve passively
|
||||
|
||||
### Risk: Over-stylizing
|
||||
|
||||
A direct copy of the reference would conflict with OpenLess's utilitarian desktop workflow.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- no dark-theme conversion
|
||||
- no decorative labels
|
||||
- no animated background
|
||||
- no major layout theatrics
|
||||
|
||||
## Verification
|
||||
|
||||
The redesign is successful when:
|
||||
|
||||
- the app visibly echoes the reference's radii, frosted surfaces, and typographic hierarchy
|
||||
- OpenLess still feels light, calm, and easy to scan
|
||||
- navigation and settings behavior remain unchanged
|
||||
- the overview page looks like part of the new skin instead of the old one
|
||||
- there is no animated light ring or equivalent visual effect anywhere in the shell
|
||||
|
||||
## Implementation Boundaries
|
||||
|
||||
The first implementation pass should stop once the following are true:
|
||||
|
||||
- shell background is updated
|
||||
- sidebar skin is updated
|
||||
- main console panel is updated
|
||||
- overview cards and containers are visually aligned
|
||||
- settings modal visually matches the shell
|
||||
|
||||
Anything beyond that should be treated as a follow-up polish pass, not folded into the first skin migration.
|
||||
@@ -8,6 +8,7 @@
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"check:aura-skin": "node scripts/aura-skin-contract.test.mjs",
|
||||
"check:macos-capsule-spaces": "node scripts/macos-capsule-spaces-contract.test.mjs",
|
||||
"check:hotkey-injection": "node scripts/check-hotkey-injection.mjs"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const root = new URL('../', import.meta.url);
|
||||
|
||||
async function read(relPath) {
|
||||
return readFile(new URL(relPath, root), 'utf8');
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
function assertUsesClassName(source, className, message) {
|
||||
const escapedClassName = escapeRegExp(className);
|
||||
const patterns = [
|
||||
new RegExp(`className\\s*=\\s*(?:\\{\\s*)?"(?:[^"]*\\s)?${escapedClassName}(?:\\s[^"]*)?"(?:\\s*\\})?`),
|
||||
new RegExp(`className\\s*=\\s*(?:\\{\\s*)?'(?:[^']*\\s)?${escapedClassName}(?:\\s[^']*)?'(?:\\s*\\})?`),
|
||||
new RegExp(`className\\s*=\\s*(?:\\{\\s*)?\`(?:[^\`]*\\s)?${escapedClassName}(?:\\s[^\`]*)?\`(?:\\s*\\})?`),
|
||||
];
|
||||
|
||||
assert.ok(patterns.some((pattern) => pattern.test(source)), message);
|
||||
}
|
||||
|
||||
assert.throws(
|
||||
() => assertUsesClassName('<div>ol-app-shell-bg</div>', 'ol-app-shell-bg', 'sample must require className usage'),
|
||||
/sample must require className usage/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
assertUsesClassName(
|
||||
'<div className="foo-ol-app-shell-bg-bar" />',
|
||||
'ol-app-shell-bg',
|
||||
'sample must require an exact class token',
|
||||
),
|
||||
/sample must require an exact class token/,
|
||||
);
|
||||
assertUsesClassName(
|
||||
'<div className="foo ol-app-shell-bg bar" />',
|
||||
'ol-app-shell-bg',
|
||||
'sample should accept className usage',
|
||||
);
|
||||
|
||||
const [tokens, globalCss, shell, settingsModal, overview] = await Promise.all([
|
||||
read('src/styles/tokens.css'),
|
||||
read('src/styles/global.css'),
|
||||
read('src/components/FloatingShell.tsx'),
|
||||
read('src/components/SettingsModal.tsx'),
|
||||
read('src/pages/Overview.tsx'),
|
||||
]);
|
||||
|
||||
assert.match(tokens, /--ol-shell-radius:/, 'tokens.css must define --ol-shell-radius');
|
||||
assert.match(tokens, /--ol-panel-radius:/, 'tokens.css must define --ol-panel-radius');
|
||||
assert.match(tokens, /--ol-aura-shadow:/, 'tokens.css must define --ol-aura-shadow');
|
||||
assert.match(tokens, /--ol-font-display:/, 'tokens.css must define --ol-font-display');
|
||||
|
||||
assert.match(globalCss, /\.ol-app-shell-bg\b/, 'global.css must expose .ol-app-shell-bg');
|
||||
assert.match(globalCss, /\.ol-aura-panel\b/, 'global.css must expose .ol-aura-panel');
|
||||
assert.doesNotMatch(globalCss, /@keyframes ol-aura-halo/, 'global.css must not add an animated halo');
|
||||
|
||||
assertUsesClassName(shell, 'ol-app-shell-bg', 'FloatingShell must use the app shell background class');
|
||||
assertUsesClassName(shell, 'ol-aura-sidebar', 'FloatingShell must expose an Aura sidebar hook');
|
||||
assertUsesClassName(shell, 'ol-aura-panel', 'FloatingShell must expose an Aura panel hook');
|
||||
|
||||
assertUsesClassName(settingsModal, 'ol-aura-settings', 'SettingsModal must expose an Aura settings wrapper');
|
||||
assertUsesClassName(overview, 'ol-overview-hero', 'Overview must expose a high-visibility overview surface hook');
|
||||
|
||||
console.log('Aura skin contract OK');
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from './lib/windowHotkeyFallback';
|
||||
import { QaPanel } from './pages/QaPanel';
|
||||
import { HotkeySettingsProvider } from './state/HotkeySettingsContext';
|
||||
import { APP_THEME_KEY, applyAppTheme, readAppTheme } from './lib/appTheme';
|
||||
|
||||
interface AppProps {
|
||||
isCapsule: boolean;
|
||||
@@ -39,6 +40,16 @@ export function App({ isCapsule, isQa, forcedOs }: AppProps) {
|
||||
// Windows 启动不应被权限探测阻塞首屏。
|
||||
const [gate, setGate] = useState<Gate>('ready');
|
||||
|
||||
useEffect(() => {
|
||||
applyAppTheme(readAppTheme());
|
||||
const syncTheme = (event: StorageEvent) => {
|
||||
if (event.key !== APP_THEME_KEY) return;
|
||||
applyAppTheme(readAppTheme());
|
||||
};
|
||||
window.addEventListener('storage', syncTheme);
|
||||
return () => window.removeEventListener('storage', syncTheme);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauri) return;
|
||||
let cancelled = false;
|
||||
|
||||
@@ -181,7 +181,10 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0, paddingTop: os === 'mac' ? 28 : 0 }}>
|
||||
<div
|
||||
className="ol-app-shell-bg"
|
||||
style={{ flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', minHeight: 0, paddingTop: os === 'mac' ? 28 : 0 }}
|
||||
>
|
||||
|
||||
{/* Main shell — flush with the frosted backplate (no separate float). */}
|
||||
<div
|
||||
@@ -196,22 +199,26 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
|
||||
{/* Sidebar — 透明地坐在外层磨砂底板上,让 LOGO/导航/快捷键/BETA/footer 共用同一片磨砂玻璃 */}
|
||||
<aside
|
||||
className="ol-aura-sidebar"
|
||||
style={{
|
||||
width: 188,
|
||||
width: 196,
|
||||
flexShrink: 0,
|
||||
display: 'flex', flexDirection: 'column',
|
||||
background: 'transparent',
|
||||
padding: '10px 10px 12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
|
||||
{/* brand */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '2px 8px 12px' }}>
|
||||
<div className="ol-aura-sidebar-brand">
|
||||
<img
|
||||
className="ol-aura-sidebar-brand-mark"
|
||||
src="AppIcon.png"
|
||||
alt="OpenLess"
|
||||
style={{ width: 22, height: 22, borderRadius: 5, boxShadow: '0 1px 2px rgba(0,0,0,.1), 0 0 0 0.5px rgba(0,0,0,.06)' }} />
|
||||
/>
|
||||
|
||||
<div style={{ fontSize: 13.5, fontWeight: 600, letterSpacing: '-0.01em', color: 'var(--ol-ink)' }}>OpenLess</div>
|
||||
<div>
|
||||
<div className="ol-aura-sidebar-brand-title">OpenLess</div>
|
||||
<div className="ol-aura-sidebar-brand-kicker">VOICE CONSOLE</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* nav — 滑动指示器:active pill 是 absolute 元素,currentTab 改变时 top/height
|
||||
@@ -220,6 +227,7 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
<nav style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{pillRect && (
|
||||
<div
|
||||
className="ol-aura-sidebar-pill"
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -227,9 +235,6 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
right: 0,
|
||||
top: pillRect.top,
|
||||
height: pillRect.height,
|
||||
background: 'var(--ol-surface)',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,.05), 0 0 0 0.5px rgba(0,0,0,.06)',
|
||||
transition: 'top 0.36s var(--ol-motion-spring), height 0.36s var(--ol-motion-spring)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
@@ -243,18 +248,9 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
key={n.id}
|
||||
ref={el => { navItemRefs.current[i] = el; }}
|
||||
onClick={() => setCurrentTab(n.id)}
|
||||
className={active ? 'ol-nav-btn ol-nav-btn-active' : 'ol-nav-btn'}
|
||||
className={active ? 'ol-nav-btn ol-nav-btn-active ol-aura-sidebar-nav-btn' : 'ol-nav-btn ol-aura-sidebar-nav-btn'}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '7px 10px',
|
||||
borderRadius: 8, border: 0,
|
||||
background: 'transparent',
|
||||
fontFamily: 'inherit', fontSize: 13,
|
||||
cursor: 'default',
|
||||
transition: 'color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick)',
|
||||
textAlign: 'left',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}>
|
||||
|
||||
<Icon name={n.icon} size={14} />
|
||||
@@ -269,31 +265,10 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
{/* 底部两行:上行 = 版本 chip(含 BETA 标),下行 = 设置按钮。
|
||||
单行布局在窄 sidebar 下会把「设置」挤成两行竖字 + 版本糊一起;
|
||||
翻回两行同时把顺序反过来:设置真正落到最底,版本在它上面。 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, paddingTop: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flexWrap: 'wrap',
|
||||
padding: '0 10px',
|
||||
fontFamily: 'var(--ol-font-sans)',
|
||||
fontSize: 11,
|
||||
color: 'var(--ol-ink-4)',
|
||||
}}
|
||||
>
|
||||
<div className="ol-aura-sidebar-footer">
|
||||
<div className="ol-aura-sidebar-version">
|
||||
{IS_BETA_BUILD && (
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
padding: '2px 8px',
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.04em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--ol-blue)',
|
||||
background: 'rgba(37,99,235,0.10)',
|
||||
borderRadius: 999,
|
||||
}}>{t('shell.betaTag')}</span>
|
||||
<span className="ol-aura-beta-tag">{t('shell.betaTag')}</span>
|
||||
)}
|
||||
|
||||
<span>{t('shell.footer.version', { version: APP_VERSION_LABEL })}</span>
|
||||
@@ -301,17 +276,9 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
|
||||
<button
|
||||
onClick={() => openSettings()}
|
||||
className={settingsOpen ? 'ol-nav-btn ol-nav-btn-active' : 'ol-nav-btn'}
|
||||
className={settingsOpen ? 'ol-nav-btn ol-nav-btn-active ol-aura-sidebar-settings' : 'ol-nav-btn ol-aura-sidebar-settings'}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '7px 10px',
|
||||
borderRadius: 8, border: 0,
|
||||
background: settingsOpen ? 'var(--ol-surface)' : 'transparent',
|
||||
boxShadow: settingsOpen ? '0 1px 2px rgba(0,0,0,.05), 0 0 0 0.5px rgba(0,0,0,.06)' : 'none',
|
||||
fontFamily: 'inherit', fontSize: 13,
|
||||
cursor: 'default',
|
||||
transition: 'color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick)',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
<Icon name="settings" size={14} />
|
||||
@@ -323,16 +290,10 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
{/* Main content — Linux 禁用透明窗口后使用不透明面;其他平台保留玻璃层。 */}
|
||||
<div style={{ flex: 1, minWidth: 0, padding: '4px 8px 6px 0', display: 'flex' }}>
|
||||
<main
|
||||
className="ol-console-main"
|
||||
className="ol-console-main ol-aura-panel ol-aura-console-main"
|
||||
style={{
|
||||
flex: 1, minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
background: os === 'linux' ? 'var(--ol-surface)' : 'rgba(255, 255, 255, 0.62)',
|
||||
backdropFilter: os === 'linux' ? 'none' : 'blur(18px) saturate(170%)',
|
||||
WebkitBackdropFilter: os === 'linux' ? 'none' : 'blur(18px) saturate(170%)',
|
||||
borderRadius: 'var(--ol-window-console-radius)',
|
||||
border: '0.5px solid rgba(0,0,0,0.06)',
|
||||
boxShadow: '0 1px 0 rgba(255,255,255,0.8) inset, 0 8px 24px -12px rgba(15,17,22,0.10), 0 2px 6px -2px rgba(15,17,22,0.06)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
@@ -415,12 +376,112 @@ function FloatingShellBody({ os, initialTab, initialSettings }: { os: OS; initia
|
||||
color: var(--ol-ink-3);
|
||||
font-weight: 500;
|
||||
}
|
||||
.ol-aura-sidebar {
|
||||
padding: 14px 12px 14px;
|
||||
background: var(--ol-sidebar-bg);
|
||||
border-right: 1px solid var(--ol-sidebar-border);
|
||||
}
|
||||
.ol-aura-sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 10px 16px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 0;
|
||||
background: var(--ol-sidebar-brand-bg);
|
||||
border: 1px solid var(--ol-sidebar-brand-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
.ol-aura-sidebar-brand-mark {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
.ol-aura-sidebar-brand-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-family: var(--ol-font-display);
|
||||
color: var(--ol-ink);
|
||||
}
|
||||
.ol-aura-sidebar-brand-kicker {
|
||||
font-size: 10.5px;
|
||||
color: var(--ol-ink-4);
|
||||
font-family: var(--ol-font-mono);
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
.ol-aura-sidebar-pill {
|
||||
background: var(--ol-sidebar-pill-bg);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--ol-sidebar-pill-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
.ol-aura-sidebar-nav-btn {
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
transition: color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick);
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.ol-aura-sidebar-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 10px 0;
|
||||
margin-top: 10px;
|
||||
border-top: 1px solid var(--ol-sidebar-footer-border);
|
||||
}
|
||||
.ol-aura-sidebar-version {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 12px;
|
||||
font-family: var(--ol-font-sans);
|
||||
font-size: 11px;
|
||||
color: var(--ol-ink-4);
|
||||
background: var(--ol-sidebar-version-bg);
|
||||
border: 1px solid var(--ol-sidebar-version-border);
|
||||
border-radius: var(--ol-pill-radius);
|
||||
box-shadow: none;
|
||||
}
|
||||
.ol-aura-beta-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ol-blue);
|
||||
background: rgba(37,99,235,0.10);
|
||||
border-radius: 999px;
|
||||
}
|
||||
.ol-aura-sidebar-settings {
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--ol-sidebar-settings-border);
|
||||
background: var(--ol-sidebar-settings-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
.ol-aura-sidebar-settings.ol-nav-btn-active {
|
||||
background: var(--ol-sidebar-settings-active-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
.ol-aura-console-main {
|
||||
border-radius: var(--ol-panel-radius);
|
||||
}
|
||||
.ol-nav-btn.ol-nav-btn-active {
|
||||
color: var(--ol-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
.ol-nav-btn:not(.ol-nav-btn-active):hover {
|
||||
background: rgba(0,0,0,0.04);
|
||||
background: var(--ol-nav-hover-bg);
|
||||
color: var(--ol-ink);
|
||||
}
|
||||
@keyframes ol-page-slide {
|
||||
@@ -456,7 +517,7 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void;
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 28,
|
||||
background: 'rgba(15,17,22,0.28)',
|
||||
background: 'var(--ol-overlay-bg)',
|
||||
backdropFilter: 'blur(6px) saturate(140%)',
|
||||
WebkitBackdropFilter: 'blur(6px) saturate(140%)',
|
||||
animation: 'ol-prompt-fade 0.2s var(--ol-motion-soft)',
|
||||
@@ -521,7 +582,7 @@ function ProviderSetupPrompt({ onLater, onOpenSettings }: { onLater: () => void;
|
||||
borderRadius: 8,
|
||||
border: 0,
|
||||
background: 'var(--ol-ink)',
|
||||
color: '#fff',
|
||||
color: 'var(--ol-on-accent)',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 12.5,
|
||||
fontWeight: 500,
|
||||
@@ -549,7 +610,7 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () =>
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 28,
|
||||
background: 'rgba(15,17,22,0.28)',
|
||||
background: 'var(--ol-overlay-bg)',
|
||||
backdropFilter: 'blur(6px) saturate(140%)',
|
||||
WebkitBackdropFilter: 'blur(6px) saturate(140%)',
|
||||
animation: 'ol-prompt-fade 0.2s var(--ol-motion-soft)',
|
||||
@@ -614,7 +675,7 @@ function HotkeyModeMigrationPrompt({ onLater, onOpenSettings }: { onLater: () =>
|
||||
borderRadius: 8,
|
||||
border: 0,
|
||||
background: 'var(--ol-ink)',
|
||||
color: '#fff',
|
||||
color: 'var(--ol-on-accent)',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 12.5,
|
||||
fontWeight: 500,
|
||||
|
||||
@@ -35,7 +35,7 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'rgba(15,17,22,0.32)',
|
||||
background: 'var(--ol-overlay-bg)',
|
||||
backdropFilter: 'blur(8px) saturate(140%)',
|
||||
WebkitBackdropFilter: 'blur(8px) saturate(140%)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
@@ -48,10 +48,10 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: '100%', maxWidth: 1080, height: '100%', maxHeight: 720,
|
||||
background: 'var(--ol-surface)',
|
||||
borderRadius: 14,
|
||||
border: '0.5px solid rgba(0,0,0,.08)',
|
||||
boxShadow: '0 30px 80px -20px rgba(15,17,22,.35), 0 0 0 0.5px rgba(0,0,0,.06)',
|
||||
background: 'var(--ol-style-editor-bg)',
|
||||
borderRadius: 24,
|
||||
border: '1px solid var(--ol-style-editor-border)',
|
||||
boxShadow: 'var(--ol-style-editor-shadow)',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
@@ -71,12 +71,12 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '4px 10px', borderRadius: 999,
|
||||
border: loggedIn ? '0.5px solid var(--ol-line)' : '0.5px solid rgba(239,68,68,0.32)',
|
||||
background: loggedIn ? 'rgba(255,255,255,0.85)' : 'rgba(239,68,68,0.08)',
|
||||
border: loggedIn ? '1px solid var(--ol-style-subtle-border)' : '1px solid rgba(239,68,68,0.32)',
|
||||
background: loggedIn ? 'var(--ol-style-subtle-bg)' : 'rgba(239,68,68,0.10)',
|
||||
color: loggedIn ? 'var(--ol-ink-2)' : 'var(--ol-red, #ef4444)',
|
||||
fontSize: 11.5, fontWeight: 500,
|
||||
cursor: 'default',
|
||||
boxShadow: '0 1px 2px rgba(15,17,22,0.05)',
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
backdropFilter: 'blur(8px) saturate(140%)',
|
||||
WebkitBackdropFilter: 'blur(8px) saturate(140%)',
|
||||
}}
|
||||
@@ -91,23 +91,23 @@ export function MarketplaceModal({ onClose }: MarketplaceModalProps) {
|
||||
style={{
|
||||
position: 'absolute', top: 16, right: 16, zIndex: 3,
|
||||
width: 32, height: 32,
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
border: '1px solid var(--ol-style-subtle-border)',
|
||||
borderRadius: 8,
|
||||
background: 'rgba(255,255,255,0.85)',
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'default',
|
||||
boxShadow: '0 1px 2px rgba(15,17,22,0.05)',
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
backdropFilter: 'blur(8px) saturate(140%)',
|
||||
WebkitBackdropFilter: 'blur(8px) saturate(140%)',
|
||||
transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick)',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'var(--ol-surface)';
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'var(--ol-control-solid)';
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'var(--ol-ink)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'rgba(255,255,255,0.85)';
|
||||
(e.currentTarget as HTMLButtonElement).style.background = 'var(--ol-style-subtle-bg)';
|
||||
(e.currentTarget as HTMLButtonElement).style.color = 'var(--ol-ink-2)';
|
||||
}}
|
||||
aria-label={t('common.close')}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'rgba(15,17,22,0.32)',
|
||||
background: 'var(--ol-overlay-bg)',
|
||||
backdropFilter: 'blur(8px) saturate(140%)',
|
||||
WebkitBackdropFilter: 'blur(8px) saturate(140%)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
@@ -84,32 +84,34 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
}}>
|
||||
|
||||
<div
|
||||
className="ol-aura-settings"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: '100%', maxWidth: 880, height: '100%', maxHeight: 600,
|
||||
background: 'var(--ol-surface)',
|
||||
borderRadius: 14,
|
||||
border: '0.5px solid rgba(0,0,0,.08)',
|
||||
boxShadow: '0 30px 80px -20px rgba(15,17,22,.35), 0 0 0 0.5px rgba(0,0,0,.06)',
|
||||
display: 'flex', overflow: 'hidden',
|
||||
width: '100%',
|
||||
maxWidth: 920,
|
||||
height: '100%',
|
||||
maxHeight: 620,
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
animation: 'ol-modal-card-in 0.24s var(--ol-motion-spring)',
|
||||
position: 'relative',
|
||||
}}>
|
||||
|
||||
{/* ─── 单层侧栏 ────────────────────────────────────────────── */}
|
||||
<aside
|
||||
className="ol-aura-settings-rail"
|
||||
style={{
|
||||
width: 200, flexShrink: 0,
|
||||
background: 'rgba(247,247,250,0.7)',
|
||||
borderRight: '0.5px solid var(--ol-line-soft)',
|
||||
padding: '18px 12px',
|
||||
display: 'flex', flexDirection: 'column', gap: 14,
|
||||
width: 214,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
|
||||
{/* tab 组 */}
|
||||
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{pillRect && (
|
||||
<div
|
||||
className="ol-aura-settings-pill"
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
@@ -117,9 +119,6 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
right: 0,
|
||||
top: pillRect.top,
|
||||
height: pillRect.height,
|
||||
background: '#fff',
|
||||
borderRadius: 8,
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,.05), 0 0 0 0.5px rgba(0,0,0,.06)',
|
||||
transition: 'top 0.36s var(--ol-motion-spring), height 0.36s var(--ol-motion-spring)',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
@@ -133,7 +132,7 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
key={it.id}
|
||||
ref={el => { tabRefs.current[idx] = el; }}
|
||||
onClick={() => setSection(it.id as SettingsSectionId)}
|
||||
className={active ? 'ol-nav-btn ol-nav-btn-active' : 'ol-nav-btn'}
|
||||
className={active ? 'ol-nav-btn ol-nav-btn-active ol-aura-settings-nav-btn' : 'ol-nav-btn ol-aura-settings-nav-btn'}
|
||||
style={navBtnStyle}>
|
||||
<Icon name={it.icon} size={14} />
|
||||
<span style={{ flex: 1 }}>{t(`modal.sections.${it.id}`)}</span>
|
||||
@@ -143,12 +142,12 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
</div>
|
||||
|
||||
{/* 外链组 */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 1, paddingTop: 8, borderTop: '0.5px solid var(--ol-line-soft)' }}>
|
||||
<div className="ol-aura-settings-links">
|
||||
{LINK_ITEMS.map(it => (
|
||||
<button
|
||||
key={it.id}
|
||||
onClick={() => { if (it.href) void openExternal(it.href); }}
|
||||
className="ol-nav-btn"
|
||||
className="ol-nav-btn ol-aura-settings-nav-btn"
|
||||
style={navBtnStyle}>
|
||||
<Icon name={it.icon} size={14} />
|
||||
<span style={{ flex: 1 }}>{t(`modal.sections.${it.id}`)}</span>
|
||||
@@ -161,7 +160,17 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
{/* ─── 内容区 ──────────────────────────────────────────────
|
||||
父容器 overflow:hidden + 列向 flex;关闭按钮、section 标题固定在头部,
|
||||
只有最里层的 scroll wrapper 真正滚动。 */}
|
||||
<div style={{ flex: 1, minWidth: 0, overflow: 'hidden', position: 'relative', display: 'flex', flexDirection: 'column' }}>
|
||||
<div
|
||||
className="ol-aura-settings-content"
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* "已保存" toast:right:54 避开 28×28 关闭按钮 + 12px gap。 */}
|
||||
<SavedToast
|
||||
saveState={savedToast.state}
|
||||
@@ -170,22 +179,19 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
offsetStyle={{ position: 'absolute', top: 16, right: 54 }}
|
||||
/>
|
||||
<button
|
||||
className="ol-aura-settings-close"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
position: 'absolute', top: 14, right: 14, zIndex: 2,
|
||||
width: 28, height: 28, border: 0, borderRadius: 999,
|
||||
background: 'transparent', color: 'var(--ol-ink-3)',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'default',
|
||||
transition: 'background 0.16s var(--ol-motion-quick)',
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.background = 'rgba(0,0,0,0.05)')}
|
||||
onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
|
||||
title={t('common.close')}>
|
||||
<Icon name="close" size={14} />
|
||||
</button>
|
||||
|
||||
<h2 style={{ margin: 0, padding: '22px 28px 8px', fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em', flexShrink: 0 }}>
|
||||
<h2 className="ol-aura-settings-title" style={{ margin: 0, flexShrink: 0 }}>
|
||||
{t(`modal.sections.${section}`)}
|
||||
</h2>
|
||||
|
||||
@@ -205,18 +211,69 @@ export function SettingsModal({ os: _os, onClose, initialSettingsSection }: Sett
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>{`
|
||||
.ol-aura-settings {
|
||||
background: var(--ol-panel-bg);
|
||||
border-radius: var(--ol-shell-radius);
|
||||
border: 1px solid var(--ol-panel-border);
|
||||
box-shadow: var(--ol-panel-shadow);
|
||||
}
|
||||
.ol-aura-settings-rail {
|
||||
padding: 20px 14px;
|
||||
gap: 16px;
|
||||
background: var(--ol-settings-rail-bg);
|
||||
border-right: 1px solid var(--ol-settings-rail-border);
|
||||
}
|
||||
.ol-aura-settings-pill {
|
||||
background: var(--ol-sidebar-pill-bg);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--ol-sidebar-pill-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
.ol-aura-settings-nav-btn {
|
||||
padding: 7px 10px;
|
||||
border-radius: 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick);
|
||||
}
|
||||
.ol-aura-settings-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--ol-settings-links-border);
|
||||
}
|
||||
.ol-aura-settings-content {
|
||||
background: var(--ol-settings-content-bg);
|
||||
}
|
||||
.ol-aura-settings-close {
|
||||
background: var(--ol-settings-close-bg);
|
||||
color: var(--ol-ink-3);
|
||||
transition: background 0.16s var(--ol-motion-quick);
|
||||
}
|
||||
.ol-aura-settings-close:hover {
|
||||
background: var(--ol-settings-close-hover-bg);
|
||||
}
|
||||
.ol-aura-settings-title {
|
||||
padding: 24px 28px 10px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
font-family: var(--ol-font-display);
|
||||
color: var(--ol-ink);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const navBtnStyle = {
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '7px 10px',
|
||||
borderRadius: 8, border: 0,
|
||||
background: 'transparent',
|
||||
fontFamily: 'inherit', fontSize: 13,
|
||||
cursor: 'default', textAlign: 'left' as const,
|
||||
position: 'relative' as const,
|
||||
zIndex: 1,
|
||||
transition: 'color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick)',
|
||||
};
|
||||
@@ -39,13 +39,11 @@ export function WindowChrome({
|
||||
|
||||
// macOS / Windows 共用半透明玻璃 background + backdropFilter。
|
||||
// macOS: NSVisualEffectView 提供材质;Windows: Tauri apply_mica 提供 Mica;
|
||||
// Linux: 透明窗口 / WebKitGTK 合成层不稳定,走不透明 surface。
|
||||
const background = `
|
||||
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 60%),
|
||||
radial-gradient(100% 70% at 100% 100%, rgba(37,99,235,0.07) 0%, rgba(37,99,235,0) 55%),
|
||||
linear-gradient(180deg, rgba(245,245,247,0.92) 0%, rgba(232,232,236,0.92) 100%)
|
||||
`;
|
||||
const useSolidSurface = os === 'linux';
|
||||
// Linux: decorations:false 后 CSS 磨砂玻璃自成背景。
|
||||
//
|
||||
// 注意:三层渐变的参数与 global.css 中 [data-ol-no-compositing] .ol-winchrome
|
||||
// 的回退 background 同步(只 opacity 从 0.92 提到 0.96)。改这里时请同步更新 CSS。
|
||||
const background = 'var(--ol-window-bg)';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -62,10 +60,10 @@ export function WindowChrome({
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
border: os === 'win' ? 'none' : os === 'mac' ? 'none' : '0.5px solid rgba(0,0,0,.10)',
|
||||
background: useSolidSurface ? 'var(--ol-surface)' : background,
|
||||
backdropFilter: useSolidSurface ? 'none' : 'blur(var(--ol-glass-blur-strong)) saturate(190%)',
|
||||
WebkitBackdropFilter: useSolidSurface ? 'none' : 'blur(var(--ol-glass-blur-strong)) saturate(190%)',
|
||||
border: os === 'win' ? 'none' : os === 'mac' ? 'none' : '0.5px solid var(--ol-window-border)',
|
||||
background,
|
||||
backdropFilter: 'blur(var(--ol-glass-blur-strong)) saturate(190%)',
|
||||
WebkitBackdropFilter: 'blur(var(--ol-glass-blur-strong)) saturate(190%)',
|
||||
animation: os === 'win' ? undefined : 'ol-window-enter 0.42s var(--ol-motion-spring) both',
|
||||
transition: 'box-shadow 0.28s var(--ol-motion-soft), border-color 0.28s var(--ol-motion-soft), backdrop-filter 0.28s var(--ol-motion-soft)',
|
||||
willChange: 'opacity, transform, filter',
|
||||
@@ -158,10 +156,10 @@ function LinuxTitlebar() {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 6px 0 14px',
|
||||
background: 'var(--ol-surface)',
|
||||
backdropFilter: 'none',
|
||||
WebkitBackdropFilter: 'none',
|
||||
borderBottom: '0.5px solid rgba(0,0,0,0.08)',
|
||||
background: 'var(--ol-linux-titlebar-bg)',
|
||||
backdropFilter: 'blur(12px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(12px) saturate(180%)',
|
||||
borderBottom: '0.5px solid var(--ol-linux-titlebar-border)',
|
||||
color: 'var(--ol-ink-3)',
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
|
||||
@@ -10,7 +10,7 @@ interface SegSimpleProps {
|
||||
export function SegSimple({ options, active }: SegSimpleProps) {
|
||||
const [v, setV] = useState(active);
|
||||
return (
|
||||
<div style={{ display: 'inline-flex', padding: 2, borderRadius: 8, background: 'rgba(0,0,0,0.05)' }}>
|
||||
<div style={{ display: 'inline-flex', padding: 2, borderRadius: 8, background: 'var(--ol-segmented-bg)' }}>
|
||||
{options.map((o) => (
|
||||
<button
|
||||
key={o}
|
||||
@@ -18,9 +18,9 @@ export function SegSimple({ options, active }: SegSimpleProps) {
|
||||
style={{
|
||||
padding: '5px 12px', fontSize: 12, fontWeight: 500, border: 0, borderRadius: 6,
|
||||
fontFamily: 'inherit',
|
||||
background: v === o ? '#fff' : 'transparent',
|
||||
background: v === o ? 'var(--ol-segmented-active-bg)' : 'transparent',
|
||||
color: v === o ? 'var(--ol-ink)' : 'var(--ol-ink-3)',
|
||||
boxShadow: v === o ? '0 1px 2px rgba(0,0,0,.08)' : 'none',
|
||||
boxShadow: v === o ? 'var(--ol-segmented-active-shadow)' : 'none',
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function SwitchLite({ on: initial = false }: SwitchLiteProps) {
|
||||
onClick={() => setOn(!on)}
|
||||
style={{
|
||||
position: 'relative', width: 32, height: 18, borderRadius: 999, border: 0,
|
||||
background: on ? 'var(--ol-blue)' : 'rgba(0,0,0,0.18)',
|
||||
background: on ? 'var(--ol-blue)' : 'var(--ol-toggle-off-bg)',
|
||||
cursor: 'default',
|
||||
outline: 'none',
|
||||
}}
|
||||
@@ -23,7 +23,7 @@ export function SwitchLite({ on: initial = false }: SwitchLiteProps) {
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute', top: 2, left: on ? 16 : 2,
|
||||
width: 14, height: 14, borderRadius: 999, background: '#fff',
|
||||
width: 14, height: 14, borderRadius: 999, background: 'var(--ol-toggle-knob)',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,.25)', transition: 'left .16s var(--ol-motion-spring)',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -843,6 +843,10 @@ export const en: typeof zhCN = {
|
||||
releaseNotes: 'Release notes',
|
||||
},
|
||||
personalize: {
|
||||
theme: 'Theme',
|
||||
themeDesc: 'Switch the app shell between light and dark appearance.',
|
||||
themeLight: 'Light',
|
||||
themeDark: 'Dark',
|
||||
font: 'Font size',
|
||||
fontDesc: 'Scale the entire UI font size — applies instantly.',
|
||||
fontSmall: 'Small',
|
||||
|
||||
@@ -845,6 +845,10 @@ export const ja: typeof zhCN = {
|
||||
releaseNotes: 'リリースノート',
|
||||
},
|
||||
personalize: {
|
||||
theme: 'テーマ',
|
||||
themeDesc: 'アプリ外観をライトとダークで切り替えます。',
|
||||
themeLight: 'ライト',
|
||||
themeDark: 'ダーク',
|
||||
font: 'フォントサイズ',
|
||||
fontDesc: 'UI のフォントサイズを全体的にスケール。即時反映。',
|
||||
fontSmall: '小',
|
||||
|
||||
@@ -845,6 +845,10 @@ export const ko: typeof zhCN = {
|
||||
releaseNotes: '릴리스 노트',
|
||||
},
|
||||
personalize: {
|
||||
theme: '테마',
|
||||
themeDesc: '앱 외관을 라이트와 다크로 전환합니다.',
|
||||
themeLight: '라이트',
|
||||
themeDark: '다크',
|
||||
font: '글꼴 크기',
|
||||
fontDesc: 'UI 글꼴 크기를 전체 스케일. 즉시 반영.',
|
||||
fontSmall: '소',
|
||||
|
||||
@@ -841,6 +841,10 @@ export const zhCN = {
|
||||
releaseNotes: '发布日志',
|
||||
},
|
||||
personalize: {
|
||||
theme: '主题',
|
||||
themeDesc: '切换应用外观的浅色与深色版本。',
|
||||
themeLight: '浅色',
|
||||
themeDark: '深色',
|
||||
font: '字体大小',
|
||||
fontDesc: '整体缩放界面字号,立即生效。',
|
||||
fontSmall: '小',
|
||||
|
||||
@@ -843,6 +843,10 @@ export const zhTW: typeof zhCN = {
|
||||
releaseNotes: '發佈日誌',
|
||||
},
|
||||
personalize: {
|
||||
theme: '主題',
|
||||
themeDesc: '切換應用外觀的淺色與深色版本。',
|
||||
themeLight: '淺色',
|
||||
themeDark: '深色',
|
||||
font: '字體大小',
|
||||
fontDesc: '整體縮放界面字號,立即生效。',
|
||||
fontSmall: '小',
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export type AppThemeId = 'light' | 'dark';
|
||||
|
||||
export const APP_THEME_KEY = 'ol-app-theme';
|
||||
|
||||
export function readAppTheme(): AppThemeId {
|
||||
try {
|
||||
const value = window.localStorage.getItem(APP_THEME_KEY);
|
||||
if (value === 'light' || value === 'dark') return value;
|
||||
} catch {
|
||||
// Ignore storage access failures and fall back to the default theme.
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
|
||||
export function applyAppTheme(theme: AppThemeId): void {
|
||||
document.documentElement.dataset.olTheme = theme;
|
||||
}
|
||||
|
||||
export function setAppTheme(theme: AppThemeId): void {
|
||||
applyAppTheme(theme);
|
||||
try {
|
||||
window.localStorage.setItem(APP_THEME_KEY, theme);
|
||||
} catch {
|
||||
// Ignore storage write failures.
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import ReactDOM from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import { detectOS } from "./components/WindowChrome";
|
||||
import i18n from "./i18n"; // 副作用:触发 i18next init
|
||||
import { applyAppTheme, readAppTheme } from "./lib/appTheme";
|
||||
import "./styles/tokens.css";
|
||||
import "./styles/global.css";
|
||||
|
||||
@@ -16,6 +17,8 @@ const osQuery = params.get("os") as OS | null;
|
||||
const os = osQuery ?? detectOS();
|
||||
document.documentElement.dataset.olPlatform = os;
|
||||
|
||||
applyAppTheme(readAppTheme());
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root")!);
|
||||
|
||||
const renderApp = () => {
|
||||
|
||||
@@ -170,9 +170,16 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title={t('overview.title')} />
|
||||
<PageHeader
|
||||
kicker="AURA OVERVIEW"
|
||||
title={t('overview.title')}
|
||||
desc={t('overview.metricTotalTrend')}
|
||||
/>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 18 }}>
|
||||
<div
|
||||
className="ol-overview-hero"
|
||||
style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, marginBottom: 20 }}
|
||||
>
|
||||
<ProviderCard
|
||||
kind={t('overview.asrKind')}
|
||||
name={asrProviderName}
|
||||
@@ -187,7 +194,7 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 12, marginBottom: 18 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14, marginBottom: 20 }}>
|
||||
<Metric icon="hash" label={t('overview.metricChars')} value={historyError ? '—' : metrics.charsToday.toLocaleString()} trend={historyError ? t('overview.historyLoadError') : t('overview.metricSegments', { count: metrics.segmentsToday })} />
|
||||
<Metric icon="mic" label={t('overview.metricDuration')} value={historyError ? '—' : formatDuration(metrics.totalDurationMs, t)} trend={historyError ? t('overview.historyLoadError') : ''} />
|
||||
<Metric icon="clock" label={t('overview.metricAvg')} value={historyError ? '—' : formatDuration(metrics.avgLatencyMs, t)} trend={historyError ? t('overview.historyLoadError') : metrics.segmentsToday > 0 ? t('overview.metricAvgTrend') : t('overview.metricNoData')} />
|
||||
@@ -197,8 +204,8 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
{/* 底部一行 = flex:1 撑满剩余高度(父 wrapper 是 display:flex/column)。
|
||||
只有「最近识别」内部允许滚动;其他卡片按内容自然高度,不破裂底部圆角。
|
||||
issue #243 follow-up:去掉外层 overflow 后底部圆角被裁的视觉问题。 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.4fr', gap: 12, flex: 1, minHeight: 0 }}>
|
||||
<Card padding={18} style={{ display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.4fr', gap: 14, flex: 1, minHeight: 0 }}>
|
||||
<Card className="ol-overview-hero" padding={20} style={{ display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ol-ink-2)' }}>{t('overview.weekTitle')}</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--ol-ink-4)' }}>{t('overview.weekUnit')}</span>
|
||||
@@ -215,7 +222,7 @@ export function Overview({ onOpenHistory }: OverviewProps) {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card padding={0} style={{ display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
|
||||
<Card className="ol-overview-hero" padding={0} style={{ display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' }}>
|
||||
<div style={{ padding: '14px 18px', borderBottom: '0.5px solid var(--ol-line)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: 'var(--ol-ink-2)' }}>{t('overview.recentTitle')}</span>
|
||||
<Btn size="sm" variant="ghost" onClick={onOpenHistory}>{t('overview.recentAll')}</Btn>
|
||||
|
||||
@@ -551,7 +551,20 @@ export function Style() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card padding={0} style={{ overflow: 'hidden', flex: '1 1 0', minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
<Card
|
||||
padding={0}
|
||||
glassy
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
flex: '1 1 0',
|
||||
minHeight: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'var(--ol-style-shell-bg)',
|
||||
border: '1px solid var(--ol-style-shell-border)',
|
||||
boxShadow: 'var(--ol-style-shell-shadow)',
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: 18, borderBottom: '0.5px solid var(--ol-line)', flexShrink: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', minWidth: 0 }}>
|
||||
@@ -572,13 +585,16 @@ export function Style() {
|
||||
padding: '6px 12px',
|
||||
borderRadius: 999,
|
||||
border: '0.5px solid',
|
||||
borderColor: rawPack.active ? 'var(--ol-blue)' : 'var(--ol-line-strong)',
|
||||
background: rawPack.active ? 'var(--ol-blue-soft)' : 'transparent',
|
||||
borderColor: rawPack.active ? 'var(--ol-blue)' : 'var(--ol-style-subtle-border)',
|
||||
background: rawPack.active ? 'rgba(96,165,250,0.18)' : 'var(--ol-style-subtle-bg)',
|
||||
color: rawPack.active ? 'var(--ol-blue)' : 'var(--ol-ink-2)',
|
||||
fontSize: 12.5,
|
||||
fontWeight: rawPack.active ? 600 : 500,
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: rawPack.active ? 'default' : 'pointer',
|
||||
backdropFilter: 'blur(18px) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(18px) saturate(150%)',
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
transition: 'border-color 0.16s var(--ol-motion-quick), background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick)',
|
||||
}}
|
||||
>
|
||||
@@ -591,7 +607,7 @@ export function Style() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="ol-thinscroll" style={{ padding: 18, overflow: 'auto', flex: '1 1 0', minHeight: 0 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 12 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(220px, 100%), 1fr))', gap: 12 }}>
|
||||
<AnimatePresence mode="sync">
|
||||
{bodyPacks.map(pack => {
|
||||
const isBuiltin = pack.kind === 'builtin';
|
||||
@@ -612,24 +628,23 @@ export function Style() {
|
||||
flexDirection: 'column',
|
||||
textAlign: 'left',
|
||||
position: 'relative',
|
||||
border: '0.5px solid',
|
||||
borderColor: pack.active ? 'var(--ol-blue)' : 'var(--ol-line)',
|
||||
background: pack.active
|
||||
? 'linear-gradient(180deg, rgba(239,246,255,0.92), rgba(255,255,255,0.98))'
|
||||
: isBuiltin
|
||||
? 'linear-gradient(180deg, rgba(248,250,252,0.92), rgba(241,245,249,0.85))'
|
||||
: 'linear-gradient(180deg, rgba(255,255,255,0.98), rgba(248,250,252,0.92))',
|
||||
border: '1px solid',
|
||||
borderColor: pack.active ? 'var(--ol-style-card-border-active)' : 'var(--ol-style-card-border)',
|
||||
background: pack.active ? 'var(--ol-style-card-bg-active)' : 'var(--ol-style-card-bg)',
|
||||
backdropFilter: 'blur(28px) saturate(165%)',
|
||||
WebkitBackdropFilter: 'blur(28px) saturate(165%)',
|
||||
borderRadius: 18,
|
||||
padding: 16,
|
||||
boxShadow: pack.active ? '0 0 0 3px var(--ol-blue-ring)' : 'none',
|
||||
boxShadow: pack.active ? 'var(--ol-style-card-shadow-active)' : 'var(--ol-style-card-shadow)',
|
||||
cursor: 'default',
|
||||
minHeight: 204,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 10, marginBottom: 10 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 600, color: isBuiltin && !pack.active ? 'var(--ol-ink-2)' : 'var(--ol-ink)' }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--ol-style-card-ink)' }}>
|
||||
{pack.name}
|
||||
</div>
|
||||
<Pill tone={isBuiltin ? 'outline' : 'blue'} size="sm">
|
||||
@@ -646,7 +661,7 @@ export function Style() {
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12.5,
|
||||
color: 'var(--ol-ink-3)',
|
||||
color: 'var(--ol-style-card-ink-3)',
|
||||
lineHeight: 1.6,
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
@@ -665,9 +680,11 @@ export function Style() {
|
||||
style={{
|
||||
width: 36, height: 36, borderRadius: 12,
|
||||
display: 'grid', placeItems: 'center',
|
||||
background: pack.active ? 'rgba(37,99,235,0.12)' : 'rgba(15,23,42,0.05)',
|
||||
color: pack.active ? 'var(--ol-blue)' : 'var(--ol-ink-3)',
|
||||
background: pack.active ? 'var(--ol-style-card-icon-bg-active)' : 'var(--ol-style-card-icon-bg)',
|
||||
color: pack.active ? 'var(--ol-blue)' : 'var(--ol-style-card-ink-3)',
|
||||
flexShrink: 0,
|
||||
backdropFilter: 'blur(14px)',
|
||||
WebkitBackdropFilter: 'blur(14px)',
|
||||
}}
|
||||
>
|
||||
<Icon name="sparkle" size={16} />
|
||||
@@ -683,8 +700,10 @@ export function Style() {
|
||||
width: 36, height: 36, borderRadius: 12,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
border: '0.5px solid rgba(239,68,68,0.32)',
|
||||
background: 'rgba(254,242,242,0.6)',
|
||||
border: '1px solid var(--ol-style-delete-border)',
|
||||
background: 'var(--ol-style-delete-bg)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
WebkitBackdropFilter: 'blur(14px)',
|
||||
color: 'var(--ol-red, #ef4444)',
|
||||
cursor: busy === 'deleting' ? 'wait' : 'pointer',
|
||||
opacity: busy === 'deleting' ? 0.55 : 1,
|
||||
@@ -755,10 +774,12 @@ export function Style() {
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
textAlign: 'center',
|
||||
border: '0.5px dashed var(--ol-line-strong)',
|
||||
border: '1px dashed var(--ol-style-add-tile-border)',
|
||||
borderRadius: 18,
|
||||
padding: 16,
|
||||
background: 'transparent',
|
||||
background: 'var(--ol-style-add-tile-bg)',
|
||||
backdropFilter: 'blur(24px) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(24px) saturate(150%)',
|
||||
color: 'var(--ol-ink-3)',
|
||||
cursor: busy === 'creating' ? 'wait' : 'pointer',
|
||||
opacity: busy === 'creating' ? 0.55 : 1,
|
||||
@@ -770,8 +791,10 @@ export function Style() {
|
||||
style={{
|
||||
width: 44, height: 44, borderRadius: 999,
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(15,23,42,0.04)',
|
||||
background: 'var(--ol-style-add-tile-icon-bg)',
|
||||
color: 'var(--ol-ink-2)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
WebkitBackdropFilter: 'blur(14px)',
|
||||
}}
|
||||
>
|
||||
<Icon name="plus" size={22} />
|
||||
@@ -827,7 +850,9 @@ export function Style() {
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto minmax(0, 1fr)',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 24px 80px rgba(15,23,42,0.22)',
|
||||
background: 'var(--ol-style-editor-bg)',
|
||||
border: '1px solid var(--ol-style-editor-border)',
|
||||
boxShadow: 'var(--ol-style-editor-shadow)',
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: 18, borderBottom: '0.5px solid var(--ol-line)' }}>
|
||||
@@ -990,8 +1015,11 @@ export function Style() {
|
||||
<Card
|
||||
padding={16}
|
||||
style={{
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.98), rgba(246,248,252,0.95))',
|
||||
border: '0.5px solid rgba(148,163,184,0.24)',
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
border: '1px solid var(--ol-style-subtle-border)',
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
backdropFilter: 'blur(20px) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(20px) saturate(150%)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
|
||||
@@ -1057,9 +1085,12 @@ export function Style() {
|
||||
<div
|
||||
style={{
|
||||
padding: 14,
|
||||
borderRadius: 14,
|
||||
background: 'linear-gradient(180deg, rgba(248,250,252,0.98), rgba(241,245,249,0.95))',
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
borderRadius: 16,
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
border: '1px solid var(--ol-style-subtle-border)',
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
backdropFilter: 'blur(18px) saturate(145%)',
|
||||
WebkitBackdropFilter: 'blur(18px) saturate(145%)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
@@ -1095,7 +1126,11 @@ export function Style() {
|
||||
key={`${draft.id}-example-${index}`}
|
||||
padding={16}
|
||||
style={{
|
||||
background: 'linear-gradient(180deg, rgba(255,255,255,0.98), rgba(248,250,252,0.98))',
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
border: '1px solid var(--ol-style-subtle-border)',
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
backdropFilter: 'blur(18px) saturate(145%)',
|
||||
WebkitBackdropFilter: 'blur(18px) saturate(145%)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginBottom: 12 }}>
|
||||
@@ -1128,10 +1163,13 @@ export function Style() {
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', gap: 12 }}>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
border: '0.5px solid rgba(148,163,184,0.22)',
|
||||
background: 'rgba(248,250,252,0.9)',
|
||||
borderRadius: 16,
|
||||
border: '1px solid var(--ol-style-subtle-border)',
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
padding: 14,
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
backdropFilter: 'blur(16px) saturate(145%)',
|
||||
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
@@ -1140,16 +1178,19 @@ export function Style() {
|
||||
<textarea
|
||||
value={example.input}
|
||||
onChange={event => patchExample(index, { input: event.target.value })}
|
||||
style={{ ...textareaStyle, minHeight: 120, background: '#fff' }}
|
||||
style={{ ...textareaStyle, minHeight: 120 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 14,
|
||||
border: '0.5px solid rgba(37,99,235,0.16)',
|
||||
background: 'rgba(239,246,255,0.86)',
|
||||
borderRadius: 16,
|
||||
border: '1px solid rgba(96,165,250,0.24)',
|
||||
background: 'color-mix(in srgb, var(--ol-style-subtle-bg) 82%, rgba(96,165,250,0.16) 18%)',
|
||||
padding: 14,
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
backdropFilter: 'blur(16px) saturate(145%)',
|
||||
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
||||
@@ -1158,7 +1199,7 @@ export function Style() {
|
||||
<textarea
|
||||
value={example.output}
|
||||
onChange={event => patchExample(index, { output: event.target.value })}
|
||||
style={{ ...textareaStyle, minHeight: 120, background: '#fff' }}
|
||||
style={{ ...textareaStyle, minHeight: 120 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1180,10 +1221,13 @@ function MetaItem({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
border: '0.5px solid rgba(148,163,184,0.2)',
|
||||
background: 'rgba(255,255,255,0.92)',
|
||||
borderRadius: 14,
|
||||
border: '1px solid var(--ol-style-subtle-border)',
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
padding: '10px 12px',
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
backdropFilter: 'blur(16px) saturate(145%)',
|
||||
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 11, textTransform: 'uppercase', letterSpacing: '.08em', color: 'var(--ol-ink-4)', marginBottom: 6 }}>
|
||||
@@ -1217,9 +1261,12 @@ function DirectiveRow({
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 12,
|
||||
border: '0.5px solid rgba(148,163,184,0.2)',
|
||||
background: 'rgba(255,255,255,0.92)',
|
||||
borderRadius: 14,
|
||||
border: '1px solid var(--ol-style-subtle-border)',
|
||||
background: 'var(--ol-style-subtle-bg)',
|
||||
boxShadow: 'var(--ol-style-subtle-shadow)',
|
||||
backdropFilter: 'blur(16px) saturate(145%)',
|
||||
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
@@ -1238,24 +1285,30 @@ const inputStyle: CSSProperties = {
|
||||
boxSizing: 'border-box',
|
||||
minHeight: 38,
|
||||
padding: '9px 11px',
|
||||
borderRadius: 10,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: '#fff',
|
||||
borderRadius: 12,
|
||||
border: '1px solid var(--ol-style-input-border)',
|
||||
background: 'var(--ol-style-input-bg)',
|
||||
color: 'var(--ol-ink)',
|
||||
font: 'inherit',
|
||||
fontSize: 12.5,
|
||||
boxShadow: 'var(--ol-style-input-shadow)',
|
||||
backdropFilter: 'blur(16px) saturate(145%)',
|
||||
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
|
||||
};
|
||||
|
||||
const textareaStyle: CSSProperties = {
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
padding: '11px 12px',
|
||||
borderRadius: 12,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: '#fff',
|
||||
borderRadius: 14,
|
||||
border: '1px solid var(--ol-style-input-border)',
|
||||
background: 'var(--ol-style-input-bg)',
|
||||
color: 'var(--ol-ink)',
|
||||
font: 'inherit',
|
||||
fontSize: 12.5,
|
||||
lineHeight: 1.65,
|
||||
resize: 'vertical',
|
||||
boxShadow: 'var(--ol-style-input-shadow)',
|
||||
backdropFilter: 'blur(16px) saturate(145%)',
|
||||
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
|
||||
};
|
||||
@@ -15,16 +15,39 @@ interface PageHeaderProps {
|
||||
|
||||
export function PageHeader({ kicker, title, desc, right, titleRight }: PageHeaderProps) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24, marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24, marginBottom: 28 }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
{kicker && (
|
||||
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: '.08em', textTransform: 'uppercase', color: 'var(--ol-ink-4)', marginBottom: 8 }}>{kicker}</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '.14em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'var(--ol-ink-4)',
|
||||
marginBottom: 10,
|
||||
fontFamily: 'var(--ol-font-mono)',
|
||||
}}
|
||||
>
|
||||
{kicker}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<h1 style={{ margin: 0, fontSize: 26, fontWeight: 600, letterSpacing: '-0.02em', color: 'var(--ol-ink)' }}>{title}</h1>
|
||||
<h1
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 34,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '-0.04em',
|
||||
color: 'var(--ol-ink)',
|
||||
fontFamily: 'var(--ol-font-display)',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{titleRight}
|
||||
</div>
|
||||
{desc && <p style={{ margin: '8px 0 0', fontSize: 13, color: 'var(--ol-ink-3)', maxWidth: 640, lineHeight: 1.55 }}>{desc}</p>}
|
||||
{desc && <p style={{ margin: '10px 0 0', fontSize: 13.5, color: 'var(--ol-ink-3)', maxWidth: 680, lineHeight: 1.6 }}>{desc}</p>}
|
||||
</div>
|
||||
{right}
|
||||
</div>
|
||||
@@ -42,15 +65,15 @@ interface CardProps {
|
||||
export function Card({ children, style, padding = 18, glassy = false, className }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
className={['ol-aura-card', className].filter(Boolean).join(' ')}
|
||||
style={{
|
||||
background: glassy ? 'rgba(255,255,255,0.55)' : 'var(--ol-surface)',
|
||||
backdropFilter: glassy ? 'blur(20px) saturate(160%)' : undefined,
|
||||
WebkitBackdropFilter: glassy ? 'blur(20px) saturate(160%)' : undefined,
|
||||
border: '0.5px solid var(--ol-line)',
|
||||
borderRadius: 'var(--ol-r-lg)',
|
||||
background: glassy ? 'var(--ol-panel-bg)' : 'var(--ol-card-bg)',
|
||||
backdropFilter: glassy ? 'blur(var(--ol-aura-glass-blur)) saturate(150%)' : undefined,
|
||||
WebkitBackdropFilter: glassy ? 'blur(var(--ol-aura-glass-blur)) saturate(150%)' : undefined,
|
||||
border: `1px solid ${glassy ? 'var(--ol-panel-border)' : 'var(--ol-card-border)'}`,
|
||||
borderRadius: 'var(--ol-card-radius)',
|
||||
padding,
|
||||
boxShadow: 'var(--ol-shadow-sm)',
|
||||
boxShadow: glassy ? 'var(--ol-panel-shadow)' : 'var(--ol-card-shadow)',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
@@ -71,16 +94,16 @@ interface PillProps {
|
||||
|
||||
export function Pill({ children, tone = 'default', size = 'md', style }: PillProps) {
|
||||
const tones: Record<PillTone, { bg: string; color: string; bd: string }> = {
|
||||
default: { bg: 'rgba(0,0,0,0.05)', color: 'var(--ol-ink-2)', bd: 'transparent' },
|
||||
blue: { bg: 'var(--ol-blue-soft)',color: 'var(--ol-blue)', bd: 'transparent' },
|
||||
ok: { bg: 'var(--ol-ok-soft)', color: 'var(--ol-ok)', bd: 'transparent' },
|
||||
outline: { bg: 'transparent', color: 'var(--ol-ink-3)', bd: 'var(--ol-line-strong)' },
|
||||
dark: { bg: 'var(--ol-ink)', color: '#fff', bd: 'transparent' },
|
||||
default: { bg: 'var(--ol-pill-bg)', color: 'var(--ol-ink-2)', bd: 'var(--ol-pill-border)' },
|
||||
blue: { bg: 'var(--ol-pill-blue-bg)', color: 'var(--ol-blue)', bd: 'var(--ol-pill-blue-border)' },
|
||||
ok: { bg: 'var(--ol-pill-ok-bg)', color: 'var(--ol-ok)', bd: 'var(--ol-pill-ok-border)' },
|
||||
outline: { bg: 'var(--ol-pill-bg)', color: 'var(--ol-ink-3)', bd: 'var(--ol-pill-border)' },
|
||||
dark: { bg: 'var(--ol-pill-dark-bg)', color: 'var(--ol-on-accent)', bd: 'var(--ol-pill-dark-border)' },
|
||||
};
|
||||
const t = tones[tone];
|
||||
const sz = size === 'sm'
|
||||
? { padding: '2px 8px', fontSize: 10.5 }
|
||||
: { padding: '4px 10px', fontSize: 11.5 };
|
||||
? { padding: '3px 10px', fontSize: 11 }
|
||||
: { padding: '5px 12px', fontSize: 11.5 };
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
@@ -88,8 +111,13 @@ export function Pill({ children, tone = 'default', size = 'md', style }: PillPro
|
||||
borderRadius: 999,
|
||||
background: t.bg,
|
||||
color: t.color,
|
||||
border: t.bd === 'transparent' ? '0.5px solid transparent' : `0.5px solid ${t.bd}`,
|
||||
fontWeight: 500,
|
||||
border: `1px solid ${t.bd}`,
|
||||
boxShadow: tone === 'dark' ? 'var(--ol-pill-dark-shadow)' : 'var(--ol-pill-shadow)',
|
||||
backdropFilter: 'blur(16px) saturate(145%)',
|
||||
WebkitBackdropFilter: 'blur(16px) saturate(145%)',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '.01em',
|
||||
lineHeight: 1.2,
|
||||
whiteSpace: 'nowrap',
|
||||
flexShrink: 0,
|
||||
...sz,
|
||||
@@ -116,10 +144,10 @@ interface BtnProps {
|
||||
|
||||
export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onClick, disabled = false }: BtnProps) {
|
||||
const variants: Record<BtnVariant, { bg: string; color: string; bd: string; sh: string }> = {
|
||||
primary: { bg: 'var(--ol-ink)', color: '#fff', bd: 'transparent', sh: '0 1px 2px rgba(0,0,0,.08)' },
|
||||
blue: { bg: 'var(--ol-blue)', color: '#fff', bd: 'transparent', sh: '0 1px 2px rgba(37,99,235,.18)' },
|
||||
ghost: { bg: 'transparent', color: 'var(--ol-ink-2)', bd: 'var(--ol-line-strong)', sh: 'none' },
|
||||
soft: { bg: 'rgba(0,0,0,0.04)', color: 'var(--ol-ink-2)', bd: 'transparent', sh: 'none' },
|
||||
primary: { bg: 'linear-gradient(180deg, #141821, #0d1016)', color: 'var(--ol-on-accent)', bd: 'transparent', sh: '0 14px 32px -22px rgba(15,23,42,0.55)' },
|
||||
blue: { bg: 'linear-gradient(180deg, var(--ol-blue), var(--ol-blue-hover))', color: 'var(--ol-on-accent)', bd: 'transparent', sh: '0 14px 32px -22px rgba(37,99,235,.45)' },
|
||||
ghost: { bg: 'var(--ol-control-muted)', color: 'var(--ol-ink-2)', bd: 'var(--ol-control-border)', sh: 'var(--ol-control-shadow)' },
|
||||
soft: { bg: 'var(--ol-control-muted-strong)', color: 'var(--ol-ink-2)', bd: 'transparent', sh: 'var(--ol-control-shadow)' },
|
||||
};
|
||||
const v = variants[variant];
|
||||
const sizes: Record<BtnSize, { padding: string; fontSize: number }> = {
|
||||
@@ -134,7 +162,7 @@ export function Btn({ children, variant = 'ghost', size = 'md', icon, style, onC
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
background: v.bg, color: v.color,
|
||||
border: v.bd === 'transparent' ? '0.5px solid transparent' : `0.5px solid ${v.bd}`,
|
||||
borderRadius: 8,
|
||||
borderRadius: 999,
|
||||
boxShadow: v.sh,
|
||||
fontFamily: 'inherit', fontWeight: 500,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Icon } from '../../components/Icon';
|
||||
import { Row } from '../../components/ui/Row';
|
||||
import { openExternal } from '../../lib/ipc';
|
||||
import { APP_VERSION_LABEL } from '../../lib/appVersion';
|
||||
import { readAppTheme, setAppTheme, type AppThemeId } from '../../lib/appTheme';
|
||||
import { readFontScale, setFontScale, type FontScaleId } from '../../lib/fontScale';
|
||||
import { Card } from '../_atoms';
|
||||
import { SectionTitle } from './shared';
|
||||
@@ -54,6 +55,7 @@ export function AboutSection() {
|
||||
{/* ─── 个性化(字体大小)—— 原 personalize tab 并入此处 ──────────── */}
|
||||
<Card>
|
||||
<SectionTitle>{t('modal.sections.personalize')}</SectionTitle>
|
||||
<ThemeRow />
|
||||
<FontSizeRow />
|
||||
</Card>
|
||||
|
||||
@@ -95,6 +97,51 @@ export function AboutSection() {
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeRow() {
|
||||
const { t } = useTranslation();
|
||||
const [theme, setThemeState] = useState<AppThemeId>(() => readAppTheme());
|
||||
const themeOptions: Array<[AppThemeId, string]> = [
|
||||
['light', t('modal.personalize.themeLight')],
|
||||
['dark', t('modal.personalize.themeDark')],
|
||||
];
|
||||
|
||||
return (
|
||||
<Row label={t('modal.personalize.theme')} desc={t('modal.personalize.themeDesc')}>
|
||||
<div style={{ display: 'flex', gap: 4, padding: 2, background: 'var(--ol-segmented-bg)', borderRadius: 10 }}>
|
||||
{themeOptions.map(([id, label]) => {
|
||||
const selected = theme === id;
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
onClick={() => {
|
||||
setThemeState(id);
|
||||
setAppTheme(id);
|
||||
}}
|
||||
style={{
|
||||
minWidth: 74,
|
||||
height: 30,
|
||||
border: 0,
|
||||
borderRadius: 8,
|
||||
background: selected ? 'var(--ol-segmented-active-bg)' : 'transparent',
|
||||
color: selected ? 'var(--ol-ink)' : 'var(--ol-ink-3)',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 12,
|
||||
fontWeight: selected ? 600 : 500,
|
||||
cursor: 'default',
|
||||
boxShadow: selected ? 'var(--ol-segmented-active-shadow)' : 'none',
|
||||
transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick), box-shadow 0.18s var(--ol-motion-soft)',
|
||||
padding: '0 12px',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
// 字体大小 —— 整体缩放界面字号,立即生效(fontScale.ts 走 html.style.zoom)。
|
||||
function FontSizeRow() {
|
||||
const { t } = useTranslation();
|
||||
@@ -109,8 +156,8 @@ function FontSizeRow() {
|
||||
['large', t('modal.personalize.fontLarge')],
|
||||
];
|
||||
return (
|
||||
<Row label={t('modal.personalize.font')}>
|
||||
<div style={{ display: 'flex', gap: 4, padding: 2, background: 'rgba(0,0,0,0.04)', borderRadius: 8 }}>
|
||||
<Row label={t('modal.personalize.font')} desc={t('modal.personalize.fontDesc')}>
|
||||
<div style={{ display: 'flex', gap: 4, padding: 2, background: 'var(--ol-segmented-bg)', borderRadius: 10 }}>
|
||||
{fontOptions.map(([id, label]) => {
|
||||
const selected = fontScale === id;
|
||||
return (
|
||||
@@ -121,14 +168,14 @@ function FontSizeRow() {
|
||||
minWidth: 64,
|
||||
height: 28,
|
||||
border: 0,
|
||||
borderRadius: 6,
|
||||
background: selected ? '#fff' : 'transparent',
|
||||
borderRadius: 8,
|
||||
background: selected ? 'var(--ol-segmented-active-bg)' : 'transparent',
|
||||
color: selected ? 'var(--ol-ink)' : 'var(--ol-ink-3)',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 12,
|
||||
fontWeight: selected ? 600 : 500,
|
||||
cursor: 'default',
|
||||
boxShadow: selected ? '0 1px 2px rgba(0,0,0,.06), 0 0 0 0.5px rgba(0,0,0,.06)' : 'none',
|
||||
boxShadow: selected ? 'var(--ol-segmented-active-shadow)' : 'none',
|
||||
transition: 'background 0.16s var(--ol-motion-quick), color 0.16s var(--ol-motion-quick), box-shadow 0.18s var(--ol-motion-soft)',
|
||||
padding: '0 12px',
|
||||
}}
|
||||
@@ -145,7 +192,7 @@ function FontSizeRow() {
|
||||
const btnGhost: CSSProperties = {
|
||||
padding: '5px 10px', fontSize: 12, borderRadius: 6,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: '#fff', color: 'var(--ol-ink-2)',
|
||||
background: 'var(--ol-control-solid)', color: 'var(--ol-ink-2)',
|
||||
cursor: 'default', fontFamily: 'inherit',
|
||||
transition: 'background 0.16s var(--ol-motion-quick), border-color 0.16s var(--ol-motion-quick)',
|
||||
};
|
||||
@@ -72,7 +72,7 @@ export function CheckUpdateButton({ channel }: { channel: UpdateChannel }) {
|
||||
const checkBtnStyle: CSSProperties = {
|
||||
padding: '5px 10px', fontSize: 12, borderRadius: 6,
|
||||
border: '0.5px solid var(--ol-line-strong)',
|
||||
background: '#fff',
|
||||
background: 'var(--ol-control-solid)',
|
||||
cursor: 'default', fontFamily: 'inherit',
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
minWidth: 84,
|
||||
|
||||
@@ -130,7 +130,7 @@ export function Toggle({
|
||||
height: 20,
|
||||
borderRadius: 999,
|
||||
border: 0,
|
||||
background: on ? "var(--ol-blue)" : "rgba(0,0,0,0.15)",
|
||||
background: on ? "var(--ol-blue)" : "var(--ol-toggle-off-bg)",
|
||||
boxShadow: "inset 0 1px 2px rgba(0,0,0,0.06)",
|
||||
cursor: "default",
|
||||
transition: "background 0.16s var(--ol-motion-quick)",
|
||||
@@ -144,7 +144,7 @@ export function Toggle({
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 999,
|
||||
background: "#fff",
|
||||
background: "var(--ol-toggle-knob)",
|
||||
boxShadow:
|
||||
"0 1px 2px rgba(0,0,0,.25), 0 0 0 0.5px rgba(0,0,0,.04)",
|
||||
transition: "left .16s var(--ol-motion-spring)",
|
||||
@@ -163,7 +163,7 @@ export const inputStyle: CSSProperties = {
|
||||
fontSize: 12.5,
|
||||
fontFamily: "inherit",
|
||||
outline: "none",
|
||||
background: "var(--ol-surface-2)",
|
||||
background: "var(--ol-control-elevated)",
|
||||
width: "100%",
|
||||
maxWidth: 360,
|
||||
transition:
|
||||
|
||||
@@ -14,9 +14,9 @@ html[data-ol-platform="linux"] #root {
|
||||
}
|
||||
|
||||
body {
|
||||
background: transparent;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
background: var(--ol-bg-base);
|
||||
}
|
||||
|
||||
#root {
|
||||
@@ -70,6 +70,24 @@ input, textarea {
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
|
||||
.ol-app-shell-bg {
|
||||
background: var(--ol-app-shell-bg);
|
||||
}
|
||||
|
||||
.ol-aura-panel {
|
||||
background: var(--ol-panel-bg);
|
||||
backdrop-filter: blur(var(--ol-aura-glass-blur)) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(var(--ol-aura-glass-blur)) saturate(150%);
|
||||
border: 1px solid var(--ol-panel-border);
|
||||
box-shadow: var(--ol-panel-shadow);
|
||||
}
|
||||
|
||||
.ol-aura-card {
|
||||
background: var(--ol-card-bg);
|
||||
border: 1px solid var(--ol-card-border);
|
||||
box-shadow: var(--ol-card-shadow);
|
||||
}
|
||||
|
||||
@keyframes ol-window-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
/* OpenLess design tokens — black + white + electric blue accent, glassy */
|
||||
/* OpenLess design tokens with Aura-specific extensions. */
|
||||
|
||||
:root {
|
||||
/* Palette — neutrals */
|
||||
/* Shared palette */
|
||||
--ol-white: #ffffff;
|
||||
--ol-canvas: #f7f7f8; /* outer chrome wash */
|
||||
--ol-canvas: #f7f7f8;
|
||||
--ol-surface: #ffffff;
|
||||
--ol-surface-2: #fafafa;
|
||||
--ol-surface-solid: #ffffff;
|
||||
--ol-line: rgba(0, 0, 0, 0.08);
|
||||
--ol-line-strong: rgba(0, 0, 0, 0.14);
|
||||
--ol-line-soft: rgba(0, 0, 0, 0.05);
|
||||
|
||||
/* Aura palette */
|
||||
--ol-bg-base: #f3f4f6;
|
||||
--ol-bg-elevated: #fbfbfc;
|
||||
--ol-aura-surface: rgba(255, 255, 255, 0.72);
|
||||
--ol-aura-surface-2: rgba(255, 255, 255, 0.88);
|
||||
--ol-aura-surface-solid: #ffffff;
|
||||
--ol-aura-line: rgba(15, 23, 42, 0.08);
|
||||
--ol-aura-line-strong: rgba(15, 23, 42, 0.14);
|
||||
--ol-aura-line-soft: rgba(255, 255, 255, 0.55);
|
||||
|
||||
/* Ink */
|
||||
--ol-ink: #0a0a0b;
|
||||
--ol-ink-2: #2a2a2d;
|
||||
@@ -22,19 +33,20 @@
|
||||
--ol-blue-hover: #1d4ed8;
|
||||
--ol-blue-soft: #eff4ff;
|
||||
--ol-blue-ring: rgba(37, 99, 235, 0.22);
|
||||
--ol-on-accent: #ffffff;
|
||||
|
||||
/* Glass */
|
||||
/* Shared glass */
|
||||
--ol-glass-bg: rgba(255, 255, 255, 0.62);
|
||||
--ol-glass-bg-strong: rgba(255, 255, 255, 0.78);
|
||||
--ol-glass-border: rgba(255, 255, 255, 0.7);
|
||||
--ol-glass-blur: 20px;
|
||||
--ol-glass-blur-strong: 36px;
|
||||
|
||||
/* 假毛玻璃噪点 —— qa 追问框 / capsule 语音胶囊等透明浮窗用。详见 global.css .ol-frost。
|
||||
关键点:feColorMatrix 用 matrix 把分形噪点**去色 + 强制 alpha=1**(不透明灰度噪点)。
|
||||
早期版本用 saturate=0 保留了 feTurbulence 的随机 alpha,噪点本身就半透明,再叠
|
||||
::before 的低 opacity,等于洗没了 —— 看不出磨砂颗粒。现在用不透明灰度噪点,由
|
||||
::before 的 opacity 单独、可控地决定颗粒强度。 */
|
||||
/* Aura glass */
|
||||
--ol-aura-glass-border: rgba(255, 255, 255, 0.58);
|
||||
--ol-aura-glass-blur: 24px;
|
||||
|
||||
/* Frost grain texture used by the .ol-frost helpers in global.css. */
|
||||
--ol-frost-grain: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='100'%20height='100'%3E%3Cfilter%20id='g'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.95'%20numOctaves='1'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='matrix'%20values='0.34%200.34%200.34%200%200%200.34%200.34%200.34%200%200%200.34%200.34%200.34%200%200%200%200%200%200%201'/%3E%3C/filter%3E%3Crect%20width='100'%20height='100'%20filter='url(%23g)'/%3E%3C/svg%3E");
|
||||
|
||||
/* Motion */
|
||||
@@ -47,25 +59,263 @@
|
||||
--ol-shadow-md: 0 1px 2px rgba(15, 17, 22, 0.05), 0 6px 24px -12px rgba(15, 17, 22, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.04);
|
||||
--ol-shadow-lg: 0 20px 60px -20px rgba(15, 17, 22, 0.18), 0 8px 32px -16px rgba(15, 17, 22, 0.10), 0 0 0 0.5px rgba(0, 0, 0, 0.06);
|
||||
--ol-shadow-xl: 0 40px 120px -40px rgba(15, 17, 22, 0.30), 0 24px 60px -24px rgba(15, 17, 22, 0.15), 0 0 0 0.5px rgba(0, 0, 0, 0.06);
|
||||
--ol-aura-shadow: 0 24px 80px -32px rgba(15, 23, 42, 0.24), 0 12px 36px -18px rgba(15, 23, 42, 0.12);
|
||||
--ol-aura-shadow-soft: 0 10px 30px -18px rgba(15, 23, 42, 0.16), 0 0 0 0.5px rgba(255, 255, 255, 0.55) inset;
|
||||
|
||||
/* Radii */
|
||||
--ol-shell-radius: 32px;
|
||||
--ol-panel-radius: 28px;
|
||||
--ol-card-radius: 22px;
|
||||
--ol-pill-radius: 999px;
|
||||
--ol-r-sm: 6px;
|
||||
--ol-r-md: 10px;
|
||||
--ol-r-lg: 14px;
|
||||
--ol-r-xl: 18px;
|
||||
--ol-r-2xl: 22px;
|
||||
--ol-r-pill: 999px;
|
||||
--ol-r-2xl: var(--ol-card-radius);
|
||||
--ol-r-pill: var(--ol-pill-radius);
|
||||
|
||||
/* Typography */
|
||||
--ol-font-display: "Aptos", "Segoe UI Variable Display", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
--ol-font-sans: system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
--ol-font-mono: "SF Mono", "Cascadia Code", Consolas, monospace;
|
||||
|
||||
/* Status colors (sparse use only) */
|
||||
/* Status colors */
|
||||
--ol-ok: #16a34a;
|
||||
--ol-ok-soft: #ecfdf5;
|
||||
--ol-warn: #d97706;
|
||||
--ol-warn-soft: #fff7ed;
|
||||
--ol-err: #dc2626;
|
||||
|
||||
/* Theme-aware shell and control tokens */
|
||||
--ol-app-shell-bg:
|
||||
radial-gradient(circle at top left, rgba(255, 255, 255, 0.68), rgba(255, 255, 255, 0) 38%),
|
||||
radial-gradient(circle at 86% 18%, rgba(47, 109, 246, 0.08), rgba(47, 109, 246, 0) 30%),
|
||||
linear-gradient(180deg, var(--ol-bg-elevated) 0%, var(--ol-bg-base) 100%);
|
||||
--ol-window-bg:
|
||||
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0) 60%),
|
||||
radial-gradient(100% 70% at 100% 100%, rgba(37,99,235,0.07) 0%, rgba(37,99,235,0) 55%),
|
||||
linear-gradient(180deg, rgba(245,245,247,0.92) 0%, rgba(232,232,236,0.92) 100%);
|
||||
--ol-window-border: rgba(0, 0, 0, 0.10);
|
||||
--ol-linux-titlebar-bg: rgba(245, 245, 247, 0.85);
|
||||
--ol-linux-titlebar-border: rgba(0, 0, 0, 0.08);
|
||||
|
||||
--ol-panel-bg: var(--ol-aura-surface);
|
||||
--ol-panel-border: var(--ol-aura-glass-border);
|
||||
--ol-panel-shadow: var(--ol-aura-shadow);
|
||||
--ol-card-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(252, 252, 253, 0.82));
|
||||
--ol-card-border: rgba(255, 255, 255, 0.74);
|
||||
--ol-card-shadow: var(--ol-aura-shadow-soft);
|
||||
|
||||
--ol-sidebar-bg: linear-gradient(180deg, rgba(255,255,255,0.34), rgba(255,255,255,0.12));
|
||||
--ol-sidebar-border: rgba(255,255,255,0.38);
|
||||
--ol-sidebar-brand-bg: linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0.36));
|
||||
--ol-sidebar-brand-border: rgba(255,255,255,0.50);
|
||||
--ol-sidebar-pill-bg: linear-gradient(180deg, rgba(255,255,255,0.84), rgba(255,255,255,0.58));
|
||||
--ol-sidebar-pill-border: rgba(255,255,255,0.62);
|
||||
--ol-sidebar-footer-border: rgba(255,255,255,0.36);
|
||||
--ol-sidebar-version-bg: linear-gradient(180deg, rgba(255,255,255,0.68), rgba(255,255,255,0.34));
|
||||
--ol-sidebar-version-border: rgba(255,255,255,0.56);
|
||||
--ol-sidebar-settings-bg: linear-gradient(180deg, rgba(255,255,255,0.48), rgba(255,255,255,0.24));
|
||||
--ol-sidebar-settings-border: rgba(255,255,255,0.50);
|
||||
--ol-sidebar-settings-active-bg: linear-gradient(180deg, rgba(255,255,255,0.82), rgba(255,255,255,0.48));
|
||||
--ol-nav-hover-bg: rgba(0, 0, 0, 0.04);
|
||||
|
||||
--ol-settings-rail-bg: linear-gradient(180deg, rgba(255,255,255,0.58), rgba(246,248,252,0.72));
|
||||
--ol-settings-rail-border: rgba(255,255,255,0.52);
|
||||
--ol-settings-links-border: rgba(255,255,255,0.46);
|
||||
--ol-settings-content-bg: linear-gradient(180deg, rgba(255,255,255,0.56), rgba(249,250,252,0.74));
|
||||
--ol-settings-close-bg: rgba(255,255,255,0.38);
|
||||
--ol-settings-close-hover-bg: rgba(255,255,255,0.72);
|
||||
|
||||
--ol-control-muted: rgba(255,255,255,0.42);
|
||||
--ol-control-muted-strong: rgba(255,255,255,0.58);
|
||||
--ol-control-border: rgba(255,255,255,0.60);
|
||||
--ol-control-solid: var(--ol-surface);
|
||||
--ol-control-elevated: var(--ol-surface-2);
|
||||
--ol-control-shadow: 0 10px 20px -18px rgba(15,23,42,0.2);
|
||||
--ol-pill-bg: linear-gradient(180deg, rgba(255,255,255,0.84), rgba(245,247,250,0.68));
|
||||
--ol-pill-border: rgba(148,163,184,0.22);
|
||||
--ol-pill-shadow: inset 0 1px 0 0 rgba(255,255,255,0.72), 0 10px 20px -18px rgba(15,23,42,0.18);
|
||||
--ol-pill-blue-bg: rgba(96,165,250,0.14);
|
||||
--ol-pill-blue-border: rgba(96,165,250,0.24);
|
||||
--ol-pill-ok-bg: rgba(74,222,128,0.14);
|
||||
--ol-pill-ok-border: rgba(74,222,128,0.22);
|
||||
--ol-pill-dark-bg: linear-gradient(180deg, rgba(28,34,45,0.92), rgba(18,23,31,0.88));
|
||||
--ol-pill-dark-border: rgba(15,23,42,0.10);
|
||||
--ol-pill-dark-shadow: 0 12px 20px -18px rgba(15,23,42,0.22);
|
||||
--ol-segmented-bg: rgba(0,0,0,0.04);
|
||||
--ol-segmented-active-bg: #ffffff;
|
||||
--ol-segmented-active-shadow: 0 1px 2px rgba(0,0,0,.06), 0 0 0 0.5px rgba(0,0,0,.06);
|
||||
--ol-toggle-off-bg: rgba(0,0,0,0.15);
|
||||
--ol-toggle-knob: #ffffff;
|
||||
--ol-overlay-bg: rgba(15,17,22,0.32);
|
||||
|
||||
--ol-style-shell-bg: linear-gradient(180deg, rgba(255,255,255,0.84), rgba(249,250,252,0.72));
|
||||
--ol-style-shell-border: rgba(255,255,255,0.72);
|
||||
--ol-style-shell-shadow: 0 32px 80px -40px rgba(15, 23, 42, 0.26);
|
||||
--ol-style-card-bg: linear-gradient(180deg, rgba(255,255,255,0.96), rgba(247,249,252,0.90));
|
||||
--ol-style-card-bg-active: linear-gradient(180deg, rgba(255,255,255,0.98), rgba(244,248,255,0.94));
|
||||
--ol-style-card-border: rgba(255,255,255,0.82);
|
||||
--ol-style-card-border-active: rgba(96,165,250,0.82);
|
||||
--ol-style-card-shadow: 0 24px 48px -34px rgba(15, 23, 42, 0.18), inset 0 1px 0 0 rgba(255,255,255,0.65);
|
||||
--ol-style-card-shadow-active: 0 24px 56px -28px rgba(37,99,235,0.28), 0 0 0 1px rgba(96,165,250,0.78), 0 0 0 4px rgba(96,165,250,0.16), inset 0 1px 0 0 rgba(255,255,255,0.72);
|
||||
--ol-style-card-ink: #0f172a;
|
||||
--ol-style-card-ink-2: rgba(15, 23, 42, 0.82);
|
||||
--ol-style-card-ink-3: rgba(15, 23, 42, 0.56);
|
||||
--ol-style-card-icon-bg: rgba(226,232,240,0.62);
|
||||
--ol-style-card-icon-bg-active: rgba(96,165,250,0.14);
|
||||
--ol-style-delete-bg: rgba(255,255,255,0.56);
|
||||
--ol-style-delete-border: rgba(248,113,113,0.36);
|
||||
--ol-style-add-tile-bg: rgba(255,255,255,0.12);
|
||||
--ol-style-add-tile-icon-bg: rgba(255,255,255,0.48);
|
||||
--ol-style-add-tile-border: rgba(255,255,255,0.32);
|
||||
--ol-style-input-bg: rgba(255,255,255,0.86);
|
||||
--ol-style-input-border: rgba(255,255,255,0.74);
|
||||
--ol-style-input-shadow: inset 0 1px 0 0 rgba(255,255,255,0.84), 0 10px 24px -22px rgba(15,23,42,0.16);
|
||||
--ol-style-subtle-bg: rgba(255,255,255,0.74);
|
||||
--ol-style-subtle-border: rgba(255,255,255,0.62);
|
||||
--ol-style-subtle-shadow: inset 0 1px 0 0 rgba(255,255,255,0.74);
|
||||
--ol-style-editor-bg: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(247,249,252,0.82));
|
||||
--ol-style-editor-border: rgba(255,255,255,0.74);
|
||||
--ol-style-editor-shadow: 0 32px 96px -32px rgba(15,23,42,0.34);
|
||||
}
|
||||
|
||||
[data-ol-theme='dark'] {
|
||||
--ol-white: #11141a;
|
||||
--ol-canvas: #0b0e13;
|
||||
--ol-surface: #141922;
|
||||
--ol-surface-2: #1a202b;
|
||||
--ol-surface-solid: #171d27;
|
||||
--ol-line: rgba(255, 255, 255, 0.09);
|
||||
--ol-line-strong: rgba(255, 255, 255, 0.16);
|
||||
--ol-line-soft: rgba(255, 255, 255, 0.07);
|
||||
|
||||
--ol-bg-base: #0a0d12;
|
||||
--ol-bg-elevated: #10151d;
|
||||
--ol-aura-surface: rgba(17, 23, 31, 0.76);
|
||||
--ol-aura-surface-2: rgba(22, 29, 39, 0.88);
|
||||
--ol-aura-surface-solid: #171d26;
|
||||
--ol-aura-line: rgba(255, 255, 255, 0.10);
|
||||
--ol-aura-line-strong: rgba(255, 255, 255, 0.16);
|
||||
--ol-aura-line-soft: rgba(255, 255, 255, 0.08);
|
||||
|
||||
--ol-ink: #f4f7fb;
|
||||
--ol-ink-2: #d8dfeb;
|
||||
--ol-ink-3: rgba(244, 247, 251, 0.74);
|
||||
--ol-ink-4: rgba(244, 247, 251, 0.50);
|
||||
--ol-ink-5: rgba(244, 247, 251, 0.26);
|
||||
|
||||
--ol-blue: #60a5fa;
|
||||
--ol-blue-hover: #3b82f6;
|
||||
--ol-blue-soft: rgba(96, 165, 250, 0.14);
|
||||
--ol-blue-ring: rgba(96, 165, 250, 0.30);
|
||||
--ol-on-accent: #f8fbff;
|
||||
|
||||
--ol-glass-bg: rgba(15, 19, 27, 0.68);
|
||||
--ol-glass-bg-strong: rgba(17, 22, 30, 0.84);
|
||||
--ol-glass-border: rgba(255, 255, 255, 0.10);
|
||||
--ol-aura-glass-border: rgba(255, 255, 255, 0.10);
|
||||
|
||||
--ol-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.34), 0 0 0 0.5px rgba(255, 255, 255, 0.04);
|
||||
--ol-shadow-md: 0 10px 30px -20px rgba(0, 0, 0, 0.42), 0 0 0 0.5px rgba(255, 255, 255, 0.05);
|
||||
--ol-shadow-lg: 0 24px 72px -32px rgba(0, 0, 0, 0.58), 0 0 0 0.5px rgba(255, 255, 255, 0.05);
|
||||
--ol-shadow-xl: 0 48px 120px -52px rgba(0, 0, 0, 0.72), 0 0 0 0.5px rgba(255, 255, 255, 0.06);
|
||||
--ol-aura-shadow: 0 28px 90px -44px rgba(0, 0, 0, 0.72), 0 12px 36px -24px rgba(2, 8, 20, 0.48);
|
||||
--ol-aura-shadow-soft: 0 18px 40px -28px rgba(0, 0, 0, 0.62), 0 0 0 0.5px rgba(255, 255, 255, 0.06) inset;
|
||||
|
||||
--ol-ok: #4ade80;
|
||||
--ol-ok-soft: rgba(74, 222, 128, 0.14);
|
||||
--ol-warn: #f59e0b;
|
||||
--ol-warn-soft: rgba(245, 158, 11, 0.14);
|
||||
--ol-err: #f87171;
|
||||
|
||||
--ol-app-shell-bg: linear-gradient(180deg, #050607 0%, #090a0c 100%);
|
||||
--ol-window-bg:
|
||||
radial-gradient(120% 80% at 0% 0%, rgba(255,255,255,0.04) 0%, rgba(255,255,255,0) 42%),
|
||||
linear-gradient(180deg, rgba(31,31,34,0.98) 0%, rgba(23,23,25,0.98) 100%);
|
||||
--ol-window-border: rgba(255,255,255,0.08);
|
||||
--ol-linux-titlebar-bg: rgba(15, 20, 28, 0.90);
|
||||
--ol-linux-titlebar-border: rgba(255,255,255,0.08);
|
||||
|
||||
--ol-panel-bg: linear-gradient(180deg, rgba(33,33,36,0.96), rgba(28,28,31,0.94));
|
||||
--ol-panel-border: rgba(255,255,255,0.07);
|
||||
--ol-panel-shadow: 0 18px 44px -34px rgba(0,0,0,0.62);
|
||||
--ol-card-bg: linear-gradient(180deg, rgba(34,34,37,0.96), rgba(29,29,32,0.94));
|
||||
--ol-card-border: rgba(255,255,255,0.06);
|
||||
--ol-card-shadow: none;
|
||||
|
||||
--ol-sidebar-bg: transparent;
|
||||
--ol-sidebar-border: rgba(255,255,255,0.06);
|
||||
--ol-sidebar-brand-bg: transparent;
|
||||
--ol-sidebar-brand-border: transparent;
|
||||
--ol-sidebar-pill-bg: rgba(255,255,255,0.06);
|
||||
--ol-sidebar-pill-border: rgba(255,255,255,0.07);
|
||||
--ol-sidebar-footer-border: rgba(255,255,255,0.07);
|
||||
--ol-sidebar-version-bg: transparent;
|
||||
--ol-sidebar-version-border: transparent;
|
||||
--ol-sidebar-settings-bg: transparent;
|
||||
--ol-sidebar-settings-border: transparent;
|
||||
--ol-sidebar-settings-active-bg: rgba(255,255,255,0.08);
|
||||
--ol-nav-hover-bg: rgba(255,255,255,0.05);
|
||||
|
||||
--ol-settings-rail-bg: rgba(255,255,255,0.02);
|
||||
--ol-settings-rail-border: rgba(255,255,255,0.06);
|
||||
--ol-settings-links-border: rgba(255,255,255,0.06);
|
||||
--ol-settings-content-bg: linear-gradient(180deg, rgba(29,29,32,0.96), rgba(24,24,27,0.94));
|
||||
--ol-settings-close-bg: rgba(255,255,255,0.06);
|
||||
--ol-settings-close-hover-bg: rgba(255,255,255,0.14);
|
||||
|
||||
--ol-control-muted: rgba(255,255,255,0.05);
|
||||
--ol-control-muted-strong: rgba(255,255,255,0.08);
|
||||
--ol-control-border: rgba(255,255,255,0.08);
|
||||
--ol-control-solid: rgba(24,31,41,0.96);
|
||||
--ol-control-elevated: rgba(21,27,36,0.98);
|
||||
--ol-control-shadow: none;
|
||||
--ol-pill-bg: rgba(255,255,255,0.05);
|
||||
--ol-pill-border: rgba(255,255,255,0.08);
|
||||
--ol-pill-shadow: none;
|
||||
--ol-pill-blue-bg: rgba(59,130,246,0.14);
|
||||
--ol-pill-blue-border: rgba(96,165,250,0.28);
|
||||
--ol-pill-ok-bg: rgba(74,222,128,0.16);
|
||||
--ol-pill-ok-border: rgba(74,222,128,0.24);
|
||||
--ol-pill-dark-bg: rgba(255,255,255,0.08);
|
||||
--ol-pill-dark-border: rgba(255,255,255,0.12);
|
||||
--ol-pill-dark-shadow: none;
|
||||
--ol-segmented-bg: rgba(255,255,255,0.06);
|
||||
--ol-segmented-active-bg: rgba(255,255,255,0.12);
|
||||
--ol-segmented-active-shadow: none;
|
||||
--ol-toggle-off-bg: rgba(255,255,255,0.16);
|
||||
--ol-toggle-knob: #f8fbff;
|
||||
--ol-overlay-bg: rgba(4, 8, 14, 0.58);
|
||||
|
||||
--ol-style-shell-bg: linear-gradient(180deg, rgba(29,29,32,0.94), rgba(24,24,27,0.92));
|
||||
--ol-style-shell-border: rgba(255,255,255,0.07);
|
||||
--ol-style-shell-shadow: none;
|
||||
--ol-style-card-bg: rgba(34,34,37,0.94);
|
||||
--ol-style-card-bg-active: rgba(39,39,43,0.96);
|
||||
--ol-style-card-border: rgba(255,255,255,0.07);
|
||||
--ol-style-card-border-active: rgba(96,165,250,0.72);
|
||||
--ol-style-card-shadow: none;
|
||||
--ol-style-card-shadow-active: 0 0 0 1px rgba(96,165,250,0.72);
|
||||
--ol-style-card-ink: #f4f7fb;
|
||||
--ol-style-card-ink-2: rgba(244,247,251,0.86);
|
||||
--ol-style-card-ink-3: rgba(244,247,251,0.64);
|
||||
--ol-style-card-icon-bg: rgba(255,255,255,0.06);
|
||||
--ol-style-card-icon-bg-active: rgba(96,165,250,0.16);
|
||||
--ol-style-delete-bg: rgba(255,255,255,0.05);
|
||||
--ol-style-delete-border: rgba(248,113,113,0.34);
|
||||
--ol-style-add-tile-bg: rgba(255,255,255,0.02);
|
||||
--ol-style-add-tile-icon-bg: rgba(255,255,255,0.10);
|
||||
--ol-style-add-tile-border: rgba(255,255,255,0.10);
|
||||
--ol-style-input-bg: rgba(255,255,255,0.04);
|
||||
--ol-style-input-border: rgba(255,255,255,0.08);
|
||||
--ol-style-input-shadow: none;
|
||||
--ol-style-subtle-bg: rgba(255,255,255,0.03);
|
||||
--ol-style-subtle-border: rgba(255,255,255,0.06);
|
||||
--ol-style-subtle-shadow: none;
|
||||
--ol-style-editor-bg: linear-gradient(180deg, rgba(27,27,30,0.96), rgba(23,23,26,0.94));
|
||||
--ol-style-editor-border: rgba(255,255,255,0.07);
|
||||
--ol-style-editor-shadow: none;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
@@ -83,7 +333,7 @@ body {
|
||||
.ol-noscrollbar::-webkit-scrollbar { display: none; }
|
||||
.ol-noscrollbar { scrollbar-width: none; }
|
||||
|
||||
/* Thin scrollbar — 细而可见,保留"可以滚"的可见性,但不抢眼。issue #243 */
|
||||
/* Thin scrollbar */
|
||||
.ol-thinscroll::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.ol-thinscroll::-webkit-scrollbar-track { background: transparent; }
|
||||
.ol-thinscroll::-webkit-scrollbar-thumb {
|
||||
|
||||
Reference in new issue
Block a user