sync: update from private repo (d8074a7)
Some checks failed
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync 2026-06-05 06:05:30 +00:00
parent a44f6b41e2
commit e00ea9fb0c
91 changed files with 530 additions and 257 deletions

4
.gitignore vendored
View File

@ -16,6 +16,10 @@ logs/
.claude/ .claude/
.playwright-mcp/ .playwright-mcp/
.superpowers/ .superpowers/
# Agent skill installs (modern-web-guidance etc. via `skills add`) — local tooling,
# not project source.
.agents/
skills-lock.json
orch.pid orch.pid
.server.pid .server.pid
src/generated/ src/generated/

View File

@ -1,6 +1,6 @@
// src/config-manager.test.ts // src/config-manager.test.ts
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
import { mkdtempSync, writeFileSync, readFileSync } from 'fs'; import { mkdtempSync, writeFileSync, readFileSync, existsSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import { ConfigManager } from './config-manager.js'; import { ConfigManager } from './config-manager.js';
@ -85,6 +85,36 @@ describe('ConfigManager', () => {
expect(cm.getConfig().provider.workers[0]?.model).toBe('new-model'); expect(cm.getConfig().provider.workers[0]?.model).toBe('new-model');
}); });
it('creates config.yaml on first save when the file does not exist (fresh install on defaults)', () => {
// Fresh OSS deploy: the server boots on defaults with no config.yaml on
// disk. The first Settings-UI save must CREATE the file, not crash with
// ENOENT trying to back up a non-existent file.
const freshPath = join(tempDir, 'fresh-config.yaml');
expect(existsSync(freshPath)).toBe(false);
const cm = new ConfigManager(freshPath);
const result = cm.updateConfig({
llm: {
workers: [{
id: 'w1',
connectionType: 'direct',
endpoint: 'http://host:11434/v1',
model: 'qwen3:8b',
roles: ['auto', 'fast'],
maxConcurrency: 1,
enabled: true,
}],
},
});
expect(result.ok).toBe(true);
expect(existsSync(freshPath)).toBe(true);
const raw = readFileSync(freshPath, 'utf-8');
expect(raw).toContain('config_version: 2');
expect(raw).toContain('http://host:11434/v1');
expect(cm.getConfig().provider.workers[0]?.model).toBe('qwen3:8b');
});
it('rejects invalid config (unparseable YAML file)', () => { it('rejects invalid config (unparseable YAML file)', () => {
const cm = new ConfigManager(configPath); const cm = new ConfigManager(configPath);
// Corrupt the file, then try to reload — loadConfig will fall back to defaults // Corrupt the file, then try to reload — loadConfig will fall back to defaults

View File

@ -1,6 +1,6 @@
// src/config-manager.ts // src/config-manager.ts
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { readFileSync, writeFileSync, statSync } from 'fs'; import { readFileSync, writeFileSync, statSync, existsSync, unlinkSync } from 'fs';
import { stringify } from 'yaml'; import { stringify } from 'yaml';
import { loadConfig, toSnakeKeys, type AppConfig } from './config.js'; import { loadConfig, toSnakeKeys, type AppConfig } from './config.js';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
@ -155,15 +155,25 @@ export class ConfigManager {
const snakeConfig = toSnakeKeys(merged) as Record<string, unknown>; const snakeConfig = toSnakeKeys(merged) as Record<string, unknown>;
const yamlStr = stringify(snakeConfig, { lineWidth: 120 }); const yamlStr = stringify(snakeConfig, { lineWidth: 120 });
// Validate BEFORE writing: backup, write, validate, rollback on failure // Validate BEFORE writing: backup, write, validate, rollback on failure.
const backupContent = readFileSync(this.configPath, 'utf-8'); // config.yaml may not exist yet — a fresh install boots on defaults with no
// file on disk, and the first save must CREATE it rather than ENOENT trying
// to back up a missing file. With no prior content, a failed save is rolled
// back by removing the file we just created.
const backupContent = existsSync(this.configPath)
? readFileSync(this.configPath, 'utf-8')
: null;
try { try {
writeFileSync(this.configPath, yamlStr, 'utf-8'); writeFileSync(this.configPath, yamlStr, 'utf-8');
logger.info(`[config-manager] config written to ${this.configPath}`); logger.info(`[config-manager] config written to ${this.configPath}`);
this.currentConfig = loadConfig(this.configPath); this.currentConfig = loadConfig(this.configPath);
} catch (e) { } catch (e) {
// Restore original file on validation failure // Restore original on validation failure (or drop the file we created).
writeFileSync(this.configPath, backupContent, 'utf-8'); if (backupContent !== null) {
writeFileSync(this.configPath, backupContent, 'utf-8');
} else {
try { unlinkSync(this.configPath); } catch { /* nothing to revert */ }
}
logger.warn(`[config-manager] config update failed, reverted: ${e}`); logger.warn(`[config-manager] config update failed, reverted: ${e}`);
return { ok: false, errors: e, message: 'Invalid config — changes reverted' }; return { ok: false, errors: e, message: 'Invalid config — changes reverted' };
} }

View File

@ -3,6 +3,21 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="color-scheme" content="light dark" />
<script>
// Set data-theme before first paint to avoid a light flash in dark mode.
(function () {
try {
var p = localStorage.getItem('maestro.theme');
if (p !== 'light' && p !== 'dark' && p !== 'system') p = 'system';
var dark = p === 'dark' ||
(p === 'system' && matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
} catch (e) {
document.documentElement.dataset.theme = 'light';
}
})();
</script>
<link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/ui/favicon.svg" />
<link rel="manifest" href="/ui/manifest.webmanifest" /> <link rel="manifest" href="/ui/manifest.webmanifest" />
<meta name="theme-color" content="#2563eb" /> <meta name="theme-color" content="#2563eb" />
@ -23,10 +38,13 @@
min-height: 100dvh; min-height: 100dvh;
} }
html { background: #ffffff; }
html[data-theme="dark"] { background: #0a0a0c; }
body { body {
margin: 0; margin: 0;
overflow-x: hidden; overflow-x: hidden;
background: #f3f6fb; background: transparent;
} }
input, input,

View File

@ -411,7 +411,7 @@ function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnable
<div className="block sm:hidden h-full"> <div className="block sm:hidden h-full">
{!panelOpen ? ( {!panelOpen ? (
<div className="p-2 h-full"> <div className="p-2 h-full">
<div className="bg-white border border-hairline rounded-md h-full overflow-hidden"> <div className="bg-canvas border border-hairline rounded-md h-full overflow-hidden">
<TaskListWithSidePanel <TaskListWithSidePanel
upper={<div className="h-full overflow-hidden p-3"><TaskListPanel {...taskListProps} /></div>} upper={<div className="h-full overflow-hidden p-3"><TaskListPanel {...taskListProps} /></div>}
activeWidgetSlug={dashboardWidget} activeWidgetSlug={dashboardWidget}
@ -421,7 +421,7 @@ function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnable
</div> </div>
) : ( ) : (
<MobileDetailFlow mobileTab={mobileTab} onTabChange={(id) => setUrlState(prev => ({ ...prev, mobileTab: id }))} onSwipeRightFromEdge={() => setUrlState(prev => ({ ...prev, taskId: null, mobileTab: 'chat' as MobileTabId }))} visibleTabs={mobileVisibleTabIds}> <MobileDetailFlow mobileTab={mobileTab} onTabChange={(id) => setUrlState(prev => ({ ...prev, mobileTab: id }))} onSwipeRightFromEdge={() => setUrlState(prev => ({ ...prev, taskId: null, mobileTab: 'chat' as MobileTabId }))} visibleTabs={mobileVisibleTabIds}>
<div className="flex-shrink-0 flex border-b border-hairline bg-white px-2 pt-[env(safe-area-inset-top)]"> <div className="flex-shrink-0 flex border-b border-hairline bg-canvas px-2 pt-[env(safe-area-inset-top)]">
{mobileVisibleTabs.map(({ id, label }) => ( {mobileVisibleTabs.map(({ id, label }) => (
<button <button
key={id} key={id}
@ -489,14 +489,14 @@ function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnable
{/* タブレット: 2カラム (sm 〜 lg) */} {/* タブレット: 2カラム (sm 〜 lg) */}
<div className="hidden sm:grid lg:hidden gap-2 p-2 h-full" style={{ gridTemplateColumns: 'clamp(220px, 30vw, 280px) minmax(0, 1fr)' }}> <div className="hidden sm:grid lg:hidden gap-2 p-2 h-full" style={{ gridTemplateColumns: 'clamp(220px, 30vw, 280px) minmax(0, 1fr)' }}>
<div className="bg-white border border-hairline rounded-md overflow-hidden"> <div className="bg-canvas border border-hairline rounded-md overflow-hidden">
<TaskListWithSidePanel <TaskListWithSidePanel
upper={<div className="h-full overflow-hidden p-3"><TaskListPanel {...taskListProps} /></div>} upper={<div className="h-full overflow-hidden p-3"><TaskListPanel {...taskListProps} /></div>}
activeWidgetSlug={dashboardWidget} activeWidgetSlug={dashboardWidget}
onActiveWidgetSlugChange={setDashboardWidget} onActiveWidgetSlugChange={setDashboardWidget}
/> />
</div> </div>
<div className="bg-white border border-hairline rounded-md overflow-hidden"> <div className="bg-canvas border border-hairline rounded-md overflow-hidden">
{chatReady ? ( {chatReady ? (
<ChatPane task={localTask!} comments={localComments} onSubmit={handleComment} onCancel={handleCancel} onOpenDetail={() => setTabletDetailOpen(true)} /> <ChatPane task={localTask!} comments={localComments} onSubmit={handleComment} onCancel={handleCancel} onOpenDetail={() => setTabletDetailOpen(true)} />
) : panelOpen ? ( ) : panelOpen ? (
@ -514,7 +514,7 @@ function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnable
style={gridStyle} style={gridStyle}
> >
{/* col 1: list or rail. wrapper が bg/border を保持。 */} {/* col 1: list or rail. wrapper が bg/border を保持。 */}
<div className="bg-white border border-hairline rounded-md overflow-hidden"> <div className="bg-canvas border border-hairline rounded-md overflow-hidden">
<TaskListWithSidePanel <TaskListWithSidePanel
upper={ upper={
isFocused isFocused
@ -531,7 +531,7 @@ function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnable
/> />
</div> </div>
{/* col 2: Chat */} {/* col 2: Chat */}
<div className="bg-white border border-hairline rounded-md overflow-hidden"> <div className="bg-canvas border border-hairline rounded-md overflow-hidden">
{chatReady ? ( {chatReady ? (
<ChatPane task={localTask!} comments={localComments} onSubmit={handleComment} onCancel={handleCancel} /> <ChatPane task={localTask!} comments={localComments} onSubmit={handleComment} onCancel={handleCancel} />
) : panelOpen ? ( ) : panelOpen ? (
@ -557,7 +557,7 @@ function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnable
)} )}
{/* col 4: Workspace (detail) */} {/* col 4: Workspace (detail) */}
{panelOpen && ( {panelOpen && (
<div className="bg-white border border-hairline rounded-md overflow-hidden"> <div className="bg-canvas border border-hairline rounded-md overflow-hidden">
{localTaskId && <LocalDetailPanel {...detailPanelProps()} />} {localTaskId && <LocalDetailPanel {...detailPanelProps()} />}
</div> </div>
)} )}
@ -567,7 +567,7 @@ function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnable
{/* Tablet: detail overlay */} {/* Tablet: detail overlay */}
{tabletDetailOpen && panelOpen && ( {tabletDetailOpen && panelOpen && (
<div className="hidden sm:block lg:hidden fixed inset-0 bg-black/40 z-40" onClick={() => setTabletDetailOpen(false)}> <div className="hidden sm:block lg:hidden fixed inset-0 bg-black/40 z-40" onClick={() => setTabletDetailOpen(false)}>
<div className="absolute right-0 top-0 bottom-0 bg-white shadow-2xl flex flex-col overflow-hidden" style={{ width: 'min(480px, 90vw)' }} onClick={e => e.stopPropagation()}> <div className="absolute right-0 top-0 bottom-0 bg-canvas shadow-2xl flex flex-col overflow-hidden" style={{ width: 'min(480px, 90vw)' }} onClick={e => e.stopPropagation()}>
{localTaskId && ( {localTaskId && (
<LocalDetailPanel <LocalDetailPanel
{...detailPanelProps({ {...detailPanelProps({
@ -602,7 +602,7 @@ function AppInner({ isAdmin, authEnabled, user }: { isAdmin: boolean; authEnable
/> />
)} )}
{branding.footerText && ( {branding.footerText && (
<footer className="flex-shrink-0 border-t border-slate-200 bg-white px-4 py-1.5 text-[10px] text-slate-500 text-center"> <footer className="flex-shrink-0 border-t border-slate-200 bg-canvas px-4 py-1.5 text-[10px] text-slate-500 text-center">
{branding.footerText} {branding.footerText}
</footer> </footer>
)} )}

View File

@ -30,7 +30,7 @@ export const ActivityEventCard = memo(function ActivityEventCard({ event, isLast
<div className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${colors.dot}`} /> <div className={`w-2.5 h-2.5 rounded-full flex-shrink-0 ${colors.dot}`} />
{!isLast && <div className="flex-1 w-px bg-slate-200 mt-1" />} {!isLast && <div className="flex-1 w-px bg-slate-200 mt-1" />}
</div> </div>
<div className={`border rounded-xl p-2.5 mb-2.5 ${colors.border} bg-white`}> <div className={`border rounded-xl p-2.5 mb-2.5 ${colors.border} bg-canvas`}>
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold font-mono tracking-wide ${colors.badge} ${colors.badgeText}`}> <span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold font-mono tracking-wide ${colors.badge} ${colors.badgeText}`}>
{activityKindLabel(event.kind)} {activityKindLabel(event.kind)}

View File

@ -27,7 +27,7 @@ const REASON_HINT: Record<string, string> = {
export function PipButton({ pip, className, hideWhenUnsupported = true }: Props) { export function PipButton({ pip, className, hideWhenUnsupported = true }: Props) {
if (!pip.supported && hideWhenUnsupported) return null; if (!pip.supported && hideWhenUnsupported) return null;
const baseClass = 'text-2xs px-2 py-1 rounded-md border border-hairline bg-white hover:bg-surface text-slate-700 disabled:opacity-50'; const baseClass = 'text-2xs px-2 py-1 rounded-md border border-hairline bg-canvas hover:bg-surface text-slate-700 disabled:opacity-50';
const merged = className ? `${baseClass} ${className}` : baseClass; const merged = className ? `${baseClass} ${className}` : baseClass;
if (!pip.supported) { if (!pip.supported) {

View File

@ -23,7 +23,7 @@ export function SaveRecordingButton({ taskId, className }: Props) {
const [linkVisible, setLinkVisible] = useState(false); const [linkVisible, setLinkVisible] = useState(false);
const baseClass = const baseClass =
'text-2xs px-2 py-1 rounded-md border border-hairline bg-white hover:bg-surface text-slate-700 disabled:opacity-50'; 'text-2xs px-2 py-1 rounded-md border border-hairline bg-canvas hover:bg-surface text-slate-700 disabled:opacity-50';
const merged = className ? `${baseClass} ${className}` : baseClass; const merged = className ? `${baseClass} ${className}` : baseClass;
async function handleClick() { async function handleClick() {

View File

@ -153,7 +153,7 @@ function ChecklistCard({ comment }: { comment: LocalTaskComment }) {
return ( return (
<div className="flex justify-center"> <div className="flex justify-center">
<div className="bg-white border border-hairline rounded-md px-3.5 py-2.5 max-w-[90%] w-full"> <div className="bg-canvas border border-hairline rounded-md px-3.5 py-2.5 max-w-[90%] w-full">
{/* Header */} {/* Header */}
<button <button
onClick={() => setExpanded(!expanded)} onClick={() => setExpanded(!expanded)}
@ -194,7 +194,7 @@ function ChecklistCard({ comment }: { comment: LocalTaskComment }) {
</span> </span>
)} )}
{summary.remaining > 0 && ( {summary.remaining > 0 && (
<span className="inline-flex items-center gap-1 bg-white text-slate-500 border border-hairline px-1.5 py-0.5 rounded font-mono tabular-nums"> <span className="inline-flex items-center gap-1 bg-canvas text-slate-500 border border-hairline px-1.5 py-0.5 rounded font-mono tabular-nums">
<StatusIcon status="pending" />{summary.remaining} <StatusIcon status="pending" />{summary.remaining}
</span> </span>
)} )}
@ -403,7 +403,7 @@ export function ChatMessage({ comment, taskId, imageBaseUrl, isStaleThinking }:
// Fallback for unknown kinds // Fallback for unknown kinds
return ( return (
<div className="flex justify-start"> <div className="flex justify-start">
<div className="max-w-[82%] bg-white border border-slate-200 text-slate-900 rounded-2xl rounded-bl-md px-4 py-3 shadow-sm"> <div className="max-w-[82%] bg-canvas border border-slate-200 text-slate-900 rounded-2xl rounded-bl-md px-4 py-3 shadow-sm">
<div className="text-2xs text-slate-400 mb-1.5"> <div className="text-2xs text-slate-400 mb-1.5">
{author} · {new Date(createdAt).toLocaleString()} {author} · {new Date(createdAt).toLocaleString()}
</div> </div>

View File

@ -234,7 +234,7 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
className="hidden sm:block" className="hidden sm:block"
/> />
{/* Header */} {/* Header */}
<div className="flex-shrink-0 border-b border-hairline bg-white px-4 py-2.5"> <div className="flex-shrink-0 border-b border-hairline bg-canvas px-4 py-2.5">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="min-w-0"> <div className="min-w-0">
<h2 className="text-sm font-semibold text-slate-900 truncate">{task.title}</h2> <h2 className="text-sm font-semibold text-slate-900 truncate">{task.title}</h2>
@ -260,7 +260,7 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
{onOpenDetail && ( {onOpenDetail && (
<button <button
onClick={onOpenDetail} onClick={onOpenDetail}
className="px-2.5 h-7 text-2xs font-medium text-slate-700 border border-hairline bg-white hover:bg-surface rounded-md transition-colors" className="px-2.5 h-7 text-2xs font-medium text-slate-700 border border-hairline bg-canvas hover:bg-surface rounded-md transition-colors"
title="詳細を表示" title="詳細を表示"
> >
@ -336,7 +336,7 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
</div> </div>
</div> </div>
) : streamingText ? ( ) : streamingText ? (
<div className="max-w-[80%] min-w-0 px-3 py-2 bg-white border border-hairline rounded-lg text-[13px] text-slate-800 leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] opacity-70"> <div className="max-w-[80%] min-w-0 px-3 py-2 bg-canvas border border-hairline rounded-lg text-[13px] text-slate-800 leading-relaxed whitespace-pre-wrap break-words [overflow-wrap:anywhere] opacity-70">
{streamingText} {streamingText}
<span className="inline-block w-0.5 h-3.5 bg-slate-400 animate-pulse ml-0.5 align-text-bottom" /> <span className="inline-block w-0.5 h-3.5 bg-slate-400 animate-pulse ml-0.5 align-text-bottom" />
</div> </div>
@ -366,7 +366,7 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
{!isAtBottom && ( {!isAtBottom && (
<button <button
onClick={scrollToBottom} onClick={scrollToBottom}
className="absolute bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1.5 px-3 py-1.5 bg-white border border-slate-200 rounded-full shadow-md text-xs text-slate-600 hover:bg-slate-50 transition-colors z-10" className="absolute bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1.5 px-3 py-1.5 bg-canvas border border-slate-200 rounded-full shadow-md text-xs text-slate-600 hover:bg-slate-50 transition-colors z-10"
> >
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 6l4 4 4-4" /> <path d="M4 6l4 4 4-4" />
@ -381,7 +381,7 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
</div> </div>
{/* Composer */} {/* Composer */}
<div className="flex-shrink-0 border-t border-hairline bg-white p-3" style={{ paddingBottom: 'calc(12px + env(safe-area-inset-bottom, 0px))' }}> <div className="flex-shrink-0 border-t border-hairline bg-canvas p-3" style={{ paddingBottom: 'calc(12px + env(safe-area-inset-bottom, 0px))' }}>
{isBusy && ( {isBusy && (
<div className={`flex items-center gap-2 mb-2 px-2.5 py-1 rounded-md text-2xs ${ <div className={`flex items-center gap-2 mb-2 px-2.5 py-1 rounded-md text-2xs ${
canInterject canInterject
@ -402,7 +402,7 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
type="button" type="button"
onClick={() => void handleSubmit()} onClick={() => void handleSubmit()}
disabled={submitting} disabled={submitting}
className="flex-shrink-0 px-2 h-6 bg-white border border-red-200 rounded text-[10px] font-medium text-red-700 hover:bg-red-100 disabled:opacity-50" className="flex-shrink-0 px-2 h-6 bg-canvas border border-red-200 rounded text-[10px] font-medium text-red-700 hover:bg-red-100 disabled:opacity-50"
> >
</button> </button>
@ -461,7 +461,7 @@ export function ChatPane({ task, comments, onSubmit, onCancel, onOpenDetail }: C
<button <button
disabled={cancelling} disabled={cancelling}
onClick={() => void handleCancel()} onClick={() => void handleCancel()}
className="px-3 h-9 bg-white border border-red-200 text-red-700 rounded-md text-xs font-semibold disabled:opacity-50 hover:bg-red-50 flex-shrink-0 transition-colors" className="px-3 h-9 bg-canvas border border-red-200 text-red-700 rounded-md text-xs font-semibold disabled:opacity-50 hover:bg-red-50 flex-shrink-0 transition-colors"
title="エージェントの実行を停止" title="エージェントの実行を停止"
> >
{cancelling ? '停止中...' : '停止'} {cancelling ? '停止中...' : '停止'}

View File

@ -59,7 +59,7 @@ export function SubtaskInlineCard({ subtasks, subtaskCount, subtaskCompleted }:
return ( return (
<div className="flex justify-center"> <div className="flex justify-center">
<div className="bg-white border border-hairline rounded-md px-3.5 py-2.5 max-w-[90%] w-full"> <div className="bg-canvas border border-hairline rounded-md px-3.5 py-2.5 max-w-[90%] w-full">
{/* Header */} {/* Header */}
<button <button
onClick={() => setExpanded(!expanded)} onClick={() => setExpanded(!expanded)}
@ -107,7 +107,7 @@ export function SubtaskInlineCard({ subtasks, subtaskCount, subtaskCompleted }:
</span> </span>
)} )}
{subtaskCount - subtaskCompleted - running - failed > 0 && ( {subtaskCount - subtaskCompleted - running - failed > 0 && (
<span className="inline-flex items-center gap-1 bg-white text-slate-500 border border-hairline px-1.5 py-0.5 rounded font-mono tabular-nums"> <span className="inline-flex items-center gap-1 bg-canvas text-slate-500 border border-hairline px-1.5 py-0.5 rounded font-mono tabular-nums">
<SubtaskStatusIcon status="queued" />{subtaskCount - subtaskCompleted - running - failed} <SubtaskStatusIcon status="queued" />{subtaskCount - subtaskCompleted - running - failed}
</span> </span>
)} )}

View File

@ -31,7 +31,7 @@ export function AttachmentDropzone({ attachments, onFilesChange }: AttachmentDro
return ( return (
<div <div
className={`border-2 border-dashed rounded-xl p-4 transition-colors ${ className={`border-2 border-dashed rounded-xl p-4 transition-colors ${
dragOver ? 'border-accent bg-accent-soft' : 'border-slate-200 bg-white' dragOver ? 'border-accent bg-accent-soft' : 'border-slate-200 bg-canvas'
}`} }`}
onDragOver={e => { e.preventDefault(); setDragOver(true); }} onDragOver={e => { e.preventDefault(); setDragOver(true); }}
onDragLeave={() => setDragOver(false)} onDragLeave={() => setDragOver(false)}

View File

@ -143,7 +143,7 @@ export function CreateTaskDialog({ onClose, onSubmit, initialPiece, initialBody,
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-slate-900/50 z-30" /> <Dialog.Overlay className="fixed inset-0 bg-slate-900/50 z-30" />
<Dialog.Content <Dialog.Content
className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-white rounded-2xl shadow-2xl w-full overflow-auto z-40 focus:outline-none" className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-surface rounded-2xl shadow-2xl w-full overflow-auto z-40 focus:outline-none"
style={{ maxWidth: 'min(860px, 92vw)', maxHeight: '88dvh' }} style={{ maxWidth: 'min(860px, 92vw)', maxHeight: '88dvh' }}
onOpenAutoFocus={e => { onOpenAutoFocus={e => {
e.preventDefault(); e.preventDefault();

View File

@ -49,7 +49,7 @@ export function AddWidgetDialog({ open, existingSlugs, onClose, onCreate }: Prop
onClick={() => !saving && onClose()} onClick={() => !saving && onClose()}
> >
<div <div
className="bg-white rounded-md shadow-lg w-[320px] p-4 flex flex-col gap-3" className="bg-surface rounded-md shadow-lg w-[320px] p-4 flex flex-col gap-3"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div className="text-sm font-semibold"></div> <div className="text-sm font-semibold"></div>

View File

@ -19,7 +19,7 @@ export function MarkdownWidget({ widget, onSave, onDelete }: Props) {
<button <button
type="button" type="button"
onClick={() => { setDraftContent(widget.markdownContent); setEditing(true); }} onClick={() => { setDraftContent(widget.markdownContent); setEditing(true); }}
className="absolute top-2 right-2 px-2 py-1 text-[11px] bg-white border border-hairline rounded hover:bg-surface-2" className="absolute top-2 right-2 px-2 py-1 text-[11px] bg-canvas border border-hairline rounded hover:bg-surface-2"
aria-label="編集" aria-label="編集"
> >
@ -58,7 +58,7 @@ export function MarkdownWidget({ widget, onSave, onDelete }: Props) {
<button <button
type="button" type="button"
onClick={() => setEditing(false)} onClick={() => setEditing(false)}
className="px-3 py-1 bg-white border border-hairline text-xs rounded hover:bg-surface-2" className="px-3 py-1 bg-canvas border border-hairline text-xs rounded hover:bg-surface-2"
> >
</button> </button>

View File

@ -30,7 +30,7 @@ export function SideInfoPanel({
const activeWidget = widgets.find(w => w.slug === activeSlug); const activeWidget = widgets.find(w => w.slug === activeSlug);
return ( return (
<div className="flex flex-col h-full overflow-hidden bg-white"> <div className="flex flex-col h-full overflow-hidden bg-canvas">
<WidgetTabBar <WidgetTabBar
widgets={widgets} widgets={widgets}
activeSlug={activeSlug} activeSlug={activeSlug}

View File

@ -40,7 +40,7 @@ export function ContextUsageGauge({ promptTokens, limitTokens, jobStatus }: Cont
const label = pickLabel(jobStatus); const label = pickLabel(jobStatus);
return ( return (
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm"> <div className="bg-canvas border border-slate-200 rounded-xl p-4 shadow-sm">
<div className="flex items-baseline justify-between mb-2"> <div className="flex items-baseline justify-between mb-2">
<span className="text-sm font-semibold text-slate-700">{label}</span> <span className="text-sm font-semibold text-slate-700">{label}</span>
<span className="text-xs text-slate-500 tabular-nums"> <span className="text-xs text-slate-500 tabular-nums">

View File

@ -67,7 +67,7 @@ export function ContinueWithPieceDialog({
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={e => { if (e.target === e.currentTarget) onClose(); }} onClick={e => { if (e.target === e.currentTarget) onClose(); }}
> >
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg mx-4 overflow-hidden flex flex-col max-h-[90vh]"> <div className="bg-surface rounded-xl shadow-xl w-full max-w-lg mx-4 overflow-hidden flex flex-col max-h-[90vh]">
{/* Header */} {/* Header */}
<div className="flex items-center gap-3 px-5 py-4 border-b border-hairline"> <div className="flex items-center gap-3 px-5 py-4 border-b border-hairline">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">

View File

@ -70,7 +70,7 @@ function ShareButton({ taskId, shareToken, onShareChange }: { taskId: number; sh
disabled={shareMutation.isPending} disabled={shareMutation.isPending}
title={shareMutation.isPending ? '共有中...' : '公開リンクを発行'} title={shareMutation.isPending ? '共有中...' : '公開リンクを発行'}
aria-label="公開リンクを発行" aria-label="公開リンクを発行"
className={`${iconBtnBase} border-hairline bg-white text-slate-600 hover:text-slate-900 hover:bg-surface`} className={`${iconBtnBase} border-hairline bg-canvas text-slate-600 hover:text-slate-900 hover:bg-surface`}
> >
{shareMutation.isPending ? ( {shareMutation.isPending ? (
<svg className="w-3.5 h-3.5 animate-spin" viewBox="0 0 24 24" fill="none"> <svg className="w-3.5 h-3.5 animate-spin" viewBox="0 0 24 24" fill="none">
@ -102,7 +102,7 @@ function ShareButton({ taskId, shareToken, onShareChange }: { taskId: number; sh
onClick={handleCopy} onClick={handleCopy}
title={copied ? 'コピーしました' : '共有リンクをコピー'} title={copied ? 'コピーしました' : '共有リンクをコピー'}
aria-label="共有リンクをコピー" aria-label="共有リンクをコピー"
className={`${iconBtnBase} ${copied ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : 'border-hairline bg-white text-slate-600 hover:text-slate-900 hover:bg-surface'}`} className={`${iconBtnBase} ${copied ? 'border-emerald-200 bg-emerald-50 text-emerald-700' : 'border-hairline bg-canvas text-slate-600 hover:text-slate-900 hover:bg-surface'}`}
> >
{copied ? ( {copied ? (
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
@ -120,7 +120,7 @@ function ShareButton({ taskId, shareToken, onShareChange }: { taskId: number; sh
disabled={unshareMutation.isPending} disabled={unshareMutation.isPending}
title="共有を停止" title="共有を停止"
aria-label="共有を停止" aria-label="共有を停止"
className={`${iconBtnBase} border-hairline bg-white text-slate-500 hover:text-red-700 hover:border-red-200 hover:bg-red-50`} className={`${iconBtnBase} border-hairline bg-canvas text-slate-500 hover:text-red-700 hover:border-red-200 hover:bg-red-50`}
> >
{unshareMutation.isPending ? ( {unshareMutation.isPending ? (
<svg className="w-3.5 h-3.5 animate-spin" viewBox="0 0 24 24" fill="none"> <svg className="w-3.5 h-3.5 animate-spin" viewBox="0 0 24 24" fill="none">
@ -150,7 +150,7 @@ function ContinueButton({ latestJobStatus, onClick }: { latestJobStatus: string
disabled={!enabled} disabled={!enabled}
title={enabled ? '別 piece で続ける' : 'タスクが進行中のため続行できません'} title={enabled ? '別 piece で続ける' : 'タスクが進行中のため続行できません'}
aria-label="別 piece で続ける" aria-label="別 piece で続ける"
className={`${iconBtnBase} border-hairline bg-white text-slate-600 hover:text-slate-900 hover:bg-surface`} className={`${iconBtnBase} border-hairline bg-canvas text-slate-600 hover:text-slate-900 hover:bg-surface`}
> >
{/* arrow divider: cueFileBrowser refresh {/* arrow divider: cueFileBrowser refresh
() skip-forward */} () skip-forward */}
@ -168,7 +168,7 @@ export function DetailHeader({ title, subtitle, tabs, activeTab, tabTransitionPe
// renders its own mobile-level top tab bar with the same controls. // renders its own mobile-level top tab bar with the same controls.
// Two close buttons / two tab bars on iPhone was visually redundant. // Two close buttons / two tab bars on iPhone was visually redundant.
return ( return (
<div className="flex-shrink-0 border-b border-hairline bg-white px-4 pt-3 pb-3 sm:pb-0" id="detail-panel-title"> <div className="flex-shrink-0 border-b border-hairline bg-canvas px-4 pt-3 pb-3 sm:pb-0" id="detail-panel-title">
<div className="flex items-start justify-between gap-2 mb-0 sm:mb-3"> <div className="flex items-start justify-between gap-2 mb-0 sm:mb-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="text-[10px] font-mono uppercase tracking-wider text-slate-400">{subtitle}</div> <div className="text-[10px] font-mono uppercase tracking-wider text-slate-400">{subtitle}</div>

View File

@ -195,7 +195,7 @@ export function LocalDetailPanel({
)} )}
</div> </div>
{editingVisibility && ( {editingVisibility && (
<div className="mb-3 p-2.5 border border-hairline rounded-md bg-white text-xs"> <div className="mb-3 p-2.5 border border-hairline rounded-md bg-canvas text-xs">
<div className="flex gap-3 flex-wrap"> <div className="flex gap-3 flex-wrap">
<label className="flex items-center gap-1"> <label className="flex items-center gap-1">
<input <input
@ -228,7 +228,7 @@ export function LocalDetailPanel({
</div> </div>
{editVisibility === 'org' && orgs.length > 1 && ( {editVisibility === 'org' && orgs.length > 1 && (
<select <select
className="mt-2 px-2 h-7 border border-hairline rounded-md text-xs bg-white focus:outline-none focus:ring-2 focus:ring-accent-ring" className="mt-2 px-2 h-7 border border-hairline rounded-md text-xs bg-canvas focus:outline-none focus:ring-2 focus:ring-accent-ring"
value={editScopeOrgId ?? ''} value={editScopeOrgId ?? ''}
onChange={e => setEditScopeOrgId(e.target.value)} onChange={e => setEditScopeOrgId(e.target.value)}
> >
@ -273,13 +273,13 @@ export function LocalDetailPanel({
)} )}
</div> </div>
{!loading && task && ( {!loading && task && (
<div className="flex-shrink-0 border-t border-hairline bg-white px-3 py-2.5"> <div className="flex-shrink-0 border-t border-hairline bg-canvas px-3 py-2.5">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{onDelete && !isActiveJob ? ( {onDelete && !isActiveJob ? (
<button <button
disabled={deleting} disabled={deleting}
onClick={handleDelete} onClick={handleDelete}
className="px-3 h-7 bg-white border border-red-200 text-red-700 rounded-md text-xs font-medium disabled:opacity-50 hover:bg-red-50 transition-colors" className="px-3 h-7 bg-canvas border border-red-200 text-red-700 rounded-md text-xs font-medium disabled:opacity-50 hover:bg-red-50 transition-colors"
> >
{deleting ? '削除中...' : '削除'} {deleting ? '削除中...' : '削除'}
</button> </button>

View File

@ -76,7 +76,7 @@ export function BrowserTab({ taskId }: { taskId: number }) {
if (!data?.available) { if (!data?.available) {
if (data?.reason === 'novnc_not_installed') { if (data?.reason === 'novnc_not_installed') {
return ( return (
<div className="bg-white border border-amber-300 rounded-md p-6 text-sm text-slate-700"> <div className="bg-canvas border border-amber-300 rounded-md p-6 text-sm text-slate-700">
<p className="font-medium text-amber-800 mb-2">noVNC Web (vnc.html) </p> <p className="font-medium text-amber-800 mb-2">noVNC Web (vnc.html) </p>
<p className="text-xs leading-relaxed mb-2"> <p className="text-xs leading-relaxed mb-2">
noVNC HTML/JS noVNC HTML/JS
@ -94,7 +94,7 @@ export function BrowserTab({ taskId }: { taskId: number }) {
); );
} }
return ( return (
<div className="bg-white border border-hairline rounded-md p-6 text-center text-sm text-slate-600"> <div className="bg-canvas border border-hairline rounded-md p-6 text-center text-sm text-slate-600">
<p className="font-medium text-slate-800 mb-1"></p> <p className="font-medium text-slate-800 mb-1"></p>
<p className="text-xs leading-relaxed"> <p className="text-xs leading-relaxed">
BrowseWeb / InteractiveBrowse BrowseWeb / InteractiveBrowse
@ -105,7 +105,7 @@ export function BrowserTab({ taskId }: { taskId: number }) {
} }
return ( return (
<div className="bg-white border border-hairline rounded-md overflow-hidden flex flex-col" style={{ minHeight: '480px' }}> <div className="bg-canvas border border-hairline rounded-md overflow-hidden flex flex-col" style={{ minHeight: '480px' }}>
<div className="flex items-center justify-between border-b border-hairline px-3 py-2 text-2xs text-slate-500 gap-2"> <div className="flex items-center justify-between border-b border-hairline px-3 py-2 text-2xs text-slate-500 gap-2">
<span className="truncate"> <span className="truncate">
state: <span className="font-mono text-slate-700">{data.state ?? '-'}</span> state: <span className="font-mono text-slate-700">{data.state ?? '-'}</span>
@ -130,7 +130,7 @@ export function BrowserTab({ taskId }: { taskId: number }) {
} }
}} }}
disabled={release.isPending} disabled={release.isPending}
className="px-2 py-1 rounded-md text-2xs border border-hairline bg-white hover:bg-surface text-slate-700 disabled:opacity-50" className="px-2 py-1 rounded-md text-2xs border border-hairline bg-canvas hover:bg-surface text-slate-700 disabled:opacity-50"
title="セッションを destroy する。次回 BrowseWeb 実行時に再生成される" title="セッションを destroy する。次回 BrowseWeb 実行時に再生成される"
> >
{release.isPending ? '終了中…' : 'セッション終了'} {release.isPending ? '終了中…' : 'セッション終了'}

View File

@ -16,7 +16,7 @@ interface FilesTabProps {
export function FilesTab(props: FilesTabProps) { export function FilesTab(props: FilesTabProps) {
return ( return (
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm"> <div className="bg-canvas border border-slate-200 rounded-xl p-4 shadow-sm">
<FileBrowser {...props} /> <FileBrowser {...props} />
</div> </div>
); );

View File

@ -8,7 +8,7 @@ interface OutputTabProps {
export function OutputTab({ outputPreviewName, outputPreviewContent, onViewFull }: OutputTabProps) { export function OutputTab({ outputPreviewName, outputPreviewContent, onViewFull }: OutputTabProps) {
return ( return (
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm"> <div className="bg-canvas border border-slate-200 rounded-xl p-4 shadow-sm">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<div className="font-bold text-[13px] text-slate-800"></div> <div className="font-bold text-[13px] text-slate-800"></div>
{outputPreviewName && ( {outputPreviewName && (

View File

@ -47,7 +47,7 @@ function FeedbackPanel({ task }: { task: LocalTask }) {
if (!editing && hasFeedback) { if (!editing && hasFeedback) {
return ( return (
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm"> <div className="bg-canvas border border-slate-200 rounded-xl p-4 shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm font-semibold text-slate-700"></span> <span className="text-sm font-semibold text-slate-700"></span>
@ -77,7 +77,7 @@ function FeedbackPanel({ task }: { task: LocalTask }) {
} }
return ( return (
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm"> <div className="bg-canvas border border-slate-200 rounded-xl p-4 shadow-sm">
<div className="text-sm font-semibold text-slate-700 mb-2"></div> <div className="text-sm font-semibold text-slate-700 mb-2"></div>
<div className="flex gap-2 mb-3"> <div className="flex gap-2 mb-3">
<button <button
@ -195,7 +195,7 @@ function MissionCard({ task }: { task: LocalTask }) {
const isEmpty = !current.goal && !current.done && !current.open && !current.clarifications; const isEmpty = !current.goal && !current.done && !current.open && !current.clarifications;
return ( return (
<div className="bg-white border border-hairline rounded-md p-3.5"> <div className="bg-canvas border border-hairline rounded-md p-3.5">
<div className="flex items-center justify-between mb-2.5"> <div className="flex items-center justify-between mb-2.5">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<svg className="w-3.5 h-3.5 text-slate-500" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"> <svg className="w-3.5 h-3.5 text-slate-500" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
@ -208,7 +208,7 @@ function MissionCard({ task }: { task: LocalTask }) {
<button <button
type="button" type="button"
onClick={() => { setDraft(current); setEditing(true); setError(null); }} onClick={() => { setDraft(current); setEditing(true); setError(null); }}
className="px-2 h-7 text-2xs font-medium border border-hairline bg-white text-slate-700 hover:bg-surface rounded-md transition-colors" className="px-2 h-7 text-2xs font-medium border border-hairline bg-canvas text-slate-700 hover:bg-surface rounded-md transition-colors"
> >
</button> </button>
@ -235,7 +235,7 @@ function MissionCard({ task }: { task: LocalTask }) {
type="button" type="button"
onClick={() => { setEditing(false); setError(null); setDraft(current); }} onClick={() => { setEditing(false); setError(null); setDraft(current); }}
disabled={mutation.isPending} disabled={mutation.isPending}
className="px-3 h-7 text-xs rounded-md border border-hairline bg-white text-slate-700 hover:bg-surface transition-colors disabled:opacity-50" className="px-3 h-7 text-xs rounded-md border border-hairline bg-canvas text-slate-700 hover:bg-surface transition-colors disabled:opacity-50"
> >
</button> </button>
@ -286,7 +286,7 @@ export function OverviewTab({ task, subtaskActivities, onSubtaskFilePreview }: O
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm"> <div className="bg-canvas border border-slate-200 rounded-xl p-4 shadow-sm">
<div className="text-lg font-extrabold text-slate-900">{task.title}</div> <div className="text-lg font-extrabold text-slate-900">{task.title}</div>
<div className="flex flex-wrap gap-2 mt-2"> <div className="flex flex-wrap gap-2 mt-2">
<StatusBadge status={status} /> <StatusBadge status={status} />

View File

@ -19,7 +19,7 @@ export function ProgressTab({ task, onViewFullLog, subtaskActivities }: Progress
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm"> <div className="bg-canvas border border-slate-200 rounded-xl p-4 shadow-sm">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<div className="font-bold text-[13px] text-slate-800"> Timeline</div> <div className="font-bold text-[13px] text-slate-800"> Timeline</div>
<div className="text-2xs text-slate-400">{activityEvents.length} </div> <div className="text-2xs text-slate-400">{activityEvents.length} </div>
@ -38,7 +38,7 @@ export function ProgressTab({ task, onViewFullLog, subtaskActivities }: Progress
/> />
</div> </div>
{hasSubtasks && <SubtaskActivitySection subtaskActivities={subtaskActivities!} />} {hasSubtasks && <SubtaskActivitySection subtaskActivities={subtaskActivities!} />}
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm"> <div className="bg-canvas border border-slate-200 rounded-xl p-4 shadow-sm">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<div className="font-bold text-[13px] text-slate-800">Raw activity.log</div> <div className="font-bold text-[13px] text-slate-800">Raw activity.log</div>
<button onClick={onViewFullLog} className="text-2xs text-blue-600 font-bold hover:underline"></button> <button onClick={onViewFullLog} className="text-2xs text-blue-600 font-bold hover:underline"></button>

View File

@ -58,7 +58,7 @@ export function SubtaskActivitySection({ subtaskActivities }: SubtaskActivitySec
const progressPct = total > 0 ? Math.round((completed / total) * 100) : 0; const progressPct = total > 0 ? Math.round((completed / total) * 100) : 0;
return ( return (
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm"> <div className="bg-canvas border border-slate-200 rounded-xl p-4 shadow-sm">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="text-[13px] font-bold text-slate-800"></div> <div className="text-[13px] font-bold text-slate-800"></div>
<div className="text-xs text-slate-500">{completed}/{total} </div> <div className="text-xs text-slate-500">{completed}/{total} </div>

View File

@ -99,7 +99,7 @@ function SubtaskCard({ taskId, subtask, activity, onFilePreview }: SubtaskCardPr
const hasFiles = Object.values(categories).some(f => f.length > 0); const hasFiles = Object.values(categories).some(f => f.length > 0);
return ( return (
<div className="border border-slate-200 rounded-lg bg-white overflow-hidden"> <div className="border border-slate-200 rounded-lg bg-canvas overflow-hidden">
<button <button
className="w-full text-left px-3 py-2.5 flex items-start gap-2 hover:bg-slate-50 transition-colors" className="w-full text-left px-3 py-2.5 flex items-start gap-2 hover:bg-slate-50 transition-colors"
onClick={() => setExpanded(prev => !prev)} onClick={() => setExpanded(prev => !prev)}
@ -193,7 +193,7 @@ export function SubtasksPanel({ taskId, subtasks, subtaskCount, subtaskCompleted
const progressPct = subtaskCount > 0 ? Math.round((subtaskCompleted / subtaskCount) * 100) : 0; const progressPct = subtaskCount > 0 ? Math.round((subtaskCompleted / subtaskCount) * 100) : 0;
return ( return (
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-sm"> <div className="bg-canvas border border-slate-200 rounded-xl p-4 shadow-sm">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="text-sm font-bold text-slate-800"></div> <div className="text-sm font-bold text-slate-800"></div>
<div className="text-xs text-slate-500">{subtaskCompleted}/{subtaskCount} </div> <div className="text-xs text-slate-500">{subtaskCompleted}/{subtaskCount} </div>

View File

@ -19,7 +19,7 @@ export function TimelineTab({ comments }: { comments: LocalTaskComment[] }) {
); );
} }
return ( return (
<div key={c.id} className="bg-white border border-slate-200 rounded-xl p-3 shadow-sm"> <div key={c.id} className="bg-canvas border border-slate-200 rounded-xl p-3 shadow-sm">
<div className="flex justify-between items-center mb-1.5"> <div className="flex justify-between items-center mb-1.5">
<div className="text-xs font-bold text-slate-700">{c.author}</div> <div className="text-xs font-bold text-slate-700">{c.author}</div>
<div className="text-2xs text-slate-400">{new Date(c.createdAt).toLocaleString()}</div> <div className="text-2xs text-slate-400">{new Date(c.createdAt).toLocaleString()}</div>

View File

@ -60,7 +60,7 @@ function parseEventsJsonl(raw: string): ParseSummary {
const CATEGORIES: Array<{ id: string; label: string; kinds: string[]; tone: string }> = [ const CATEGORIES: Array<{ id: string; label: string; kinds: string[]; tone: string }> = [
{ id: 'run', label: 'Run', kinds: ['run_start', 'run_complete'], tone: 'bg-surface-2 text-slate-700 border-hairline' }, { id: 'run', label: 'Run', kinds: ['run_start', 'run_complete'], tone: 'bg-surface-2 text-slate-700 border-hairline' },
{ id: 'movement', label: 'Movement', kinds: ['movement_start', 'movement_complete', 'transition', 'complete'], tone: 'bg-blue-50 text-blue-800 border-blue-100' }, { id: 'movement', label: 'Movement', kinds: ['movement_start', 'movement_complete', 'transition', 'complete'], tone: 'bg-blue-50 text-blue-800 border-blue-100' },
{ id: 'tool', label: 'Tool', kinds: ['tool_call', 'tool_result'], tone: 'bg-white text-slate-700 border-hairline' }, { id: 'tool', label: 'Tool', kinds: ['tool_call', 'tool_result'], tone: 'bg-canvas text-slate-700 border-hairline' },
{ id: 'llm', label: 'LLM', kinds: ['llm_call_start', 'llm_call_end'], tone: 'bg-indigo-50 text-indigo-800 border-indigo-100' }, { id: 'llm', label: 'LLM', kinds: ['llm_call_start', 'llm_call_end'], tone: 'bg-indigo-50 text-indigo-800 border-indigo-100' },
{ id: 'cache', label: 'Cache', kinds: ['cache_set', 'cache_hit', 'cache_invalidate'], tone: 'bg-amber-50 text-amber-800 border-amber-100' }, { id: 'cache', label: 'Cache', kinds: ['cache_set', 'cache_hit', 'cache_invalidate'], tone: 'bg-amber-50 text-amber-800 border-amber-100' },
{ id: 'memory', label: 'Memory', kinds: ['memory_invalidate', 'memory_update_call', 'memory_handoff_write', 'memory_handoff_read', 'memory_delta_write', 'memory_delta_absorb', 'memory_snapshot_written', 'memory_snapshot_failed'], tone: 'bg-emerald-50 text-emerald-800 border-emerald-100' }, { id: 'memory', label: 'Memory', kinds: ['memory_invalidate', 'memory_update_call', 'memory_handoff_write', 'memory_handoff_read', 'memory_delta_write', 'memory_delta_absorb', 'memory_snapshot_written', 'memory_snapshot_failed'], tone: 'bg-emerald-50 text-emerald-800 border-emerald-100' },
@ -96,7 +96,7 @@ function formatDurationLabel(ms: number): string {
function toneFor(kind: string): string { function toneFor(kind: string): string {
for (const c of CATEGORIES) if (c.kinds.includes(kind)) return c.tone; for (const c of CATEGORIES) if (c.kinds.includes(kind)) return c.tone;
return 'bg-white text-slate-600 border-slate-200'; return 'bg-canvas text-slate-600 border-slate-200';
} }
function categoryFor(kind: string): string { function categoryFor(kind: string): string {
@ -351,7 +351,7 @@ export function TraceTab({ taskId }: TraceTabProps) {
key={c.id} key={c.id}
onClick={() => toggleCategory(c.id)} onClick={() => toggleCategory(c.id)}
className={`h-6 px-2 text-[10px] font-medium border rounded transition-colors ${ className={`h-6 px-2 text-[10px] font-medium border rounded transition-colors ${
on ? c.tone : 'bg-white text-slate-400 border-hairline hover:text-slate-600' on ? c.tone : 'bg-canvas text-slate-400 border-hairline hover:text-slate-600'
}`} }`}
> >
{c.label} {c.label}
@ -362,8 +362,8 @@ export function TraceTab({ taskId }: TraceTabProps) {
onClick={() => toggleCategory('other')} onClick={() => toggleCategory('other')}
className={`h-6 px-2 text-[10px] font-medium border rounded transition-colors ${ className={`h-6 px-2 text-[10px] font-medium border rounded transition-colors ${
enabledCategories.has('other') enabledCategories.has('other')
? 'bg-white text-slate-700 border-hairline' ? 'bg-canvas text-slate-700 border-hairline'
: 'bg-white text-slate-400 border-hairline-soft hover:text-slate-600' : 'bg-canvas text-slate-400 border-hairline-soft hover:text-slate-600'
}`} }`}
> >
Other Other
@ -373,13 +373,13 @@ export function TraceTab({ taskId }: TraceTabProps) {
<select <select
value={movementFilter} value={movementFilter}
onChange={(e) => setMovementFilter(e.target.value)} onChange={(e) => setMovementFilter(e.target.value)}
className="h-7 text-2xs border border-hairline rounded-md px-2 bg-white text-slate-700 focus:outline-none focus:ring-2 focus:ring-accent-ring" className="h-7 text-2xs border border-hairline rounded-md px-2 bg-canvas text-slate-700 focus:outline-none focus:ring-2 focus:ring-accent-ring"
> >
{movements.map((m) => ( {movements.map((m) => (
<option key={m} value={m}>{m === 'all' ? 'all movements' : m}</option> <option key={m} value={m}>{m === 'all' ? 'all movements' : m}</option>
))} ))}
</select> </select>
<div className="flex-1 min-w-0 flex items-center gap-1.5 bg-white border border-hairline rounded-md h-7 px-2 focus-within:ring-2 focus-within:ring-accent-ring"> <div className="flex-1 min-w-0 flex items-center gap-1.5 bg-canvas border border-hairline rounded-md h-7 px-2 focus-within:ring-2 focus-within:ring-accent-ring">
<svg aria-hidden="true" className="w-3 h-3 text-slate-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg aria-hidden="true" className="w-3 h-3 text-slate-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
@ -393,7 +393,7 @@ export function TraceTab({ taskId }: TraceTabProps) {
</div> </div>
<button <button
onClick={() => setRefreshKey((k) => k + 1)} onClick={() => setRefreshKey((k) => k + 1)}
className="h-7 w-7 flex items-center justify-center text-xs border border-hairline rounded-md text-slate-500 bg-white hover:bg-surface transition-colors" className="h-7 w-7 flex items-center justify-center text-xs border border-hairline rounded-md text-slate-500 bg-canvas hover:bg-surface transition-colors"
title="手動更新(自動 5 秒ごとにも更新されます)" title="手動更新(自動 5 秒ごとにも更新されます)"
> >
@ -408,7 +408,7 @@ export function TraceTab({ taskId }: TraceTabProps) {
{/* Tool / LLM time aggregation — surfaces "what ate the wall-clock". */} {/* Tool / LLM time aggregation — surfaces "what ate the wall-clock". */}
{(toolTimings.tools.length > 0 || toolTimings.llm.count > 0) && ( {(toolTimings.tools.length > 0 || toolTimings.llm.count > 0) && (
<div className="border border-hairline rounded-md p-2 bg-white"> <div className="border border-hairline rounded-md p-2 bg-canvas">
<div className="section-label mb-1.5">time by source</div> <div className="section-label mb-1.5">time by source</div>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
{toolTimings.llm.count > 0 && (() => { {toolTimings.llm.count > 0 && (() => {

View File

@ -29,7 +29,7 @@ export function AmazonProductsCard({ data, onExpand }: { data: AmazonData; onExp
href={p.productUrl} href={p.productUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="bg-white border border-slate-200 rounded-lg p-2 cursor-pointer hover:border-blue-300 hover:shadow-sm transition-all flex-shrink-0 no-underline" className="bg-canvas border border-slate-200 rounded-lg p-2 cursor-pointer hover:border-blue-300 hover:shadow-sm transition-all flex-shrink-0 no-underline"
style={{ minWidth: 160, maxWidth: 160 }} style={{ minWidth: 160, maxWidth: 160 }}
> >
<div className="w-full h-20 bg-slate-100 rounded flex items-center justify-center mb-2 overflow-hidden"> <div className="w-full h-20 bg-slate-100 rounded flex items-center justify-center mb-2 overflow-hidden">

View File

@ -25,7 +25,7 @@ export function AmazonProductsDetail({ data }: { data: AmazonData }) {
<div className="space-y-6"> <div className="space-y-6">
{products.map((p, i) => ( {products.map((p, i) => (
<div key={p.asin} className="bg-white border border-slate-200 rounded-xl p-4"> <div key={p.asin} className="bg-canvas border border-slate-200 rounded-xl p-4">
<div className="flex gap-4 flex-col sm:flex-row"> <div className="flex gap-4 flex-col sm:flex-row">
{/* Product image */} {/* Product image */}
<div className="w-full sm:w-40 h-40 bg-slate-50 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden"> <div className="w-full sm:w-40 h-40 bg-slate-50 rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden">

View File

@ -36,7 +36,7 @@ export function EmbedModal({ open, onClose, children }: EmbedModalProps) {
{/* Modal content */} {/* Modal content */}
<div <div
className=" className="
relative bg-white overflow-y-auto relative bg-surface overflow-y-auto
w-full h-full w-full h-full
sm:w-auto sm:h-auto sm:max-w-[720px] sm:max-h-[85vh] sm:min-w-[400px] sm:w-auto sm:h-auto sm:max-w-[720px] sm:max-h-[85vh] sm:min-w-[400px]
sm:rounded-2xl sm:shadow-2xl sm:m-4 sm:rounded-2xl sm:shadow-2xl sm:m-4

View File

@ -17,7 +17,7 @@ export function MapPlacesCard({ data, onExpand }: { data: MapData; onExpand: ()
{places.slice(0, 5).map((p, i) => ( {places.slice(0, 5).map((p, i) => (
<div <div
key={`${p.lat}-${p.lon}`} key={`${p.lat}-${p.lon}`}
className="flex items-start gap-2 bg-white border border-slate-200 rounded-lg px-3 py-2" className="flex items-start gap-2 bg-canvas border border-slate-200 rounded-lg px-3 py-2"
> >
<span className="text-slate-400 font-mono flex-shrink-0" style={{ fontSize: 11 }}>{i + 1}</span> <span className="text-slate-400 font-mono flex-shrink-0" style={{ fontSize: 11 }}>{i + 1}</span>
<div className="min-w-0"> <div className="min-w-0">

View File

@ -98,7 +98,7 @@ export function MapPlacesDetail({ data }: { data: MapData }) {
{/* Place list */} {/* Place list */}
<div className="space-y-4"> <div className="space-y-4">
{places.map((p, i) => ( {places.map((p, i) => (
<div key={`${p.lat}-${p.lon}`} className="bg-white border border-slate-200 rounded-xl p-4"> <div key={`${p.lat}-${p.lon}`} className="bg-canvas border border-slate-200 rounded-xl p-4">
<div className="text-slate-400 mb-1" style={{ fontSize: 13 }}>#{i + 1}</div> <div className="text-slate-400 mb-1" style={{ fontSize: 13 }}>#{i + 1}</div>
<h3 className="text-sm font-semibold text-slate-800 leading-snug mb-2">{p.name}</h3> <h3 className="text-sm font-semibold text-slate-800 leading-snug mb-2">{p.name}</h3>

View File

@ -26,7 +26,7 @@ export function XPostsCard({ data, onExpand }: { data: XPostData; onExpand: () =
href={p.postUrl} href={p.postUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-start gap-2 bg-white border border-slate-200 rounded-lg px-3 py-2 no-underline hover:border-blue-300 hover:shadow-sm transition-all" className="flex items-start gap-2 bg-canvas border border-slate-200 rounded-lg px-3 py-2 no-underline hover:border-blue-300 hover:shadow-sm transition-all"
> >
<img <img
src={p.authorImageUrl} src={p.authorImageUrl}

View File

@ -28,7 +28,7 @@ export function XPostsDetail({ data }: { data: XPostData }) {
<div className="space-y-4"> <div className="space-y-4">
{posts.map((p) => ( {posts.map((p) => (
<div key={p.id} className="bg-white border border-slate-200 rounded-xl p-4"> <div key={p.id} className="bg-canvas border border-slate-200 rounded-xl p-4">
{/* Author header */} {/* Author header */}
<div className="flex items-center gap-3 mb-3"> <div className="flex items-center gap-3 mb-3">
<img <img

View File

@ -20,7 +20,7 @@ export function YouTubeVideosCard({ data, onExpand }: { data: YouTubeData; onExp
href={v.videoUrl} href={v.videoUrl}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="bg-white border border-slate-200 rounded-lg overflow-hidden cursor-pointer hover:border-red-300 hover:shadow-sm transition-all flex-shrink-0 no-underline" className="bg-canvas border border-slate-200 rounded-lg overflow-hidden cursor-pointer hover:border-red-300 hover:shadow-sm transition-all flex-shrink-0 no-underline"
style={{ minWidth: 200, maxWidth: 200 }} style={{ minWidth: 200, maxWidth: 200 }}
> >
<div className="relative"> <div className="relative">

View File

@ -11,7 +11,7 @@ export function YouTubeVideosDetail({ data }: { data: YouTubeData }) {
<div className="space-y-6"> <div className="space-y-6">
{videos.map((v, i) => ( {videos.map((v, i) => (
<div key={v.videoId} className="bg-white border border-slate-200 rounded-xl p-4"> <div key={v.videoId} className="bg-canvas border border-slate-200 rounded-xl p-4">
<div className="flex gap-4 flex-col sm:flex-row"> <div className="flex gap-4 flex-col sm:flex-row">
{/* Thumbnail */} {/* Thumbnail */}
<a <a

View File

@ -85,7 +85,7 @@ function FileSortMenu({ sort, onChange }: { sort: FileSort; onChange: (s: FileSo
</svg> </svg>
</button> </button>
{open && ( {open && (
<div className="absolute right-0 top-[calc(100%+6px)] z-10 bg-white border border-hairline rounded-md shadow min-w-[140px] p-1"> <div className="absolute right-0 top-[calc(100%+6px)] z-10 bg-canvas border border-hairline rounded-md shadow min-w-[140px] p-1">
{SORT_OPTIONS.map(o => { {SORT_OPTIONS.map(o => {
const selected = sort === o.value; const selected = sort === o.value;
return ( return (
@ -149,7 +149,7 @@ export function FileBrowser({
// Icon-only action buttons. Replaces the wider "Preview" / "DL" / "Open" // Icon-only action buttons. Replaces the wider "Preview" / "DL" / "Open"
// text buttons that were squeezing long filenames before. Sized 32px for // text buttons that were squeezing long filenames before. Sized 32px for
// finger-friendly tap targets on iPhone (still compact on desktop). // finger-friendly tap targets on iPhone (still compact on desktop).
const iconBtn = 'w-8 h-8 flex items-center justify-center rounded-md border border-hairline bg-white text-slate-500 hover:text-slate-900 hover:bg-surface transition-colors'; const iconBtn = 'w-8 h-8 flex items-center justify-center rounded-md border border-hairline bg-canvas text-slate-500 hover:text-slate-900 hover:bg-surface transition-colors';
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
@ -161,7 +161,7 @@ export function FileBrowser({
className={`px-2 h-7 rounded text-2xs font-medium border transition-colors ${ className={`px-2 h-7 rounded text-2xs font-medium border transition-colors ${
section === s section === s
? 'border-accent/60 bg-accent-soft text-accent font-semibold' ? 'border-accent/60 bg-accent-soft text-accent font-semibold'
: 'border-hairline bg-white text-slate-600 hover:bg-surface' : 'border-hairline bg-canvas text-slate-600 hover:bg-surface'
}`} }`}
> >
{s} {s}
@ -193,7 +193,7 @@ export function FileBrowser({
{pathSegments.length > 0 && ( {pathSegments.length > 0 && (
<button <button
onClick={() => onNavigate(pathSegments.slice(0, -1).join('/'))} onClick={() => onNavigate(pathSegments.slice(0, -1).join('/'))}
className="self-start inline-flex items-center gap-1 px-2 h-7 rounded border border-hairline bg-white text-2xs text-slate-600 hover:bg-surface transition-colors" className="self-start inline-flex items-center gap-1 px-2 h-7 rounded border border-hairline bg-canvas text-2xs text-slate-600 hover:bg-surface transition-colors"
> >
<svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 4l-4 4 4 4M6 8h6" /> <path d="M10 4l-4 4 4 4M6 8h6" />
@ -206,7 +206,7 @@ export function FileBrowser({
{sortedEntries.map(entry => ( {sortedEntries.map(entry => (
<div <div
key={`${entry.kind}:${entry.path}`} key={`${entry.kind}:${entry.path}`}
className="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-white border border-hairline hover:bg-surface transition-colors" className="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-canvas border border-hairline hover:bg-surface transition-colors"
> >
<span className="text-slate-400 flex-shrink-0" aria-hidden="true"> <span className="text-slate-400 flex-shrink-0" aria-hidden="true">
{entry.kind === 'directory' ? ( {entry.kind === 'directory' ? (

View File

@ -75,7 +75,7 @@ function renderCsv(csv: string) {
{rows.slice(0, 120).map((r, i) => ( {rows.slice(0, 120).map((r, i) => (
<tr key={i}> <tr key={i}>
{r.slice(0, 20).map((c, j) => ( {r.slice(0, 20).map((c, j) => (
<td key={j} className={`border border-slate-200 px-2 py-1 ${i === 0 ? 'bg-slate-100 font-bold' : 'bg-white'}`}> <td key={j} className={`border border-slate-200 px-2 py-1 ${i === 0 ? 'bg-slate-100 font-bold' : 'bg-canvas'}`}>
{c} {c}
</td> </td>
))} ))}
@ -603,7 +603,7 @@ function renderJsonl(content: string): JSX.Element {
</thead> </thead>
<tbody> <tbody>
{records.map((record, i) => ( {records.map((record, i) => (
<tr key={i} className={i % 2 === 0 ? 'bg-white' : 'bg-slate-50'}> <tr key={i} className={i % 2 === 0 ? 'bg-canvas' : 'bg-slate-50'}>
{columns.map(col => ( {columns.map(col => (
<td key={col} className="px-3 py-1.5 border-b border-slate-100 align-top"> <td key={col} className="px-3 py-1.5 border-b border-slate-100 align-top">
{formatCell(col, record[col])} {formatCell(col, record[col])}
@ -678,7 +678,7 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
<div className="flex gap-2 justify-end"> <div className="flex gap-2 justify-end">
<button <button
onClick={() => { setMode('view'); setError(''); }} onClick={() => { setMode('view'); setError(''); }}
className="px-3 h-8 text-xs rounded-md border border-hairline bg-white text-slate-700 hover:bg-surface transition-colors" className="px-3 h-8 text-xs rounded-md border border-hairline bg-canvas text-slate-700 hover:bg-surface transition-colors"
> >
</button> </button>
@ -736,13 +736,13 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
return ( return (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-[env(safe-area-inset-top)_env(safe-area-inset-right)_env(safe-area-inset-bottom)_env(safe-area-inset-left)]" onClick={onClose}> <div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-[env(safe-area-inset-top)_env(safe-area-inset-right)_env(safe-area-inset-bottom)_env(safe-area-inset-left)]" onClick={onClose}>
<div <div
className="bg-white rounded-md border border-hairline shadow-md flex flex-col overflow-hidden" className="bg-surface rounded-md border border-hairline shadow-md flex flex-col overflow-hidden"
style={{ width: modalWidth, maxHeight: 'min(90vh, calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 24px))' }} style={{ width: modalWidth, maxHeight: 'min(90vh, calc(100dvh - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px) - 24px))' }}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
> >
<div className="flex justify-between items-center px-4 py-2.5 border-b border-hairline flex-shrink-0 sticky top-0 bg-white z-10 gap-2"> <div className="flex justify-between items-center px-4 py-2.5 border-b border-hairline flex-shrink-0 sticky top-0 bg-surface z-10 gap-2">
<div className="font-mono text-xs text-slate-700 truncate" title={name}>{name}</div> <div className="font-mono text-xs text-slate-700 truncate" title={name}>{name}</div>
<div className="flex items-center gap-1 flex-shrink-0"> <div className="flex items-center gap-1 flex-shrink-0">
{isMarkdownFile && mode === 'view' && ( {isMarkdownFile && mode === 'view' && (
@ -750,7 +750,7 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
onClick={handlePrint} onClick={handlePrint}
disabled={printing} disabled={printing}
title="ブラウザの印刷ダイアログから PDF として保存または印刷" title="ブラウザの印刷ダイアログから PDF として保存または印刷"
className="inline-flex items-center gap-1 px-2.5 h-7 text-2xs font-medium rounded-md border border-hairline bg-white text-slate-700 hover:bg-surface disabled:opacity-50 transition-colors" className="inline-flex items-center gap-1 px-2.5 h-7 text-2xs font-medium rounded-md border border-hairline bg-canvas text-slate-700 hover:bg-surface disabled:opacity-50 transition-colors"
> >
<svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"> <svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 5V2h8v3M4 11H2.5A1.5 1.5 0 0 1 1 9.5v-3A1.5 1.5 0 0 1 2.5 5h11A1.5 1.5 0 0 1 15 6.5v3a1.5 1.5 0 0 1-1.5 1.5H12M4 9.5h8v4.5H4z" /> <path d="M4 5V2h8v3M4 11H2.5A1.5 1.5 0 0 1 1 9.5v-3A1.5 1.5 0 0 1 2.5 5h11A1.5 1.5 0 0 1 15 6.5v3a1.5 1.5 0 0 1-1.5 1.5H12M4 9.5h8v4.5H4z" />
@ -761,7 +761,7 @@ export function FilePreview({ name, content, imageSrc, markdownImageBaseUrl, onC
{canEdit && mode === 'view' && ( {canEdit && mode === 'view' && (
<button <button
onClick={() => { setEditContent(currentContent); setMode('edit'); }} onClick={() => { setEditContent(currentContent); setMode('edit'); }}
className="inline-flex items-center gap-1 px-2.5 h-7 text-2xs font-medium rounded-md border border-hairline bg-white text-slate-700 hover:bg-surface transition-colors" className="inline-flex items-center gap-1 px-2.5 h-7 text-2xs font-medium rounded-md border border-hairline bg-canvas text-slate-700 hover:bg-surface transition-colors"
> >
<svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"> <svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M11.5 2.5l2 2L5 13l-2.5.5L3 11l8.5-8.5z" /> <path d="M11.5 2.5l2 2L5 13l-2.5.5L3 11l8.5-8.5z" />

View File

@ -166,7 +166,7 @@ export function NavDrawer({
tabIndex={-1} tabIndex={-1}
onKeyDown={onPanelKeyDown} onKeyDown={onPanelKeyDown}
{...(!open && { inert: '' })} {...(!open && { inert: '' })}
className={`fixed left-0 top-0 bottom-0 z-50 w-[min(280px,80vw)] bg-white shadow-xl flex flex-col motion-safe:transition-transform duration-200 ease-out ${ className={`fixed left-0 top-0 bottom-0 z-50 w-[min(280px,80vw)] bg-surface shadow-xl flex flex-col motion-safe:transition-transform duration-200 ease-out ${
open ? 'translate-x-0' : '-translate-x-full' open ? 'translate-x-0' : '-translate-x-full'
}`} }`}
style={{ style={{

View File

@ -74,7 +74,7 @@ export function TopBar({
return ( return (
<div <div
className="flex-shrink-0 bg-white border-b border-hairline px-4 flex items-center" className="flex-shrink-0 bg-canvas border-b border-hairline px-4 flex items-center"
style={{ style={{
paddingTop: 'env(safe-area-inset-top, 0px)', paddingTop: 'env(safe-area-inset-top, 0px)',
minHeight: 'calc(48px + env(safe-area-inset-top, 0px))', minHeight: 'calc(48px + env(safe-area-inset-top, 0px))',

View File

@ -87,7 +87,7 @@ function SortMenu({
</svg> </svg>
</button> </button>
{open && ( {open && (
<div className="absolute right-0 top-[calc(100%+6px)] z-10 bg-white border border-hairline rounded-md shadow min-w-[160px] p-1"> <div className="absolute right-0 top-[calc(100%+6px)] z-10 bg-surface border border-hairline rounded-md shadow min-w-[160px] p-1">
{SORT_OPTIONS.map(o => { {SORT_OPTIONS.map(o => {
const selected = sortMode === o.value; const selected = sortMode === o.value;
return ( return (
@ -138,7 +138,7 @@ export function FilterBar({
}: FilterBarProps) { }: FilterBarProps) {
return ( return (
<div className="flex flex-col gap-2 pb-3 border-b border-hairline"> <div className="flex flex-col gap-2 pb-3 border-b border-hairline">
<div className="flex items-center gap-1.5 bg-white border border-hairline rounded-md pl-2.5 pr-1 h-8"> <div className="flex items-center gap-1.5 bg-canvas border border-hairline rounded-md pl-2.5 pr-1 h-8">
<svg aria-hidden="true" className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg aria-hidden="true" className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
@ -161,7 +161,7 @@ export function FilterBar({
className={`flex-shrink-0 px-2 h-7 rounded text-2xs font-medium border transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${ className={`flex-shrink-0 px-2 h-7 rounded text-2xs font-medium border transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
selectedStatus === 'all' selectedStatus === 'all'
? 'border-accent/60 bg-accent-soft text-accent font-semibold' ? 'border-accent/60 bg-accent-soft text-accent font-semibold'
: 'border-hairline bg-white text-slate-600 hover:bg-surface' : 'border-hairline bg-canvas text-slate-600 hover:bg-surface'
}`} }`}
> >
<span className="text-slate-400 ml-0.5 font-mono tabular-nums">{totalCount}</span> <span className="text-slate-400 ml-0.5 font-mono tabular-nums">{totalCount}</span>
@ -175,7 +175,7 @@ export function FilterBar({
className={`flex-shrink-0 px-2 h-7 rounded text-2xs font-medium border transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${ className={`flex-shrink-0 px-2 h-7 rounded text-2xs font-medium border transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
selectedStatus === status selectedStatus === status
? 'border-accent/60 bg-accent-soft text-accent font-semibold' ? 'border-accent/60 bg-accent-soft text-accent font-semibold'
: 'border-hairline bg-white text-slate-600 hover:bg-surface' : 'border-hairline bg-canvas text-slate-600 hover:bg-surface'
}`} }`}
> >
{COLUMN_LABELS[status]} <span className="text-slate-400 ml-0.5 font-mono tabular-nums">{counts[status] ?? 0}</span> {COLUMN_LABELS[status]} <span className="text-slate-400 ml-0.5 font-mono tabular-nums">{counts[status] ?? 0}</span>

View File

@ -18,7 +18,7 @@ export const LocalTaskListItem = memo(function LocalTaskListItem({ task, active,
className={`w-full text-left px-3 py-2.5 rounded-md border transition-colors ${ className={`w-full text-left px-3 py-2.5 rounded-md border transition-colors ${
active active
? 'border-accent/60 bg-accent-soft' ? 'border-accent/60 bg-accent-soft'
: 'border-hairline bg-white hover:bg-surface' : 'border-hairline bg-canvas hover:bg-surface'
}`} }`}
> >
<div className="flex items-center justify-between gap-2 min-w-0"> <div className="flex items-center justify-between gap-2 min-w-0">

View File

@ -90,7 +90,7 @@ function AssetUploader({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div <div
className={`h-12 w-12 flex-shrink-0 rounded-md border border-hairline bg-surface flex items-center justify-center overflow-hidden ${ className={`h-12 w-12 flex-shrink-0 rounded-md border border-hairline bg-surface flex items-center justify-center overflow-hidden ${
kind === 'favicon' ? 'bg-white' : '' kind === 'favicon' ? 'bg-canvas' : ''
}`} }`}
> >
{currentUrl ? ( {currentUrl ? (
@ -115,7 +115,7 @@ function AssetUploader({
type="button" type="button"
onClick={handlePick} onClick={handlePick}
disabled={busy} disabled={busy}
className="px-2.5 h-7 text-2xs font-medium bg-white border border-hairline rounded-md text-slate-700 hover:bg-surface disabled:opacity-50 transition-colors" className="px-2.5 h-7 text-2xs font-medium bg-canvas border border-hairline rounded-md text-slate-700 hover:bg-surface disabled:opacity-50 transition-colors"
> >
{currentUrl ? '差し替え' : 'アップロード'} {currentUrl ? '差し替え' : 'アップロード'}
</button> </button>
@ -124,7 +124,7 @@ function AssetUploader({
type="button" type="button"
onClick={() => void handleClear()} onClick={() => void handleClear()}
disabled={busy} disabled={busy}
className="px-2.5 h-7 text-2xs font-medium text-red-700 border border-red-200 bg-white hover:bg-red-50 rounded-md disabled:opacity-50 transition-colors" className="px-2.5 h-7 text-2xs font-medium text-red-700 border border-red-200 bg-canvas hover:bg-red-50 rounded-md disabled:opacity-50 transition-colors"
> >
</button> </button>

View File

@ -243,7 +243,7 @@ function ConfigFormInner({ section }: ConfigFormProps) {
className={`sticky bottom-0 px-3 py-2.5 mt-6 border rounded-md flex items-center justify-end gap-2 transition-colors ${ className={`sticky bottom-0 px-3 py-2.5 mt-6 border rounded-md flex items-center justify-end gap-2 transition-colors ${
dirty dirty
? 'bg-amber-50 border-amber-300 shadow-[0_2px_8px_rgba(180,83,9,0.08)]' ? 'bg-amber-50 border-amber-300 shadow-[0_2px_8px_rgba(180,83,9,0.08)]'
: 'bg-white border-hairline' : 'bg-canvas border-hairline'
}`} }`}
> >
{toast ? ( {toast ? (
@ -262,7 +262,7 @@ function ConfigFormInner({ section }: ConfigFormProps) {
<button <button
onClick={handleDiscard} onClick={handleDiscard}
disabled={!dirty} disabled={!dirty}
className="px-3 h-8 text-xs text-slate-700 border border-hairline bg-white rounded-md hover:bg-surface disabled:opacity-50 transition-colors whitespace-nowrap flex-shrink-0" className="px-3 h-8 text-xs text-slate-700 border border-hairline bg-canvas rounded-md hover:bg-surface disabled:opacity-50 transition-colors whitespace-nowrap flex-shrink-0"
> >
<span className="hidden sm:inline">Discard Changes</span> <span className="hidden sm:inline">Discard Changes</span>
<span className="sm:hidden">Discard</span> <span className="sm:hidden">Discard</span>

View File

@ -70,7 +70,7 @@ export function GatewayKeyCreateDialog({ onCancel, onSubmit, submitting, error }
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6" className="bg-surface rounded-lg shadow-xl max-w-md w-full mx-4 p-6"
> >
<h3 className="text-lg font-semibold text-slate-800 mb-4"> Gateway Key </h3> <h3 className="text-lg font-semibold text-slate-800 mb-4"> Gateway Key </h3>

View File

@ -100,7 +100,7 @@ export function GatewayKeyRawKeyDialog({ rawKey, team, reason, onClose }: Props)
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-lg shadow-xl max-w-lg w-full mx-4 p-6"> <div className="bg-surface rounded-lg shadow-xl max-w-lg w-full mx-4 p-6">
<h3 className="text-lg font-semibold text-slate-800 mb-1"> <h3 className="text-lg font-semibold text-slate-800 mb-1">
{reason === 'created' ? '新しい Gateway Key を発行しました' : 'Gateway Key をローテーションしました'} {reason === 'created' ? '新しい Gateway Key を発行しました' : 'Gateway Key をローテーションしました'}
</h3> </h3>

View File

@ -36,7 +36,7 @@ export function GatewayKeyUsagePanel({ keyId, onClose }: Props) {
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 p-6"> <div className="bg-surface rounded-lg shadow-xl max-w-2xl w-full mx-4 p-6">
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<div> <div>
<h3 className="text-lg font-semibold text-slate-800">Key 使</h3> <h3 className="text-lg font-semibold text-slate-800">Key 使</h3>

View File

@ -215,7 +215,7 @@ export function LlmWorkersForm({ config, onChange, overriddenByEnv }: SectionFor
proxy: next === 'aao_gateway' ? true : undefined, proxy: next === 'aao_gateway' ? true : undefined,
}); });
}} }}
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white" className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-canvas"
> >
<option value="direct">Direct (Ollama / vLLM / llama.cpp)</option> <option value="direct">Direct (Ollama / vLLM / llama.cpp)</option>
<option value="aao_gateway">AAO Gateway</option> <option value="aao_gateway">AAO Gateway</option>

View File

@ -235,7 +235,7 @@ function MemoryEntryModal({
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-white rounded-lg shadow-xl w-full max-w-lg mx-4 flex flex-col max-h-[90vh]"> <div className="bg-surface rounded-lg shadow-xl w-full max-w-lg mx-4 flex flex-col max-h-[90vh]">
<div className="flex items-center justify-between px-4 py-3 border-b border-hairline"> <div className="flex items-center justify-between px-4 py-3 border-b border-hairline">
<h3 className="text-sm font-semibold text-slate-800"> <h3 className="text-sm font-semibold text-slate-800">
{isNew ? '新しいメモリエントリ' : `編集 — ${initial.name}`} {isNew ? '新しいメモリエントリ' : `編集 — ${initial.name}`}
@ -263,7 +263,7 @@ function MemoryEntryModal({
disabled={!isNew} disabled={!isNew}
placeholder="my-fact" placeholder="my-fact"
className={`w-full h-8 px-2.5 text-[13px] font-mono border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${ className={`w-full h-8 px-2.5 text-[13px] font-mono border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none ${
!isNew ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : 'bg-white' !isNew ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : 'bg-canvas'
}`} }`}
/> />
</div> </div>
@ -284,7 +284,7 @@ function MemoryEntryModal({
<select <select
value={form.type} value={form.type}
onChange={e => set('type', e.target.value as MemoryType)} onChange={e => set('type', e.target.value as MemoryType)}
className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring outline-none bg-white" className="w-full h-8 px-2 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring outline-none bg-canvas"
> >
{MEMORY_TYPES.map(t => ( {MEMORY_TYPES.map(t => (
<option key={t} value={t}>{t}</option> <option key={t} value={t}>{t}</option>
@ -316,7 +316,7 @@ function MemoryEntryModal({
<div className="flex justify-end gap-2 px-4 py-3 border-t border-hairline"> <div className="flex justify-end gap-2 px-4 py-3 border-t border-hairline">
<button <button
onClick={onClose} onClick={onClose}
className="px-3 h-8 text-xs text-slate-700 border border-hairline bg-white rounded-md hover:bg-surface transition-colors" className="px-3 h-8 text-xs text-slate-700 border border-hairline bg-canvas rounded-md hover:bg-surface transition-colors"
> >
</button> </button>
@ -449,14 +449,14 @@ function MemoryEntriesPanel() {
<div className="flex gap-1.5 flex-shrink-0 mt-0.5"> <div className="flex gap-1.5 flex-shrink-0 mt-0.5">
<button <button
onClick={() => handleEdit(entry)} onClick={() => handleEdit(entry)}
className="px-2 h-6 text-2xs text-slate-600 border border-hairline bg-white hover:bg-surface rounded transition-colors" className="px-2 h-6 text-2xs text-slate-600 border border-hairline bg-canvas hover:bg-surface rounded transition-colors"
> >
</button> </button>
<button <button
onClick={() => void handleDelete(entry.name)} onClick={() => void handleDelete(entry.name)}
disabled={deleting === entry.name} disabled={deleting === entry.name}
className="px-2 h-6 text-2xs text-red-700 border border-red-200 bg-white hover:bg-red-50 rounded transition-colors disabled:opacity-50" className="px-2 h-6 text-2xs text-red-700 border border-red-200 bg-canvas hover:bg-red-50 rounded transition-colors disabled:opacity-50"
> >
{deleting === entry.name ? '…' : '削除'} {deleting === entry.name ? '…' : '削除'}
</button> </button>
@ -593,7 +593,7 @@ function SnapshotCard({ item, onReverted }: { item: SnapshotIndexEntry; onRevert
<div className="text-[10px] font-medium text-slate-500 uppercase tracking-wide mb-1"> <div className="text-[10px] font-medium text-slate-500 uppercase tracking-wide mb-1">
</div> </div>
<pre className="text-2xs text-slate-700 bg-white border border-hairline rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap"> <pre className="text-2xs text-slate-700 bg-canvas border border-hairline rounded px-2 py-1.5 overflow-x-auto whitespace-pre-wrap">
{d.diff} {d.diff}
</pre> </pre>
</div> </div>
@ -613,13 +613,13 @@ function SnapshotCard({ item, onReverted }: { item: SnapshotIndexEntry; onRevert
<div className="grid grid-cols-2 gap-2"> <div className="grid grid-cols-2 gap-2">
<div> <div>
<div className="text-[10px] text-slate-400 mb-0.5"></div> <div className="text-[10px] text-slate-400 mb-0.5"></div>
<pre className="text-[10px] bg-white border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap"> <pre className="text-[10px] bg-canvas border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap">
{d.pieceBeforeYaml} {d.pieceBeforeYaml}
</pre> </pre>
</div> </div>
<div> <div>
<div className="text-[10px] text-slate-400 mb-0.5"></div> <div className="text-[10px] text-slate-400 mb-0.5"></div>
<pre className="text-[10px] bg-white border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap"> <pre className="text-[10px] bg-canvas border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap">
{d.pieceAfterYaml} {d.pieceAfterYaml}
</pre> </pre>
</div> </div>
@ -661,7 +661,7 @@ function SnapshotCard({ item, onReverted }: { item: SnapshotIndexEntry; onRevert
<button <button
type="button" type="button"
onClick={() => setConfirmRevert(false)} onClick={() => setConfirmRevert(false)}
className="px-2.5 h-7 text-2xs text-slate-600 border border-hairline bg-white hover:bg-surface rounded transition-colors" className="px-2.5 h-7 text-2xs text-slate-600 border border-hairline bg-canvas hover:bg-surface rounded transition-colors"
> >
</button> </button>
@ -742,7 +742,7 @@ function BeforeAfterDiff({
{!isAdded && ( {!isAdded && (
<div> <div>
<div className="text-[10px] text-slate-400 mb-0.5"></div> <div className="text-[10px] text-slate-400 mb-0.5"></div>
<pre className="text-[10px] bg-white border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap"> <pre className="text-[10px] bg-canvas border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap">
{before ?? '(空)'} {before ?? '(空)'}
</pre> </pre>
</div> </div>
@ -750,7 +750,7 @@ function BeforeAfterDiff({
{!isRemoved && ( {!isRemoved && (
<div className={isAdded ? 'col-span-2' : ''}> <div className={isAdded ? 'col-span-2' : ''}>
<div className="text-[10px] text-slate-400 mb-0.5"></div> <div className="text-[10px] text-slate-400 mb-0.5"></div>
<pre className="text-[10px] bg-white border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap"> <pre className="text-[10px] bg-canvas border border-hairline rounded px-2 py-1 overflow-auto max-h-40 whitespace-pre-wrap">
{after ?? '(空)'} {after ?? '(空)'}
</pre> </pre>
</div> </div>
@ -803,7 +803,7 @@ function MetricsSummary() {
].map(({ label, value }) => ( ].map(({ label, value }) => (
<div <div
key={label} key={label}
className="bg-white border border-hairline rounded px-2 py-1.5 text-center" className="bg-canvas border border-hairline rounded px-2 py-1.5 text-center"
> >
<div className="text-2xs font-semibold text-slate-800">{value}</div> <div className="text-2xs font-semibold text-slate-800">{value}</div>
<div className="text-[10px] text-slate-400 mt-0.5">{label}</div> <div className="text-[10px] text-slate-400 mt-0.5">{label}</div>
@ -953,7 +953,7 @@ function ReflectionTimelinePanel() {
type="button" type="button"
onClick={() => void fetchNextPage()} onClick={() => void fetchNextPage()}
disabled={isFetchingNextPage} disabled={isFetchingNextPage}
className="px-3 h-8 text-xs text-slate-600 border border-hairline bg-white hover:bg-surface rounded-md disabled:opacity-50 transition-colors" className="px-3 h-8 text-xs text-slate-600 border border-hairline bg-canvas hover:bg-surface rounded-md disabled:opacity-50 transition-colors"
> >
{isFetchingNextPage ? '読み込み中…' : 'さらに表示'} {isFetchingNextPage ? '読み込み中…' : 'さらに表示'}
</button> </button>

View File

@ -121,7 +121,7 @@ export function ModelSelect({ value, onChange, endpoint, apiKeyRaw }: ModelSelec
value={value} value={value}
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
placeholder={loading ? 'loading...' : 'choose or type a model'} placeholder={loading ? 'loading...' : 'choose or type a model'}
className="w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white" className="w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-canvas"
/> />
<datalist id="llm-workers-model-options"> <datalist id="llm-workers-model-options">
{options.map(m => <option key={m} value={m} />)} {options.map(m => <option key={m} value={m} />)}
@ -143,7 +143,7 @@ export function ModelSelect({ value, onChange, endpoint, apiKeyRaw }: ModelSelec
value={value} value={value}
onChange={e => onChange(e.target.value)} onChange={e => onChange(e.target.value)}
placeholder={loading ? 'loading...' : 'qwen3:8b'} placeholder={loading ? 'loading...' : 'qwen3:8b'}
className="w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white" className="w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-canvas"
/> />
{error && ( {error && (
<p className="text-2xs text-amber-700 bg-amber-50 border border-amber-100 px-2 py-1 rounded mt-1"> <p className="text-2xs text-amber-700 bg-amber-50 border border-amber-100 px-2 py-1 rounded mt-1">

View File

@ -28,7 +28,7 @@ export function MovementAccordion({ movements, onChange, onAdd, onRemove, onMove
const ruleCount = (movement.rules ?? []).length; const ruleCount = (movement.rules ?? []).length;
return ( return (
<div key={i} className="bg-white border border-slate-200 rounded-lg"> <div key={i} className="bg-canvas border border-slate-200 rounded-lg">
{/* Collapsed header */} {/* Collapsed header */}
<div <div
className="flex items-center gap-2 p-3 cursor-pointer select-none" className="flex items-center gap-2 p-3 cursor-pointer select-none"

View File

@ -48,7 +48,7 @@ export function MovementForm({ movement, movementNames, onChange, disabled = fal
value={movement.default_next ?? 'COMPLETE'} value={movement.default_next ?? 'COMPLETE'}
onChange={(e) => onChange('default_next', e.target.value)} onChange={(e) => onChange('default_next', e.target.value)}
disabled={disabled} disabled={disabled}
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white ${disabledClass}`} className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-canvas ${disabledClass}`}
> >
{nextOptions.map((opt) => ( {nextOptions.map((opt) => (
<option key={opt} value={opt}>{opt}</option> <option key={opt} value={opt}>{opt}</option>

View File

@ -242,7 +242,7 @@ export function PieceEditor({ name, isAdmin = true, source }: PieceEditorProps)
onClick={() => editMode === 'yaml' ? switchToVisual() : undefined} onClick={() => editMode === 'yaml' ? switchToVisual() : undefined}
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${ className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
editMode === 'visual' editMode === 'visual'
? 'bg-white text-slate-800 shadow-sm' ? 'bg-canvas text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-slate-700' : 'text-slate-500 hover:text-slate-700'
}`} }`}
> >
@ -252,7 +252,7 @@ export function PieceEditor({ name, isAdmin = true, source }: PieceEditorProps)
onClick={() => editMode === 'visual' ? switchToYaml() : undefined} onClick={() => editMode === 'visual' ? switchToYaml() : undefined}
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${ className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
editMode === 'yaml' editMode === 'yaml'
? 'bg-white text-slate-800 shadow-sm' ? 'bg-canvas text-slate-800 shadow-sm'
: 'text-slate-500 hover:text-slate-700' : 'text-slate-500 hover:text-slate-700'
}`} }`}
> >

View File

@ -58,7 +58,7 @@ export function PieceMetaForm({ piece, onChange, movementNames, disabled = false
value={piece.initial_movement ?? ''} value={piece.initial_movement ?? ''}
onChange={(e) => onChange('initial_movement', e.target.value)} onChange={(e) => onChange('initial_movement', e.target.value)}
disabled={disabled} disabled={disabled}
className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white ${disabledClass}`} className={`w-full px-3 py-2 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-canvas ${disabledClass}`}
> >
{movementNames.length === 0 && <option value="">--</option>} {movementNames.length === 0 && <option value="">--</option>}
{movementNames.map((name) => ( {movementNames.map((name) => (

View File

@ -61,7 +61,7 @@ export function ReflectionForm({ config, onChange }: SectionFormProps) {
enqueue <strong>LLM Workers</strong> worker enqueue <strong>LLM Workers</strong> worker
: :
</div> </div>
<pre className="mt-2 text-2xs font-mono bg-white border border-amber-200 rounded p-2 overflow-auto">{`id: reflection-1 <pre className="mt-2 text-2xs font-mono bg-canvas border border-amber-200 rounded p-2 overflow-auto">{`id: reflection-1
connection_type: direct connection_type: direct
endpoint: http://localhost:11434/v1 endpoint: http://localhost:11434/v1
model: qwen2.5:3b # cheap model: qwen2.5:3b # cheap

View File

@ -56,7 +56,7 @@ export function RulesTable({ rules, movementNames, onChange, disabled = false }:
value={rule.next} value={rule.next}
onChange={(e) => updateRule(i, 'next', e.target.value)} onChange={(e) => updateRule(i, 'next', e.target.value)}
disabled={disabled} disabled={disabled}
className={`w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white ${disabledClass}`} className={`w-full px-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-canvas ${disabledClass}`}
> >
{nextOptions.map((opt) => ( {nextOptions.map((opt) => (
<option key={opt} value={opt}>{opt}</option> <option key={opt} value={opt}>{opt}</option>

View File

@ -106,7 +106,7 @@ export function SecretInput({ rawValue, onChange, placeholder }: SecretInputProp
emit({ type: 'literal', value: e.target.value }); emit({ type: 'literal', value: e.target.value });
}} }}
placeholder={placeholder ?? 'sk-...'} placeholder={placeholder ?? 'sk-...'}
className="w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white" className="w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-canvas"
/> />
)} )}
@ -124,7 +124,7 @@ export function SecretInput({ rawValue, onChange, placeholder }: SecretInputProp
emit({ type: 'env_ref', env_name: next }); emit({ type: 'env_ref', env_name: next });
}} }}
placeholder="ENV_VAR_NAME" placeholder="ENV_VAR_NAME"
className="flex-1 h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-white font-mono" className="flex-1 h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none bg-canvas font-mono"
/> />
<span className="inline-flex items-center px-2 h-8 text-2xs rounded bg-slate-100 text-slate-600 font-mono"> <span className="inline-flex items-center px-2 h-8 text-2xs rounded bg-slate-100 text-slate-600 font-mono">
{'}'} {'}'}

View File

@ -120,7 +120,7 @@ export function SettingsSidebar({ activeSection, onSelectSection, isAdmin }: Set
const visibleGroups = CONFIG_GROUPS.filter(g => isAdmin || !('adminOnly' in g) || !g.adminOnly); const visibleGroups = CONFIG_GROUPS.filter(g => isAdmin || !('adminOnly' in g) || !g.adminOnly);
return ( return (
<div className="h-full overflow-y-auto border-r border-hairline bg-white p-3"> <div className="h-full overflow-y-auto border-r border-hairline bg-canvas p-3">
{visibleGroups.map(group => ( {visibleGroups.map(group => (
<div key={group.label} className="mb-3"> <div key={group.label} className="mb-3">
<div className="section-label px-2 py-1"> <div className="section-label px-2 py-1">

View File

@ -193,7 +193,7 @@ export function SkillsForm() {
placeholder="Install from URL..." placeholder="Install from URL..."
value={installUrl} value={installUrl}
onChange={e => setInstallUrl(e.target.value)} onChange={e => setInstallUrl(e.target.value)}
className="flex-1 min-w-0 px-2 py-1 text-xs border border-hairline rounded bg-white text-slate-700 placeholder:text-slate-400" className="flex-1 min-w-0 px-2 py-1 text-xs border border-hairline rounded bg-canvas text-slate-700 placeholder:text-slate-400"
/> />
<button <button
onClick={() => installMut.mutate()} onClick={() => installMut.mutate()}
@ -256,7 +256,7 @@ export function SkillsForm() {
value={newName} value={newName}
onChange={e => setNewName(e.target.value)} onChange={e => setNewName(e.target.value)}
placeholder="my-skill-name" placeholder="my-skill-name"
className="mt-1 block w-full px-2 py-1.5 text-xs border border-hairline rounded bg-white text-slate-700 placeholder:text-slate-400" className="mt-1 block w-full px-2 py-1.5 text-xs border border-hairline rounded bg-canvas text-slate-700 placeholder:text-slate-400"
/> />
{newName && !NAME_RE.test(newName) && ( {newName && !NAME_RE.test(newName) && (
<span className="text-[10px] text-red-500 mt-0.5">Lowercase letters, numbers, hyphens, underscores only</span> <span className="text-[10px] text-red-500 mt-0.5">Lowercase letters, numbers, hyphens, underscores only</span>
@ -269,7 +269,7 @@ export function SkillsForm() {
value={newContent} value={newContent}
onChange={e => setNewContent(e.target.value)} onChange={e => setNewContent(e.target.value)}
rows={14} rows={14}
className="mt-1 block w-full px-2 py-1.5 text-xs font-mono border border-hairline rounded bg-white text-slate-700 resize-y" className="mt-1 block w-full px-2 py-1.5 text-xs font-mono border border-hairline rounded bg-canvas text-slate-700 resize-y"
placeholder="# My Skill&#10;&#10;Instructions for the agent..." placeholder="# My Skill&#10;&#10;Instructions for the agent..."
/> />
</label> </label>
@ -350,7 +350,7 @@ export function SkillsForm() {
value={editContent} value={editContent}
onChange={e => setEditContent(e.target.value)} onChange={e => setEditContent(e.target.value)}
rows={18} rows={18}
className="block w-full px-2 py-1.5 text-xs font-mono border border-hairline rounded bg-white text-slate-700 resize-y" className="block w-full px-2 py-1.5 text-xs font-mono border border-hairline rounded bg-canvas text-slate-700 resize-y"
/> />
<div className="flex gap-2"> <div className="flex gap-2">
<button <button

View File

@ -143,7 +143,7 @@ export function SshAuditLog() {
<th className="px-2 py-1 font-semibold text-slate-700">Detail</th> <th className="px-2 py-1 font-semibold text-slate-700">Detail</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-hairline bg-white"> <tbody className="divide-y divide-hairline bg-canvas">
{(data ?? []).map(r => ( {(data ?? []).map(r => (
<tr key={r.id} className="hover:bg-surface/40"> <tr key={r.id} className="hover:bg-surface/40">
<td className="px-2 py-1 font-mono text-slate-600 whitespace-nowrap">{r.startedAt}</td> <td className="px-2 py-1 font-mono text-slate-600 whitespace-nowrap">{r.startedAt}</td>

View File

@ -211,7 +211,7 @@ export function SshGlobalConnectionsForm({ showToast, onChange }: Props) {
{error && <div className="text-xs text-red-500">{String(error)}</div>} {error && <div className="text-xs text-red-500">{String(error)}</div>}
{creating && ( {creating && (
<section className="border border-accent/40 rounded-md bg-white p-4"> <section className="border border-accent/40 rounded-md bg-canvas p-4">
<h4 className="text-xs font-semibold text-slate-700 mb-2"></h4> <h4 className="text-xs font-semibold text-slate-700 mb-2"></h4>
<SshConnectionForm <SshConnectionForm
existing={null} existing={null}
@ -370,7 +370,7 @@ function ReasonModal({ title, warning, onCancel, onSubmit }: ReasonModalProps) {
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="w-full max-w-md bg-white rounded-md shadow-lg border border-hairline overflow-hidden"> <div className="w-full max-w-md bg-surface rounded-md shadow-lg border border-hairline overflow-hidden">
<div className={`px-4 py-3 border-b border-hairline ${warning ? 'bg-red-50' : ''}`}> <div className={`px-4 py-3 border-b border-hairline ${warning ? 'bg-red-50' : ''}`}>
<h3 className={`text-sm font-semibold ${warning ? 'text-red-800' : 'text-slate-900'}`}>{title}</h3> <h3 className={`text-sm font-semibold ${warning ? 'text-red-800' : 'text-slate-900'}`}>{title}</h3>
</div> </div>

View File

@ -149,7 +149,7 @@ export function SshGrantsForm({ showToast }: Props) {
{globalConns.map(c => { {globalConns.map(c => {
const grants = grantsByConn.get(c.id) ?? []; const grants = grantsByConn.get(c.id) ?? [];
return ( return (
<section key={c.id} className="border border-hairline rounded-md bg-white"> <section key={c.id} className="border border-hairline rounded-md bg-canvas">
<header className="px-3 py-2 border-b border-hairline bg-surface/40 flex items-center justify-between"> <header className="px-3 py-2 border-b border-hairline bg-surface/40 flex items-center justify-between">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="text-xs font-semibold text-slate-900 truncate">{c.label}</div> <div className="text-xs font-semibold text-slate-900 truncate">{c.label}</div>
@ -262,7 +262,7 @@ function CreateGrantForm({ connections, pieces, onSubmit, onCancel }: CreateGran
const inputCls = 'w-full text-xs px-2 py-1.5 border border-hairline rounded'; const inputCls = 'w-full text-xs px-2 py-1.5 border border-hairline rounded';
return ( return (
<form onSubmit={handleSubmit} className="border border-accent/40 rounded-md bg-white p-4 space-y-3"> <form onSubmit={handleSubmit} className="border border-accent/40 rounded-md bg-canvas p-4 space-y-3">
<h4 className="text-xs font-semibold text-slate-700">Grant </h4> <h4 className="text-xs font-semibold text-slate-700">Grant </h4>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<label className="block"> <label className="block">
@ -351,7 +351,7 @@ function CreateGrantForm({ connections, pieces, onSubmit, onCancel }: CreateGran
</div> </div>
{error && <div className="text-xs text-red-600">{error}</div>} {error && <div className="text-xs text-red-600">{error}</div>}
<div className="flex items-center justify-end gap-2 pt-2 border-t border-hairline"> <div className="flex items-center justify-end gap-2 pt-2 border-t border-hairline">
<button type="button" onClick={onCancel} disabled={submitting} className="px-3 h-7 text-xs text-slate-700 border border-hairline bg-white rounded-md hover:bg-surface disabled:opacity-50"> <button type="button" onClick={onCancel} disabled={submitting} className="px-3 h-7 text-xs text-slate-700 border border-hairline bg-canvas rounded-md hover:bg-surface disabled:opacity-50">
</button> </button>
<button type="submit" disabled={!valid || submitting} className="px-3 h-7 text-xs font-semibold bg-accent text-accent-fg rounded-md hover:bg-accent-deep disabled:opacity-50"> <button type="submit" disabled={!valid || submitting} className="px-3 h-7 text-xs font-semibold bg-accent text-accent-fg rounded-md hover:bg-accent-deep disabled:opacity-50">
@ -385,7 +385,7 @@ function ReasonModal({ title, warning, onCancel, onSubmit }: ReasonModalProps) {
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="w-full max-w-md bg-white rounded-md shadow-lg border border-hairline overflow-hidden"> <div className="w-full max-w-md bg-surface rounded-md shadow-lg border border-hairline overflow-hidden">
<div className={`px-4 py-3 border-b border-hairline ${warning ? 'bg-red-50' : ''}`}> <div className={`px-4 py-3 border-b border-hairline ${warning ? 'bg-red-50' : ''}`}>
<h3 className={`text-sm font-semibold ${warning ? 'text-red-800' : 'text-slate-900'}`}>{title}</h3> <h3 className={`text-sm font-semibold ${warning ? 'text-red-800' : 'text-slate-900'}`}>{title}</h3>
</div> </div>

View File

@ -91,7 +91,7 @@ export function SshMasterKeyRotationForm({ showToast }: Props) {
</p> </p>
</div> </div>
<div className="rounded-md border border-hairline bg-white p-3"> <div className="rounded-md border border-hairline bg-canvas p-3">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<div className="text-2xs font-semibold text-slate-500 uppercase tracking-wide"></div> <div className="text-2xs font-semibold text-slate-500 uppercase tracking-wide"></div>
@ -168,7 +168,7 @@ function ConfirmDialog({
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="w-full max-w-lg bg-white rounded-md shadow-lg border border-amber-300 overflow-hidden"> <div className="w-full max-w-lg bg-surface rounded-md shadow-lg border border-amber-300 overflow-hidden">
<div className="px-4 py-3 border-b border-amber-200 bg-amber-50"> <div className="px-4 py-3 border-b border-amber-200 bg-amber-50">
<h3 className="text-sm font-semibold text-amber-900"> Master Key Rotation </h3> <h3 className="text-sm font-semibold text-amber-900"> Master Key Rotation </h3>
</div> </div>

View File

@ -149,7 +149,7 @@ export function ToolTagInput({ value, onChange, disabled = false }: ToolTagInput
)} )}
</div> </div>
{!disabled && showDropdown && groupedSuggestions.length > 0 && ( {!disabled && showDropdown && groupedSuggestions.length > 0 && (
<div className="absolute z-10 mt-1 w-full max-h-72 overflow-y-auto bg-white border border-slate-200 rounded-lg shadow-lg"> <div className="absolute z-10 mt-1 w-full max-h-72 overflow-y-auto bg-surface border border-slate-200 rounded-lg shadow-lg">
{groupedSuggestions.map((g) => ( {groupedSuggestions.map((g) => (
<div key={g.key}> <div key={g.key}>
<div className="sticky top-0 px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-500 bg-slate-50 border-b border-slate-100"> <div className="sticky top-0 px-3 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-500 bg-slate-50 border-b border-slate-100">

View File

@ -31,7 +31,7 @@ export function FieldInput({ value, onChange, type = 'text', placeholder, disabl
disabled={disabled} disabled={disabled}
title={disabled ? disabledReason : undefined} title={disabled ? disabledReason : undefined}
className={`w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow ${ className={`w-full h-8 px-2.5 text-[13px] border border-hairline rounded-md focus:ring-2 focus:ring-accent-ring focus:border-accent outline-none transition-shadow ${
disabled ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : 'bg-white' disabled ? 'bg-slate-50 text-slate-500 cursor-not-allowed' : 'bg-canvas'
}`} }`}
/> />
); );

View File

@ -7,7 +7,7 @@ interface StatChipProps {
export function StatChip({ label, value, valueClassName }: StatChipProps) { export function StatChip({ label, value, valueClassName }: StatChipProps) {
const isNumber = typeof value === 'number'; const isNumber = typeof value === 'number';
return ( return (
<div className="flex-1 min-w-[72px] bg-white border border-slate-200 rounded-xl px-3 py-2 shadow-sm"> <div className="flex-1 min-w-[72px] bg-canvas border border-slate-200 rounded-xl px-3 py-2 shadow-sm">
<div className="text-[10px] font-bold text-slate-500 uppercase tracking-wide">{label}</div> <div className="text-[10px] font-bold text-slate-500 uppercase tracking-wide">{label}</div>
<div <div
className={ className={

View File

@ -85,7 +85,7 @@ export function AddBrowserSessionDialog({ existingProfile, onClose }: Props) {
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className={`bg-white rounded-lg shadow-xl ${dialogSize} overflow-hidden flex flex-col`}> <div className={`bg-surface rounded-lg shadow-xl ${dialogSize} overflow-hidden flex flex-col`}>
<div className="px-4 py-3 border-b border-hairline flex items-center justify-between"> <div className="px-4 py-3 border-b border-hairline flex items-center justify-between">
<h3 className="text-sm font-semibold text-slate-800"> <h3 className="text-sm font-semibold text-slate-800">
{existingProfile ? `再ログイン: ${existingProfile.label}` : 'ブラウザセッションを追加'} {existingProfile ? `再ログイン: ${existingProfile.label}` : 'ブラウザセッションを追加'}

View File

@ -99,7 +99,7 @@ export function MonacoFileEditor({ subdir, filename, content, mtime, size, onSav
return ( return (
<div className="flex flex-col h-full overflow-hidden"> <div className="flex flex-col h-full overflow-hidden">
{/* File header */} {/* File header */}
<div className="flex-shrink-0 flex items-center gap-2 px-4 py-2.5 border-b border-hairline bg-white"> <div className="flex-shrink-0 flex items-center gap-2 px-4 py-2.5 border-b border-hairline bg-canvas">
<span className="text-xs font-mono font-semibold text-slate-800 truncate"> <span className="text-xs font-mono font-semibold text-slate-800 truncate">
{filename} {filename}
</span> </span>
@ -146,7 +146,7 @@ export function MonacoFileEditor({ subdir, filename, content, mtime, size, onSav
</div> </div>
{/* Footer: save button + metadata */} {/* Footer: save button + metadata */}
<div className="flex-shrink-0 flex items-center gap-3 px-4 py-2.5 border-t border-hairline bg-white"> <div className="flex-shrink-0 flex items-center gap-3 px-4 py-2.5 border-t border-hairline bg-canvas">
{!isReadOnly && ( {!isReadOnly && (
<button <button
type="button" type="button"

View File

@ -99,14 +99,14 @@ function NewNoteForm({ onCreated }: { onCreated: (filePath: string) => void }) {
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<input <input
className="flex-1 border border-hairline rounded px-2 py-1 text-[13px] bg-white focus:outline-none focus:ring-1 focus:ring-accent" className="flex-1 border border-hairline rounded px-2 py-1 text-[13px] bg-canvas focus:outline-none focus:ring-1 focus:ring-accent"
placeholder="フォルダー名 (例: cve)" placeholder="フォルダー名 (例: cve)"
value={folder} value={folder}
onChange={(e) => setFolder(e.target.value)} onChange={(e) => setFolder(e.target.value)}
/> />
<span className="text-slate-400 text-[13px]">/</span> <span className="text-slate-400 text-[13px]">/</span>
<input <input
className="flex-1 border border-hairline rounded px-2 py-1 text-[13px] bg-white focus:outline-none focus:ring-1 focus:ring-accent" className="flex-1 border border-hairline rounded px-2 py-1 text-[13px] bg-canvas focus:outline-none focus:ring-1 focus:ring-accent"
placeholder="ファイル名 (例: foo.md)" placeholder="ファイル名 (例: foo.md)"
value={fileName} value={fileName}
onChange={(e) => setFileName(e.target.value)} onChange={(e) => setFileName(e.target.value)}
@ -278,7 +278,7 @@ tags: [security, cve]
<label className="flex flex-col gap-0.5"> <label className="flex flex-col gap-0.5">
<span className="text-2xs font-medium text-slate-500 uppercase tracking-wide">Title</span> <span className="text-2xs font-medium text-slate-500 uppercase tracking-wide">Title</span>
<input <input
className="border border-hairline rounded px-2 py-1 text-[13px] bg-white focus:outline-none focus:ring-1 focus:ring-accent" className="border border-hairline rounded px-2 py-1 text-[13px] bg-canvas focus:outline-none focus:ring-1 focus:ring-accent"
value={state.title} value={state.title}
onChange={(e) => setState({ ...state, title: e.target.value })} onChange={(e) => setState({ ...state, title: e.target.value })}
placeholder="(optional)" placeholder="(optional)"
@ -289,7 +289,7 @@ tags: [security, cve]
<label className="flex flex-col gap-0.5"> <label className="flex flex-col gap-0.5">
<span className="text-2xs font-medium text-slate-500 uppercase tracking-wide">Visibility</span> <span className="text-2xs font-medium text-slate-500 uppercase tracking-wide">Visibility</span>
<select <select
className="border border-hairline rounded px-2 py-1 text-[13px] bg-white focus:outline-none focus:ring-1 focus:ring-accent" className="border border-hairline rounded px-2 py-1 text-[13px] bg-canvas focus:outline-none focus:ring-1 focus:ring-accent"
value={state.visibility} value={state.visibility}
onChange={(e) => setState({ ...state, visibility: e.target.value as ParsedFm['visibility'] })} onChange={(e) => setState({ ...state, visibility: e.target.value as ParsedFm['visibility'] })}
> >
@ -304,7 +304,7 @@ tags: [security, cve]
<label className="flex flex-col gap-0.5"> <label className="flex flex-col gap-0.5">
<span className="text-2xs font-medium text-slate-500 uppercase tracking-wide">Scope Org ID</span> <span className="text-2xs font-medium text-slate-500 uppercase tracking-wide">Scope Org ID</span>
<input <input
className="border border-hairline rounded px-2 py-1 text-[13px] bg-white focus:outline-none focus:ring-1 focus:ring-accent" className="border border-hairline rounded px-2 py-1 text-[13px] bg-canvas focus:outline-none focus:ring-1 focus:ring-accent"
value={state.scope_org_id} value={state.scope_org_id}
onChange={(e) => setState({ ...state, scope_org_id: e.target.value })} onChange={(e) => setState({ ...state, scope_org_id: e.target.value })}
placeholder="gitea-org-name" placeholder="gitea-org-name"
@ -316,7 +316,7 @@ tags: [security, cve]
<label className="flex flex-col gap-0.5"> <label className="flex flex-col gap-0.5">
<span className="text-2xs font-medium text-slate-500 uppercase tracking-wide">Mode Hint</span> <span className="text-2xs font-medium text-slate-500 uppercase tracking-wide">Mode Hint</span>
<select <select
className="border border-hairline rounded px-2 py-1 text-[13px] bg-white focus:outline-none focus:ring-1 focus:ring-accent" className="border border-hairline rounded px-2 py-1 text-[13px] bg-canvas focus:outline-none focus:ring-1 focus:ring-accent"
value={state.mode_hint} value={state.mode_hint}
onChange={(e) => setState({ ...state, mode_hint: e.target.value as ParsedFm['mode_hint'] })} onChange={(e) => setState({ ...state, mode_hint: e.target.value as ParsedFm['mode_hint'] })}
> >
@ -330,7 +330,7 @@ tags: [security, cve]
<label className="flex flex-col gap-0.5 col-span-2"> <label className="flex flex-col gap-0.5 col-span-2">
<span className="text-2xs font-medium text-slate-500 uppercase tracking-wide">Tags ()</span> <span className="text-2xs font-medium text-slate-500 uppercase tracking-wide">Tags ()</span>
<input <input
className="border border-hairline rounded px-2 py-1 text-[13px] bg-white focus:outline-none focus:ring-1 focus:ring-accent" className="border border-hairline rounded px-2 py-1 text-[13px] bg-canvas focus:outline-none focus:ring-1 focus:ring-accent"
value={state.tags} value={state.tags}
onChange={(e) => setState({ ...state, tags: e.target.value })} onChange={(e) => setState({ ...state, tags: e.target.value })}
placeholder="security, cve, ..." placeholder="security, cve, ..."
@ -342,7 +342,7 @@ tags: [security, cve]
{/* Markdown body */} {/* Markdown body */}
<div className="flex-1 min-h-0 overflow-hidden"> <div className="flex-1 min-h-0 overflow-hidden">
<textarea <textarea
className="w-full h-full resize-none p-4 font-mono text-[13px] text-slate-800 bg-white focus:outline-none" className="w-full h-full resize-none p-4 font-mono text-[13px] text-slate-800 bg-canvas focus:outline-none"
value={state.body} value={state.body}
onChange={(e) => setState({ ...state, body: e.target.value })} onChange={(e) => setState({ ...state, body: e.target.value })}
placeholder="Markdown body…" placeholder="Markdown body…"

View File

@ -43,7 +43,7 @@ function ToggleRow({
}`} }`}
> >
<span <span
className={`absolute left-0 top-0.5 h-5 w-5 rounded-full bg-white shadow-sm transition-transform ${ className={`absolute left-0 top-0.5 h-5 w-5 rounded-full bg-canvas shadow-sm transition-transform ${
checked ? 'translate-x-5' : 'translate-x-0.5' checked ? 'translate-x-5' : 'translate-x-0.5'
}`} }`}
/> />

View File

@ -143,7 +143,7 @@ export function SaveAsScriptDialog({ recordingName, onClose, onSuccess }: SaveAs
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={e => { if (e.target === e.currentTarget) onClose(); }} onClick={e => { if (e.target === e.currentTarget) onClose(); }}
> >
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg mx-4 overflow-hidden flex flex-col max-h-[90vh]"> <div className="bg-surface rounded-xl shadow-xl w-full max-w-lg mx-4 overflow-hidden flex flex-col max-h-[90vh]">
{/* Header */} {/* Header */}
<div className="flex items-center gap-3 px-5 py-4 border-b border-hairline"> <div className="flex items-center gap-3 px-5 py-4 border-b border-hairline">
<span className="text-sm font-semibold text-slate-800 flex-1">Save as Script</span> <span className="text-sm font-semibold text-slate-800 flex-1">Save as Script</span>
@ -251,19 +251,19 @@ export function SaveAsScriptDialog({ recordingName, onClose, onSuccess }: SaveAs
value={hint.name} value={hint.name}
onChange={e => updateHint(idx, { name: e.target.value })} onChange={e => updateHint(idx, { name: e.target.value })}
placeholder="param name" placeholder="param name"
className="px-2 py-1 rounded border border-hairline text-2xs font-mono bg-white focus:outline-none focus:ring-1 focus:ring-accent/30" className="px-2 py-1 rounded border border-hairline text-2xs font-mono bg-canvas focus:outline-none focus:ring-1 focus:ring-accent/30"
/> />
<input <input
type="text" type="text"
value={hint.valueToReplace} value={hint.valueToReplace}
onChange={e => updateHint(idx, { valueToReplace: e.target.value })} onChange={e => updateHint(idx, { valueToReplace: e.target.value })}
placeholder="value to replace (literal)" placeholder="value to replace (literal)"
className="px-2 py-1 rounded border border-hairline text-2xs font-mono bg-white focus:outline-none focus:ring-1 focus:ring-accent/30" className="px-2 py-1 rounded border border-hairline text-2xs font-mono bg-canvas focus:outline-none focus:ring-1 focus:ring-accent/30"
/> />
<select <select
value={hint.type} value={hint.type}
onChange={e => updateHint(idx, { type: e.target.value as ParamHint['type'] })} onChange={e => updateHint(idx, { type: e.target.value as ParamHint['type'] })}
className="px-2 py-1 rounded border border-hairline text-2xs bg-white focus:outline-none focus:ring-1 focus:ring-accent/30" className="px-2 py-1 rounded border border-hairline text-2xs bg-canvas focus:outline-none focus:ring-1 focus:ring-accent/30"
> >
<option value="string">string</option> <option value="string">string</option>
<option value="number">number</option> <option value="number">number</option>

View File

@ -379,7 +379,7 @@ export function SshConnectionForm({ existing, adminContext, onSubmit, onCancel }
type="button" type="button"
onClick={onCancel} onClick={onCancel}
disabled={submitting} disabled={submitting}
className="px-3 h-7 text-xs text-slate-700 border border-hairline bg-white rounded-md hover:bg-surface disabled:opacity-50" className="px-3 h-7 text-xs text-slate-700 border border-hairline bg-canvas rounded-md hover:bg-surface disabled:opacity-50"
> >
</button> </button>

View File

@ -258,7 +258,7 @@ export function SshConnectionsPanel({ showToast }: SshConnectionsPanelProps = {}
{error && <div className="text-xs text-red-500">: {String(error)}</div>} {error && <div className="text-xs text-red-500">: {String(error)}</div>}
{creating && ( {creating && (
<section className="mb-5 border border-accent/40 rounded-md bg-white p-4"> <section className="mb-5 border border-accent/40 rounded-md bg-canvas p-4">
<h3 className="text-xs font-semibold text-slate-700 mb-2"> SSH </h3> <h3 className="text-xs font-semibold text-slate-700 mb-2"> SSH </h3>
<SshConnectionForm <SshConnectionForm
existing={null} existing={null}

View File

@ -49,7 +49,7 @@ export function SshHostKeyDialog({ test, replaceMode, onClose, onVerify }: SshHo
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="w-full max-w-lg bg-white rounded-md shadow-lg border border-hairline overflow-hidden"> <div className="w-full max-w-lg bg-surface rounded-md shadow-lg border border-hairline overflow-hidden">
<div className="px-5 py-3 border-b border-hairline"> <div className="px-5 py-3 border-b border-hairline">
<h3 className="text-sm font-semibold text-slate-900"> <h3 className="text-sm font-semibold text-slate-900">
{test.verdict === 'first_observe' ? 'ホストキーを記録' : 'ホストキーを置き換え'} {test.verdict === 'first_observe' ? 'ホストキーを記録' : 'ホストキーを置き換え'}
@ -98,7 +98,7 @@ export function SshHostKeyDialog({ test, replaceMode, onClose, onVerify }: SshHo
type="button" type="button"
onClick={onClose} onClick={onClose}
disabled={submitting} disabled={submitting}
className="px-3 h-7 text-xs text-slate-700 border border-hairline bg-white rounded-md hover:bg-surface disabled:opacity-50" className="px-3 h-7 text-xs text-slate-700 border border-hairline bg-canvas rounded-md hover:bg-surface disabled:opacity-50"
> >
</button> </button>

View File

@ -33,7 +33,7 @@ export function SshPublicKeyDialog({ publicKey, label, freshlyGenerated, onClose
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
<div className="w-full max-w-2xl bg-white rounded-md shadow-lg border border-hairline overflow-hidden"> <div className="w-full max-w-2xl bg-surface rounded-md shadow-lg border border-hairline overflow-hidden">
<div className="px-4 py-3 border-b border-hairline bg-surface/40"> <div className="px-4 py-3 border-b border-hairline bg-surface/40">
<h3 className="text-sm font-semibold text-slate-900"> <h3 className="text-sm font-semibold text-slate-900">
{label && <span className="font-normal text-slate-500"> {label}</span>} {label && <span className="font-normal text-slate-500"> {label}</span>}
@ -83,7 +83,7 @@ export function SshPublicKeyDialog({ publicKey, label, freshlyGenerated, onClose
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="px-3 h-7 text-xs text-slate-700 border border-hairline bg-white rounded-md hover:bg-surface" className="px-3 h-7 text-xs text-slate-700 border border-hairline bg-canvas rounded-md hover:bg-surface"
> >
</button> </button>

View File

@ -117,7 +117,7 @@ function NoteContentModal({
onClick={onClose} onClick={onClose}
> >
<div <div
className="bg-white rounded-md shadow-lg w-full max-w-3xl max-h-[85vh] flex flex-col overflow-hidden" className="bg-surface rounded-md shadow-lg w-full max-w-3xl max-h-[85vh] flex flex-col overflow-hidden"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<header className="flex items-center justify-between border-b border-hairline px-4 py-3 flex-shrink-0"> <header className="flex items-center justify-between border-b border-hairline px-4 py-3 flex-shrink-0">
@ -157,7 +157,7 @@ function ModeSelect({
}) { }) {
return ( return (
<select <select
className="border border-hairline rounded text-2xs px-1 py-0.5 bg-white focus:outline-none focus:ring-1 focus:ring-accent" className="border border-hairline rounded text-2xs px-1 py-0.5 bg-canvas focus:outline-none focus:ring-1 focus:ring-accent"
value={mode} value={mode}
onChange={(e) => onChange(e.target.value as 'search' | 'inject')} onChange={(e) => onChange(e.target.value as 'search' | 'inject')}
> >
@ -412,7 +412,7 @@ export function SubscriptionsPanel({ currentUserId }: { currentUserId: string })
<section> <section>
<h3 className="text-[13px] font-semibold text-slate-800 mb-2">Discover</h3> <h3 className="text-[13px] font-semibold text-slate-800 mb-2">Discover</h3>
<input <input
className="border border-hairline rounded px-2 py-1.5 mb-3 w-full text-[13px] bg-white focus:outline-none focus:ring-1 focus:ring-accent placeholder:text-slate-400" className="border border-hairline rounded px-2 py-1.5 mb-3 w-full text-[13px] bg-canvas focus:outline-none focus:ring-1 focus:ring-accent placeholder:text-slate-400"
placeholder="Search by title, tag, body…" placeholder="Search by title, tag, body…"
value={q} value={q}
onChange={(e) => setQ(e.target.value)} onChange={(e) => setQ(e.target.value)}

View File

@ -325,7 +325,7 @@ export function UserFolderTab({ showToast }: UserFolderTabProps = {}) {
<div className="flex h-full gap-2 p-2 overflow-hidden"> <div className="flex h-full gap-2 p-2 overflow-hidden">
{/* Left: file tree */} {/* Left: file tree */}
<div <div
className="bg-white border border-hairline rounded-md overflow-hidden flex flex-col" className="bg-canvas border border-hairline rounded-md overflow-hidden flex flex-col"
style={{ width: 'clamp(200px, 22vw, 280px)', flexShrink: 0 }} style={{ width: 'clamp(200px, 22vw, 280px)', flexShrink: 0 }}
> >
<div className="flex-shrink-0 px-3 py-2.5 border-b border-hairline"> <div className="flex-shrink-0 px-3 py-2.5 border-b border-hairline">
@ -346,7 +346,7 @@ export function UserFolderTab({ showToast }: UserFolderTabProps = {}) {
</div> </div>
{/* Right: editor / virtual panel */} {/* Right: editor / virtual panel */}
<div className="flex-1 min-w-0 bg-white border border-hairline rounded-md overflow-hidden flex flex-col"> <div className="flex-1 min-w-0 bg-canvas border border-hairline rounded-md overflow-hidden flex flex-col">
{/* agents-md virtual pane */} {/* agents-md virtual pane */}
{isVirtualSelected && selectedSubdir === 'agents-md' && ( {isVirtualSelected && selectedSubdir === 'agents-md' && (
<AgentsMdPanel onDirtyChange={setEditorDirty} /> <AgentsMdPanel onDirtyChange={setEditorDirty} />

View File

@ -6,6 +6,50 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
/* theme:light */
:root {
color-scheme: light;
/* Neutral ramp Tailwind's default slate values. Components use
*-slate-* with semantic intent (50/100 = surfaces, 700/900 = text),
so [data-theme=dark] inverts the ramp to flip fg/bg coherently. */
--slate-50: #f8fafc; --slate-100: #f1f5f9; --slate-200: #e2e8f0;
--slate-300: #cbd5e1; --slate-400: #94a3b8; --slate-500: #64748b;
--slate-600: #475569; --slate-700: #334155; --slate-800: #1e293b;
--slate-900: #0f172a; --slate-950: #020617;
--gray-400: #9ca3af; --gray-500: #6b7280; --gray-700: #374151;
/* Semantic surface tokens (mirror tailwind.config.js). */
--canvas: #ffffff; --surface: #fafafa; --surface-2: #f4f4f5;
--hairline: #e4e4e7; --hairline-soft: #f4f4f5;
--ink: #0f172a; --muted: #64748b;
--scrollbar-thumb: #d4d4d8; --scrollbar-thumb-hover: #a1a1aa;
/* Dark accent fallback (used only when /api/branding hasn't set
--brand-primary*). Full runtime-branding + WCAG pairing is Phase 2. */
--accent-on-dark: #fafafa; --accent-on-dark-fg: #18181b;
}
[data-theme="dark"] {
color-scheme: dark;
/* Inverted neutral ramp (zinc-tuned). TUNE ON A REAL DISPLAY dense UI
borders rely on hairline; verify surface/surface-2/hairline separation
and WCAG AA text contrast before shipping (spec §4.1 #6). */
--slate-50: #0a0a0c; --slate-100: #131316; --slate-200: #202024;
--slate-300: #2e2e34; --slate-400: #52525b; --slate-500: #8b8b93;
--slate-600: #a1a1aa; --slate-700: #c4c4cc; --slate-800: #dedee2;
--slate-900: #f1f1f3; --slate-950: #fafafa;
--gray-400: #6b7280; --gray-500: #9ca3af; --gray-700: #d1d5db;
--canvas: #0a0a0c; --surface: #16161a; --surface-2: #202024;
--hairline: #2e2e34; --hairline-soft: #202024;
--ink: #e7e7ea; --muted: #a1a1aa;
--scrollbar-thumb: #3f3f46; --scrollbar-thumb-hover: #52525b;
--accent-on-dark: #fafafa; --accent-on-dark-fg: #18181b;
}
/* When stored pref is 'system' the inline script sets data-theme already,
but keep an OS fallback for the brief pre-script window / no-JS. */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) { color-scheme: dark; }
}
html, body, #root { html, body, #root {
min-height: 100dvh; min-height: 100dvh;
} }
@ -13,8 +57,8 @@
body { body {
margin: 0; margin: 0;
/* Refero-inspired refresh: cleaner canvas, slightly cooler text. */ /* Refero-inspired refresh: cleaner canvas, slightly cooler text. */
background-color: #ffffff; background-color: var(--canvas);
color: #18181b; color: var(--ink);
font-family: 'IBM Plex Sans JP', 'Hiragino Sans', system-ui, sans-serif; font-family: 'IBM Plex Sans JP', 'Hiragino Sans', system-ui, sans-serif;
font-size: 13px; font-size: 13px;
font-feature-settings: 'cv11', 'ss01', 'ss03'; font-feature-settings: 'cv11', 'ss01', 'ss03';
@ -63,11 +107,11 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #d4d4d8; background: var(--scrollbar-thumb);
border-radius: 999px; border-radius: 999px;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #a1a1aa; background: var(--scrollbar-thumb-hover);
} }
.scrollbar-none { .scrollbar-none {
@ -94,7 +138,7 @@
/* Refined input baseline. Components opt in by adding `.input`. */ /* Refined input baseline. Components opt in by adding `.input`. */
.input { .input {
@apply h-8 px-2.5 rounded-md border border-hairline bg-white text-[13px] text-slate-900; @apply h-8 px-2.5 rounded-md border border-hairline bg-canvas text-[13px] text-slate-900;
@apply focus:outline-none focus:ring-2 focus:ring-accent-ring focus:border-accent; @apply focus:outline-none focus:ring-2 focus:ring-accent-ring focus:border-accent;
@apply transition-shadow; @apply transition-shadow;
} }
@ -118,10 +162,10 @@
@apply bg-accent text-accent-fg border-accent hover:bg-accent-deep; @apply bg-accent text-accent-fg border-accent hover:bg-accent-deep;
} }
.btn-ghost { .btn-ghost {
@apply bg-white text-slate-700 border-hairline hover:bg-surface; @apply bg-canvas text-slate-700 border-hairline hover:bg-surface;
} }
.btn-danger { .btn-danger {
@apply bg-white text-red-700 border-red-200 hover:bg-red-50; @apply bg-canvas text-red-700 border-red-200 hover:bg-red-50;
} }
.chat-pet-overlay { .chat-pet-overlay {
@ -217,7 +261,7 @@
border-radius: 999px; border-radius: 999px;
display: grid; display: grid;
place-items: center; place-items: center;
background: #ffffff; background: var(--canvas);
color: #0f766e; color: #0f766e;
border: 1px solid rgb(15 23 42 / 0.08); border: 1px solid rgb(15 23 42 / 0.08);
box-shadow: 0 10px 24px rgb(15 23 42 / 0.22), 0 0 0 6px rgb(45 212 191 / 0.18); box-shadow: 0 10px 24px rgb(15 23 42 / 0.22), 0 0 0 6px rgb(45 212 191 / 0.18);

View File

@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
const cssPath = resolve(dirname(fileURLToPath(import.meta.url)), '../index.css');
/** Collect `--var` names declared in the first `{...}` block after `selector`. */
function varsInBlock(css: string, selector: string): Set<string> {
const i = css.indexOf(selector);
if (i === -1) throw new Error(`selector not found in index.css: ${selector}`);
const open = css.indexOf('{', i);
const close = css.indexOf('}', open);
const body = css.slice(open + 1, close);
const names = new Set<string>();
for (const m of body.matchAll(/(--[\w-]+)\s*:/g)) names.add(m[1]);
return names;
}
describe('theme CSS var parity', () => {
const css = readFileSync(cssPath, 'utf8');
it(':root and [data-theme="dark"] declare identical custom properties', () => {
const light = varsInBlock(css, '/* theme:light */');
const dark = varsInBlock(css, '[data-theme="dark"]');
expect([...light].filter((v) => !dark.has(v))).toEqual([]);
expect([...dark].filter((v) => !light.has(v))).toEqual([]);
});
it('defines the core neutral ramp', () => {
const light = varsInBlock(css, '/* theme:light */');
for (const v of ['--slate-50', '--slate-500', '--slate-900', '--canvas', '--ink']) {
expect(light.has(v)).toBe(true);
}
});
});

67
ui/src/lib/theme.test.ts Normal file
View File

@ -0,0 +1,67 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { resolveTheme, isThemePref, readStoredTheme, writeStoredTheme, THEME_STORAGE_KEY } from './theme';
describe('resolveTheme', () => {
it('explicit dark/light win regardless of system', () => {
expect(resolveTheme('dark', false)).toBe('dark');
expect(resolveTheme('dark', true)).toBe('dark');
expect(resolveTheme('light', true)).toBe('light');
expect(resolveTheme('light', false)).toBe('light');
});
it('system follows the OS preference', () => {
expect(resolveTheme('system', true)).toBe('dark');
expect(resolveTheme('system', false)).toBe('light');
});
});
describe('isThemePref', () => {
it('accepts valid prefs, rejects junk', () => {
expect(isThemePref('system')).toBe(true);
expect(isThemePref('light')).toBe(true);
expect(isThemePref('dark')).toBe(true);
expect(isThemePref('blue')).toBe(false);
expect(isThemePref(null)).toBe(false);
expect(isThemePref(undefined)).toBe(false);
});
});
describe('readStoredTheme', () => {
afterEach(() => vi.unstubAllGlobals());
it('returns the stored pref when valid', () => {
vi.stubGlobal('localStorage', { getItem: () => 'dark', setItem: () => {} });
expect(readStoredTheme()).toBe('dark');
});
it('falls back to system when missing or invalid', () => {
vi.stubGlobal('localStorage', { getItem: () => null, setItem: () => {} });
expect(readStoredTheme()).toBe('system');
vi.stubGlobal('localStorage', { getItem: () => 'bogus', setItem: () => {} });
expect(readStoredTheme()).toBe('system');
});
it('falls back to system when localStorage throws', () => {
vi.stubGlobal('localStorage', {
getItem: () => {
throw new Error('blocked');
},
setItem: () => {},
});
expect(readStoredTheme()).toBe('system');
});
});
describe('writeStoredTheme', () => {
afterEach(() => vi.unstubAllGlobals());
it('writes the pref under the storage key', () => {
const setItem = vi.fn();
vi.stubGlobal('localStorage', { getItem: () => null, setItem });
writeStoredTheme('dark');
expect(setItem).toHaveBeenCalledWith(THEME_STORAGE_KEY, 'dark');
});
it('swallows errors when storage throws', () => {
vi.stubGlobal('localStorage', {
setItem: () => {
throw new Error('blocked');
},
});
expect(() => writeStoredTheme('dark')).not.toThrow();
});
});

53
ui/src/lib/theme.ts Normal file
View File

@ -0,0 +1,53 @@
export type ThemePref = 'system' | 'light' | 'dark';
export type ResolvedTheme = 'light' | 'dark';
export const THEME_STORAGE_KEY = 'maestro.theme';
export function isThemePref(v: unknown): v is ThemePref {
return v === 'system' || v === 'light' || v === 'dark';
}
/** Resolve the effective theme from the stored preference + OS state. Pure. */
export function resolveTheme(pref: ThemePref, systemPrefersDark: boolean): ResolvedTheme {
if (pref === 'dark') return 'dark';
if (pref === 'light') return 'light';
return systemPrefersDark ? 'dark' : 'light';
}
export function readStoredTheme(): ThemePref {
try {
const v = localStorage.getItem(THEME_STORAGE_KEY);
return isThemePref(v) ? v : 'system';
} catch {
return 'system';
}
}
export function writeStoredTheme(pref: ThemePref): void {
try {
localStorage.setItem(THEME_STORAGE_KEY, pref);
} catch {
/* private mode / disabled storage — ignore */
}
}
/** Apply the resolved theme to the document root (drives [data-theme="…"]). */
export function applyTheme(root: HTMLElement, resolved: ResolvedTheme): void {
root.dataset.theme = resolved;
}
const DARK_MQ = '(prefers-color-scheme: dark)';
/**
* Wire runtime theme. The INITIAL attribute is set by the inline script in
* index.html (pre-paint, no FOUC); this keeps it live and re-applies when the
* OS theme changes while the stored pref is 'system'. Returns an unsubscribe.
*/
export function initTheme(): () => void {
const mq = window.matchMedia(DARK_MQ);
const sync = () =>
applyTheme(document.documentElement, resolveTheme(readStoredTheme(), mq.matches));
sync();
mq.addEventListener('change', sync);
return () => mq.removeEventListener('change', sync);
}

View File

@ -2,8 +2,13 @@ import { createRoot } from 'react-dom/client';
import { QueryClientProvider } from '@tanstack/react-query'; import { QueryClientProvider } from '@tanstack/react-query';
import { App } from './App'; import { App } from './App';
import { queryClient } from './lib/queryClient'; import { queryClient } from './lib/queryClient';
import { initTheme } from './lib/theme';
import './index.css'; import './index.css';
// Keep [data-theme] in sync with the OS while pref is 'system'. The initial
// attribute is already set pre-paint by the inline script in index.html.
initTheme();
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<App /> <App />

View File

@ -85,7 +85,7 @@ export function AdminCaptchaPage({ isAdmin }: { isAdmin: boolean }) {
return ( return (
<div className="h-full flex flex-col bg-surface"> <div className="h-full flex flex-col bg-surface">
<div className="flex items-center justify-between border-b border-hairline bg-white px-4 py-3 gap-3"> <div className="flex items-center justify-between border-b border-hairline bg-canvas px-4 py-3 gap-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h1 className="text-base font-semibold text-slate-900">CAPTCHA Pool</h1> <h1 className="text-base font-semibold text-slate-900">CAPTCHA Pool</h1>
<span className="text-2xs font-mono text-slate-400">admin</span> <span className="text-2xs font-mono text-slate-400">admin</span>
@ -101,7 +101,7 @@ export function AdminCaptchaPage({ isAdmin }: { isAdmin: boolean }) {
type="button" type="button"
onClick={() => reset.mutate()} onClick={() => reset.mutate()}
disabled={reset.isPending} disabled={reset.isPending}
className="px-3 py-1.5 rounded-md text-xs font-medium border border-hairline bg-white text-slate-700 hover:bg-surface transition-colors disabled:opacity-50" className="px-3 py-1.5 rounded-md text-xs font-medium border border-hairline bg-canvas text-slate-700 hover:bg-surface transition-colors disabled:opacity-50"
title="Pool を destroy する。次に WebSearch が CAPTCHA を踏むと自動的に再生成される" title="Pool を destroy する。次に WebSearch が CAPTCHA を踏むと自動的に再生成される"
> >
{reset.isPending ? 'リセット中…' : 'Pool をリセット'} {reset.isPending ? 'リセット中…' : 'Pool をリセット'}
@ -112,7 +112,7 @@ export function AdminCaptchaPage({ isAdmin }: { isAdmin: boolean }) {
<div className="flex-1 min-h-0 p-3"> <div className="flex-1 min-h-0 p-3">
{data?.available && data.novncPath ? ( {data?.available && data.novncPath ? (
<div className="h-full bg-white border border-hairline rounded-md overflow-hidden flex flex-col"> <div className="h-full bg-canvas border border-hairline rounded-md overflow-hidden flex flex-col">
<div className="border-b border-hairline px-3 py-2 text-2xs text-slate-500 flex items-center justify-between gap-2"> <div className="border-b border-hairline px-3 py-2 text-2xs text-slate-500 flex items-center justify-between gap-2">
<span className="truncate">display: {data.display ?? '-'} / sessionId: <span className="font-mono">{data.sessionId}</span></span> <span className="truncate">display: {data.display ?? '-'} / sessionId: <span className="font-mono">{data.sessionId}</span></span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -141,7 +141,7 @@ export function AdminCaptchaPage({ isAdmin }: { isAdmin: boolean }) {
)} )}
</div> </div>
) : ( ) : (
<div className="h-full bg-white border border-hairline rounded-md flex items-center justify-center p-8"> <div className="h-full bg-canvas border border-hairline rounded-md flex items-center justify-center p-8">
<div className="max-w-md text-center text-sm text-slate-600"> <div className="max-w-md text-center text-sm text-slate-600">
<p className="font-medium text-slate-800 mb-1">Pool </p> <p className="font-medium text-slate-800 mb-1">Pool </p>
<p> <p>

View File

@ -65,7 +65,7 @@ export function HelpPage({ isAdmin, onAskAi, selectedId, onSelect }: HelpPagePro
return ( return (
<div className="flex h-full overflow-hidden bg-surface"> <div className="flex h-full overflow-hidden bg-surface">
{/* Left: TOC */} {/* Left: TOC */}
<aside className="w-72 shrink-0 border-r border-hairline bg-white overflow-y-auto flex flex-col"> <aside className="w-72 shrink-0 border-r border-hairline bg-canvas overflow-y-auto flex flex-col">
<div className="p-4 border-b border-hairline"> <div className="p-4 border-b border-hairline">
<h2 className="text-sm font-semibold text-slate-900 m-0"></h2> <h2 className="text-sm font-semibold text-slate-900 m-0"></h2>
<button <button
@ -121,7 +121,7 @@ export function HelpPage({ isAdmin, onAskAi, selectedId, onSelect }: HelpPagePro
</aside> </aside>
{/* Right: rendered markdown + heading nav */} {/* Right: rendered markdown + heading nav */}
<main ref={mainRef} className="flex-1 min-w-0 overflow-y-auto bg-white"> <main ref={mainRef} className="flex-1 min-w-0 overflow-y-auto bg-canvas">
<div className="flex max-w-5xl mx-auto px-8 py-8 gap-8"> <div className="flex max-w-5xl mx-auto px-8 py-8 gap-8">
<article <article
className="prose prose-sm flex-1 min-w-0" className="prose prose-sm flex-1 min-w-0"

View File

@ -44,7 +44,7 @@ function DriftBadge({ drift }: { drift: DriftStatus }) {
</button> </button>
{open && ( {open && (
<div className="absolute left-0 top-full mt-1 z-50 w-52 rounded-md border border-hairline bg-white shadow-lg p-2.5 text-2xs text-slate-700"> <div className="absolute left-0 top-full mt-1 z-50 w-52 rounded-md border border-hairline bg-canvas shadow-lg p-2.5 text-2xs text-slate-700">
<div className="font-semibold text-slate-800 mb-1.5"> Piece </div> <div className="font-semibold text-slate-800 mb-1.5"> Piece </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@ -218,7 +218,7 @@ function PiecesSidebar({
const { defaults, customs } = splitPieces(pieces ?? []); const { defaults, customs } = splitPieces(pieces ?? []);
return ( return (
<div className="h-full overflow-y-auto border-r border-hairline bg-white p-3"> <div className="h-full overflow-y-auto border-r border-hairline bg-canvas p-3">
{/* Default Pieces section */} {/* Default Pieces section */}
<div className="flex items-center justify-between mb-2 px-2"> <div className="flex items-center justify-between mb-2 px-2">
<span className="section-label">Default Pieces</span> <span className="section-label">Default Pieces</span>
@ -289,7 +289,7 @@ function PiecesSidebar({
onClick={cancelDuplicate} onClick={cancelDuplicate}
> >
<div <div
className="w-full max-w-sm rounded-lg border border-hairline bg-white p-4 shadow-xl" className="w-full max-w-sm rounded-lg border border-hairline bg-canvas p-4 shadow-xl"
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
> >
<div id="dup-piece-label" className="text-[13px] font-semibold text-slate-800 mb-2"> <div id="dup-piece-label" className="text-[13px] font-semibold text-slate-800 mb-2">

View File

@ -305,7 +305,7 @@ export function SchedulesPage({ showToast }: SchedulesPageProps = {}) {
return ( return (
<div className="flex h-full min-h-0"> <div className="flex h-full min-h-0">
<div className={`${mobileShowDetail ? 'hidden sm:flex' : 'flex'} w-full sm:w-[320px] flex-shrink-0 sm:border-r sm:border-hairline bg-white p-3 flex-col min-h-0`}> <div className={`${mobileShowDetail ? 'hidden sm:flex' : 'flex'} w-full sm:w-[320px] flex-shrink-0 sm:border-r sm:border-hairline bg-canvas p-3 flex-col min-h-0`}>
<ScheduleListPane <ScheduleListPane
tasks={filtered} tasks={filtered}
activeId={mode === 'new' ? null : active?.id ?? null} activeId={mode === 'new' ? null : active?.id ?? null}
@ -321,7 +321,7 @@ export function SchedulesPage({ showToast }: SchedulesPageProps = {}) {
isLoading={isLoading} isLoading={isLoading}
/> />
</div> </div>
<div className={`${mobileShowDetail ? 'flex' : 'hidden sm:flex'} flex-1 min-w-0 bg-white flex-col`}> <div className={`${mobileShowDetail ? 'flex' : 'hidden sm:flex'} flex-1 min-w-0 bg-canvas flex-col`}>
<ScheduleDetailPane <ScheduleDetailPane
task={active} task={active}
mode={mode} mode={mode}
@ -410,7 +410,7 @@ function ScheduleListPane({
</div> </div>
<div className="flex flex-col gap-2 pb-3 border-b border-hairline"> <div className="flex flex-col gap-2 pb-3 border-b border-hairline">
<div className="flex items-center gap-1.5 bg-white border border-hairline rounded-md pl-2.5 pr-1 h-8"> <div className="flex items-center gap-1.5 bg-canvas border border-hairline rounded-md pl-2.5 pr-1 h-8">
<svg aria-hidden="true" className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg aria-hidden="true" className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
@ -437,7 +437,7 @@ function ScheduleListPane({
className={`flex-shrink-0 px-2 h-7 rounded text-2xs font-medium border transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${ className={`flex-shrink-0 px-2 h-7 rounded text-2xs font-medium border transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
filter === key filter === key
? 'border-accent/60 bg-accent-soft text-accent font-semibold' ? 'border-accent/60 bg-accent-soft text-accent font-semibold'
: 'border-hairline bg-white text-slate-600 hover:bg-surface' : 'border-hairline bg-canvas text-slate-600 hover:bg-surface'
}`} }`}
> >
{label} <span className="text-slate-400 ml-0.5 font-mono tabular-nums">{n}</span> {label} <span className="text-slate-400 ml-0.5 font-mono tabular-nums">{n}</span>
@ -458,7 +458,7 @@ function ScheduleListPane({
<button <button
type="button" type="button"
onClick={onClearFilters} onClick={onClearFilters}
className="px-3 py-1.5 rounded-md text-xs font-semibold bg-white border border-hairline text-slate-700 hover:border-hairline transition-colors" className="px-3 py-1.5 rounded-md text-xs font-semibold bg-canvas border border-hairline text-slate-700 hover:border-hairline transition-colors"
> >
</button> </button>
@ -494,7 +494,7 @@ function ScheduleListItem({ task, active, onClick }: { task: ScheduledTask; acti
onClick={onClick} onClick={onClick}
aria-current={active ? 'true' : undefined} aria-current={active ? 'true' : undefined}
className={`w-full text-left px-3 py-2.5 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${ className={`w-full text-left px-3 py-2.5 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
active ? 'border-accent/60 bg-accent-soft' : 'border-hairline bg-white hover:bg-surface' active ? 'border-accent/60 bg-accent-soft' : 'border-hairline bg-canvas hover:bg-surface'
}`} }`}
> >
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
@ -604,7 +604,7 @@ function ScheduleDetailPane({
return ( return (
<div className="flex flex-col h-full overflow-hidden"> <div className="flex flex-col h-full overflow-hidden">
<div className="flex-shrink-0 px-3 sm:px-5 py-3 sm:py-3.5 border-b border-hairline bg-white flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-3"> <div className="flex-shrink-0 px-3 sm:px-5 py-3 sm:py-3.5 border-b border-hairline bg-canvas flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-3">
<div className="flex items-center gap-2 sm:gap-2.5 min-w-0"> <div className="flex items-center gap-2 sm:gap-2.5 min-w-0">
{onMobileBack && ( {onMobileBack && (
<button <button
@ -636,7 +636,7 @@ function ScheduleDetailPane({
type="button" type="button"
onClick={() => onTrigger(task.id)} onClick={() => onTrigger(task.id)}
disabled={isTriggering} disabled={isTriggering}
className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-semibold bg-white border border-accent text-accent hover:bg-accent-soft active:scale-[0.97] active:bg-accent-soft transition-[transform,background-color,color] duration-100 inline-flex items-center gap-1 whitespace-nowrap disabled:opacity-70 disabled:cursor-wait" className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-semibold bg-canvas border border-accent text-accent hover:bg-accent-soft active:scale-[0.97] active:bg-accent-soft transition-[transform,background-color,color] duration-100 inline-flex items-center gap-1 whitespace-nowrap disabled:opacity-70 disabled:cursor-wait"
> >
{isTriggering ? ( {isTriggering ? (
<span className="inline-block w-3 h-3 border-2 border-accent border-t-transparent rounded-full animate-spin" aria-hidden="true" /> <span className="inline-block w-3 h-3 border-2 border-accent border-t-transparent rounded-full animate-spin" aria-hidden="true" />
@ -651,21 +651,21 @@ function ScheduleDetailPane({
type="button" type="button"
onClick={() => onToggle(task.id, !task.isActive)} onClick={() => onToggle(task.id, !task.isActive)}
disabled={isToggling} disabled={isToggling}
className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-medium bg-white border border-hairline text-slate-700 hover:bg-surface active:scale-[0.97] active:bg-surface transition-[transform,background-color,color] duration-100 whitespace-nowrap disabled:opacity-70 disabled:cursor-wait" className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-medium bg-canvas border border-hairline text-slate-700 hover:bg-surface active:scale-[0.97] active:bg-surface transition-[transform,background-color,color] duration-100 whitespace-nowrap disabled:opacity-70 disabled:cursor-wait"
> >
{isToggling ? '...' : (task.isActive ? '停止' : '再開')} {isToggling ? '...' : (task.isActive ? '停止' : '再開')}
</button> </button>
<button <button
type="button" type="button"
onClick={onEdit} onClick={onEdit}
className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-medium bg-white border border-hairline text-slate-700 hover:bg-surface active:scale-[0.97] active:bg-surface transition-[transform,background-color,color] duration-100 whitespace-nowrap" className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-medium bg-canvas border border-hairline text-slate-700 hover:bg-surface active:scale-[0.97] active:bg-surface transition-[transform,background-color,color] duration-100 whitespace-nowrap"
> >
</button> </button>
<button <button
type="button" type="button"
onClick={() => onDelete(task.id)} onClick={() => onDelete(task.id)}
className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-medium bg-white border border-red-200 text-red-700 hover:bg-red-50 active:scale-[0.97] active:bg-red-50 transition-[transform,background-color,color] duration-100 whitespace-nowrap" className="px-2.5 sm:px-3 h-7 sm:h-8 rounded-md text-xs font-medium bg-canvas border border-red-200 text-red-700 hover:bg-red-50 active:scale-[0.97] active:bg-red-50 transition-[transform,background-color,color] duration-100 whitespace-nowrap"
> >
</button> </button>
@ -687,7 +687,7 @@ function ScheduleDetailPane({
/> />
</div> </div>
<div className="bg-white border border-hairline rounded-md p-5"> <div className="bg-canvas border border-hairline rounded-md p-5">
<div className="section-label mb-3.5"> <div className="section-label mb-3.5">
</div> </div>
@ -731,7 +731,7 @@ function ScheduleDetailPane({
</dl> </dl>
</div> </div>
<div className="bg-white border border-hairline rounded-md p-5 mt-4"> <div className="bg-canvas border border-hairline rounded-md p-5 mt-4">
<div className="section-label mb-3.5"> <div className="section-label mb-3.5">
</div> </div>
@ -916,7 +916,7 @@ function ScheduleEditor({ mode, initialTask, onCancel, onSaved }: ScheduleEditor
return ( return (
<div className="flex flex-col h-full overflow-hidden"> <div className="flex flex-col h-full overflow-hidden">
<div className="flex-shrink-0 px-5 py-3.5 border-b border-hairline bg-white flex items-center justify-between gap-3"> <div className="flex-shrink-0 px-5 py-3.5 border-b border-hairline bg-canvas flex items-center justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<div className="flex items-center gap-2.5 min-w-0"> <div className="flex items-center gap-2.5 min-w-0">
<div className="section-label font-mono flex-shrink-0"> <div className="section-label font-mono flex-shrink-0">
@ -944,7 +944,7 @@ function ScheduleEditor({ mode, initialTask, onCancel, onSaved }: ScheduleEditor
<div className="flex-1 overflow-y-auto px-6 py-5 bg-surface"> <div className="flex-1 overflow-y-auto px-6 py-5 bg-surface">
<div className="max-w-[640px] mx-auto space-y-4"> <div className="max-w-[640px] mx-auto space-y-4">
<div className="bg-white border border-hairline rounded-md p-5"> <div className="bg-canvas border border-hairline rounded-md p-5">
<div className="section-label mb-3.5"> <div className="section-label mb-3.5">
</div> </div>
@ -960,7 +960,7 @@ function ScheduleEditor({ mode, initialTask, onCancel, onSaved }: ScheduleEditor
onClick={() => setForm(p => ({ ...p, taskKind: k }))} onClick={() => setForm(p => ({ ...p, taskKind: k }))}
aria-pressed={selected} aria-pressed={selected}
className={`px-3 py-1.5 rounded-full text-xs font-semibold border transition-colors ${ className={`px-3 py-1.5 rounded-full text-xs font-semibold border transition-colors ${
selected ? 'border-accent bg-accent-soft text-accent' : 'border-hairline bg-white text-slate-600' selected ? 'border-accent bg-accent-soft text-accent' : 'border-hairline bg-canvas text-slate-600'
}`} }`}
> >
{k === 'agent' ? 'Agent (LLM)' : 'Script (直接)'} {k === 'agent' ? 'Agent (LLM)' : 'Script (直接)'}
@ -1050,7 +1050,7 @@ function ScheduleEditor({ mode, initialTask, onCancel, onSaved }: ScheduleEditor
</div> </div>
</div> </div>
<div className="bg-white border border-hairline rounded-md p-5"> <div className="bg-canvas border border-hairline rounded-md p-5">
<div className="section-label mb-3.5"> <div className="section-label mb-3.5">
</div> </div>
@ -1069,7 +1069,7 @@ function ScheduleEditor({ mode, initialTask, onCancel, onSaved }: ScheduleEditor
className={`px-3 py-1.5 rounded-full text-xs font-semibold border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${ className={`px-3 py-1.5 rounded-full text-xs font-semibold border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
selected selected
? 'border-accent bg-accent-soft text-accent' ? 'border-accent bg-accent-soft text-accent'
: 'border-hairline bg-white text-slate-600 hover:border-hairline' : 'border-hairline bg-canvas text-slate-600 hover:border-hairline'
}`} }`}
> >
{opt.label} {opt.label}
@ -1165,7 +1165,7 @@ function ScheduleEditor({ mode, initialTask, onCancel, onSaved }: ScheduleEditor
</div> </div>
{authState.mode === 'authenticated' && ( {authState.mode === 'authenticated' && (
<div className="bg-white border border-hairline rounded-md p-5"> <div className="bg-canvas border border-hairline rounded-md p-5">
<div className="section-label mb-3.5"> <div className="section-label mb-3.5">
</div> </div>
@ -1219,7 +1219,7 @@ function ScheduleEditor({ mode, initialTask, onCancel, onSaved }: ScheduleEditor
)} )}
{activeSessionProfiles.length > 0 && ( {activeSessionProfiles.length > 0 && (
<div className="bg-white border border-hairline rounded-md p-5"> <div className="bg-canvas border border-hairline rounded-md p-5">
<div className="section-label mb-3.5"> <div className="section-label mb-3.5">
</div> </div>
@ -1257,7 +1257,7 @@ function ScheduleEditor({ mode, initialTask, onCancel, onSaved }: ScheduleEditor
</div> </div>
</div> </div>
<div className="flex-shrink-0 px-6 py-3.5 border-t border-hairline flex justify-end gap-2 bg-white"> <div className="flex-shrink-0 px-6 py-3.5 border-t border-hairline flex justify-end gap-2 bg-canvas">
<button <button
type="button" type="button"
onClick={onCancel} onClick={onCancel}

View File

@ -25,7 +25,7 @@ type TabId = 'overview' | 'files';
type FileSort = 'name' | 'newest'; type FileSort = 'name' | 'newest';
const ICON_BTN = const ICON_BTN =
'w-8 h-8 flex items-center justify-center rounded-md border border-hairline bg-white text-slate-500 hover:text-slate-900 hover:bg-surface transition-colors'; 'w-8 h-8 flex items-center justify-center rounded-md border border-hairline bg-canvas text-slate-500 hover:text-slate-900 hover:bg-surface transition-colors';
function sortEntries(entries: LocalFileEntry[], mode: FileSort): LocalFileEntry[] { function sortEntries(entries: LocalFileEntry[], mode: FileSort): LocalFileEntry[] {
const dirs = entries.filter(e => e.kind === 'directory'); const dirs = entries.filter(e => e.kind === 'directory');
@ -77,7 +77,7 @@ function SharedFileBrowser({ token, onPreview }: SharedFileBrowserProps) {
const pathSegments = currentPath ? currentPath.split('/').filter(Boolean) : []; const pathSegments = currentPath ? currentPath.split('/').filter(Boolean) : [];
return ( return (
<div className="bg-white border border-hairline rounded-md p-3.5"> <div className="bg-canvas border border-hairline rounded-md p-3.5">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="text-2xs text-slate-500 font-mono break-all min-w-0 flex-1"> <div className="text-2xs text-slate-500 font-mono break-all min-w-0 flex-1">
@ -86,7 +86,7 @@ function SharedFileBrowser({ token, onPreview }: SharedFileBrowserProps) {
<select <select
value={sort} value={sort}
onChange={e => setSort(e.target.value as FileSort)} onChange={e => setSort(e.target.value as FileSort)}
className="flex-shrink-0 px-2 h-7 text-2xs rounded-md border border-hairline bg-white text-slate-700 hover:bg-surface focus:outline-none focus:ring-2 focus:ring-accent-ring" className="flex-shrink-0 px-2 h-7 text-2xs rounded-md border border-hairline bg-canvas text-slate-700 hover:bg-surface focus:outline-none focus:ring-2 focus:ring-accent-ring"
aria-label="並び順" aria-label="並び順"
> >
<option value="name"></option> <option value="name"></option>
@ -97,7 +97,7 @@ function SharedFileBrowser({ token, onPreview }: SharedFileBrowserProps) {
{pathSegments.length > 0 && ( {pathSegments.length > 0 && (
<button <button
onClick={() => setCurrentPath(pathSegments.slice(0, -1).join('/'))} onClick={() => setCurrentPath(pathSegments.slice(0, -1).join('/'))}
className="self-start inline-flex items-center gap-1 px-2 h-7 rounded-md border border-hairline bg-white text-2xs text-slate-600 hover:bg-surface transition-colors" className="self-start inline-flex items-center gap-1 px-2 h-7 rounded-md border border-hairline bg-canvas text-2xs text-slate-600 hover:bg-surface transition-colors"
> >
<svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg className="w-3 h-3" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M10 4l-4 4 4 4M6 8h6" /> <path d="M10 4l-4 4 4 4M6 8h6" />
@ -114,7 +114,7 @@ function SharedFileBrowser({ token, onPreview }: SharedFileBrowserProps) {
{sortedEntries.map(entry => ( {sortedEntries.map(entry => (
<div <div
key={`${entry.kind}:${entry.path}`} key={`${entry.kind}:${entry.path}`}
className="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-white border border-hairline hover:bg-surface transition-colors" className="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-canvas border border-hairline hover:bg-surface transition-colors"
> >
<span className="text-slate-400 flex-shrink-0" aria-hidden="true"> <span className="text-slate-400 flex-shrink-0" aria-hidden="true">
{entry.kind === 'directory' ? ( {entry.kind === 'directory' ? (
@ -286,7 +286,7 @@ export function SharedView({ token }: SharedViewProps) {
return ( return (
<div className="min-h-dvh bg-surface text-slate-900 flex flex-col"> <div className="min-h-dvh bg-surface text-slate-900 flex flex-col">
<header className="bg-white border-b border-hairline px-4 py-3 sm:py-4"> <header className="bg-canvas border-b border-hairline px-4 py-3 sm:py-4">
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@ -315,7 +315,7 @@ export function SharedView({ token }: SharedViewProps) {
</div> </div>
</header> </header>
<div className="bg-white border-b border-hairline px-4"> <div className="bg-canvas border-b border-hairline px-4">
<div className="max-w-4xl mx-auto flex gap-0"> <div className="max-w-4xl mx-auto flex gap-0">
{(['overview', 'files'] as const).map(tab => ( {(['overview', 'files'] as const).map(tab => (
<button <button
@ -352,7 +352,7 @@ export function SharedView({ token }: SharedViewProps) {
)} )}
{resultComments.length === 0 && ( {resultComments.length === 0 && (
<div className="bg-white border border-hairline rounded-md p-6 text-center text-slate-500 text-[13px]"> <div className="bg-canvas border border-hairline rounded-md p-6 text-center text-slate-500 text-[13px]">
</div> </div>
)} )}

View File

@ -187,7 +187,7 @@ export function UsersPage() {
return ( return (
<div className="flex h-full min-h-0"> <div className="flex h-full min-h-0">
<div className={`${mobileShowDetail ? 'hidden sm:flex' : 'flex'} w-full sm:w-[320px] flex-shrink-0 sm:border-r sm:border-hairline bg-white p-3 flex-col min-h-0`}> <div className={`${mobileShowDetail ? 'hidden sm:flex' : 'flex'} w-full sm:w-[320px] flex-shrink-0 sm:border-r sm:border-hairline bg-canvas p-3 flex-col min-h-0`}>
<UserListPane <UserListPane
users={filtered} users={filtered}
activeId={active?.id ?? null} activeId={active?.id ?? null}
@ -201,7 +201,7 @@ export function UsersPage() {
isLoading={isLoading} isLoading={isLoading}
/> />
</div> </div>
<div className={`${mobileShowDetail ? 'flex' : 'hidden sm:flex'} flex-1 min-w-0 bg-white flex-col`}> <div className={`${mobileShowDetail ? 'flex' : 'hidden sm:flex'} flex-1 min-w-0 bg-canvas flex-col`}>
<UserDetailPane <UserDetailPane
user={active} user={active}
onMobileBack={handleMobileBack} onMobileBack={handleMobileBack}
@ -258,7 +258,7 @@ function UserListPane({
</div> </div>
<div className="flex flex-col gap-2 pb-3 border-b border-hairline"> <div className="flex flex-col gap-2 pb-3 border-b border-hairline">
<div className="flex items-center gap-1.5 bg-white border border-hairline rounded-md pl-2.5 pr-1 h-8"> <div className="flex items-center gap-1.5 bg-canvas border border-hairline rounded-md pl-2.5 pr-1 h-8">
<svg aria-hidden="true" className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg aria-hidden="true" className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
@ -281,7 +281,7 @@ function UserListPane({
className={`flex-shrink-0 px-2 h-7 rounded text-2xs font-medium border transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${ className={`flex-shrink-0 px-2 h-7 rounded text-2xs font-medium border transition-colors whitespace-nowrap focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring ${
filter === key filter === key
? 'border-accent/60 bg-accent-soft text-accent font-semibold' ? 'border-accent/60 bg-accent-soft text-accent font-semibold'
: 'border-hairline bg-white text-slate-600 hover:bg-surface' : 'border-hairline bg-canvas text-slate-600 hover:bg-surface'
}`} }`}
> >
{label} <span className="text-slate-400 ml-0.5 font-mono tabular-nums">{n}</span> {label} <span className="text-slate-400 ml-0.5 font-mono tabular-nums">{n}</span>
@ -302,7 +302,7 @@ function UserListPane({
<button <button
type="button" type="button"
onClick={onClearFilters} onClick={onClearFilters}
className="px-3 py-1.5 rounded-md text-xs font-semibold bg-white border border-hairline text-slate-700 hover:border-hairline transition-colors" className="px-3 py-1.5 rounded-md text-xs font-semibold bg-canvas border border-hairline text-slate-700 hover:border-hairline transition-colors"
> >
</button> </button>
@ -337,7 +337,7 @@ function UserListItem({ user, active, onClick }: { user: UserRecord; active: boo
onClick={onClick} onClick={onClick}
aria-current={active ? 'true' : undefined} aria-current={active ? 'true' : undefined}
className={`w-full text-left px-3 py-2.5 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring flex items-center gap-2.5 ${ className={`w-full text-left px-3 py-2.5 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-ring flex items-center gap-2.5 ${
active ? 'border-accent/60 bg-accent-soft' : 'border-hairline bg-white hover:border-hairline' active ? 'border-accent/60 bg-accent-soft' : 'border-hairline bg-canvas hover:border-hairline'
}`} }`}
> >
<Avatar name={user.name} email={user.email} size={36} /> <Avatar name={user.name} email={user.email} size={36} />
@ -385,7 +385,7 @@ function UserDetailPane({ user, onPatch, onDelete, onMobileBack }: UserDetailPan
return ( return (
<div className="flex flex-col h-full overflow-hidden"> <div className="flex flex-col h-full overflow-hidden">
<div className="flex-shrink-0 px-3 sm:px-5 py-3 sm:py-3.5 border-b border-hairline bg-white flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-3"> <div className="flex-shrink-0 px-3 sm:px-5 py-3 sm:py-3.5 border-b border-hairline bg-canvas flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-3">
<div className="flex items-center gap-2 sm:gap-3 min-w-0"> <div className="flex items-center gap-2 sm:gap-3 min-w-0">
{onMobileBack && ( {onMobileBack && (
<button <button
@ -427,7 +427,7 @@ function UserDetailPane({ user, onPatch, onDelete, onMobileBack }: UserDetailPan
<button <button
type="button" type="button"
onClick={() => onPatch(user.id, { status: 'disabled' })} onClick={() => onPatch(user.id, { status: 'disabled' })}
className="px-3 h-7 rounded-md text-xs font-medium bg-white border border-amber-200 text-amber-700 hover:bg-amber-50 transition-colors whitespace-nowrap" className="px-3 h-7 rounded-md text-xs font-medium bg-canvas border border-amber-200 text-amber-700 hover:bg-amber-50 transition-colors whitespace-nowrap"
> >
</button> </button>
@ -436,7 +436,7 @@ function UserDetailPane({ user, onPatch, onDelete, onMobileBack }: UserDetailPan
<button <button
type="button" type="button"
onClick={() => onPatch(user.id, { status: 'active' })} onClick={() => onPatch(user.id, { status: 'active' })}
className="px-3 h-7 rounded-md text-xs font-medium bg-white border border-hairline text-slate-700 hover:bg-surface transition-colors whitespace-nowrap" className="px-3 h-7 rounded-md text-xs font-medium bg-canvas border border-hairline text-slate-700 hover:bg-surface transition-colors whitespace-nowrap"
> >
</button> </button>
@ -444,7 +444,7 @@ function UserDetailPane({ user, onPatch, onDelete, onMobileBack }: UserDetailPan
<button <button
type="button" type="button"
onClick={() => onDelete(user.id)} onClick={() => onDelete(user.id)}
className="px-3 h-7 rounded-md text-xs font-medium bg-white border border-red-200 text-red-700 hover:bg-red-50 transition-colors whitespace-nowrap" className="px-3 h-7 rounded-md text-xs font-medium bg-canvas border border-red-200 text-red-700 hover:bg-red-50 transition-colors whitespace-nowrap"
> >
</button> </button>
@ -460,7 +460,7 @@ function UserDetailPane({ user, onPatch, onDelete, onMobileBack }: UserDetailPan
<StatChip label="所属 Org" value={user.orgs?.length ?? 0} /> <StatChip label="所属 Org" value={user.orgs?.length ?? 0} />
</div> </div>
<div className="bg-white border border-hairline rounded-md p-5"> <div className="bg-canvas border border-hairline rounded-md p-5">
<div className="section-label mb-3.5"> <div className="section-label mb-3.5">
</div> </div>
@ -476,13 +476,13 @@ function UserDetailPane({ user, onPatch, onDelete, onMobileBack }: UserDetailPan
className={`w-full text-left px-3.5 py-3 rounded-md border mb-2 last:mb-0 flex items-center gap-3 transition-colors ${ className={`w-full text-left px-3.5 py-3 rounded-md border mb-2 last:mb-0 flex items-center gap-3 transition-colors ${
selected selected
? 'border-accent/60 bg-accent-soft' ? 'border-accent/60 bg-accent-soft'
: 'border-hairline bg-white hover:border-hairline' : 'border-hairline bg-canvas hover:border-hairline'
}`} }`}
> >
<span <span
aria-hidden="true" aria-hidden="true"
className={`w-[18px] h-[18px] rounded-full flex-shrink-0 border-2 inline-flex items-center justify-center ${ className={`w-[18px] h-[18px] rounded-full flex-shrink-0 border-2 inline-flex items-center justify-center ${
selected ? 'border-accent bg-accent' : 'border-hairline bg-white' selected ? 'border-accent bg-accent' : 'border-hairline bg-canvas'
}`} }`}
> >
{selected && ( {selected && (
@ -500,7 +500,7 @@ function UserDetailPane({ user, onPatch, onDelete, onMobileBack }: UserDetailPan
})} })}
</div> </div>
<div className="bg-white border border-hairline rounded-md p-5 mt-4"> <div className="bg-canvas border border-hairline rounded-md p-5 mt-4">
<div className="section-label mb-3.5"> <div className="section-label mb-3.5">
Gitea Gitea
</div> </div>

View File

@ -1,13 +1,13 @@
import typography from '@tailwindcss/typography'; import typography from '@tailwindcss/typography';
/** /**
* UI redesign (Refero-inspired minimal+dense). * UI redesign (Refero-inspired minimal+dense) + dark mode.
* *
* Neutral palette stays on Tailwind's `slate` scale because the * The neutral `slate` scale and the semantic surface tokens are mapped to CSS
* codebase has 700+ existing references; replacing it would be churn * variables (defined in src/index.css) so existing *-slate-* classes (1454
* without payoff. Refinements happen at the CSS-var layer (canvas / * uses) flip between light and dark with zero component edits. Dark inverts
* surface / hairline tokens) and rely on Tailwind's stock semantic * the slate ramp (50<->950) so fg/bg stay coherent. State colors
* scales (emerald/amber/red/blue/indigo) for state colors. * (emerald/amber/red/blue/indigo) keep Tailwind's stock scales for now.
*/ */
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
@ -15,21 +15,29 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
// Brand accent stays runtime-configurable via /api/branding.
// Fallback updated to the minimal-design accent (zinc-900).
accent: 'var(--brand-primary, #18181b)', accent: 'var(--brand-primary, #18181b)',
'accent-deep': 'var(--brand-primary-deep, #09090b)', 'accent-deep': 'var(--brand-primary-deep, #09090b)',
'accent-soft': 'var(--brand-primary-soft, #f4f4f5)', 'accent-soft': 'var(--brand-primary-soft, #f4f4f5)',
'accent-ring': 'var(--brand-primary-ring, rgba(24, 24, 27, 0.25))', 'accent-ring': 'var(--brand-primary-ring, rgba(24, 24, 27, 0.25))',
'accent-fg': 'var(--brand-primary-fg, #ffffff)', 'accent-fg': 'var(--brand-primary-fg, #ffffff)',
ink: '#0f172a', ink: 'var(--ink, #0f172a)',
muted: '#64748b', muted: 'var(--muted, #64748b)',
// Refero-inspired surface tokens. canvas: 'var(--canvas, #ffffff)',
canvas: '#ffffff', surface: 'var(--surface, #fafafa)',
surface: '#fafafa', 'surface-2': 'var(--surface-2, #f4f4f5)',
'surface-2': '#f4f4f5', hairline: 'var(--hairline, #e4e4e7)',
hairline: '#e4e4e7', 'hairline-soft': 'var(--hairline-soft, #f4f4f5)',
'hairline-soft': '#f4f4f5', // Neutral ramp via CSS vars so *-slate-* auto-themes (1454 uses, no
// component edits). Light = Tailwind defaults; dark = inverted ramp.
slate: {
50: 'var(--slate-50)', 100: 'var(--slate-100)', 200: 'var(--slate-200)',
300: 'var(--slate-300)', 400: 'var(--slate-400)', 500: 'var(--slate-500)',
600: 'var(--slate-600)', 700: 'var(--slate-700)', 800: 'var(--slate-800)',
900: 'var(--slate-900)', 950: 'var(--slate-950)',
},
gray: {
400: 'var(--gray-400)', 500: 'var(--gray-500)', 700: 'var(--gray-700)',
},
}, },
fontFamily: { fontFamily: {
sans: ['"IBM Plex Sans JP"', '"Hiragino Sans"', 'system-ui', 'sans-serif'], sans: ['"IBM Plex Sans JP"', '"Hiragino Sans"', 'system-ui', 'sans-serif'],