mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 20:27:44 +02:00
[aidan] ui/workflows: dark mode and design-system cohesion
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { WC } from './uiKit';
|
||||
import { useWC } from './uiKit';
|
||||
|
||||
// Combobox for the run limit: pick a preset OR type any count. Self-rendered
|
||||
// (no native <select>, no portal) because a native popup over the canvas card
|
||||
@@ -12,6 +12,7 @@ const OPTIONS: Array<{ label: string; val: number | null }> = [
|
||||
];
|
||||
|
||||
const RepeatField: React.FC<{ value: number | null; onChange: (n: number | null) => void }> = ({ value, onChange }) => {
|
||||
const WC = useWC();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState('');
|
||||
@@ -43,7 +44,7 @@ const RepeatField: React.FC<{ value: number | null; onChange: (n: number | null)
|
||||
|
||||
return (
|
||||
<div ref={ref} style={{ position: 'relative', width: 134 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', background: '#FFFFFF', border: `1px solid ${open ? WC.accent : 'rgba(33,30,27,0.12)'}`, borderRadius: 8, height: 32, padding: '0 2px 0 9px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', background: WC.raised, border: `1px solid ${open ? WC.accent : `rgba(${WC.inkRGB},0.12)`}`, borderRadius: 8, height: 32, padding: '0 2px 0 9px' }}>
|
||||
<input
|
||||
value={editing ? draft : display}
|
||||
placeholder="Forever"
|
||||
@@ -61,7 +62,7 @@ const RepeatField: React.FC<{ value: number | null; onChange: (n: number | null)
|
||||
</button>
|
||||
</div>
|
||||
{open && (
|
||||
<div style={{ position: 'absolute', top: 37, left: 0, right: 0, background: '#FFFFFF', border: `1px solid ${WC.line}`, borderRadius: 9, boxShadow: '0 8px 22px rgba(33,30,27,0.16)', padding: 4, zIndex: 40 }}>
|
||||
<div style={{ position: 'absolute', top: 37, left: 0, right: 0, background: WC.raised, border: `1px solid ${WC.line}`, borderRadius: WC.radius.md, boxShadow: WC.shadow.md, padding: 4, zIndex: 40 }}>
|
||||
{OPTIONS.map((o) => {
|
||||
const active = (o.val == null && value == null) || o.val === value;
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { WC } from './uiKit';
|
||||
import { useWC } from './uiKit';
|
||||
|
||||
// Test-first nudge before scheduling: a test run grants the tool access the
|
||||
// workflow needs, so unattended runs don't stall reaching for them.
|
||||
@@ -8,29 +8,32 @@ const SaveGuard: React.FC<{
|
||||
onClose: () => void;
|
||||
onSaveAnyway: () => void;
|
||||
onRunTest: () => void;
|
||||
}> = ({ title, onClose, onSaveAnyway, onRunTest }) => (
|
||||
<div onClick={onClose} style={{ position: 'absolute', inset: 0, background: 'rgba(33,30,27,0.34)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 60, padding: 28 }}>
|
||||
<div onClick={(e) => e.stopPropagation()} style={{ width: 430, maxWidth: '100%', background: WC.paper, borderRadius: 16, boxShadow: '0 30px 70px -22px rgba(0,0,0,0.45)', overflow: 'hidden' }}>
|
||||
<div style={{ padding: '24px 24px 18px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||
<div style={{ width: 30, height: 30, borderRadius: 9, background: 'rgba(185,138,46,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke={WC.warn} strokeWidth="2"><path d="M12 3l9 16H3z" /><path d="M12 10v4" /><path d="M12 17h.01" /></svg>
|
||||
}> = ({ title, onClose, onSaveAnyway, onRunTest }) => {
|
||||
const WC = useWC();
|
||||
return (
|
||||
<div onClick={onClose} style={{ position: 'absolute', inset: 0, background: `rgba(${WC.inkRGB},0.34)`, display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 60, padding: 28 }}>
|
||||
<div onClick={(e) => e.stopPropagation()} style={{ width: 430, maxWidth: '100%', background: WC.paper, borderRadius: WC.radius.lg, boxShadow: WC.shadow.lg, overflow: 'hidden' }}>
|
||||
<div style={{ padding: '24px 24px 18px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||
<div style={{ width: 30, height: 30, borderRadius: 9, background: 'rgba(185,138,46,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 'none' }}>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke={WC.warn} strokeWidth="2"><path d="M12 3l9 16H3z" /><path d="M12 10v4" /><path d="M12 17h.01" /></svg>
|
||||
</div>
|
||||
<h3 style={{ margin: 0, fontFamily: "'Newsreader',serif", fontSize: 20, fontWeight: 500, color: WC.ink }}>Test run recommended</h3>
|
||||
</div>
|
||||
<h3 style={{ margin: 0, fontFamily: "'Newsreader',serif", fontSize: 20, fontWeight: 500, color: WC.ink }}>Test run recommended</h3>
|
||||
<p style={{ margin: 0, fontSize: 13, lineHeight: 1.6, color: WC.ink4 }}>
|
||||
You haven’t tested “{title}” yet. A quick test run confirms the steps work and grants the tool access it needs before it goes on a schedule.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: '0 24px 22px', display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
|
||||
<button onClick={onSaveAnyway} style={{ background: 'transparent', border: `1px solid rgba(${WC.inkRGB},0.16)`, borderRadius: 9, padding: '9px 16px', fontSize: 13, fontWeight: 600, color: WC.ink3, cursor: 'pointer' }}>Save anyway</button>
|
||||
<button onClick={onRunTest} style={{ display: 'flex', alignItems: 'center', gap: 7, background: WC.accent, color: '#fff', border: 'none', borderRadius: 9, padding: '9px 18px', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
|
||||
<div style={{ width: 0, height: 0, borderTop: '5px solid transparent', borderBottom: '5px solid transparent', borderLeft: '8px solid #fff', flex: 'none' }} />
|
||||
<span>Run test now</span>
|
||||
</button>
|
||||
</div>
|
||||
<p style={{ margin: 0, fontSize: 13, lineHeight: 1.6, color: '#6B655C' }}>
|
||||
You haven’t tested “{title}” yet. A quick test run confirms the steps work and grants the tool access it needs before it goes on a schedule.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ padding: '0 24px 22px', display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
|
||||
<button onClick={onSaveAnyway} style={{ background: 'transparent', border: '1px solid rgba(33,30,27,0.16)', borderRadius: 9, padding: '9px 16px', fontSize: 13, fontWeight: 600, color: WC.ink3, cursor: 'pointer' }}>Save anyway</button>
|
||||
<button onClick={onRunTest} style={{ display: 'flex', alignItems: 'center', gap: 7, background: WC.accent, color: '#fff', border: 'none', borderRadius: 9, padding: '9px 18px', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
|
||||
<div style={{ width: 0, height: 0, borderTop: '5px solid transparent', borderBottom: '5px solid transparent', borderLeft: '8px solid #fff', flex: 'none' }} />
|
||||
<span>Run test now</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveGuard;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { closeWorkflowsApp, setWorkflowsHubPosition, setWorkflowsHubSize } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { WC, FONT_SERIF } from './uiKit';
|
||||
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||
import { useWC, FONT_SERIF } from './uiKit';
|
||||
import WorkflowsAppContent from './WorkflowsAppContent';
|
||||
|
||||
type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
||||
@@ -53,6 +54,7 @@ const WorkflowsAppCard: React.FC<Props> = ({
|
||||
isSelected = false, isHighlighted = false, multiDragDelta = null,
|
||||
onCardSelect, onDragStart, onDragMove, onDragEnd, onBringToFront,
|
||||
}) => {
|
||||
const WC = useWC();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const panRef = useRef({ panX, panY });
|
||||
@@ -175,7 +177,7 @@ const WorkflowsAppCard: React.FC<Props> = ({
|
||||
const dw = localResize?.w ?? cardWidth;
|
||||
const dh = localResize?.h ?? cardHeight;
|
||||
|
||||
const border = isHighlighted ? `2px solid ${WC.accent}` : isSelected ? '2px solid #3b82f6' : '1px solid rgba(33,30,27,0.12)';
|
||||
const border = isHighlighted ? `2px solid ${WC.accent}` : isSelected ? '2px solid #3b82f6' : `1px solid ${WC.border.subtle}`;
|
||||
const noTransition = isDragging || isResizing || (isSelected && !!multiDragDelta);
|
||||
|
||||
return (
|
||||
@@ -201,8 +203,8 @@ const WorkflowsAppCard: React.FC<Props> = ({
|
||||
left: dx, top: dy, width: dw, height: dh,
|
||||
background: WC.paper,
|
||||
border,
|
||||
borderRadius: 15,
|
||||
boxShadow: '0 30px 80px -24px rgba(33,30,27,0.34), 0 8px 24px -12px rgba(33,30,27,0.18)',
|
||||
borderRadius: WC.radius.lg,
|
||||
boxShadow: (isDragging || isResizing) ? WC.shadow.lg : WC.shadow.md,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -218,11 +220,8 @@ const WorkflowsAppCard: React.FC<Props> = ({
|
||||
style={{ height: 42, flex: 'none', display: 'flex', alignItems: 'center', padding: '0 16px', borderBottom: `1px solid ${WC.line}`, background: WC.panel, gap: 14, cursor: isDragging ? 'grabbing' : 'grab', touchAction: 'none', userSelect: 'none' }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={WC.accent} strokeWidth="1.9">
|
||||
<circle cx="6" cy="6" r="2.5" /><circle cx="6" cy="18" r="2.5" /><circle cx="18" cy="12" r="2.5" />
|
||||
<path d="M8.2 7.1l7.6 3.8M8.2 16.9l7.6-3.8" />
|
||||
</svg>
|
||||
<span style={{ fontFamily: FONT_SERIF, fontSize: 14.5, fontWeight: 500, color: WC.ink, letterSpacing: '-0.01em' }}>Workflows</span>
|
||||
<EventRepeatIcon sx={{ fontSize: 18, color: WC.accent, display: 'block' }} />
|
||||
<span style={{ fontFamily: FONT_SERIF, fontSize: 14.5, fontWeight: 500, color: WC.ink, letterSpacing: '-0.01em', lineHeight: 1, transform: 'translateY(2.5px)' }}>Workflows</span>
|
||||
</div>
|
||||
<div style={{ flex: 1 }} />
|
||||
<div
|
||||
@@ -266,7 +265,12 @@ function ensureAssets(): void {
|
||||
document.head.appendChild(link);
|
||||
const style = document.createElement('style');
|
||||
style.id = 'workflows-app-keyframes';
|
||||
style.textContent = '@keyframes os-spin { to { transform: rotate(360deg); } }';
|
||||
style.textContent = [
|
||||
'@keyframes os-spin { to { transform: rotate(360deg); } }',
|
||||
'@keyframes os-flow { to { stroke-dashoffset: -18; } }',
|
||||
'@keyframes os-pulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.55); opacity: 0.45; } }',
|
||||
'@keyframes os-slidein { from { opacity: 0; transform: translateX(-22px) scale(0.97); } to { opacity: 1; transform: none; } }',
|
||||
].join('\n');
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,60 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useThemeMode, useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
|
||||
// The Workflows app renders in its own warm-paper visual language (from the
|
||||
// Claude design), deliberately separate from the MUI theme so the window reads
|
||||
// as a focused app. All tokens live here so the panes stay consistent.
|
||||
export const WC = {
|
||||
// as a focused app. Two palettes (light/dark) keyed to the app theme; grab one
|
||||
// per component with useWC(). inkRGB is the ink color as a bare "r,g,b" so the
|
||||
// many rgba(ink, opacity) borders/hovers can flip with the theme too.
|
||||
export interface WCPalette {
|
||||
accent: string;
|
||||
paper: string;
|
||||
panel: string;
|
||||
rail: string;
|
||||
inset: string;
|
||||
raised: string;
|
||||
ink: string;
|
||||
ink2: string;
|
||||
ink3: string;
|
||||
ink4: string;
|
||||
muted: string;
|
||||
muted2: string;
|
||||
faint: string;
|
||||
inkRGB: string;
|
||||
line: string;
|
||||
line2: string;
|
||||
hover: string;
|
||||
selBg: string;
|
||||
success: string;
|
||||
successBg: string;
|
||||
danger: string;
|
||||
dangerBg: string;
|
||||
warn: string;
|
||||
warnBg: string;
|
||||
trackOff: string;
|
||||
// Structural primitives shared with the rest of OpenSwarm (sourced from
|
||||
// claudeTokens in useWC), so the window stops looking built-separate.
|
||||
shadow: ClaudeTokens['shadow'];
|
||||
radius: ClaudeTokens['radius'];
|
||||
border: ClaudeTokens['border'];
|
||||
}
|
||||
|
||||
// The identity half of the palette: accent, text, status, hairlines. The neutral
|
||||
// surfaces + structural primitives are blended in from claudeTokens by useWC(),
|
||||
// so the window is the same material as every other card.
|
||||
type WCColors = Omit<WCPalette, 'shadow' | 'radius' | 'border' | 'paper' | 'panel' | 'rail' | 'inset' | 'raised'>;
|
||||
|
||||
export const WC_LIGHT: WCColors = {
|
||||
accent: '#C25A36',
|
||||
paper: '#FBFAF7',
|
||||
panel: '#F4F2EC',
|
||||
rail: '#F4F2EC',
|
||||
inset: '#F0EEE7',
|
||||
ink: '#211E1B',
|
||||
ink2: '#2B2722',
|
||||
ink3: '#4B463E',
|
||||
muted: '#8C857A',
|
||||
muted2: '#A39C92',
|
||||
faint: '#B5AEA3',
|
||||
ink4: '#6B655C',
|
||||
muted: '#73726C',
|
||||
muted2: '#8C857A',
|
||||
faint: '#A39C92',
|
||||
inkRGB: '33,30,27',
|
||||
line: 'rgba(33,30,27,0.07)',
|
||||
line2: 'rgba(33,30,27,0.12)',
|
||||
hover: 'rgba(33,30,27,0.045)',
|
||||
@@ -24,7 +64,53 @@ export const WC = {
|
||||
danger: '#C2483A',
|
||||
dangerBg: 'rgba(194,72,58,0.10)',
|
||||
warn: '#B98A2E',
|
||||
} as const;
|
||||
warnBg: 'rgba(185,138,46,0.14)',
|
||||
trackOff: '#D5D1C8',
|
||||
};
|
||||
|
||||
export const WC_DARK: WCColors = {
|
||||
accent: '#C25A36',
|
||||
ink: '#F2EFE9',
|
||||
ink2: '#E2DDD4',
|
||||
ink3: '#B3ABA0',
|
||||
ink4: '#9A9389',
|
||||
muted: '#938C82',
|
||||
muted2: '#736D64',
|
||||
faint: '#6E6860',
|
||||
inkRGB: '240,236,228',
|
||||
line: 'rgba(240,236,228,0.07)',
|
||||
line2: 'rgba(240,236,228,0.12)',
|
||||
hover: 'rgba(240,236,228,0.045)',
|
||||
selBg: 'rgba(240,236,228,0.06)',
|
||||
success: '#2E7D5B',
|
||||
successBg: 'rgba(46,125,91,0.22)',
|
||||
danger: '#C2483A',
|
||||
dangerBg: 'rgba(194,72,58,0.18)',
|
||||
warn: '#B98A2E',
|
||||
warnBg: 'rgba(185,138,46,0.18)',
|
||||
trackOff: 'rgba(240,236,228,0.18)',
|
||||
};
|
||||
|
||||
export function useWC(): WCPalette {
|
||||
const { mode } = useThemeMode();
|
||||
const c = useClaudeTokens();
|
||||
const base = mode === 'dark' ? WC_DARK : WC_LIGHT;
|
||||
return {
|
||||
...base,
|
||||
// Surfaces straight from the app tokens, so the window is the same material
|
||||
// as every other card in both themes (the warmth lives in accent + text).
|
||||
// Title bar + sidebar stay on bg.surface (not a darker fill) so the whole
|
||||
// window reads as one card like the chat card; borders separate the regions.
|
||||
paper: c.bg.surface,
|
||||
panel: c.bg.surface,
|
||||
rail: c.bg.surface,
|
||||
inset: c.bg.page,
|
||||
raised: c.bg.elevated,
|
||||
shadow: c.shadow,
|
||||
radius: c.radius,
|
||||
border: c.border,
|
||||
};
|
||||
}
|
||||
|
||||
export const FONT_SERIF = "'Newsreader', Georgia, serif";
|
||||
export const FONT_SANS = "'Hanken Grotesk', system-ui, sans-serif";
|
||||
@@ -44,16 +130,22 @@ export function colorForId(id: string): string {
|
||||
return WORKFLOW_PALETTE[h % WORKFLOW_PALETTE.length];
|
||||
}
|
||||
|
||||
// Prefer the user's chosen swatch; fall back to the stable id-hash hue when
|
||||
// they haven't picked one. Single source of truth for every dot/bar.
|
||||
export function colorForWorkflow(wf: { id: string; color?: string | null }): string {
|
||||
return wf.color || colorForId(wf.id);
|
||||
}
|
||||
|
||||
export type RunStatus = 'success' | 'failure' | 'ran_late' | 'running' | 'skipped' | 'paused';
|
||||
|
||||
export function statusChip(status: RunStatus): CSSProperties {
|
||||
export function statusChip(status: RunStatus, wc: WCPalette): CSSProperties {
|
||||
const map: Record<string, [string, string]> = {
|
||||
success: [WC.success, WC.successBg],
|
||||
ran_late: [WC.warn, 'rgba(185,138,46,0.14)'],
|
||||
failure: [WC.danger, 'rgba(194,72,58,0.12)'],
|
||||
skipped: [WC.muted, 'rgba(33,30,27,0.07)'],
|
||||
running: [WC.accent, 'rgba(0,0,0,0.04)'],
|
||||
paused: [WC.muted, 'rgba(33,30,27,0.07)'],
|
||||
success: [wc.success, wc.successBg],
|
||||
ran_late: [wc.warn, wc.warnBg],
|
||||
failure: [wc.danger, wc.dangerBg],
|
||||
skipped: [wc.muted, `rgba(${wc.inkRGB},0.07)`],
|
||||
running: [wc.accent, `rgba(${wc.inkRGB},0.06)`],
|
||||
paused: [wc.muted, `rgba(${wc.inkRGB},0.07)`],
|
||||
};
|
||||
const [color, background] = map[status] || map.paused;
|
||||
return {
|
||||
@@ -62,17 +154,17 @@ export function statusChip(status: RunStatus): CSSProperties {
|
||||
};
|
||||
}
|
||||
|
||||
export function statusDot(status: RunStatus): CSSProperties {
|
||||
export function statusDot(status: RunStatus, wc: WCPalette): CSSProperties {
|
||||
const map: Record<string, string> = {
|
||||
success: WC.success, ran_late: WC.warn, failure: WC.danger,
|
||||
running: WC.accent, skipped: WC.faint, paused: WC.faint,
|
||||
success: wc.success, ran_late: wc.warn, failure: wc.danger,
|
||||
running: wc.accent, skipped: wc.faint, paused: wc.faint,
|
||||
};
|
||||
return { width: 8, height: 8, borderRadius: '50%', background: map[status] || WC.faint, flex: 'none' };
|
||||
return { width: 8, height: 8, borderRadius: '50%', background: map[status] || wc.faint, flex: 'none' };
|
||||
}
|
||||
|
||||
export function track(on: boolean): CSSProperties {
|
||||
export function track(on: boolean, wc: WCPalette): CSSProperties {
|
||||
return {
|
||||
width: 34, height: 20, borderRadius: 999, background: on ? WC.accent : '#D5D1C8',
|
||||
width: 34, height: 20, borderRadius: 999, background: on ? wc.accent : wc.trackOff,
|
||||
position: 'relative', cursor: 'pointer', transition: 'background .15s', flex: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user