feat(calendar): create events from a quick editor next to the clicked slot

Clicking or dragging over empty time in the week or day view used to
open the full event panel. On wide screens it now draws the new event
at that spot as a "(No title)" block and opens a small editor beside
it with a title field, the picked time, a calendar picker, Save and
"More options". More options carries the title, time and calendar
over into the full editor. Enter saves, Escape or a click elsewhere
discards. Phones keep opening the full editor.

The placeholder for an event being created is now drawn like a saved
event, in the calendar's colour, instead of a dashed outline.
This commit is contained in:
Linus Rath
2026-10-10 22:57:37 +02:00
parent 95d21c49a9
commit d5ce67e167
32 changed files with 419 additions and 46 deletions
+105 -6
View File
@@ -63,10 +63,12 @@ import {
} from "@/lib/recurrence-instances";
import { getClientByLocalAccountId } from "@/stores/client-registry";
import { getEventStartDate } from "@/lib/calendar-utils";
import { displayNow } from "@/lib/timezone";
import { displayNow, getEffectiveTimeZone } from "@/lib/timezone";
import { canCreateEventsIn } from "@/lib/calendar-editability";
import { QuickCreatePopover, type QuickCreateDraft } from "@/components/calendar/quick-create-popover";
import { useTaskStore } from "@/stores/task-store";
import { useContactStore } from "@/stores/contact-store";
import { cn } from "@/lib/utils";
import { cn, generateUUID } from "@/lib/utils";
import type { Calendar, CalendarEvent, CalendarParticipant, CalendarRights, CalendarTask } from "@/lib/jmap/types";
import { ShareCollectionDialog } from "@/components/settings/share-collection-dialog";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
@@ -188,6 +190,10 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
const [detailEvent, setDetailEvent] = useState<CalendarEvent | null>(null);
const [detailAnchorRect, setDetailAnchorRect] = useState<DOMRect | null>(null);
const [pendingPreview, setPendingPreview] = useState<PendingEventPreview | null>(null);
// Quick create (#calendar redesign): an empty slot clicked in the week or
// day view opens a small editor beside a placeholder block.
const [quickDraft, setQuickDraft] = useState<QuickCreateDraft | null>(null);
const [defaultModalTitle, setDefaultModalTitle] = useState<string | undefined>();
const [showTaskModal, setShowTaskModal] = useState(false);
const [editTask, setEditTask] = useState<import("@/lib/jmap/types").CalendarTask | null>(null);
const [mobileReturnToMonth, setMobileReturnToMonth] = useState(false);
@@ -599,6 +605,8 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
}, [jumpTo]);
const openCreateModal = useCallback((date?: Date, endDate?: Date, allDay?: boolean) => {
setQuickDraft(null);
setDefaultModalTitle(undefined);
setEditEvent(null);
const d = date || selectedDate;
setDefaultModalDate(d);
@@ -956,6 +964,83 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
await save(requestedScheduling);
}, [client, editEvent, createEvent, updateEvent, focusCalendarOnEvent, confirmAction, t]);
// Calendars a new event can go into: never a subscription or read-only one (#762).
const writableCalendars = useMemo(
() => calendars.filter((c) => !c.isTasksOnly && canCreateEventsIn(c, isSubscriptionCalendar)),
[calendars, isSubscriptionCalendar],
);
// A click or drag on empty grid space. On wide screens it opens the quick
// editor at that spot; on phones the full editor, as before.
const handleGridCreate = useCallback((start: Date, end?: Date) => {
if (isMobile) {
openCreateModal(start, end);
return;
}
const finalEnd = end ?? new Date(start.getTime() + 60 * 60 * 1000);
const preferred = writableCalendars.find((c) => c.isDefault) ?? writableCalendars[0];
setShowEventModal(false);
setEditEvent(null);
setShowTaskModal(false);
setEditTask(null);
setDetailEvent(null);
setDetailAnchorRect(null);
setQuickDraft((prev) => ({
start,
end: finalEnd,
title: "",
calendarId: prev && writableCalendars.some((c) => c.id === prev.calendarId)
? prev.calendarId
: preferred?.id ?? "",
}));
}, [isMobile, openCreateModal, writableCalendars]);
const closeQuickCreate = useCallback(() => setQuickDraft(null), []);
const handleQuickCreateMoreOptions = useCallback(() => {
if (!quickDraft) return;
const draft = quickDraft;
openCreateModal(draft.start, draft.end);
setDefaultCalendarIdForCreate(draft.calendarId || undefined);
setDefaultModalTitle(draft.title || undefined);
}, [quickDraft, openCreateModal]);
const handleQuickCreateSave = useCallback(async () => {
if (!quickDraft) return;
const { start, end, title, calendarId } = quickDraft;
const minutes = Math.max(15, Math.round((end.getTime() - start.getTime()) / 60000));
const hours = Math.floor(minutes / 60);
const rest = minutes % 60;
const duration = `PT${hours > 0 ? `${hours}H` : ""}${rest > 0 ? `${rest}M` : ""}`;
setQuickDraft(null);
// Grid dates are wall-clock times in the user's zone (#755), the same
// as the full editor's fields, so they are labelled with that zone.
await handleSaveEvent({
uid: generateUUID(),
title: title.trim(),
start: format(start, "yyyy-MM-dd'T'HH:mm:ss"),
duration,
timeZone: getEffectiveTimeZone(),
showWithoutTime: false,
calendarIds: { [calendarId]: true },
status: "confirmed",
freeBusyStatus: "busy",
privacy: "public",
});
}, [quickDraft, handleSaveEvent]);
// While the quick editor is open its placeholder takes the preview slot.
const gridPreview = useMemo<PendingEventPreview | null>(() => {
if (!quickDraft) return pendingPreview;
return {
start: quickDraft.start,
end: quickDraft.end,
title: quickDraft.title.trim() || t("events.no_title"),
allDay: false,
calendarId: quickDraft.calendarId,
};
}, [quickDraft, pendingPreview, t]);
const handleDuplicateEvent = useCallback(async (data: Partial<CalendarEvent>) => {
if (!client) { toast.error(t("notifications.event_error")); return; }
try {
@@ -1502,11 +1587,11 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
onHoverLeave={handleHoverLeave}
onContextMenuEvent={handleContextMenuEvent}
onContextMenuEmpty={handleContextMenuEmpty}
onCreateAtTime={openCreateModal}
onCreateAtTime={handleGridCreate}
firstDayOfWeek={firstDayOfWeek}
timeFormat={timeFormat}
isMobile={isMobile}
pendingPreview={pendingPreview}
pendingPreview={gridPreview}
tasks={calendarTasks}
onToggleTaskComplete={handleToggleTaskComplete}
onSelectTask={openEditTaskModal}
@@ -1525,10 +1610,10 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
onHoverLeave={handleHoverLeave}
onContextMenuEvent={handleContextMenuEvent}
onContextMenuEmpty={handleContextMenuEmpty}
onCreateAtTime={openCreateModal}
onCreateAtTime={handleGridCreate}
timeFormat={timeFormat}
isMobile={isMobile}
pendingPreview={pendingPreview}
pendingPreview={gridPreview}
tasks={calendarTasks}
onToggleTaskComplete={handleToggleTaskComplete}
onSelectTask={openEditTaskModal}
@@ -1804,6 +1889,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
defaultDate={defaultModalDate}
defaultEndDate={defaultModalEndDate}
defaultAllDay={defaultModalAllDay}
defaultTitle={defaultModalTitle}
defaultCalendarId={defaultCalendarIdForCreate}
onSave={handleSaveEvent}
onDelete={handleDeleteEvent}
@@ -1912,6 +1998,18 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
);
})()}
{quickDraft && !isMobile && (
<QuickCreatePopover
draft={quickDraft}
calendars={writableCalendars}
timeFormat={timeFormat}
onChange={setQuickDraft}
onSave={handleQuickCreateSave}
onMoreOptions={handleQuickCreateMoreOptions}
onClose={closeQuickCreate}
/>
)}
{detailEvent && detailAnchorRect && (
<EventDetailPopover
event={detailEvent}
@@ -1940,6 +2038,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
defaultDate={defaultModalDate}
defaultEndDate={defaultModalEndDate}
defaultAllDay={defaultModalAllDay}
defaultTitle={defaultModalTitle}
defaultCalendarId={defaultCalendarIdForCreate}
onSave={handleSaveEvent}
onDelete={handleDeleteEvent}
+10 -6
View File
@@ -19,6 +19,7 @@ import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions";
import { useScrollWindow, getScrollStart, setScrollStart, scrollToStart } from "@/hooks/use-scroll-window";
import { dayKey, type ScrollWindowViewProps } from "@/lib/calendar-scroll-window";
import type { PendingEventPreview } from "./event-modal";
import { readableTextOn } from "@/lib/color-transform";
interface CalendarDayViewProps extends ScrollWindowViewProps {
selectedDate: Date;
@@ -604,21 +605,24 @@ export function CalendarDayView({
if (startMin + durationMin <= hours.startMinutes || startMin >= hours.endMinutes) return null;
const clip = clipToDisplayHours(startMin, startMin + durationMin, hours);
const cal = calendars.find(c => c.id === pendingPreview.calendarId);
const color = cal?.color || "hsl(var(--primary))";
const color = cal?.color || "#3b82f6";
// Drawn like a saved event, so the new event shows where
// it will land while it is being created.
return (
<div
className="absolute left-2 right-2 z-10 rounded-md pointer-events-none border-2 border-dashed overflow-hidden"
data-pending-preview
className="absolute left-0.5 right-0.5 z-30 rounded-xs pointer-events-none overflow-hidden px-1.5 py-0.5 text-xs shadow-md"
style={{
top: ((clip.startMinutes - hours.startMinutes) / 60) * HOUR_HEIGHT,
height: Math.max(24, ((clip.endMinutes - clip.startMinutes) / 60) * HOUR_HEIGHT),
borderColor: color,
backgroundColor: `${color}10`,
backgroundColor: color,
color: readableTextOn(color),
}}
>
<div className="text-[10px] font-medium px-1.5 py-0.5 truncate" style={{ color }}>
<div className="font-medium truncate">
{pendingPreview.title}
</div>
<div className="text-[9px] px-1.5 opacity-70" style={{ color }}>
<div className="text-[10.5px] opacity-85 truncate">
{formatSnapTime(startMin, timeFormat)} – {formatSnapTime(startMin + durationMin, timeFormat)}
</div>
</div>
+10 -6
View File
@@ -22,6 +22,7 @@ import { useTimeGridInteractions } from "@/hooks/use-time-grid-interactions";
import { useScrollWindow, getScrollStart, setScrollStart, scrollToStart } from "@/hooks/use-scroll-window";
import { dayKey, type ScrollWindowViewProps } from "@/lib/calendar-scroll-window";
import type { PendingEventPreview } from "./event-modal";
import { readableTextOn } from "@/lib/color-transform";
interface CalendarWeekViewProps extends ScrollWindowViewProps {
selectedDate: Date;
@@ -738,21 +739,24 @@ export function CalendarWeekView({
if (startMin + durationMin <= hours.startMinutes || startMin >= hours.endMinutes) return null;
const clip = clipToDisplayHours(startMin, startMin + durationMin, hours);
const cal = calendars.find(c => c.id === pendingPreview.calendarId);
const color = cal?.color || "hsl(var(--primary))";
const color = cal?.color || "#3b82f6";
// Drawn like a saved event, so the new event shows where
// it will land while it is being created.
return (
<div
className="absolute left-1 right-1 z-10 rounded-md pointer-events-none border-2 border-dashed overflow-hidden"
data-pending-preview
className="absolute left-0.5 right-0.5 z-30 rounded-xs pointer-events-none overflow-hidden px-1.5 py-0.5 text-xs shadow-md"
style={{
top: ((clip.startMinutes - hours.startMinutes) / 60) * HOUR_HEIGHT,
height: Math.max(20, ((clip.endMinutes - clip.startMinutes) / 60) * HOUR_HEIGHT),
borderColor: color,
backgroundColor: `${color}10`,
backgroundColor: color,
color: readableTextOn(color),
}}
>
<div className="text-[10px] font-medium px-1.5 py-0.5 truncate" style={{ color }}>
<div className="font-medium truncate">
{pendingPreview.title}
</div>
<div className="text-[9px] px-1.5 opacity-70" style={{ color }}>
<div className="text-[10.5px] opacity-85 truncate">
{formatSnapTime(startMin, timeFormat)} – {formatSnapTime(startMin + durationMin, timeFormat)}
</div>
</div>
+4 -1
View File
@@ -49,6 +49,8 @@ interface EventModalProps {
defaultDate?: Date;
defaultEndDate?: Date;
defaultAllDay?: boolean;
/** Title to start a new event with (from the quick editor's "More options"). */
defaultTitle?: string;
defaultCalendarId?: string;
onSave: (data: Partial<CalendarEvent>, sendSchedulingMessages?: boolean) => void | Promise<void>;
onDelete?: (id: string, sendSchedulingMessages?: boolean) => void;
@@ -192,6 +194,7 @@ export function EventModal({
defaultDate,
defaultEndDate,
defaultAllDay,
defaultTitle,
defaultCalendarId,
onSave,
onDelete,
@@ -299,7 +302,7 @@ export function EventModal({
return addHours(getInitialStart(), 1);
};
const [title, setTitle] = useState(event?.title || "");
const [title, setTitle] = useState(event?.title || defaultTitle || "");
const [description, setDescription] = useState(event?.description || "");
const [location, setLocation] = useState(
event?.locations ? Object.values(event.locations)[0]?.name || "" : ""
@@ -0,0 +1,209 @@
"use client";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useTranslations } from "next-intl";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Clock, X } from "@/components/icons";
import type { Calendar } from "@/lib/jmap/types";
import { useDisplayDateFormatter } from "@/hooks/use-display-date-formatter";
export interface QuickCreateDraft {
start: Date;
end: Date;
title: string;
calendarId: string;
}
interface QuickCreatePopoverProps {
draft: QuickCreateDraft;
/** Calendars new events can go into. */
calendars: Calendar[];
timeFormat?: "12h" | "24h";
onChange: (draft: QuickCreateDraft) => void;
onSave: () => void;
onMoreOptions: () => void;
onClose: () => void;
}
const WIDTH = 340;
const GAP = 8;
const MARGIN = 12;
// The views draw the new event's placeholder block with this attribute; the
// popover sits beside it, or under it when neither side has room.
const PLACEHOLDER_SELECTOR = "[data-pending-preview]";
function computePosition(anchor: DOMRect | null, height: number): { top: number; left: number } {
const vw = window.innerWidth;
const vh = window.innerHeight;
if (!anchor) return { top: Math.max(MARGIN, (vh - height) / 2), left: Math.max(MARGIN, (vw - WIDTH) / 2) };
const clampTop = (top: number) => Math.min(Math.max(MARGIN, top), vh - height - MARGIN);
const clampLeft = (left: number) => Math.min(Math.max(MARGIN, left), vw - WIDTH - MARGIN);
if (anchor.right + GAP + WIDTH + MARGIN <= vw) return { top: clampTop(anchor.top), left: anchor.right + GAP };
if (anchor.left - GAP - WIDTH >= MARGIN) return { top: clampTop(anchor.top), left: anchor.left - GAP - WIDTH };
if (anchor.bottom + GAP + height + MARGIN <= vh) return { top: anchor.bottom + GAP, left: clampLeft(anchor.left) };
return { top: Math.max(MARGIN, anchor.top - GAP - height), left: clampLeft(anchor.left) };
}
/**
* Compact editor that opens next to an empty slot clicked in the week or
* day view: a title, the time that was picked, the calendar, and a way into
* the full editor. Enter saves, Escape or a click elsewhere discards.
*/
export function QuickCreatePopover({
draft,
calendars,
timeFormat = "24h",
onChange,
onSave,
onMoreOptions,
onClose,
}: QuickCreatePopoverProps) {
const t = useTranslations("calendar");
const intlFormatter = useDisplayDateFormatter();
const ref = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [position, setPosition] = useState<{ top: number; left: number } | null>(null);
// Re-anchor when the draft moves (a new slot) and once the placeholder
// block has been drawn by the view.
const startKey = draft.start.getTime();
const endKey = draft.end.getTime();
useLayoutEffect(() => {
const place = () => {
const node = ref.current;
if (!node) return;
const anchor = document.querySelector(PLACEHOLDER_SELECTOR)?.getBoundingClientRect() ?? null;
setPosition(computePosition(anchor, node.offsetHeight));
};
place();
const frame = requestAnimationFrame(place);
return () => cancelAnimationFrame(frame);
}, [startKey, endKey]);
// Focus once placed. Chrome sometimes refuses the first focus() right
// after the slot click that opened the popover, so try again briefly.
const placed = position !== null;
useEffect(() => {
if (!placed) return;
let attempts = 0;
let timer: ReturnType<typeof setTimeout> | null = null;
const tryFocus = () => {
const el = inputRef.current;
if (!el) return;
el.focus();
if (document.activeElement !== el && ++attempts < 5) timer = setTimeout(tryFocus, 50);
};
const frame = requestAnimationFrame(tryFocus);
return () => {
cancelAnimationFrame(frame);
if (timer) clearTimeout(timer);
};
}, [startKey, placed]);
useEffect(() => {
const handleMouseDown = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
};
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
onClose();
}
};
const timer = setTimeout(() => document.addEventListener("mousedown", handleMouseDown), 0);
document.addEventListener("keydown", handleKey);
return () => {
clearTimeout(timer);
document.removeEventListener("mousedown", handleMouseDown);
document.removeEventListener("keydown", handleKey);
};
}, [onClose]);
const canSave = draft.title.trim().length > 0 && !!draft.calendarId;
const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
if (canSave) onSave();
}, [canSave, onSave]);
const timeFmt = timeFormat === "12h" ? "h:mm a" : "HH:mm";
const dateLabel = intlFormatter.dateTime(draft.start, { weekday: "long", month: "long", day: "numeric" });
const timeLabel = `${format(draft.start, timeFmt)} – ${format(draft.end, timeFmt)}`;
return createPortal(
<div
ref={ref}
role="dialog"
aria-label={t("quick_create.aria_label")}
className="fixed z-[60] bg-background border border-border rounded-lg shadow-xl"
style={{
width: WIDTH,
top: position?.top ?? -9999,
left: position?.left ?? -9999,
visibility: position ? "visible" : "hidden",
}}
>
<form onSubmit={handleSubmit}>
<div className="flex justify-end px-2 pt-2">
<button
type="button"
onClick={onClose}
className="p-1.5 rounded-md hover:bg-muted transition-colors text-muted-foreground hover:text-foreground"
aria-label={t("form.cancel")}
>
<X className="w-4 h-4" />
</button>
</div>
<div className="px-4 pb-3 space-y-3">
<Input
ref={inputRef}
value={draft.title}
onChange={(e) => onChange({ ...draft, title: e.target.value })}
placeholder={t("quick_create.add_title")}
aria-label={t("form.title")}
maxLength={500}
className="text-base"
/>
<div className="flex items-start gap-2.5 text-sm">
<Clock className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
<div>
<div className="font-medium text-foreground">{dateLabel}</div>
<div className="text-muted-foreground">{timeLabel}</div>
</div>
</div>
{calendars.length > 0 && (
<div className="flex items-center gap-2.5">
<span
className="w-4 h-4 rounded-sm flex-shrink-0"
style={{ backgroundColor: calendars.find((c) => c.id === draft.calendarId)?.color || "#3b82f6" }}
aria-hidden="true"
/>
<select
value={draft.calendarId}
onChange={(e) => onChange({ ...draft, calendarId: e.target.value })}
aria-label={t("form.calendar_select")}
className="flex-1 min-w-0 rounded-md border border-input bg-background px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
>
{calendars.map((c) => (
<option key={c.id} value={c.id}>{c.name}</option>
))}
</select>
</div>
)}
</div>
<div className="flex items-center justify-end gap-2 px-4 py-2.5 border-t border-border">
<Button type="button" variant="ghost" size="sm" className="h-8" onClick={onMoreOptions}>
{t("quick_create.more_options")}
</Button>
<Button type="submit" size="sm" className="h-8" disabled={!canSave}>
{t("form.save")}
</Button>
</div>
</form>
</div>,
document.body,
);
}
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "عنوان الحدث الجديد",
"aria_label": "إنشاء حدث سريع"
"aria_label": "إنشاء حدث سريع",
"add_title": "أضف عنوانًا",
"more_options": "خيارات أخرى"
},
"nav_prev": "السابق",
"nav_next": "التالي",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "Títol del nou esdeveniment",
"aria_label": "Crea un esdeveniment ràpidament"
"aria_label": "Crea un esdeveniment ràpidament",
"add_title": "Afegeix un títol",
"more_options": "Més opcions"
},
"nav_prev": "Anterior",
"nav_next": "Següent",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "Název nové události",
"aria_label": "Rychlé vytvoření události"
"aria_label": "Rychlé vytvoření události",
"add_title": "Přidat název",
"more_options": "Další možnosti"
},
"nav_prev": "Předchozí",
"nav_next": "Další",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "Ny begivenhedstitel",
"aria_label": "Hurtig oprettelse af begivenhed"
"aria_label": "Hurtig oprettelse af begivenhed",
"add_title": "Tilføj titel",
"more_options": "Flere indstillinger"
},
"nav_prev": "Forrige",
"nav_next": "Næste",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "Titel des neuen Termins",
"aria_label": "Schnellerstellung eines Termins"
"aria_label": "Schnellerstellung eines Termins",
"add_title": "Titel hinzufügen",
"more_options": "Weitere Optionen"
},
"nav_prev": "Zurück",
"nav_next": "Weiter",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "New event title",
"aria_label": "Quick create event"
"aria_label": "Quick create event",
"add_title": "Add title",
"more_options": "More options"
},
"nav_prev": "Previous",
"nav_next": "Next",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "Título del nuevo evento",
"aria_label": "Creación rápida de evento"
"aria_label": "Creación rápida de evento",
"add_title": "Añadir título",
"more_options": "Más opciones"
},
"nav_prev": "Anterior",
"nav_next": "Siguiente",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "عنوان رویداد جدید",
"aria_label": "ایجاد سریع رویداد"
"aria_label": "ایجاد سریع رویداد",
"add_title": "افزودن عنوان",
"more_options": "گزینه‌های بیشتر"
},
"nav_prev": "قبلی",
"nav_next": "بعدی",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "Titre du nouvel événement",
"aria_label": "Création rapide d'événement"
"aria_label": "Création rapide d'événement",
"add_title": "Ajouter un titre",
"more_options": "Plus d'options"
},
"nav_prev": "Précédent",
"nav_next": "Suivant",
+3 -1
View File
@@ -3175,7 +3175,9 @@
},
"quick_create": {
"placeholder": "כותרת אירוע חדשה",
"aria_label": "אירוע יצירה מהיר"
"aria_label": "אירוע יצירה מהיר",
"add_title": "הוספת כותרת",
"more_options": "אפשרויות נוספות"
},
"nav_prev": "הקודם",
"nav_next": "הבא",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "Új esemény címe",
"aria_label": "Gyors esemény létrehozása"
"aria_label": "Gyors esemény létrehozása",
"add_title": "Cím hozzáadása",
"more_options": "További beállítások"
},
"nav_prev": "Előző",
"nav_next": "Következő",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "Titolo del nuovo evento",
"aria_label": "Creazione rapida evento"
"aria_label": "Creazione rapida evento",
"add_title": "Aggiungi titolo",
"more_options": "Altre opzioni"
},
"nav_prev": "Precedente",
"nav_next": "Successivo",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "新しい予定のタイトル",
"aria_label": "予定をすばやく作成"
"aria_label": "予定をすばやく作成",
"add_title": "タイトルを追加",
"more_options": "その他のオプション"
},
"nav_prev": "前へ",
"nav_next": "次へ",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "새 일정 제목",
"aria_label": "빠른 일정 만들기"
"aria_label": "빠른 일정 만들기",
"add_title": "제목 추가",
"more_options": "추가 옵션"
},
"nav_prev": "이전",
"nav_next": "다음",
+3 -1
View File
@@ -3250,7 +3250,9 @@
},
"quick_create": {
"placeholder": "Jauna pasākuma nosaukums",
"aria_label": "Ātra pasākuma izveide"
"aria_label": "Ātra pasākuma izveide",
"add_title": "Pievienot nosaukumu",
"more_options": "Vairāk opciju"
},
"nav_prev": "Iepriekšējais",
"nav_next": "Nākamais",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "Шинэ үйл явдлын гарчиг",
"aria_label": "Түргэн үүсгэх арга хэмжээ"
"aria_label": "Түргэн үүсгэх арга хэмжээ",
"add_title": "Гарчиг нэмэх",
"more_options": "Бусад тохиргоо"
},
"nav_prev": "Өмнөх",
"nav_next": "Дараагийн",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "Tittel på ny hendelse",
"aria_label": "Opprett en hendelse raskt"
"aria_label": "Opprett en hendelse raskt",
"add_title": "Legg til tittel",
"more_options": "Flere alternativer"
},
"nav_prev": "Forrige",
"nav_next": "Neste",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "Titel nieuw evenement",
"aria_label": "Snel evenement aanmaken"
"aria_label": "Snel evenement aanmaken",
"add_title": "Titel toevoegen",
"more_options": "Meer opties"
},
"nav_prev": "Vorige",
"nav_next": "Volgende",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "Tytuł nowego wydarzenia",
"aria_label": "Szybkie tworzenie wydarzenia"
"aria_label": "Szybkie tworzenie wydarzenia",
"add_title": "Dodaj tytuł",
"more_options": "Więcej opcji"
},
"nav_prev": "Poprzedni",
"nav_next": "Następny",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "Título do novo evento",
"aria_label": "Criação rápida de evento"
"aria_label": "Criação rápida de evento",
"add_title": "Adicionar título",
"more_options": "Mais opções"
},
"nav_prev": "Anterior",
"nav_next": "Próximo",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "Titlul unui eveniment nou",
"aria_label": "Creare rapidă a unui eveniment"
"aria_label": "Creare rapidă a unui eveniment",
"add_title": "Adaugă titlu",
"more_options": "Mai multe opțiuni"
},
"nav_prev": "Anterior",
"nav_next": "Următorul",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "Название нового события",
"aria_label": "Быстрое создание события"
"aria_label": "Быстрое создание события",
"add_title": "Добавьте название",
"more_options": "Другие параметры"
},
"nav_prev": "Предыдущий",
"nav_next": "Следующий",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "Názov novej udalosti",
"aria_label": "Rýchle vytvorenie udalosti"
"aria_label": "Rýchle vytvorenie udalosti",
"add_title": "Pridať názov",
"more_options": "Ďalšie možnosti"
},
"nav_prev": "Predchádzajúci",
"nav_next": "Ďalší",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "Yeni etkinlik başlığı",
"aria_label": "Hızlı etkinlik oluştur"
"aria_label": "Hızlı etkinlik oluştur",
"add_title": "Başlık ekle",
"more_options": "Diğer seçenekler"
},
"nav_prev": "Önceki",
"nav_next": "Sonraki",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "Нова назва події",
"aria_label": "Швидке створення події"
"aria_label": "Швидке створення події",
"add_title": "Додайте назву",
"more_options": "Більше параметрів"
},
"nav_prev": "Попередній",
"nav_next": "Далі",
+3 -1
View File
@@ -3277,7 +3277,9 @@
},
"quick_create": {
"placeholder": "新活動標題",
"aria_label": "快速建立活動"
"aria_label": "快速建立活動",
"add_title": "新增標題",
"more_options": "更多選項"
},
"nav_prev": "上一項",
"nav_next": "下一項",
+3 -1
View File
@@ -3251,7 +3251,9 @@
},
"quick_create": {
"placeholder": "新活动标题",
"aria_label": "快速创建事件"
"aria_label": "快速创建事件",
"add_title": "添加标题",
"more_options": "更多选项"
},
"nav_prev": "上一项",
"nav_next": "下一项",