sync: update from private repo (d8074a7)
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
This commit is contained in:
parent
a44f6b41e2
commit
e00ea9fb0c
4
.gitignore
vendored
4
.gitignore
vendored
@ -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/
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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).
|
||||||
|
if (backupContent !== null) {
|
||||||
writeFileSync(this.configPath, backupContent, 'utf-8');
|
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' };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 ? '停止中...' : '停止'}
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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: 「次のフェーズへ進む」cue。FileBrowser の refresh
|
{/* arrow → divider: 「次のフェーズへ進む」cue。FileBrowser の 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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 ? '終了中…' : 'セッション終了'}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 && (() => {
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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' ? (
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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={{
|
||||||
|
|||||||
@ -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))',
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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) => (
|
||||||
|
|||||||
@ -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 モデル推奨
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
{'}'}
|
{'}'}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 Instructions for the agent..."
|
placeholder="# My Skill 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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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={
|
||||||
|
|||||||
@ -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}` : 'ブラウザセッションを追加'}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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…"
|
||||||
|
|||||||
@ -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'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
34
ui/src/lib/theme-css-parity.test.ts
Normal file
34
ui/src/lib/theme-css-parity.test.ts
Normal 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
67
ui/src/lib/theme.test.ts
Normal 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
53
ui/src/lib/theme.ts
Normal 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);
|
||||||
|
}
|
||||||
@ -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 />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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'],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user