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 -
- - - - - - -
-
- {{-- Calendar --}} -
-
- + {{-- Title --}} +
+
+
+
+ + +
+
+
-
- + {{-- Calendar --}} +
+
+ +
+
+
+
+ - + - -
-
-
- - {{-- 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 --}} -
-
- +
+
+
- {{-- recurrence --}} -