mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-10 19:57:44 +02:00
[eric] chat+dashboard: animated WebGL weather widget (vendored runtime, adapter keeps simple agent shape); AskUI Confirm wired to their native actions + orphan feedback; wallpaper v2 (aerial-perspective ridges, lit crests, 120s ambient drift, reduced-motion off)
This commit is contained in:
@@ -29,6 +29,7 @@ function parseResultResponse(pair: ToolPair): Record<string, unknown> | null {
|
||||
function AskUiBubble({ pair, sessionId, isPending, suppressReveal }: AskUiBubbleProps): React.ReactElement {
|
||||
const payload = parseShowUiPayload(pair);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [orphaned, setOrphaned] = useState(false);
|
||||
const answered = parseResultResponse(pair);
|
||||
|
||||
const componentId = payload && payload.component === 'vendored' ? String(payload.props.id || '') : '';
|
||||
@@ -41,13 +42,22 @@ function AskUiBubble({ pair, sessionId, isPending, suppressReveal }: AskUiBubble
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getAuthToken()}` },
|
||||
body: JSON.stringify({ session_id: sessionId, component_id: componentId, response }),
|
||||
}).catch(() => setSubmitted(false));
|
||||
})
|
||||
.then((r) => {
|
||||
if (!r.ok) {
|
||||
// Nothing parked server-side (agent gone or this is a replayed transcript): say so instead of silently swallowing the click.
|
||||
setSubmitted(false);
|
||||
setOrphaned(true);
|
||||
}
|
||||
})
|
||||
.catch(() => setSubmitted(false));
|
||||
},
|
||||
[submitted, sessionId, componentId],
|
||||
);
|
||||
|
||||
// Their embedded-actions contract: onAction(actionId, state) delivers the component's full state;
|
||||
// approval-card uses onConfirm/onCancel instead. Inject a default Send action when none given.
|
||||
// Their embedded-actions contract: onAction(actionId, state) delivers the component's full state,
|
||||
// and the components ship their own footer actions (Clear/Confirm), so we only wire the callback.
|
||||
// 'cancel' is a local clear, never an answer; approval-card uses onConfirm/onCancel instead.
|
||||
const extraProps = useMemo(() => {
|
||||
if (!payload || payload.component !== 'vendored') return {};
|
||||
const waiting = pair.result === null && !submitted;
|
||||
@@ -60,10 +70,11 @@ function AskUiBubble({ pair, sessionId, isPending, suppressReveal }: AskUiBubble
|
||||
: { choice: (answered?.choice as string) || undefined };
|
||||
}
|
||||
if (waiting) {
|
||||
const hasActions = Array.isArray((payload.props as { actions?: unknown[] }).actions) && (payload.props as { actions?: unknown[] }).actions!.length > 0;
|
||||
return {
|
||||
...(hasActions ? {} : { actions: [{ id: 'submit', label: 'Send' }] }),
|
||||
onAction: (actionId: string, state: unknown) => respond({ action: actionId, value: state ?? null }),
|
||||
onAction: (actionId: string, state: unknown) => {
|
||||
if (actionId === 'cancel') return;
|
||||
respond({ action: actionId, value: state ?? null });
|
||||
},
|
||||
};
|
||||
}
|
||||
return answered && 'value' in answered ? { choice: answered.value } : {};
|
||||
@@ -81,6 +92,11 @@ function AskUiBubble({ pair, sessionId, isPending, suppressReveal }: AskUiBubble
|
||||
{submitted && pair.result === null && (
|
||||
<Box sx={{ fontSize: '0.72rem', opacity: 0.55, pt: 0.5 }}>Sent to the agent...</Box>
|
||||
)}
|
||||
{orphaned && (
|
||||
<Box sx={{ fontSize: '0.72rem', opacity: 0.55, pt: 0.5 }}>
|
||||
No agent is waiting for this answer (the request expired or this is an old transcript).
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,91 +1,54 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import WbSunnyOutlinedIcon from '@mui/icons-material/WbSunnyOutlined';
|
||||
import CloudOutlinedIcon from '@mui/icons-material/CloudOutlined';
|
||||
import GrainIcon from '@mui/icons-material/Grain';
|
||||
import AcUnitIcon from '@mui/icons-material/AcUnit';
|
||||
import ThunderstormOutlinedIcon from '@mui/icons-material/ThunderstormOutlined';
|
||||
import { WeatherWidget as AnimatedWeatherWidget } from '@toolui/components/weather-widget/weather-widget-container';
|
||||
import type { WeatherConditionCode, ForecastDay } from '@toolui/components/weather-widget/schema-runtime';
|
||||
import { useThemeMode } from '@/shared/styles/ThemeContext';
|
||||
import type { WeatherProps } from './showUiPayload';
|
||||
|
||||
function conditionIcon(condition: string | undefined, size: number): React.ReactElement {
|
||||
function toConditionCode(condition: string | undefined): WeatherConditionCode {
|
||||
const cond = (condition || '').toLowerCase();
|
||||
const sx = { fontSize: size, color: 'rgba(255,255,255,0.92)' };
|
||||
if (/thunder|storm/.test(cond)) return <ThunderstormOutlinedIcon sx={sx} />;
|
||||
if (/rain|drizzle|shower/.test(cond)) return <GrainIcon sx={sx} />;
|
||||
if (/snow|sleet|ice/.test(cond)) return <AcUnitIcon sx={sx} />;
|
||||
if (/cloud|overcast|fog|mist/.test(cond)) return <CloudOutlinedIcon sx={sx} />;
|
||||
return <WbSunnyOutlinedIcon sx={sx} />;
|
||||
if (/thunder|storm/.test(cond)) return 'thunderstorm';
|
||||
if (/heavy rain|downpour/.test(cond)) return 'heavy-rain';
|
||||
if (/drizzle/.test(cond)) return 'drizzle';
|
||||
if (/rain|shower/.test(cond)) return 'rain';
|
||||
if (/sleet/.test(cond)) return 'sleet';
|
||||
if (/hail/.test(cond)) return 'hail';
|
||||
if (/snow/.test(cond)) return 'snow';
|
||||
if (/fog|mist|haze/.test(cond)) return 'fog';
|
||||
if (/overcast/.test(cond)) return 'overcast';
|
||||
if (/partly|part sun|some cloud/.test(cond)) return 'partly-cloudy';
|
||||
if (/cloud/.test(cond)) return 'cloudy';
|
||||
if (/wind/.test(cond)) return 'windy';
|
||||
return 'clear';
|
||||
}
|
||||
|
||||
/** iOS-style weather card: dusk-sky art, big thin temperature, five-day strip. */
|
||||
/** Agent-facing 'weather' shape adapted onto the vendored animated (WebGL) weather widget. */
|
||||
function WeatherWidget({ props }: { props: WeatherProps }): React.ReactElement {
|
||||
const unit = props.unit || 'F';
|
||||
const { mode } = useThemeMode();
|
||||
const forecast: ForecastDay[] = (props.forecast || []).slice(0, 7).map((d) => ({
|
||||
label: d.day,
|
||||
conditionCode: toConditionCode(d.condition),
|
||||
tempMin: Math.round(d.low ?? d.high - 8),
|
||||
tempMax: Math.round(d.high),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: 300,
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
p: 2,
|
||||
color: '#fff',
|
||||
background:
|
||||
'radial-gradient(120% 90% at 78% 62%, rgba(255,196,110,0.9) 0%, rgba(214,142,90,0.75) 30%, rgba(120,85,80,0.4) 55%, rgba(0,0,0,0) 75%), linear-gradient(180deg, #4d4048 0%, #6b5a58 45%, #8a6a55 100%)',
|
||||
boxShadow: '0 10px 30px rgba(0,0,0,0.3)',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '1.05rem', fontWeight: 600, textShadow: '0 1px 8px rgba(0,0,0,0.35)' }}>
|
||||
{props.location}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', mt: 0.5 }}>
|
||||
<Typography sx={{ fontSize: '4rem', fontWeight: 200, lineHeight: 1, letterSpacing: '-2px', textShadow: '0 2px 12px rgba(0,0,0,0.3)' }}>
|
||||
{Math.round(props.temp)}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '1.6rem', fontWeight: 300, mt: 0.5, opacity: 0.85 }}>°{unit}</Typography>
|
||||
</Box>
|
||||
{(props.high != null || props.low != null) && (
|
||||
<Box sx={{ display: 'flex', gap: 1.5, mt: 1 }}>
|
||||
{props.high != null && (
|
||||
<Typography sx={{ fontSize: '0.9rem', fontWeight: 600 }}>
|
||||
<Box component="span" sx={{ opacity: 0.6, fontWeight: 400 }}>H </Box>{Math.round(props.high)}°
|
||||
</Typography>
|
||||
)}
|
||||
{props.low != null && (
|
||||
<Typography sx={{ fontSize: '0.9rem', fontWeight: 600 }}>
|
||||
<Box component="span" sx={{ opacity: 0.6, fontWeight: 400 }}>L </Box>{Math.round(props.low)}°
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{props.forecast && props.forecast.length > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
mt: 2.5,
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(255,255,255,0.14)',
|
||||
backdropFilter: 'blur(6px)',
|
||||
WebkitBackdropFilter: 'blur(6px)',
|
||||
px: 1,
|
||||
py: 1.25,
|
||||
}}
|
||||
>
|
||||
{props.forecast.slice(0, 5).map((d, i) => (
|
||||
<Box key={`${d.day}-${i}`} sx={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
|
||||
<Typography sx={{ fontSize: '0.62rem', fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase', opacity: 0.85 }}>
|
||||
{d.day}
|
||||
</Typography>
|
||||
{conditionIcon(d.condition, 16)}
|
||||
<Typography sx={{ fontSize: '0.82rem', fontWeight: 700 }}>{Math.round(d.high)}°</Typography>
|
||||
{d.low != null && (
|
||||
<Typography sx={{ fontSize: '0.72rem', opacity: 0.6 }}>{Math.round(d.low)}°</Typography>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<div className={`tool-ui-scope${mode === 'dark' ? ' dark' : ''}`} style={{ width: 320 }}>
|
||||
<AnimatedWeatherWidget
|
||||
version="3.1"
|
||||
id={`weather-${props.location}`}
|
||||
location={{ name: props.location }}
|
||||
units={{ temperature: props.unit === 'C' ? 'celsius' : 'fahrenheit' }}
|
||||
current={{
|
||||
conditionCode: toConditionCode(props.condition),
|
||||
temperature: Math.round(props.temp),
|
||||
tempMin: Math.round(props.low ?? props.temp - 4),
|
||||
tempMax: Math.round(props.high ?? props.temp + 4),
|
||||
}}
|
||||
forecast={forecast}
|
||||
time={{ localTimeOfDay: new Date().getHours() + new Date().getMinutes() / 60 }}
|
||||
effects={{ enabled: true, quality: 'auto' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,100 +3,169 @@ import Box from '@mui/material/Box';
|
||||
import { useThemeMode } from '@/shared/styles/ThemeContext';
|
||||
|
||||
/**
|
||||
* Monterey-style layered wave wallpaper for the desktop-shell canvas. Pure SVG so it ships with
|
||||
* the bundle, scales to any viewport, and stays crisp; sits under the wash/grain/dot-grid layers.
|
||||
* Monterey-style layered-ridge wallpaper. Design rules: scenery not signal (long smooth ridges,
|
||||
* luminance ramps pale sky -> deep indigo so glass chrome always has contrast), depth via aerial
|
||||
* perspective (far ridges blurred + desaturated, near ridges sharp with a lit crest), and life via
|
||||
* ONE imperceptible compositor-only drift (120s, transform-only, off under reduced-motion).
|
||||
*/
|
||||
function DesktopWallpaper(): React.ReactElement {
|
||||
const { mode } = useThemeMode();
|
||||
return (
|
||||
<Box
|
||||
aria-hidden
|
||||
sx={{ position: 'absolute', inset: 0, pointerEvents: 'none', overflow: 'hidden' }}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
overflow: 'hidden',
|
||||
'& .osw-wp-drift': {
|
||||
animation: 'osw-wp-drift 120s ease-in-out infinite alternate',
|
||||
willChange: 'transform',
|
||||
},
|
||||
'& .osw-wp-drift-far': {
|
||||
animation: 'osw-wp-drift-far 160s ease-in-out infinite alternate',
|
||||
willChange: 'transform',
|
||||
},
|
||||
'@keyframes osw-wp-drift': {
|
||||
from: { transform: 'translateX(0px)' },
|
||||
to: { transform: 'translateX(-28px)' },
|
||||
},
|
||||
'@keyframes osw-wp-drift-far': {
|
||||
from: { transform: 'translateX(0px)' },
|
||||
to: { transform: 'translateX(18px)' },
|
||||
},
|
||||
'@media (prefers-reduced-motion: reduce)': {
|
||||
'& .osw-wp-drift, & .osw-wp-drift-far': { animation: 'none' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 1600 1000"
|
||||
preserveAspectRatio="xMidYMid slice"
|
||||
style={{ position: 'absolute', inset: 0, display: 'block' }}
|
||||
style={{ position: 'absolute', inset: '-2%', width: '104%', height: '104%', display: 'block' }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="osw-wp-sky" x1="0.5" y1="0" x2="0.5" y2="1">
|
||||
<stop offset="0" stopColor="#d6d3e4" />
|
||||
<stop offset="0.55" stopColor="#c39bc6" />
|
||||
<stop offset="1" stopColor="#b06ab8" />
|
||||
<stop offset="0" stopColor="#dcd9e8" />
|
||||
<stop offset="0.45" stopColor="#c8b2d4" />
|
||||
<stop offset="1" stopColor="#b490c4" />
|
||||
</linearGradient>
|
||||
<linearGradient id="osw-wp-pinkridge" x1="0" y1="0" x2="0.6" y2="1">
|
||||
<stop offset="0" stopColor="#e4a9c6" />
|
||||
<stop offset="0.5" stopColor="#cb9bbf" />
|
||||
<stop offset="1" stopColor="#bd5f92" />
|
||||
</linearGradient>
|
||||
<linearGradient id="osw-wp-redridge" x1="0.2" y1="0" x2="0.7" y2="1">
|
||||
<stop offset="0" stopColor="#e2547e" />
|
||||
<stop offset="0.55" stopColor="#c33f92" />
|
||||
<linearGradient id="osw-wp-redridge" x1="0.15" y1="0" x2="0.6" y2="1">
|
||||
<stop offset="0" stopColor="#e0517c" />
|
||||
<stop offset="0.5" stopColor="#c33f92" />
|
||||
<stop offset="1" stopColor="#a5349b" />
|
||||
</linearGradient>
|
||||
<linearGradient id="osw-wp-purple" x1="0.1" y1="0.2" x2="0.8" y2="1">
|
||||
<stop offset="0" stopColor="#9a3bbc" />
|
||||
<stop offset="0.6" stopColor="#7726b8" />
|
||||
<stop offset="1" stopColor="#641dab" />
|
||||
<linearGradient id="osw-wp-pink" x1="0.3" y1="0" x2="0.7" y2="1">
|
||||
<stop offset="0" stopColor="#e7b3cd" />
|
||||
<stop offset="0.55" stopColor="#cb8fb8" />
|
||||
<stop offset="1" stopColor="#bd5f92" />
|
||||
</linearGradient>
|
||||
<linearGradient id="osw-wp-indigo" x1="0.15" y1="0" x2="0.75" y2="1">
|
||||
<stop offset="0" stopColor="#6716d5" />
|
||||
<stop offset="0.65" stopColor="#3b1a9e" />
|
||||
<stop offset="1" stopColor="#271581" />
|
||||
<linearGradient id="osw-wp-magenta" x1="0.2" y1="0" x2="0.7" y2="1">
|
||||
<stop offset="0" stopColor="#c14fb4" />
|
||||
<stop offset="1" stopColor="#96319b" />
|
||||
</linearGradient>
|
||||
<linearGradient id="osw-wp-violetcrest" x1="0" y1="0" x2="1" y2="0.8">
|
||||
<stop offset="0" stopColor="#8f35e0" />
|
||||
<linearGradient id="osw-wp-purple" x1="0.1" y1="0.1" x2="0.7" y2="1">
|
||||
<stop offset="0" stopColor="#8f35c9" />
|
||||
<stop offset="0.6" stopColor="#7326b8" />
|
||||
<stop offset="1" stopColor="#5c1da6" />
|
||||
</linearGradient>
|
||||
<linearGradient id="osw-wp-violet" x1="0" y1="0" x2="0.9" y2="0.9">
|
||||
<stop offset="0" stopColor="#8d33dd" />
|
||||
<stop offset="1" stopColor="#6716d5" />
|
||||
</linearGradient>
|
||||
<filter id="osw-wp-soft" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feGaussianBlur stdDeviation="7" />
|
||||
<linearGradient id="osw-wp-indigo" x1="0.2" y1="0" x2="0.8" y2="1">
|
||||
<stop offset="0" stopColor="#5b21c9" />
|
||||
<stop offset="0.6" stopColor="#3b1a9e" />
|
||||
<stop offset="1" stopColor="#271581" />
|
||||
</linearGradient>
|
||||
<linearGradient id="osw-wp-crest" x1="0.5" y1="0" x2="0.5" y2="1">
|
||||
<stop offset="0" stopColor="#ffffff" stopOpacity="0.55" />
|
||||
<stop offset="1" stopColor="#ffffff" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<filter id="osw-wp-far" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feGaussianBlur stdDeviation="9" />
|
||||
</filter>
|
||||
<filter id="osw-wp-mid" x="-10%" y="-10%" width="120%" height="120%">
|
||||
<feGaussianBlur stdDeviation="3" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<rect width="1600" height="1000" fill="url(#osw-wp-sky)" />
|
||||
<rect x="-40" y="-20" width="1680" height="1040" fill="url(#osw-wp-sky)" />
|
||||
|
||||
{/* Far pink ridge sweeping the right half */}
|
||||
<path
|
||||
d="M820,0 C980,260 1120,380 1320,330 C1450,296 1540,220 1600,150 L1600,1000 L820,1000 Z"
|
||||
fill="url(#osw-wp-pinkridge)"
|
||||
filter="url(#osw-wp-soft)"
|
||||
opacity="0.9"
|
||||
/>
|
||||
{/* Far layer: soft pink billow owning the right half + red-magenta ridge from the top-left. Blurred + gentle = aerial distance. */}
|
||||
<g className="osw-wp-drift-far">
|
||||
<path
|
||||
d="M700,40 C900,180 1010,330 1230,360 C1400,382 1520,300 1620,190 L1620,1020 L700,1020 Z"
|
||||
fill="url(#osw-wp-pink)"
|
||||
filter="url(#osw-wp-far)"
|
||||
opacity="0.9"
|
||||
/>
|
||||
<path
|
||||
d="M-20,-20 L520,-20 C470,140 500,300 640,440 C480,610 220,600 -20,680 Z"
|
||||
fill="url(#osw-wp-redridge)"
|
||||
filter="url(#osw-wp-mid)"
|
||||
opacity="0.95"
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* Red-magenta ridge descending from the top-left */}
|
||||
<path
|
||||
d="M0,0 L560,0 C480,150 520,300 660,470 C540,650 300,640 0,720 Z"
|
||||
fill="url(#osw-wp-redridge)"
|
||||
/>
|
||||
{/* Mid layer: the magenta band bridging the composition. */}
|
||||
<g className="osw-wp-drift">
|
||||
<path
|
||||
d="M-20,600 C280,470 560,540 840,640 C1090,728 1360,700 1620,580 L1620,1020 L-20,1020 Z"
|
||||
fill="url(#osw-wp-magenta)"
|
||||
filter="url(#osw-wp-mid)"
|
||||
opacity="0.9"
|
||||
/>
|
||||
<path
|
||||
d="M-20,596 C280,466 560,536 840,636"
|
||||
fill="none"
|
||||
stroke="url(#osw-wp-crest)"
|
||||
strokeWidth="5"
|
||||
opacity="0.35"
|
||||
/>
|
||||
|
||||
{/* Mid magenta band bridging center */}
|
||||
<path
|
||||
d="M0,560 C340,430 620,520 900,660 C1120,770 1380,720 1600,600 L1600,1000 L0,1000 Z"
|
||||
fill="#ab37ad"
|
||||
opacity="0.85"
|
||||
filter="url(#osw-wp-soft)"
|
||||
/>
|
||||
{/* Deep purple shoulder anchoring the bottom-left. */}
|
||||
<path
|
||||
d="M-20,560 C220,520 420,600 580,760 C420,900 180,880 -20,910 Z"
|
||||
fill="url(#osw-wp-purple)"
|
||||
/>
|
||||
<path
|
||||
d="M-20,558 C220,518 420,598 578,756"
|
||||
fill="none"
|
||||
stroke="url(#osw-wp-crest)"
|
||||
strokeWidth="4"
|
||||
opacity="0.3"
|
||||
/>
|
||||
</g>
|
||||
|
||||
{/* Deep purple shoulder, left */}
|
||||
<path
|
||||
d="M0,520 C260,470 470,560 640,740 C460,900 200,850 0,880 Z"
|
||||
fill="url(#osw-wp-purple)"
|
||||
/>
|
||||
|
||||
{/* Violet crest above the foreground wave */}
|
||||
<path
|
||||
d="M0,830 C360,680 760,850 1060,780 C1280,730 1460,800 1600,740 L1600,1000 L0,1000 Z"
|
||||
fill="url(#osw-wp-violetcrest)"
|
||||
opacity="0.92"
|
||||
/>
|
||||
|
||||
{/* Foreground indigo wave */}
|
||||
<path
|
||||
d="M0,880 C320,760 720,910 1020,850 C1260,802 1470,880 1600,830 L1600,1000 L0,1000 Z"
|
||||
fill="url(#osw-wp-indigo)"
|
||||
/>
|
||||
{/* Near layer: violet crest + indigo foreground wave, sharpest, with lit crests. */}
|
||||
<g>
|
||||
<path
|
||||
d="M-20,820 C320,690 700,830 1010,772 C1260,726 1450,790 1620,720 L1620,1020 L-20,1020 Z"
|
||||
fill="url(#osw-wp-violet)"
|
||||
opacity="0.95"
|
||||
/>
|
||||
<path
|
||||
d="M-20,816 C320,686 700,826 1010,768 C1260,722 1450,786 1620,716"
|
||||
fill="none"
|
||||
stroke="url(#osw-wp-crest)"
|
||||
strokeWidth="4"
|
||||
opacity="0.4"
|
||||
/>
|
||||
<path
|
||||
d="M-20,882 C300,780 680,912 1000,856 C1260,810 1470,890 1620,834 L1620,1020 L-20,1020 Z"
|
||||
fill="url(#osw-wp-indigo)"
|
||||
/>
|
||||
<path
|
||||
d="M-20,878 C300,776 680,908 1000,852 C1260,806 1470,886 1620,830"
|
||||
fill="none"
|
||||
stroke="url(#osw-wp-crest)"
|
||||
strokeWidth="3"
|
||||
opacity="0.35"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
{mode === 'dark' && (
|
||||
<Box sx={{ position: 'absolute', inset: 0, background: 'rgba(12,6,24,0.38)' }} />
|
||||
|
||||
Reference in New Issue
Block a user