mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] desktop: Spaces strip goes Mission Control scale w/ thumbnails, hover header unclipped, fullscreen keeps all 3 lights, card epithets personalized by prep
This commit is contained in:
@@ -57,6 +57,8 @@ class PrepResponse(BaseModel):
|
||||
# A punchy <=10-word identity hook, read at a glance in the reveal's focal beat (the greeting is the
|
||||
# longer warm read for the chat; the headline is the scannable one-liner most people actually read).
|
||||
headline: str = ""
|
||||
# 2-4 word identity titles tailored to this user; the Swarm Card leads with these over the static list.
|
||||
epithets: List[str] = Field(default_factory=list)
|
||||
greeting: str = ""
|
||||
starters: List[PersonalizedStarter] = Field(default_factory=list)
|
||||
app_title: str = ""
|
||||
|
||||
@@ -30,7 +30,7 @@ P_SYSTEM = (
|
||||
"You write first-run starter tasks for OpenSwarm, a desktop AI agent platform that can "
|
||||
"organize local files, browse the web in a real browser, build small apps, and run agents in parallel. "
|
||||
"Given facts about the user's machine and the apps they picked, respond with STRICT JSON only: "
|
||||
'{"headline": string, "greeting": string, "starters": [{"title": string, "prompt": string, "reason": string}], "app_title": string, "app_prompt": string, "app_reason": string, "research_title": string, "research_prompt": string, "research_reason": string, "browser_title": string, "browser_prompt": string, "browser_reason": string, "automations": [{"title": string, "prompt": string, "cadence": "daily"|"weekday"|"weekly"}]}. '
|
||||
'{"headline": string, "greeting": string, "epithets": [string, string, string], "starters": [{"title": string, "prompt": string, "reason": string}], "app_title": string, "app_prompt": string, "app_reason": string, "research_title": string, "research_prompt": string, "research_reason": string, "browser_title": string, "browser_prompt": string, "browser_reason": string, "automations": [{"title": string, "prompt": string, "cadence": "daily"|"weekday"|"weekly"}]}. '
|
||||
"First, silently infer a short, confident profile of this user: who they are and what they are working on. "
|
||||
"If usage_summary is present it is the STRONGEST signal (a distilled profile of who this person is and what "
|
||||
"they actually work on, read from their real AI conversations); weight it above everything else. Do NOT let a "
|
||||
@@ -126,6 +126,7 @@ P_SYSTEM = (
|
||||
"defining trait, no filler, no full sentence, no period. It is read at a glance in big type, so it must NOT be "
|
||||
"a paragraph. Example shapes only (never copy, tailor to THEM): 'OpenSwarm founder who measures everything, "
|
||||
"vertical jump to agent latency' or 'Ships iOS apps, obsessed with the last 5% of polish'. Sharp, not wordy. "
|
||||
"epithets: exactly 3 short identity titles for this person, 2-4 plain words each (like 'QUIET POWER USER' but THEIRS, drawn from their real work and interests in the profile), confident and warm, never punny, never generic. "
|
||||
"The greeting is one or two warm, punchy sentences that make this person feel INSTANTLY understood, the "
|
||||
"'wait, it actually gets me' hook. Lead with the single most specific true thing about them from the profile "
|
||||
"(their actual project BY NAME, their real craft, the obsession they keep returning to), then add ONE more "
|
||||
@@ -165,6 +166,7 @@ def parse_prep(text: str) -> Optional[PrepResponse]:
|
||||
starters = build_starters(data.get("starters") if isinstance(data.get("starters"), list) else [])
|
||||
automations = p_build_automations(data.get("automations") if isinstance(data.get("automations"), list) else [])
|
||||
headline = str(data.get("headline", "")).strip()
|
||||
epithets = [strip_dashes(str(x)).strip() for x in (data.get("epithets") or []) if str(x).strip()][:3]
|
||||
greeting = str(data.get("greeting", "")).strip()
|
||||
app_title = str(data.get("app_title", "")).strip()
|
||||
app_prompt = str(data.get("app_prompt", "")).strip()
|
||||
@@ -213,6 +215,7 @@ def parse_prep(text: str) -> Optional[PrepResponse]:
|
||||
return None
|
||||
return PrepResponse(
|
||||
headline=strip_dashes(headline),
|
||||
epithets=epithets,
|
||||
greeting=strip_dashes(greeting),
|
||||
starters=starters[:4],
|
||||
app_title=strip_dashes(app_title),
|
||||
|
||||
@@ -208,7 +208,7 @@ const OnboardingV3Root: React.FC = () => {
|
||||
)}
|
||||
{beat === 'apps' && <BeatApps c={c} picks={picks} setPicks={setPicks} onNext={leaveApps} onBack={() => setBeat('connect')} />}
|
||||
{beat === 'theme' && <BeatTheme c={c} onNext={() => setBeat('card')} onBack={() => setBeat('apps')} />}
|
||||
{beat === 'card' && <BeatCard c={c} identity={pipeline.identity} onFinish={(name) => { void leaveCard(name); }} onBack={() => setBeat('theme')} />}
|
||||
{beat === 'card' && <BeatCard c={c} identity={pipeline.identity} personalizedEpithets={pipeline.getPrepEpithets()} onFinish={(name) => { void leaveCard(name); }} onBack={() => setBeat('theme')} />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
|
||||
@@ -47,16 +47,24 @@ function readableInk(base: string, c: ClaudeTokens): string {
|
||||
const BeatCard: React.FC<{
|
||||
c: ClaudeTokens;
|
||||
identity: ProviderIdentity[];
|
||||
// Prep-written identity titles for THIS user; the dice leads with these, statics are the floor.
|
||||
personalizedEpithets?: string[];
|
||||
onFinish: (name: string | null) => void;
|
||||
onBack: () => void;
|
||||
}> = ({ c, identity, onFinish, onBack }) => {
|
||||
}> = ({ c, identity, personalizedEpithets, onFinish, onBack }) => {
|
||||
const { accent, gradient } = useThemeAccent();
|
||||
const [name, setName] = useState(() => nameFromIdentity(identity));
|
||||
// finish() can legitimately wait up to PREP_WAIT_CAP_MS on prep; the button must say so, once.
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const seed = useMemo(() => Math.floor(Math.random() * EPITHETS.length), []);
|
||||
const pool = useMemo(() => {
|
||||
const personal = (personalizedEpithets ?? []).map((e) => e.toUpperCase()).filter(Boolean);
|
||||
return personal.length > 0 ? [...personal, ...EPITHETS] : EPITHETS;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
// Personalized titles lead; only a pure-static pool starts on a random one.
|
||||
const seed = useMemo(() => ((personalizedEpithets ?? []).length > 0 ? 0 : Math.floor(Math.random() * pool.length)), [pool]);
|
||||
const [roll, setRoll] = useState(0);
|
||||
const epithet = EPITHETS[(seed + roll) % EPITHETS.length];
|
||||
const epithet = pool[(seed + roll) % pool.length];
|
||||
const today = useMemo(() => new Date().toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }), []);
|
||||
const [tilt, setTilt] = useState<{ rx: number; ry: number; mx: number; my: number } | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface ScanResult {
|
||||
|
||||
export interface PrepResponse {
|
||||
headline: string;
|
||||
epithets?: string[];
|
||||
greeting: string;
|
||||
starters: PersonalizedStarter[];
|
||||
app_title: string;
|
||||
|
||||
@@ -118,5 +118,8 @@ export function useOnboardingV3Pipeline() {
|
||||
dispatch(setFlowActive(false));
|
||||
}, [dispatch, accent, gradient, mode]);
|
||||
|
||||
return { identity, kickIdentity, kickScan, kickUsageRead, kickPrep, finish };
|
||||
// Read synchronously at card-beat time: prep has usually resolved by the last beat.
|
||||
const getPrepEpithets = useCallback((): string[] => prepReadyRef.current?.epithets ?? [], []);
|
||||
|
||||
return { identity, kickIdentity, kickScan, kickUsageRead, kickPrep, finish, getPrepEpithets };
|
||||
}
|
||||
|
||||
@@ -906,6 +906,8 @@ const AgentCard: React.FC<Props> = ({
|
||||
border: isFullscreen ? 'none' : isSelected ? '2px solid #3b82f6' : '1px solid rgba(255,255,255,0.08)',
|
||||
borderRadius: tiledStyle ? '12px' : '20px',
|
||||
boxShadow: '0 18px 48px rgba(0,0,0,0.4)',
|
||||
// The hover header floats ABOVE the card; the root must not clip it (the chat body clips itself).
|
||||
...(tiledStyle ? {} : { overflow: 'visible' }),
|
||||
}),
|
||||
}}
|
||||
>
|
||||
@@ -1059,7 +1061,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
sx={{ display: 'flex', alignItems: 'center', mr: 0.75, flexShrink: 0 }}
|
||||
>
|
||||
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} fullscreen={isFullscreen} />
|
||||
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -1002,7 +1002,6 @@ const BrowserCard: React.FC<Props> = ({
|
||||
onMinimize={handleMinimize}
|
||||
onTile={onTile}
|
||||
tiled={!!tileZone}
|
||||
fullscreen={tileZone === 'fullscreen'}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
|
||||
@@ -636,7 +636,7 @@ const DashboardViewCard: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Box onPointerDown={(e) => e.stopPropagation()} sx={{ display: 'flex', alignItems: 'center', flexShrink: 0, mr: 0.25 }}>
|
||||
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} fullscreen={tileZone === 'fullscreen'} />
|
||||
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} />
|
||||
</Box>
|
||||
{!isMinimized && <GridViewRoundedIcon sx={{ fontSize: 16, color: c.accent.primary, flexShrink: 0 }} />}
|
||||
<Typography
|
||||
|
||||
@@ -342,7 +342,7 @@ const NoteCard: React.FC<Props> = ({
|
||||
}}
|
||||
>
|
||||
<Box onPointerDown={(e) => e.stopPropagation()} sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} fullscreen={tileZone === 'fullscreen'} />
|
||||
<WindowControls onClose={() => handleRemove()} onMinimize={onMinimize} onTile={onTile} tiled={!!tileZone} />
|
||||
</Box>
|
||||
{isMinimized && (
|
||||
<Box sx={{ flex: 1, minWidth: 0, fontSize: '0.8125rem', color: palette.text, opacity: 0.75, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import FullscreenExitRoundedIcon from '@mui/icons-material/FullscreenExitRounded';
|
||||
import { TILE_ZONES } from './tileZones';
|
||||
|
||||
interface WindowControlsProps {
|
||||
@@ -8,9 +7,6 @@ interface WindowControlsProps {
|
||||
onMinimize: () => void;
|
||||
onTile: (zone: string) => void; // a TILE_ZONES key, or 'restore'
|
||||
tiled?: boolean;
|
||||
// Full size view: the native macOS lights sit right above this corner, so a second dot cluster
|
||||
// reads as double chrome; collapse to ONE exit control and let the natives own window ops.
|
||||
fullscreen?: boolean;
|
||||
// Green = direct fullscreen toggle, no Fill/Halves/Quarters submenu (for surfaces that only
|
||||
// support fullscreen, like the Workflows window, where half-tiling has nowhere to land).
|
||||
noTileMenu?: boolean;
|
||||
@@ -57,7 +53,7 @@ export const ARC_CHIP_SX: Record<string, unknown> = {
|
||||
'.osw-pill-host:hover & .osw-window-lights > :nth-of-type(3)': { transform: 'translate(calc(-50% + 11px), calc(-50% + 5px)) scale(1)', opacity: 1, transitionDelay: '80ms' },
|
||||
};
|
||||
|
||||
function WindowControls({ onClose, onMinimize, onTile, tiled, fullscreen, noTileMenu }: WindowControlsProps): React.ReactElement {
|
||||
function WindowControls({ onClose, onMinimize, onTile, tiled, noTileMenu }: WindowControlsProps): React.ReactElement {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
// Menu DOM (12 tiles + labels, ~30 nodes) mounts on first green-dot hover, not per card at boot.
|
||||
const [menuHot, setMenuHot] = useState(false);
|
||||
@@ -71,32 +67,6 @@ function WindowControls({ onClose, onMinimize, onTile, tiled, fullscreen, noTile
|
||||
const scheduleClose = (): void => { closeTimer.current = window.setTimeout(() => setMenuOpen(false), 180); };
|
||||
const stop = (e: React.PointerEvent | React.MouseEvent): void => { e.stopPropagation(); };
|
||||
|
||||
if (fullscreen) {
|
||||
return (
|
||||
<Box className="osw-window-lights" onPointerDown={stop} sx={{ display: 'flex', alignItems: 'center', flex: 'none' }}>
|
||||
<Box
|
||||
component="button"
|
||||
type="button"
|
||||
aria-label="Exit full screen"
|
||||
title="Exit full screen (Esc)"
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); onTile('restore'); }}
|
||||
onPointerDown={stop}
|
||||
sx={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 24, height: 24, p: 0, border: 'none', borderRadius: '7px',
|
||||
// Neutral chip at rest so it reads on ANY ground (the fullscreen chat wash is dark, the
|
||||
// browser chrome is light); hover goes explicit white-on-dark so it can never disappear.
|
||||
background: 'rgba(128,128,128,0.28)', color: 'inherit', opacity: 0.9, cursor: 'pointer',
|
||||
transition: 'opacity 120ms, background 120ms, color 120ms',
|
||||
'&:hover': { opacity: 1, background: 'rgba(0,0,0,0.5)', color: '#fff' },
|
||||
}}
|
||||
>
|
||||
<FullscreenExitRoundedIcon sx={{ fontSize: 16 }} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const btn = (color: string, symbol: string, onClick: () => void, label: string): React.ReactElement => (
|
||||
<Box component="button" type="button" aria-label={label}
|
||||
onClick={(e: React.MouseEvent) => { e.stopPropagation(); onClick(); }} onPointerDown={stop} sx={dotSx(color)}>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { createDashboard } from '@/shared/state/dashboardsSlice';
|
||||
|
||||
// macOS Spaces, one for one: rest the cursor on the very top edge and a translucent bar of
|
||||
// dashboard "spaces" slides down (Mission Control's spaces row), click switches, + adds one.
|
||||
// This replaces the sidebar as the dashboard switcher; tools live in the dock, search on Cmd+K.
|
||||
// macOS Spaces, one for one: rest the cursor on the top edge and the spaces bar slides down,
|
||||
// full-size thumbnail tiles with the name beneath, + at the right end to add a space. It stays
|
||||
// up while the cursor is anywhere near the bar and only leaves once you move well below it
|
||||
// (mouseleave flicker is exactly what Mission Control does not do). Replaces the sidebar.
|
||||
const HOT_ZONE_PX = 3;
|
||||
const CLOSE_DELAY_MS = 280;
|
||||
const TILE_W = 176;
|
||||
const TILE_H = 104;
|
||||
const DISMISS_BELOW_PX = 72;
|
||||
|
||||
const SpacesStrip: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -17,7 +21,7 @@ const SpacesStrip: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const dashboards = useAppSelector((s) => s.dashboards.items);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const closeTimer = React.useRef<number | null>(null);
|
||||
const barRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const activeId = React.useMemo(() => {
|
||||
const m = location.pathname.match(/\/dashboard\/([^/]+)/);
|
||||
@@ -29,32 +33,42 @@ const SpacesStrip: React.FC = () => {
|
||||
[dashboards],
|
||||
);
|
||||
|
||||
const hold = (): void => { if (closeTimer.current) window.clearTimeout(closeTimer.current); };
|
||||
const reveal = (): void => { hold(); setOpen(true); };
|
||||
const scheduleClose = (): void => { hold(); closeTimer.current = window.setTimeout(() => setOpen(false), CLOSE_DELAY_MS); };
|
||||
// Mission Control dismissal: while open, watch the cursor globally and close only once it has
|
||||
// moved a comfortable distance BELOW the bar; hovering within or near the bar never flickers.
|
||||
React.useEffect(() => {
|
||||
if (!open) return undefined;
|
||||
const onMove = (e: MouseEvent): void => {
|
||||
const barBottom = barRef.current?.getBoundingClientRect().bottom ?? 0;
|
||||
if (e.clientY > barBottom + DISMISS_BELOW_PX) setOpen(false);
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
return () => window.removeEventListener('mousemove', onMove);
|
||||
}, [open]);
|
||||
|
||||
const addSpace = (): void => {
|
||||
void dispatch(createDashboard('Untitled Dashboard')).then((result) => {
|
||||
if (createDashboard.fulfilled.match(result)) navigate(`/dashboard/${(result.payload as { id: string }).id}`);
|
||||
if (createDashboard.fulfilled.match(result)) {
|
||||
navigate(`/dashboard/${(result.payload as { id: string }).id}`);
|
||||
setOpen(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box onMouseEnter={reveal} sx={{ position: 'fixed', top: 0, left: 0, right: 0, height: HOT_ZONE_PX, zIndex: 99998 }} />
|
||||
<Box onMouseEnter={() => setOpen(true)} sx={{ position: 'fixed', top: 0, left: 0, right: 0, height: HOT_ZONE_PX, zIndex: 99998 }} />
|
||||
<Box
|
||||
onMouseEnter={reveal}
|
||||
onMouseLeave={scheduleClose}
|
||||
ref={barRef}
|
||||
sx={{
|
||||
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 99999,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1,
|
||||
px: 2, py: 1.25,
|
||||
background: 'rgba(24,22,28,0.42)',
|
||||
backdropFilter: 'blur(26px) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(26px) saturate(150%)',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
display: 'flex', alignItems: 'flex-start', justifyContent: 'center', gap: 2.25,
|
||||
px: 3, pt: 2, pb: 1.75,
|
||||
background: 'rgba(22,20,26,0.45)',
|
||||
backdropFilter: 'blur(30px) saturate(150%)',
|
||||
WebkitBackdropFilter: 'blur(30px) saturate(150%)',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.07)',
|
||||
transform: open ? 'translateY(0)' : 'translateY(-110%)',
|
||||
transition: 'transform 220ms cubic-bezier(0.22,1,0.36,1)',
|
||||
transition: 'transform 260ms cubic-bezier(0.22,1,0.36,1)',
|
||||
pointerEvents: open ? 'auto' : 'none',
|
||||
}}
|
||||
>
|
||||
@@ -66,16 +80,31 @@ const SpacesStrip: React.FC = () => {
|
||||
component="button"
|
||||
onClick={() => { navigate(`/dashboard/${d.id}`); setOpen(false); }}
|
||||
sx={{
|
||||
px: 2, py: 0.8, borderRadius: '9px', cursor: 'pointer',
|
||||
border: active ? '2px solid rgba(255,255,255,0.85)' : '1px solid rgba(255,255,255,0.18)',
|
||||
background: active ? 'rgba(255,255,255,0.16)' : 'rgba(255,255,255,0.07)',
|
||||
color: 'rgba(255,255,255,0.92)', fontFamily: 'inherit', fontSize: '0.8125rem', fontWeight: active ? 600 : 500,
|
||||
whiteSpace: 'nowrap', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis',
|
||||
transition: 'background 130ms, border-color 130ms',
|
||||
'&:hover': { background: 'rgba(255,255,255,0.18)' },
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.75,
|
||||
p: 0, border: 'none', background: 'transparent', cursor: 'pointer', fontFamily: 'inherit',
|
||||
'&:hover .osw-space-tile': { boxShadow: '0 0 0 3px rgba(255,255,255,0.65), 0 10px 26px rgba(0,0,0,0.4)' },
|
||||
}}
|
||||
>
|
||||
{d.name || 'Untitled'}
|
||||
<Box
|
||||
className="osw-space-tile"
|
||||
sx={{
|
||||
width: TILE_W, height: TILE_H, borderRadius: '10px', overflow: 'hidden',
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
boxShadow: active
|
||||
? '0 0 0 3px rgba(255,255,255,0.9), 0 10px 26px rgba(0,0,0,0.4)'
|
||||
: '0 0 0 1px rgba(255,255,255,0.14), 0 8px 20px rgba(0,0,0,0.35)',
|
||||
transition: 'box-shadow 140ms',
|
||||
}}
|
||||
>
|
||||
{d.thumbnail ? (
|
||||
<Box component="img" src={d.thumbnail} alt="" sx={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
||||
) : (
|
||||
<Box sx={{ width: '100%', height: '100%', background: 'linear-gradient(135deg, rgba(255,255,255,0.16), rgba(255,255,255,0.05))' }} />
|
||||
)}
|
||||
</Box>
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.92)', fontSize: '0.8125rem', fontWeight: active ? 600 : 500, maxWidth: TILE_W, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{d.name || 'Untitled'}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
@@ -85,14 +114,14 @@ const SpacesStrip: React.FC = () => {
|
||||
onClick={addSpace}
|
||||
sx={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: 34, height: 34, borderRadius: '9px', cursor: 'pointer',
|
||||
border: '1px dashed rgba(255,255,255,0.35)', background: 'transparent',
|
||||
color: 'rgba(255,255,255,0.85)',
|
||||
transition: 'background 130ms',
|
||||
'&:hover': { background: 'rgba(255,255,255,0.14)' },
|
||||
width: 64, height: TILE_H, borderRadius: '10px', cursor: 'pointer',
|
||||
border: '1.5px dashed rgba(255,255,255,0.35)', background: 'rgba(255,255,255,0.05)',
|
||||
color: 'rgba(255,255,255,0.85)', flexShrink: 0,
|
||||
transition: 'background 140ms, border-color 140ms',
|
||||
'&:hover': { background: 'rgba(255,255,255,0.14)', borderColor: 'rgba(255,255,255,0.6)' },
|
||||
}}
|
||||
>
|
||||
<Plus size={16} />
|
||||
<Plus size={22} />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -85,7 +85,6 @@ const WorkflowsAppContent: React.FC<{ header: CardHeader }> = ({ header }) => {
|
||||
onMinimize={() => dispatch(closeWorkflowsApp())}
|
||||
onTile={() => dispatch(toggleWorkflowsHubFullscreen())}
|
||||
tiled={isFullscreen}
|
||||
fullscreen={isFullscreen}
|
||||
noTileMenu
|
||||
/>
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user