feat(calendar): Wiederholungen, Tagesansicht, Terminfarben, Farbpunkt als Schalter

Fünf Wünsche aus dem Durchtesten:

1. Wiederholungen einstellbar (täglich/wöchentlich/monatlich/jährlich,
   alle N, Ende nie/nach Anzahl/an einem Datum). Der eigentliche Aufwand
   lag nicht im Formular, sondern im Aufklappen: eine wöchentliche Reihe
   erschien sonst als *ein* Eintrag. Der Server klappt sie jetzt im
   angefragten Zeitfenster auf (?from=&to=, rrule-go über go-icals
   RecurrenceSet), die Ansichten fragen ihren sichtbaren Ausschnitt an.
   Geprüft: 5 Termine wöchentlich, nach Umstellung auf 14-tägig korrekt
   neu gerechnet, Fenster einer Woche liefert genau einen.

   Regeln, die das Formular nicht abbildet (etwa jeder zweite Dienstag
   im Monat), werden weiterhin korrekt *angezeigt* -- geprüft, die vier
   Termine stimmen -- aber nicht zum Bearbeiten angeboten (409). Ein
   halb verstandenes Speichern würde die Regel sonst still beschneiden.

2. Tag wird erst geöffnet, statt sofort einen Termin anzulegen: Klick
   auf einen Tag zeigt dessen Termine als Liste, dort wählt man einen
   aus oder legt bewusst einen neuen an.

3. Eigene Farbe je Termin (COLOR nach RFC 7986), leer = Kalenderfarbe.

4. Kalenderliste ohne Kästchen: der Farbpunkt selbst ist der Schalter --
   gefüllt heißt sichtbar, leerer Ring ausgeblendet.

5. Ganztägig als Schiebeschalter statt Kästchen -- es stellt das
   Formular um, das ist keine Nebenangabe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
marcandClaude Opus 5 committed 2026-09-04 00:56:28 +02:00
1 parent 105ce8de4a
commit 79a7cc6a3d
8 files changed
+795 -218

No files matched your search

@@ -0,0 +1,95 @@
import type { CSSProperties } from 'react'
/** Eine Palette für Kalender **und** Termine – zwei getrennte Listen liefen
* sonst mit der Zeit auseinander, und ein Termin in einer Farbe, die es als
* Kalenderfarbe nicht gibt, sieht aus wie ein Fehler. */
export const palette = [
'#4285f4', '#ea4335', '#fbbc04', '#34a853',
'#a142f4', '#ff6d00', '#00acc1', '#607d8b',
]
/**
* Schiebeschalter statt Kästchen.
*
* Nicht nur Zierde: „Ganztägig" schaltet das Formular um (die Uhrzeitfelder
* verschwinden). Ein Kästchen liest sich wie eine Angabe, die man nebenbei
* ankreuzt – ein Schalter zeigt, dass hier etwas umgestellt wird.
*/
export function Toggle({ checked, onChange, disabled, label }: {
checked: boolean
onChange: (v: boolean) => void
disabled?: boolean
label: string
}) {
return (
<label style={{
display: 'flex', alignItems: 'center', gap: 10,
cursor: disabled ? 'default' : 'pointer', opacity: disabled ? 0.6 : 1,
fontSize: 13, color: 'var(--color-text)',
}}>
<button
type="button"
role="switch"
aria-checked={checked}
aria-label={label}
disabled={disabled}
onClick={() => !disabled && onChange(!checked)}
style={{
width: 38, height: 22, borderRadius: 11, flexShrink: 0, padding: 0,
border: '1px solid var(--color-border)',
background: checked ? 'var(--color-primary)' : 'var(--color-surface-2)',
cursor: disabled ? 'default' : 'pointer',
transition: 'background 0.15s',
position: 'relative',
}}
>
<span style={{
position: 'absolute', top: 2, left: checked ? 18 : 2,
width: 16, height: 16, borderRadius: '50%',
background: checked ? '#fff' : 'var(--color-text-muted)',
transition: 'left 0.15s',
}} />
</button>
{label}
</label>
)
}
/**
* Farbwahl als Kreise. Der gewählte Kreis ist gefüllt, die anderen sind
* Umrisse – dieselbe Sprache wie in der Kalenderliste, wo ein gefüllter
* Punkt „wird angezeigt" heißt.
*/
export function ColorSwatches({ value, onChange, inherit, disabled }: {
value: string
onChange: (c: string) => void
/** Ist sie gesetzt, gibt es zusätzlich die Wahl „Farbe des Kalenders"
* (leerer Wert) und dieser Kreis zeigt sie an. */
inherit?: string
disabled?: boolean
}) {
const dot = (color: string, selected: boolean, onClick: () => void, key: string, title: string) => (
<button
key={key} type="button" title={title} aria-label={title} disabled={disabled}
onClick={onClick}
style={{
width: 26, height: 26, borderRadius: '50%', padding: 0, flexShrink: 0,
background: selected ? color : 'transparent',
border: `2px solid ${color}`,
cursor: disabled ? 'default' : 'pointer',
boxShadow: selected ? `0 0 0 2px var(--color-surface), 0 0 0 3.5px ${color}` : 'none',
transition: 'background 0.12s, box-shadow 0.12s',
} as CSSProperties}
/>
)
return (
<div style={{ display: 'flex', gap: 9, flexWrap: 'wrap', alignItems: 'center' }}>
{inherit !== undefined && dot(inherit, value === '', () => onChange(''), 'inherit', 'Farbe des Kalenders')}
{inherit !== undefined && (
<span style={{ width: 1, height: 20, background: 'var(--color-border)', margin: '0 2px' }} />
)}
{palette.map(c => dot(c, value.toLowerCase() === c.toLowerCase(), () => onChange(c), c, c))}
</div>
)
}