diff --git a/package.json b/package.json index a68fa30e..78956a9f 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@noble/curves": "^1.9.7", "@pomade/core": "^0.3.1", "@poppanator/sveltekit-svg": "^7.0.0", + "@svelte-plugins/datepicker": "^1.0.11", "@sveltejs/adapter-static": "^3.0.10", "@tiptap/core": "^2.27.2", "@tiptap/pm": "^2.27.2", @@ -83,7 +84,6 @@ "@welshman/util": "^0.8.16", "cheerio": "^1.2.0", "compressorjs-next": "^1.1.2", - "date-picker-svelte": "^2.17.0", "dotenv": "^16.6.1", "emoji-picker-element": "^1.28.1", "emoji-picker-element-data": "^1.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 607418d3..5e09364f 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ diff --git a/src/app/components/CalendarEventForm.svelte b/src/app/components/CalendarEventForm.svelte index 03f10341..089161a2 100644 --- a/src/app/components/CalendarEventForm.svelte +++ b/src/app/components/CalendarEventForm.svelte @@ -1,7 +1,7 @@ @@ -194,18 +185,10 @@ {#snippet label()} - Start* +

Date and time*

{/snippet} {#snippet input()} - - {/snippet} -
- - {#snippet label()} - End* - {/snippet} - {#snippet input()} - + {/snippet} diff --git a/src/lib/components/DateTimeInput.svelte b/src/lib/components/DateTimeInput.svelte index 182b766a..e048e168 100644 --- a/src/lib/components/DateTimeInput.svelte +++ b/src/lib/components/DateTimeInput.svelte @@ -1,9 +1,9 @@ -
-
- -
- {#if date} - - {:else} - {/if} -
-
- + +
diff --git a/src/lib/components/DateTimeRangeInput.svelte b/src/lib/components/DateTimeRangeInput.svelte new file mode 100644 index 00000000..c9ef5d49 --- /dev/null +++ b/src/lib/components/DateTimeRangeInput.svelte @@ -0,0 +1,79 @@ + + +
+ + + +
diff --git a/src/lib/components/base.css b/src/lib/components/base.css index 5556530c..e4ebadcc 100644 --- a/src/lib/components/base.css +++ b/src/lib/components/base.css @@ -264,6 +264,91 @@ body.keyboard-open .room__compose { padding: 0.65rem; } +/* @svelte-plugins/datepicker — the library declares its variables on :root, + where chained var() references resolve immediately, and our theme tokens + only exist under .fl; so each color is overridden individually here. The + tripled selector out-specifies the component's scoped .datepicker rule. */ +.datepicker.datepicker.datepicker { + --datepicker-font-family: var(--font-body); + --datepicker-color: var(--content); + --datepicker-border-color: var(--line); + --datepicker-state-active: var(--primary); + --datepicker-state-hover: var(--surface-more); + + --datepicker-container-background: var(--surface); + --datepicker-container-border: var(--border-thin) solid var(--line); + + --datepicker-calendar-header-color: var(--content); + --datepicker-calendar-header-text-color: var(--content); + --datepicker-calendar-header-month-nav-color: var(--content); + --datepicker-calendar-header-month-nav-background-hover: var(--surface-more); + --datepicker-calendar-header-year-nav-color: var(--content); + + --datepicker-calendar-dow-color: var(--content-muted); + --datepicker-calendar-day-color: var(--content); + --datepicker-calendar-day-color-hover: var(--content); + --datepicker-calendar-day-background-hover: var(--surface-more); + --datepicker-calendar-day-color-disabled: var(--content-subtle); + --datepicker-calendar-day-other-color: var(--content-subtle); + --datepicker-calendar-today-border: var(--border-thin) solid var(--line-more); + + /* pale wash for in-range days and the square connectors that join the + endpoint circles; endpoints themselves get full primary + primary-content */ + --fl-dp-wash: color-mix(in srgb, var(--primary) 14%, var(--surface)); + + --datepicker-calendar-range-color: var(--content); + --datepicker-calendar-range-background: var(--fl-dp-wash); + --datepicker-calendar-range-background-disabled: var(--fl-dp-wash); + --datepicker-calendar-range-start-box-shadow: inset -20px 0 0 var(--fl-dp-wash); + --datepicker-calendar-range-end-box-shadow: inset 20px 0 0 var(--fl-dp-wash); + --datepicker-calendar-range-start-box-shadow-selected: inset -20px 0 0 var(--fl-dp-wash); + --datepicker-calendar-range-end-box-shadow-selected: inset 20px 0 0 var(--fl-dp-wash); + --datepicker-calendar-range-start-end-background: var(--primary); + --datepicker-calendar-range-start-end-color: var(--primary-content); + --datepicker-calendar-range-selected-background: var(--primary); + --datepicker-calendar-range-selected-color: var(--primary-content); + --datepicker-calendar-range-included-background: var(--fl-dp-wash); + --datepicker-calendar-range-included-box-shadow: inset 20px 0 0 var(--fl-dp-wash); + --datepicker-calendar-range-included-color: var(--content); + + --datepicker-timepicker-input-border: var(--border-thin) solid var(--line); +} + +/* While only the start of a range is picked, the cell doesn't yet have the + .range class, so the library's generic day-hover rule (square, gray) paints + over the selected circle. Keep the selected look on hover instead. The :not() + chain exists to out-specify that hover rule, which carries five of its own. */ +.datepicker.datepicker.datepicker + .calendar + .date.start:not(.range):not(.other):not(.past):not(.future):not(.disabled) + span:hover, +.datepicker.datepicker.datepicker + .calendar + .date.end:not(.range):not(.other):not(.past):not(.future):not(.disabled) + span:hover { + background-color: var(--primary); + color: var(--primary-content); + border-radius: var(--datepicker-calendar-range-selected-border-radius); +} + +/* Hovering the picked start makes the cell both ends of a zero-width preview + range; the library's !important sibling rules still paint a square connector + shadow behind the circle, so remove it (their !important, out-specified) */ +.datepicker.datepicker.datepicker .calendars-container .calendar .date.rangehover.start.end { + box-shadow: none !important; +} + +/* A freshly picked start with no end and no hover preview carries the same + classes as the start of a real range, so the library paints its half-cell + connector shadow toward a range that doesn't exist yet. The only reliable + distinguisher is whether the next day is in-range. */ +.datepicker.datepicker.datepicker + .calendars-container + .calendar + .date.range.start:not(:has(+ .range, + .rangehover)) { + box-shadow: none; +} + /* link-content (tiptap pill) */ .link-content { @apply bg-surface text-content max-w-full overflow-hidden px-1 text-ellipsis whitespace-nowrap; diff --git a/src/lib/components/controls.css b/src/lib/components/controls.css index ed8251cf..a713baf8 100644 --- a/src/lib/components/controls.css +++ b/src/lib/components/controls.css @@ -68,7 +68,7 @@ } } - .range { + input.range { @apply bg-surface-more w-full cursor-pointer appearance-none rounded-full; height: 0.5rem; diff --git a/src/lib/html.ts b/src/lib/html.ts index 65fdaf25..398f70cd 100644 --- a/src/lib/html.ts +++ b/src/lib/html.ts @@ -1,6 +1,50 @@ import {sleep, randomId} from "@welshman/lib" export {preventDefault, stopPropagation} from "svelte/legacy" +// Anchors an @svelte-plugins/datepicker popup with fixed positioning so it +// escapes scroll-container clipping (e.g. inside modals). Call when the picker +// opens; returns a cleanup function that removes the listeners. +export const anchorDatepicker = (wrapper: HTMLElement) => { + const reposition = () => { + const anchor = wrapper.querySelector("label") + const container = wrapper.querySelector(".calendars-container") as HTMLElement | null + + if (!anchor || !container) return + + const margin = 8 + const rect = anchor.getBoundingClientRect() + const height = container.offsetHeight + const width = container.offsetWidth + + let top = rect.bottom + 4 + + // flip above the input when there's not enough room below, else pin to the bottom edge + if (height && top + height > innerHeight - margin) { + top = + rect.top - height - 4 >= margin + ? rect.top - height - 4 + : Math.max(margin, innerHeight - height - margin) + } + + const left = width + ? Math.max(margin, Math.min(rect.left, innerWidth - width - margin)) + : rect.left + + wrapper.style.setProperty("--datepicker-container-position", "fixed") + wrapper.style.setProperty("--datepicker-container-top", `${top}px`) + wrapper.style.setProperty("--datepicker-container-left", `${left}px`) + } + + reposition() + addEventListener("scroll", reposition, true) + addEventListener("resize", reposition) + + return () => { + removeEventListener("scroll", reposition, true) + removeEventListener("resize", reposition) + } +} + export const copyToClipboard = (text: string) => { const {activeElement} = document const input = document.createElement("textarea")