diff --git a/resources/css/etc/theme.css b/resources/css/etc/theme.css index 8c84679..0f0a5d0 100644 --- a/resources/css/etc/theme.css +++ b/resources/css/etc/theme.css @@ -74,6 +74,8 @@ --outline-width-md: 1.5px; + --background-image-scrollbar: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1)); + --radius-xs: 0.25rem; --radius-sm: 0.375rem; --radius-md: 0.6667rem; diff --git a/resources/css/lib/accordion.css b/resources/css/lib/accordion.css index ce1a5ab..c2c16f9 100644 --- a/resources/css/lib/accordion.css +++ b/resources/css/lib/accordion.css @@ -30,6 +30,17 @@ details { } } + /* simple variant */ + &.details--simple + { + summary { + &::before, + &::after { + content: none; + } + } + } + &[open] { summary::after { @apply rotate-180; diff --git a/resources/css/lib/event.css b/resources/css/lib/event.css index d989530..383c3e2 100644 --- a/resources/css/lib/event.css +++ b/resources/css/lib/event.css @@ -4,3 +4,17 @@ background-image: var(--event-map); width: calc(100% + 3rem); } + +/* event form fields */ +#event-form { + @apply flex flex-col gap-4 pt-6; + + .event-field { + display: grid; + grid-template-columns: 3rem auto; + + .event-field-icon { + @apply pt-2; + } + } +} diff --git a/resources/css/lib/input.css b/resources/css/lib/input.css index c966bbc..b2833bb 100644 --- a/resources/css/lib/input.css +++ b/resources/css/lib/input.css @@ -178,6 +178,13 @@ article.settings { h3 + .input-row { @apply mt-6; } +.input-rows { + @apply flex flex-col gap-3; + + .input-row + .input-row { + @apply mt-0; + } +} @container style(--can-scroll: 1) { .input-row--actions { @apply !border-black; diff --git a/resources/css/lib/modal.css b/resources/css/lib/modal.css index 8012ec8..4f8c7ad 100644 --- a/resources/css/lib/modal.css +++ b/resources/css/lib/modal.css @@ -22,7 +22,7 @@ dialog { max-height: calc(100dvh - 5rem); width: 91.666667%; max-width: 36rem; - transition: translate 150ms cubic-bezier(0,0,.2,1); + transition: translate 150ms ease-in-out; box-shadow: 0 1.5rem 4rem -0.5rem rgba(0, 0, 0, 0.4); /* close button */ @@ -54,11 +54,17 @@ dialog { /* main content wrapper */ section.modal-body { - @apply flex flex-col px-6 pb-8; + @apply flex flex-col px-6 pb-8 overflow-y-auto; &.no-margin { @apply p-0; } + + /* overlay gradient to signal scrollability */ + &::after { + @apply h-8 w-full absolute bottom-20 left-0 bg-scrollbar; + content: ''; + } } /* standard form with 1rem gap between rows */ @@ -73,7 +79,7 @@ dialog { /* footer */ footer { - @apply sticky bottom-0 bg-white px-6 py-4 border-t-md border-gray-300 flex justify-between; + @apply sticky bottom-0 bg-white px-6 h-20 border-t-md border-gray-300 flex items-center justify-between; } /* event modal with a map */ @@ -139,9 +145,10 @@ dialog { /* extra container over the backdrop below the modal */ #modal-aside { - @apply w-11/12 max-w-3xl justify-self-center px-4 h-0; - @apply flex flex-row justify-between items-start gap-2 translate-y-4; - @apply opacity-0 invisible; + @apply justify-self-center pl-4 h-0 flex flex-row justify-between items-start gap-2; + @apply translate-y-4 opacity-0 invisible; + width: 91.666667%; + max-width: 36rem; pointer-events: none; transition: translate 250ms ease-in-out, diff --git a/resources/js/modules/event-modal.js b/resources/js/modules/event-modal.js index 8c11d25..eb9208d 100644 --- a/resources/js/modules/event-modal.js +++ b/resources/js/modules/event-modal.js @@ -464,6 +464,28 @@ function initNaturalEventParser(root = document) { return null; }; + const isAtTimeCandidate = (candidate) => { + const normalized = String(candidate || '') + .trim() + .toLowerCase() + .replace(/\./g, ''); + + if (normalized === '') return false; + if (normalized === 'noon' || normalized === 'midnight') return true; + + // 1-2 digit + a|p|am|pm (with/without space), e.g. 3p, 3 p, 3pm, 3 pm + if (/^\d{1,2}\s*(a|p|am|pm)$/.test(normalized)) { + return true; + } + + // h:mm / hh:mm with optional a|p|am|pm, or plain 24-hour format (e.g. 15:30) + if (/^\d{1,2}:\d{2}(?:\s*(a|p|am|pm))?$/.test(normalized)) { + return true; + } + + return false; + }; + const parseTimeToken = (text) => { const lowered = text.toLowerCase(); const noonIndex = lowered.indexOf(' at noon'); @@ -476,18 +498,20 @@ function initNaturalEventParser(root = document) { return { hours: 0, minutes: 0, index: midnightIndex }; } - const match = text.match(/\bat\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i); + const match = text.match(/\bat\s+(\d{1,2})(?::(\d{2}))?\s*(am|pm|a|p)?\b/i); if (!match) return null; let hours = parseInt(match[1], 10); const minutes = parseInt(match[2] || '0', 10); - const meridiem = (match[3] || '').toLowerCase(); + const meridiem = (match[3] || '').toLowerCase().replace(/\./g, ''); if (Number.isNaN(hours) || Number.isNaN(minutes)) return null; + if (minutes < 0 || minutes > 59) return null; if (meridiem) { - if (hours === 12 && meridiem === 'am') hours = 0; - if (hours < 12 && meridiem === 'pm') hours += 12; + if (hours < 1 || hours > 12) return null; + if (hours === 12 && meridiem.startsWith('a')) hours = 0; + if (hours < 12 && meridiem.startsWith('p')) hours += 12; } else if (hours > 23) { return null; } @@ -646,9 +670,26 @@ function initNaturalEventParser(root = document) { const index = lower.lastIndexOf(' at '); if (index === -1) return null; - const candidate = text.slice(index + 4).trim().replace(/[.,;]+$/, ''); + let candidate = text.slice(index + 4).trim().replace(/[.,;]+$/, ''); if (!candidate) return null; - if (/^\d{1,2}(?::\d{2})?\s*(am|pm)?\b/i.test(candidate)) return null; + + // If the phrase ends with a dangling "at", trim it first. + // Example: "... at 11am at" -> candidate "11am" + candidate = candidate.replace(/\s+at\s*$/i, '').trim(); + if (!candidate) return null; + + // If the extracted candidate starts with a time token, strip it. + // Example: "11am at Pediatric Alliance" -> "Pediatric Alliance" + const leadingTimeMatch = candidate.match(/^(?:\d{1,2}(?::\d{2})?\s*(?:a|p|am|pm)|\d{1,2}:\d{2}|noon|midnight)\b/i); + if (leadingTimeMatch) { + candidate = candidate + .slice(leadingTimeMatch[0].length) + .replace(/^\s*at\s*/i, '') + .trim(); + } + + if (!candidate) return null; + if (isAtTimeCandidate(candidate)) return null; if (/\bfor\s+\d+\s*(minutes?|mins?|hours?|hrs?)\b/i.test(candidate)) return null; return candidate; @@ -857,8 +898,15 @@ function initNaturalEventParser(root = document) { titleInput.value = parsed.title; } - if (parsed.location && locationInput) { - locationInput.value = parsed.location; + if (locationInput) { + if (parsed.location) { + locationInput.value = parsed.location; + input.dataset.nlLocationApplied = '1'; + } else if (input.dataset.nlLocationApplied === '1') { + // Clear stale parser-written values (e.g. interim "at 3" while typing "at 3pm") + locationInput.value = ''; + input.dataset.nlLocationApplied = '0'; + } } const existingStart = startInput.type === 'date' @@ -1395,4 +1443,3 @@ export function handleEventModalAfterSwap(target) { target.closest('dialog')?.showModal(); } } - diff --git a/resources/svg/icons/calendar-clock.svg b/resources/svg/icons/calendar-clock.svg new file mode 100644 index 0000000..fad48cc --- /dev/null +++ b/resources/svg/icons/calendar-clock.svg @@ -0,0 +1 @@ + diff --git a/resources/svg/icons/description.svg b/resources/svg/icons/description.svg new file mode 100644 index 0000000..d7d7762 --- /dev/null +++ b/resources/svg/icons/description.svg @@ -0,0 +1 @@ + diff --git a/resources/views/event/partials/form-modal.blade.php b/resources/views/event/partials/form-modal.blade.php index cdb16df..66ab9ca 100644 --- a/resources/views/event/partials/form-modal.blade.php +++ b/resources/views/event/partials/form-modal.blade.php @@ -1,4 +1,4 @@ - + @if ($event->exists) {{ __('Edit event details') }} @@ -14,7 +14,7 @@ /> @endif - + @include('event.partials.form', [ 'calendar' => $calendar, 'event' => $event, diff --git a/resources/views/event/partials/form.blade.php b/resources/views/event/partials/form.blade.php index 5d9d519..80cfab2 100644 --- a/resources/views/event/partials/form.blade.php +++ b/resources/views/event/partials/form.blade.php @@ -11,417 +11,407 @@ @method('PUT') @endif - - - - - - Details - - - - - - Repeat - - - - - - Guests - - - - - - Description - - - - - - {{-- Calendar --}} - - - + {{-- Title --}} + + + + + + + + + - - + {{-- Calendar --}} + + + + + + + + - - - - {{ $selectedCalendarName ?? __('common.calendar') }} - - - + + + + {{ $selectedCalendarName ?? __('common.calendar') }} + + + - - - @foreach (($calendarPickerOptions ?? []) as $option) - - - - {{ $option['name'] }} - - - @endforeach - - - - - - - {{-- Title --}} - - - - - - - - - {{-- Description --}} - - - - {{ old('description', $event->meta?->description ?? '') }} - - - - - {{-- Location --}} - - - - - - - {{-- suggestion dropdown target --}} - - {{-- hidden fields (filled when user clicks a suggestion; handy for step #2) --}} - - - - - - - - - - - - - {{-- Start / End --}} - - - - - - - - - - - - - - {{-- All-day --}} - - - + + + @foreach (($calendarPickerOptions ?? []) as $option) + + + + {{ $option['name'] }} + + + @endforeach + + + - {{-- recurrence --}} - - - - - - + + + + + + + + + {{-- suggestion dropdown target --}} + + {{-- hidden fields (filled when user clicks a suggestion; handy for step #2) --}} + + + + + + + + + + + + + + {{-- Start / End --}} + + + + + + + + + + + + + + + + + + + + + + + + {{-- recurrence --}} + + + + + + + + + + + + + + + + + + + {{ __('calendar.event.recurrence.on_days') }} + + @foreach ($weekdayOptions as $code => $label) + + + {{ $label }} + + @endforeach + + + + + + + - + + - - - - + @for ($day = 1; $day <= 31; $day++) + + - - - + {{ $day }} + + @endfor + - - {{ __('calendar.event.recurrence.on_days') }} - - @foreach ($weekdayOptions as $code => $label) - - - {{ $label }} - - @endforeach - - - - - - - - - - - - @for ($day = 1; $day <= 31; $day++) - - - {{ $day }} - - @endfor - - - - - - - - - - - {{ __('calendar.event.recurrence.yearly_hint') }} - + + + - - - {{-- attendees --}} - - - - - - - - - {{ __('calendar.event.attendees.help') }} - - - - {{ __('calendar.event.attendees.add_button') }} - - - - - - - @foreach (($attendees ?? []) as $index => $attendee) - - - - - - - - - - - - - {{ ($attendee['name'] ?? '') !== '' ? ($attendee['name'] . ' <' . ($attendee['email'] ?? '') . '>') : ($attendee['email'] ?? '') }} - - - {{ __('calendar.event.attendees.verified') }} - - - - - {{ __('calendar.event.attendees.remove') }} - - - - - - - - {{ __('calendar.event.attendees.optional') }} - - - - - - {{ __('calendar.event.attendees.rsvp') }} - - - - @endforeach - - - - - - - - - - - - - - - - - {{ __('calendar.event.attendees.verified') }} - - - - - {{ __('calendar.event.attendees.remove') }} - - - - - - - - {{ __('calendar.event.attendees.optional') }} - - - - - - {{ __('calendar.event.attendees.rsvp') }} - - - - - - + + {{ __('calendar.event.recurrence.yearly_hint') }} + + + {{-- attendees --}} + + + + + + + + + + + + {{ __('calendar.event.attendees.help') }} + + + + {{ __('calendar.event.attendees.add_button') }} + + + + + + + @foreach (($attendees ?? []) as $index => $attendee) + + + + + + + + + + + + + {{ ($attendee['name'] ?? '') !== '' ? ($attendee['name'] . ' <' . ($attendee['email'] ?? '') . '>') : ($attendee['email'] ?? '') }} + + + {{ __('calendar.event.attendees.verified') }} + + + + + {{ __('calendar.event.attendees.remove') }} + + + + + + + + {{ __('calendar.event.attendees.optional') }} + + + + + + {{ __('calendar.event.attendees.rsvp') }} + + + + @endforeach + + + + + + + + + + + + + + + + + {{ __('calendar.event.attendees.verified') }} + + + + + {{ __('calendar.event.attendees.remove') }} + + + + + + + + {{ __('calendar.event.attendees.optional') }} + + + + + + {{ __('calendar.event.attendees.rsvp') }} + + + + + + + + + + {{-- Description --}} + + + + + + + {{ old('description', $event->meta?->description ?? '') }} + +
{{ __('calendar.event.recurrence.on_days') }}
{{ __('calendar.event.attendees.help') }}