feat(startseite): Giorgio entfernt, Belegschaft auf die fünf Agenten
Auf ausdrücklichen Wunsch: Giorgio entfällt vollständig. Das weicht bewusst von §6 des Auftrags ab, der ihn als Personalverwalter und Vorgesetzten vorsah — festgehalten, damit die Abweichung später nachvollziehbar bleibt. Entfernt: Rollendefinition `AGENT_SUPERVISOR`, sein gezeichnetes Porträt, sein Eintrag in der Auslastung und in der Belegschaftsansicht sowie die Erwähnung in der Produkttour. Die Personalverwaltung bleibt unverändert über den Menüpunkt «Meine Agenten» erreichbar — sie brauchte nie eine eigene Person am Bildschirm. Mit Giorgio entfällt auch die einzige Kennzahl, die aus den Freigabevorgängen kam; `computeAgentWorkload` braucht `agentWorkItems` deshalb nicht mehr. Ordner und Komponenten heissen jetzt nach ihrem Inhalt: `components/staff/`, `AgentStaffOverview`, `StaffBackdrop`. «Meetingraum» war seit dem Umbau auf Personenkarten irreführend, und ein falscher Name kostet den Nächsten mehr Zeit als die Umbenennung. Fünf statt sechs Karten heisst Flexlayout statt Dreierraster: drei oben, zwei mittig darunter, gleich breit und ohne Lücke. Die Karten geben ihre Höhe nicht mehr selbst vor — mit `height: 100%` beanspruchte jede die volle Fläche und die zweite Zeile fiel aus dem sichtbaren Bereich. Geprüft: 433 Tests, Typecheck, Build, ESLint, Token-Schwelle (889/925). Browser 1280–1920 px und Tablet: fünf Karten vollständig sichtbar, keine Überlappung, Titel und «Filter & Übersicht» beim Einstieg sichtbar, alle fünf Klicks auf der richtigen Subpage, «Meine Agenten» weiterhin erreichbar, keine Konsolenfehler, keine 404. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import { memo } from 'react'
|
||||
import { Box, ButtonBase, Typography } from '@mui/material'
|
||||
import { ArrowRight } from 'lucide-react'
|
||||
import { AgentAvatar } from '../team'
|
||||
import type { AgentPresence } from '../../domain/agentPresence'
|
||||
import { WorkloadTone } from '../../domain/agentWorkload'
|
||||
import { DS_ACCENT, DS_NEUTRAL } from '../../lib/ds'
|
||||
|
||||
/**
|
||||
* Farben der Auslastung auf dunklem Grund. Bewusst hellere Stufen als die
|
||||
* Standardakzente — die dunkleren Töne verschwinden vor der abgedunkelten
|
||||
* Bürofläche.
|
||||
*/
|
||||
const TONE_COLOR: Record<WorkloadTone, string> = {
|
||||
[WorkloadTone.CALM]: DS_ACCENT.success.emerald,
|
||||
[WorkloadTone.ATTENTION]: DS_ACCENT.warning.amber,
|
||||
[WorkloadTone.URGENT]: DS_ACCENT.danger.bright,
|
||||
}
|
||||
|
||||
export interface AgentStaffCardProps {
|
||||
presence: AgentPresence
|
||||
onOpen: (path: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein digitaler Mitarbeitender auf der Startseite (Runde 5, §3/§6).
|
||||
*
|
||||
* Die Karte ist bewusst wie ein Personalblatt aufgebaut und nicht wie eine
|
||||
* Kachel: Porträt, Name, Funktion, Abteilung und Personalnummer, dann ein Satz
|
||||
* in der ersten Person, darunter die zuletzt protokollierte Handlung mit
|
||||
* Zeitangabe.
|
||||
*
|
||||
* Diese letzte Zeile ist der Unterschied zwischen «KI-Funktion» und
|
||||
* «Mitarbeitender»: sie belegt, dass jemand gearbeitet hat, statt es zu
|
||||
* behaupten. Eine Kennzahl allein kann jede Software anzeigen.
|
||||
*
|
||||
* Die ganze Karte ist ein Schalter. `ButtonBase` liefert Tastaturbedienung,
|
||||
* Enter, Leertaste und Fokusring ohne eigenen Code.
|
||||
*/
|
||||
export const AgentStaffCard = memo(function AgentStaffCard({ presence, onOpen }: AgentStaffCardProps) {
|
||||
const { workload } = presence
|
||||
const toneColor = workload ? TONE_COLOR[workload.tone] : DS_NEUTRAL.stone
|
||||
|
||||
return (
|
||||
<ButtonBase
|
||||
focusRipple
|
||||
onClick={() => onOpen(presence.path)}
|
||||
aria-label={
|
||||
workload
|
||||
? `${presence.name}, ${presence.role} — ${workload.label}. Arbeitsbereich öffnen`
|
||||
: `${presence.name}, ${presence.role} — Arbeitsbereich öffnen`
|
||||
}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'stretch',
|
||||
textAlign: 'left',
|
||||
gap: 1.25,
|
||||
p: { xs: 1.75, xl: 2 },
|
||||
borderRadius: 3,
|
||||
// Keine eigene Höhe: im Flexlayout gibt die Zeile die Höhe vor, und alle
|
||||
// Karten einer Zeile werden von selbst gleich hoch. Mit `height: 100%`
|
||||
// beanspruchte jede Karte die volle Fläche und die zweite Zeile fiel
|
||||
// aus dem sichtbaren Bereich.
|
||||
// Glasfläche statt Vollton: das Büro dahinter bleibt spürbar, ohne die
|
||||
// Schrift zu stören.
|
||||
bgcolor: 'rgba(255,255,255,0.06)',
|
||||
backdropFilter: 'blur(14px)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
transition: 'background-color 0.18s ease, transform 0.18s ease, border-color 0.18s ease',
|
||||
'&:hover, &.Mui-focusVisible': {
|
||||
bgcolor: 'rgba(255,255,255,0.11)',
|
||||
borderColor: 'rgba(255,255,255,0.26)',
|
||||
transform: 'translateY(-3px)',
|
||||
},
|
||||
'&:hover .staff-go, &.Mui-focusVisible .staff-go': { opacity: 1, transform: 'translateX(0)' },
|
||||
'&.Mui-focusVisible': { outline: `2px solid ${DS_NEUTRAL.gold}`, outlineOffset: 2 },
|
||||
}}
|
||||
>
|
||||
{/* Kopfzeile: Porträt, Personalien, Dienstzustand */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
|
||||
<Box sx={{ position: 'relative', flexShrink: 0 }}>
|
||||
<AgentAvatar
|
||||
agent={{ id: presence.agentId, name: presence.name, role: presence.role }}
|
||||
size={56}
|
||||
loading="eager"
|
||||
ringColor="rgba(255,255,255,0.55)"
|
||||
ringWidth={2}
|
||||
/>
|
||||
{/* Dienstpunkt am Porträt — die kürzeste Art zu sagen «ist da». */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute', right: -1, bottom: -1,
|
||||
width: 14, height: 14, borderRadius: '50%',
|
||||
bgcolor: presence.onDuty ? DS_ACCENT.success.emerald : DS_NEUTRAL.stone,
|
||||
border: '2.5px solid rgba(12,18,28,0.95)',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography
|
||||
sx={{ fontWeight: 700, fontSize: '1.0625rem', lineHeight: 1.2, color: DS_NEUTRAL.white }}
|
||||
>
|
||||
{presence.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{ fontSize: '0.8125rem', lineHeight: 1.3, color: 'rgba(255,255,255,0.72)' }}
|
||||
>
|
||||
{presence.role}
|
||||
</Typography>
|
||||
{(presence.department || presence.personnelNumber) && (
|
||||
<Typography
|
||||
sx={{
|
||||
mt: 0.25,
|
||||
fontSize: '0.6875rem',
|
||||
lineHeight: 1.3,
|
||||
color: 'rgba(255,255,255,0.42)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{[presence.department, presence.personnelNumber].filter(Boolean).join(' · ')}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Kompetenz: was der Mitarbeitende selbst entscheiden darf. */}
|
||||
{presence.autonomy && (
|
||||
<Box
|
||||
sx={{
|
||||
alignSelf: 'flex-start',
|
||||
px: 1,
|
||||
py: 0.25,
|
||||
borderRadius: 1,
|
||||
bgcolor: 'rgba(255,255,255,0.09)',
|
||||
border: '1px solid rgba(255,255,255,0.14)',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.625rem',
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.6,
|
||||
textTransform: 'uppercase',
|
||||
color: 'rgba(255,255,255,0.66)',
|
||||
}}
|
||||
>
|
||||
{presence.autonomy}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Was er dazu sagt — in der ersten Person. */}
|
||||
{workload && (
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
|
||||
<Box
|
||||
sx={{ width: 3, alignSelf: 'stretch', borderRadius: 2, bgcolor: toneColor, flexShrink: 0 }}
|
||||
/>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.8125rem',
|
||||
lineHeight: 1.45,
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
}}
|
||||
>
|
||||
{workload.voice}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Beleg: was zuletzt tatsächlich getan wurde. */}
|
||||
<Box sx={{ mt: 'auto', pt: 1, borderTop: '1px solid rgba(255,255,255,0.10)' }}>
|
||||
{presence.lastAction ? (
|
||||
<>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.625rem',
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.8,
|
||||
textTransform: 'uppercase',
|
||||
color: 'rgba(255,255,255,0.38)',
|
||||
}}
|
||||
>
|
||||
Zuletzt · {presence.lastAction.when}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
fontSize: '0.75rem',
|
||||
lineHeight: 1.4,
|
||||
color: 'rgba(255,255,255,0.62)',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{presence.lastAction.title}
|
||||
</Typography>
|
||||
<Box
|
||||
className="staff-go"
|
||||
sx={{
|
||||
color: DS_NEUTRAL.gold,
|
||||
opacity: 0,
|
||||
transform: 'translateX(-4px)',
|
||||
transition: 'opacity 0.18s ease, transform 0.18s ease',
|
||||
flexShrink: 0,
|
||||
mt: 0.25,
|
||||
}}
|
||||
>
|
||||
<ArrowRight size={16} />
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
) : (
|
||||
<Typography sx={{ fontSize: '0.75rem', color: 'rgba(255,255,255,0.42)' }}>
|
||||
Noch keine Tätigkeit protokolliert.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useNavigate } from 'react-router'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { StaffBackdrop } from './StaffBackdrop'
|
||||
import { AgentStaffCard } from './AgentStaffCard'
|
||||
import { useAgentPresence } from '../../hooks/useAgentPresence'
|
||||
import { WorkloadTone } from '../../domain/agentWorkload'
|
||||
import { DS_NEUTRAL } from '../../lib/ds'
|
||||
|
||||
/**
|
||||
* Höhe der globalen Kopfzeile plus des Bereichs, der unter der Belegschaft
|
||||
* sichtbar bleiben muss: Titel «Meine Objekte» samt Untertitel und der Balken
|
||||
* «Filter & Übersicht» (Runde 5, §2.3).
|
||||
*
|
||||
* Deshalb wird gerechnet und nicht fest gesetzt: der Bereich bekommt, was die
|
||||
* Viewport-Höhe hergibt, und auf einem 4K-Monitor bleibt derselbe Streifen
|
||||
* unten sichtbar wie auf einem 13-Zoll-Laptop.
|
||||
*/
|
||||
const TOP_BAR_PX = 64
|
||||
const KEEP_VISIBLE_BELOW_PX = 118
|
||||
const RESERVED_PX = TOP_BAR_PX + KEEP_VISIBLE_BELOW_PX
|
||||
|
||||
/** Darunter würde die Belegschaft zur Briefmarke — dann lieber scrollen. */
|
||||
const MIN_SCENE_PX = 420
|
||||
|
||||
/**
|
||||
* Die digitale Belegschaft auf der Startseite (Runde 5, §3).
|
||||
*
|
||||
* Die fünf Mitarbeitenden an ihrem Arbeitsplatz — mit Porträt, Funktion,
|
||||
* Abteilung, Personalnummer, Dienstzustand, einem Satz in der ersten Person
|
||||
* und der zuletzt protokollierten Handlung. Ein Klick auf eine Karte öffnet
|
||||
* den jeweiligen Arbeitsbereich. Die Personalverwaltung selbst liegt im
|
||||
* Menüpunkt «Meine Agenten» und braucht keine eigene Person am Bildschirm.
|
||||
*
|
||||
* Bewusst keine nachgebaute Raumperspektive mehr: eine gezeichnete Kulisse
|
||||
* bleibt neben fotografierten Porträts erkennbar gezeichnet, und ein Raum
|
||||
* allein macht niemanden menschlich. Was menschlich wirkt, ist dasselbe wie im
|
||||
* echten Büro — dass jemand da ist, etwas zu sagen hat und nachweisbar
|
||||
* gearbeitet hat.
|
||||
*/
|
||||
export function AgentStaffOverview() {
|
||||
const navigate = useNavigate()
|
||||
const { data: staff } = useAgentPresence()
|
||||
|
||||
const handleOpen = useCallback((path: string) => navigate(path), [navigate])
|
||||
|
||||
// Ein Durchgang über dieselbe Liste statt zweier Filterläufe (CLAUDE.md §10.4).
|
||||
const { urgent, onDuty } = useMemo(() => {
|
||||
let urgentCount = 0
|
||||
let onDutyCount = 0
|
||||
for (const person of staff) {
|
||||
if (person.workload?.tone === WorkloadTone.URGENT) urgentCount += 1
|
||||
if (person.onDuty) onDutyCount += 1
|
||||
}
|
||||
return { urgent: urgentCount, onDuty: onDutyCount }
|
||||
}, [staff])
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="section"
|
||||
aria-label="Digitale Belegschaft"
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: `max(${MIN_SCENE_PX}px, calc(100vh - ${RESERVED_PX}px))`,
|
||||
'@supports (height: 100dvh)': {
|
||||
height: `max(${MIN_SCENE_PX}px, calc(100dvh - ${RESERVED_PX}px))`,
|
||||
},
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<StaffBackdrop />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
px: { xs: 2.5, lg: 4 },
|
||||
py: { xs: 2.5, lg: 3 },
|
||||
gap: { xs: 2, lg: 2.5 },
|
||||
}}
|
||||
>
|
||||
<StaffHeadline urgent={urgent} onDuty={onDuty} total={staff.length} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
// Flexlayout statt Raster: bei fünf Karten bliebe in einem
|
||||
// Dreierraster unten eine Lücke. So stehen drei oben und zwei
|
||||
// mittig darunter — gleich breit, ohne Loch.
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
alignContent: 'center',
|
||||
gap: { xs: 1.5, lg: 2 },
|
||||
'& > *': {
|
||||
flex: {
|
||||
xs: '1 1 100%',
|
||||
sm: '0 1 calc((100% - 12px) / 2)',
|
||||
lg: '0 1 calc((100% - 32px) / 3)',
|
||||
},
|
||||
},
|
||||
// Auf niedrigen Fenstern darf die Belegschaft scrollen, statt die
|
||||
// Karten bis zur Unlesbarkeit zu stauchen.
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{staff.map(person => (
|
||||
<AgentStaffCard key={person.agentId} presence={person} onOpen={handleOpen} />
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Anriss über der Belegschaft.
|
||||
*
|
||||
* Beantwortet die Frage, mit der jemand diese Seite öffnet: muss ich heute
|
||||
* irgendwo eingreifen? Ohne den Satz müsste man jede Karte einzeln lesen und
|
||||
* selbst zusammenzählen.
|
||||
*/
|
||||
function StaffHeadline({ urgent, onDuty, total }: { urgent: number; onDuty: number; total: number }) {
|
||||
const summary =
|
||||
urgent > 0
|
||||
? `${urgent} ${urgent === 1 ? 'Mitarbeitender braucht' : 'Mitarbeitende brauchen'} heute eine Entscheidung von dir`
|
||||
: 'Alle laufen — heute liegt nichts bei dir'
|
||||
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Box sx={{ width: 7, height: 7, borderRadius: '50%', bgcolor: DS_NEUTRAL.gold }} />
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.6875rem',
|
||||
fontWeight: 700,
|
||||
letterSpacing: 1.4,
|
||||
textTransform: 'uppercase',
|
||||
color: DS_NEUTRAL.gold,
|
||||
}}
|
||||
>
|
||||
{onDuty} von {total} im Dienst
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
fontWeight: 700,
|
||||
fontSize: { xs: '1.25rem', lg: '1.6rem' },
|
||||
lineHeight: 1.2,
|
||||
color: DS_NEUTRAL.white,
|
||||
}}
|
||||
>
|
||||
{summary}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { memo } from 'react'
|
||||
import { Box } from '@mui/material'
|
||||
import { ROOM_BACKDROP } from './roomBackdrop'
|
||||
import { DS_NEUTRAL } from '../../lib/ds'
|
||||
|
||||
/**
|
||||
* Bildhintergrund der Belegschaftsansicht (Runde 5, §3).
|
||||
*
|
||||
* Bewusst eine einzige Bildfläche mit Verlauf — und nicht länger ein aus
|
||||
* Flächen konstruierter Raum. Ein selbstgezeichneter Raum bleibt neben
|
||||
* fotografierten Porträts immer erkennbar gezeichnet; die Bildwirkung muss
|
||||
* deshalb aus dem Foto kommen, nicht aus Geometrie.
|
||||
*
|
||||
* Die Komponente selbst weiss nichts über das Motiv. Sie zeigt, was
|
||||
* `ROOM_BACKDROP` liefert — die Aussicht heute, eine Raumaufnahme später.
|
||||
*/
|
||||
export const StaffBackdrop = memo(function StaffBackdrop() {
|
||||
return (
|
||||
<Box sx={{ position: 'absolute', inset: 0, overflow: 'hidden', bgcolor: DS_NEUTRAL.sidebar }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={ROOM_BACKDROP.src}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
objectPosition: ROOM_BACKDROP.focus,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ position: 'absolute', inset: 0, background: ROOM_BACKDROP.scrim }} />
|
||||
|
||||
</Box>
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
// Digitale Belegschaft auf der Startseite (Runde 5, §3). Barrel-Export (CLAUDE.md §13).
|
||||
|
||||
export { AgentStaffOverview } from './AgentStaffOverview'
|
||||
export { AgentStaffCard } from './AgentStaffCard'
|
||||
export { ROOM_BACKDROP } from './roomBackdrop'
|
||||
@@ -0,0 +1,32 @@
|
||||
import buero from '../../assets/staff/buero.jpg'
|
||||
|
||||
/**
|
||||
* Umgebung der Belegschaftsansicht auf der Startseite (Runde 5, §3).
|
||||
*
|
||||
* Genau eine Stelle bestimmt, welches Bild hinter den Personenkarten liegt.
|
||||
* Ein Austausch ist damit ein Einzeiler; die Herkunft steht in `HERKUNFT.md`
|
||||
* neben der Bilddatei.
|
||||
*
|
||||
* Das Bild ist Umgebung, nicht Motiv: es zeigt ein Büro, damit die digitalen
|
||||
* Mitarbeitenden einen Arbeitsplatz haben statt eines leeren Hintergrunds. Der
|
||||
* Verlauf darüber nimmt ihm den grössten Teil des Kontrasts — sonst
|
||||
* konkurriert jeder Bürostuhl mit den Gesichtern und der Schrift davor.
|
||||
*/
|
||||
export interface RoomBackdrop {
|
||||
src: string
|
||||
/** Bildausschnitt bei `object-fit: cover`. */
|
||||
focus: string
|
||||
/** Verlauf über dem Bild: Lesbarkeit und Übergang zur Objektliste darunter. */
|
||||
scrim: string
|
||||
}
|
||||
|
||||
export const ROOM_BACKDROP: RoomBackdrop = {
|
||||
src: buero,
|
||||
focus: '50% 58%',
|
||||
scrim: [
|
||||
// Grundton: das Büro tritt weit zurück, die Karten treten hervor.
|
||||
'linear-gradient(180deg, rgba(9,14,23,0.84) 0%, rgba(9,14,23,0.76) 52%, rgba(9,14,23,0.92) 100%)',
|
||||
// Warmes Streiflicht von links, damit die Fläche nicht tot wirkt.
|
||||
'radial-gradient(85% 120% at 10% 6%, rgba(184,151,90,0.18) 0%, rgba(184,151,90,0) 58%)',
|
||||
].join(', '),
|
||||
}
|
||||
Reference in New Issue
Block a user