diff --git a/.agents/skills/flotilla-model/SKILL.md b/.agents/skills/flotilla-model/SKILL.md
index 9cc58f72..e77319cc 100644
--- a/.agents/skills/flotilla-model/SKILL.md
+++ b/.agents/skills/flotilla-model/SKILL.md
@@ -168,8 +168,8 @@ relay's URL, each call signed with a fresh NIP-98 event. Every method resolves t
| Methods | Called from |
|---|---|
| `supportedMethods`, `listMethodAssignees`, `listBannedPubkeys` (cached) | `SpaceManagement` (`src/app/management.ts`) |
-| `banPubkey`, `unbanPubkey`, `allowPubkey`, `unallowPubkey`, `listBannedPubkeys` | `ProfileDetail`, `SpaceMemberMenu`, `SpaceMemberBannedMenu`, `ReportMenuList`, `allowPubkeys` (`SpaceMembersAdd`, `RoleAddMembers`, `addRoomMembers`) |
-| `banEvent` | `RoomItemMenu`, `EventMenu`, `ReportMenuList`, `RoomJoinItem` (dismissing a join request) |
+| `banPubkey`, `unbanPubkey`, `allowPubkey`, `unallowPubkey`, `listBannedPubkeys` | `ProfileDetail`, `SpaceMemberMenu`, `SpaceMemberBannedMenu`, `ReportMenu`, `allowPubkeys` (`SpaceMembersAdd`, `RoleAddMembers`, `addRoomMembers`) |
+| `banEvent` | `RoomItemMenu`, `EventMenu`, `ReportMenu`, `RoomJoinItem` (dismissing a join request) |
| `createRole`, `editRole`, `deleteRole`, `assignRole`, `unassignRole` | `RoleCreate`, `RoleEdit`, `SpaceRoleMenu`, `SpaceMemberRoles` (via `syncAssignments`), `RoleAddMembers` |
| `listClaims`, `createClaim` | `Access.prepareInvite` |
| `changeRelayName`, `changeRelayDescription`, `changeRelayIcon` | `SpaceEdit` |
diff --git a/.agents/skills/flotilla-model/kinds.md b/.agents/skills/flotilla-model/kinds.md
index 2a57e120..21a39a02 100644
--- a/.agents/skills/flotilla-model/kinds.md
+++ b/.agents/skills/flotilla-model/kinds.md
@@ -16,7 +16,7 @@ and NIP-43 tables in [SKILL.md](SKILL.md). Routes are under `src/routes/`.
| Classifieds | `CLASSIFIED` (30402) | `Classified` | `src/app/classifieds.ts` | `spaces/[relay]/classifieds`, `classifieds/[address]` (`ClassifiedForm`) |
| Goals | `ZAP_GOAL` (9041) | `ZapGoal` | none | `spaces/[relay]/goals`, `goals/[id]` (`GoalCreate`) |
| Polls | `POLL` (1068), `POLL_RESPONSE` (1018) | `Poll`, `PollResponse` | none | `spaces/[relay]/polls`, `polls/[id]` (`PollCreate`, `PollVotes`) |
-| Library | `PINBOARD` (30067), `PIN` (39067) | `Pinboard`, `Pin` | `src/app/pinboards.ts` | `spaces/[relay]/library` (`PinboardEdit`, `PinAdd`); published by any member |
+| Library | `PINBOARD` (30067), `PIN` (39067) | `Pinboard`, `Pin` | `src/app/pinboards.ts` | `spaces/[relay]/library` (`PinboardEdit`, `PinSave`); published by any member |
| Room pins | `ROOM_PINS` (39005), `ROOM_UPDATE_PINS` (9010) | `RoomPins`, `RoomUpdatePins` | `src/app/roomPins.ts` | `RoomItemMenu`, `RoomItemMenuMobile`, `RoomPinnedMessagesAll` |
| Featured content | `APP_DATA` (30078), `d` = `flotilla/featured-content` | `AppData` | `src/app/featured.ts` | `SpaceFeaturedContent`; published as the relay |
| Bot commands (NIP-CD) | `COMMAND` (31992) | `Command` | `src/app/commands.ts` | `RoomCompose`, `ContentCommand` |
@@ -28,8 +28,8 @@ and NIP-43 tables in [SKILL.md](SKILL.md). Routes are under `src/routes/`.
|---|---|---|---|---|
| Reactions | `REACTION` (7) | `Reaction`, through the `Reactions` plugin | `src/app/reactions.ts` | `RoomItem`, `EventReactButtons`, `ContentActions`, `CommentActions`, `ArticleActionBar` |
| Zaps | `ZAP_REQUEST` (9734), `ZAP_RECEIPT` (9735) | `ZapRequest`; receipts checked by `Zappers.validZapReceipts` | `src/app/lightning.ts` (wallets) | `Zap`, `ZapButton`, `GoalSummary`; off on iOS |
-| Reports | `REPORT` (1984) | `Report` | `src/app/actionItems.ts` | `Report`, `ReportMenuList` |
-| Deletes | `DELETE` (5) | `Delete`, through the `Deletes` plugin | none | `EventDeleteConfirm`, `ReportMenuList` |
+| Reports | `REPORT` (1984) | `Report` | `src/app/actionItems.ts` | `Report`, `ReportMenu` |
+| Deletes | `DELETE` (5) | `Delete`, through the `Deletes` plugin | none | `EventDeleteConfirm`, `ReportMenu` |
## Direct messages
diff --git a/.agents/skills/flotilla-views/naming.md b/.agents/skills/flotilla-views/naming.md
index 743661bf..4210ba7d 100644
--- a/.agents/skills/flotilla-views/naming.md
+++ b/.agents/skills/flotilla-views/naming.md
@@ -10,9 +10,9 @@ PascalCase. This is what the qualifiers mean, drawn from the ~320 components the
| `Item` | one row or card in a list | `ClassifiedItem`, `NoteItem`, `PeopleItem`, `ReportItem`, `RoomItem` (a message in a room), `SpaceMenuRoomItem` |
| `Actions` | the footer row under an event: reactions, status, overflow menu | `ContentActions`, `CommentActions`, `EventActions` |
| `Menu` | the contents of a popover, taking an `onClick` that closes it | `EventMenu`, `ChatMenu`, `RoomItemMenu`, `SpaceMemberMenu` |
-| `MenuList` | the popover contents when `*Menu` is the trigger instead | `ProfileMenu` → `ProfileMenuList`, `ReportMenu` → `ReportMenuList` |
| `Mobile` | the same actions as a modal, pushed on tap instead of hovered | `RoomItemMenuMobile`, `ChatMessageMenuMobile`, `SpaceMenuMobile` |
-| `Create` / `Edit` | a modal that publishes a new or edited event | `ClassifiedCreate`, `RoomEdit`, `PinEdit` |
+| `Create` / `Edit` | a modal that publishes a new or edited event | `ClassifiedCreate`, `RoomEdit`, `GoalCreate` |
+| `Save` | one modal that creates or edits, depending on whether it's given the event | `PinSave` |
| `Form` | the fields shared by a Create/Edit pair | `ClassifiedForm`, `RoomForm`, `RoleForm` |
| `Detail` | a modal with everything known about an entity | `ProfileDetail`, `RoomDetail`, `ContentLinkDetail` |
| `Info` | a modal with an event's underlying data | `EventInfo`, `ProfileInfo` |
@@ -22,7 +22,7 @@ PascalCase. This is what the qualifiers mean, drawn from the ~320 components the
| `Bar` | a horizontal strip across the top or bottom of something | `SpaceBar`, `ComposeBar`, `EventActionBar` |
| `Compose` | a message composer | `ChatCompose`, `RoomCompose`, `CommentCompose` |
| `Confirm` | the step that confirms an action or a code | `EventDeleteConfirm`, `LogInOTPConfirm`, `SignUpEmailConfirm` |
-| `Add` | a modal that adds something to a collection | `SpaceAdd`, `PinAdd`, `RoomMembersAdd` |
+| `Add` | a modal that adds something to a collection | `SpaceAdd`, `RoomMembersAdd` |
| `Select` | a picker, as a modal or a bindable input | `PinboardSelect`, `TopicMultiSelect` |
| `Button` | a self-contained trigger | `ZapButton`, `DictationButton`, `RoomItemEmojiButton` |
| `Name`, `Image`, `Icon`, `Link`, `Circle` | one identifier rendered | `RoomName`, `RelayIcon`, `ProfileLink`, `ProfileCircle` |
diff --git a/src/app/components/BoardMenu.svelte b/src/app/components/BoardMenu.svelte
index be90f00c..74784289 100644
--- a/src/app/components/BoardMenu.svelte
+++ b/src/app/components/BoardMenu.svelte
@@ -11,7 +11,7 @@
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
- import PinAdd from "@app/components/PinAdd.svelte"
+ import PinSave from "@app/components/PinSave.svelte"
import PinboardEdit from "@app/components/PinboardEdit.svelte"
import {deletes, relays, user} from "@app/core"
import {shareEvent} from "@app/share"
@@ -30,7 +30,7 @@
const share = () => shareEvent(url, "Shelf", board.event)
- const addLink = () => pushModal(PinAdd, {url, address: board.address()})
+ const addLink = () => pushModal(PinSave, {url, address: board.address()})
const edit = () => pushModal(PinboardEdit, {url, board})
diff --git a/src/app/components/CalendarEventForm.svelte b/src/app/components/CalendarEventForm.svelte
index 8011a0db..3b8d1cb1 100644
--- a/src/app/components/CalendarEventForm.svelte
+++ b/src/app/components/CalendarEventForm.svelte
@@ -14,7 +14,7 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
- import DateTimeRangeInput from "@lib/components/DateTimeRangeInput.svelte"
+ import DateTimeInput from "@lib/components/DateTimeInput.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {publishToSpace} from "@app/rooms"
import {makeEditor} from "@app/editor"
@@ -182,7 +182,7 @@
Date and time*
{/snippet}
{#snippet input()}
-
+
{/snippet}
diff --git a/src/app/components/Chat.svelte b/src/app/components/Chat.svelte
index ee543d2a..0552db36 100644
--- a/src/app/components/Chat.svelte
+++ b/src/app/components/Chat.svelte
@@ -27,12 +27,12 @@
import PageContent from "@lib/components/PageContent.svelte"
import Divider from "@lib/components/Divider.svelte"
import Button from "@lib/components/Button.svelte"
- import ProfileName from "@app/components/ProfileName.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ChatMembers from "@app/components/ChatMembers.svelte"
+ import ChatTitle from "@app/components/ChatTitle.svelte"
import ChatMessage from "@app/components/ChatMessage.svelte"
import ComposeArea, {type ComposeValues} from "@app/components/ComposeArea.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte"
@@ -200,31 +200,16 @@
diff --git a/src/app/components/ChatItem.svelte b/src/app/components/ChatItem.svelte
index 92911c7e..1cad0b4b 100644
--- a/src/app/components/ChatItem.svelte
+++ b/src/app/components/ChatItem.svelte
@@ -6,7 +6,7 @@
import type {TrustedEvent} from "@welshman/util"
import {fade} from "@lib/transition"
import Button from "@lib/components/Button.svelte"
- import ProfileName from "@app/components/ProfileName.svelte"
+ import ChatTitle from "@app/components/ChatTitle.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import {makeChatPath, goToChat} from "@app/routes"
@@ -45,20 +45,14 @@
- {#if others.length === 0}
-
- Note to self
- {:else if others.length === 1}
-
-
- {:else}
+ {#if others.length > 1}
-
-
- and {others.length - 1}
- {others.length > 2 ? "others" : "other"}
-
+ {:else}
+
{/if}
+
+
+
{#if !active && $notifications.has(path)}
diff --git a/src/app/components/ChatTitle.svelte b/src/app/components/ChatTitle.svelte
new file mode 100644
index 00000000..3843e5fd
--- /dev/null
+++ b/src/app/components/ChatTitle.svelte
@@ -0,0 +1,24 @@
+
+
+{#if others.length > 0}
+
+ {#if others.length === 2}
+ and
+ {:else if others.length > 2}
+ and {others.length - 1} others
+ {/if}
+{:else}
+ Note to self
+{/if}
diff --git a/src/app/components/HomeInboxItem.svelte b/src/app/components/HomeInboxItem.svelte
index 8eee9bd3..a1b8a3ec 100644
--- a/src/app/components/HomeInboxItem.svelte
+++ b/src/app/components/HomeInboxItem.svelte
@@ -1,7 +1,9 @@
-{#if conversation.url && conversation.h}
-
-{:else if conversation.url}
-
+{#if url}
+
+ {#snippet icon()}
+
+ {/snippet}
+ {#snippet title()}
+ {#if h}
+
+ {:else}
+ Chat
+ {/if}
+ {/snippet}
+ {#snippet subtitle()}
+
+ {/snippet}
+
{:else}
-
+
{/if}
diff --git a/src/app/components/HomeInboxItemChat.svelte b/src/app/components/HomeInboxItemChat.svelte
index 865be043..82f61cc7 100644
--- a/src/app/components/HomeInboxItemChat.svelte
+++ b/src/app/components/HomeInboxItemChat.svelte
@@ -2,8 +2,8 @@
import {remove, uniq} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import HomeInboxItemCard from "@app/components/HomeInboxItemCard.svelte"
+ import ChatTitle from "@app/components/ChatTitle.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
- import ProfileName from "@app/components/ProfileName.svelte"
import {user} from "@app/core"
type Props = {
@@ -22,14 +22,7 @@
{/snippet}
{#snippet title()}
- {#if others.length > 0}
-
- {#if others.length > 1}
- and {others.length - 1} {others.length > 2 ? "others" : "other"}
- {/if}
- {:else}
- Note to self
- {/if}
+
{/snippet}
{#snippet subtitle()}
direct message
diff --git a/src/app/components/HomeInboxItemRoom.svelte b/src/app/components/HomeInboxItemRoom.svelte
deleted file mode 100644
index d2b03762..00000000
--- a/src/app/components/HomeInboxItemRoom.svelte
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
- {#snippet icon()}
-
- {/snippet}
- {#snippet title()}
-
- {/snippet}
- {#snippet subtitle()}
-
- {/snippet}
-
diff --git a/src/app/components/HomeInboxItemSpaceChat.svelte b/src/app/components/HomeInboxItemSpaceChat.svelte
deleted file mode 100644
index d693b679..00000000
--- a/src/app/components/HomeInboxItemSpaceChat.svelte
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
- {#snippet icon()}
-
- {/snippet}
- {#snippet title()}
- Chat
- {/snippet}
- {#snippet subtitle()}
-
- {/snippet}
-
diff --git a/src/app/components/PinAdd.svelte b/src/app/components/PinAdd.svelte
deleted file mode 100644
index 9473660f..00000000
--- a/src/app/components/PinAdd.svelte
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
diff --git a/src/app/components/PinEdit.svelte b/src/app/components/PinEdit.svelte
deleted file mode 100644
index 241cdaf1..00000000
--- a/src/app/components/PinEdit.svelte
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
diff --git a/src/app/components/PinMenu.svelte b/src/app/components/PinMenu.svelte
index 2f298897..e504c68e 100644
--- a/src/app/components/PinMenu.svelte
+++ b/src/app/components/PinMenu.svelte
@@ -10,7 +10,7 @@
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
- import PinEdit from "@app/components/PinEdit.svelte"
+ import PinSave from "@app/components/PinSave.svelte"
import {deletes, relays, user} from "@app/core"
import {shareEvent} from "@app/share"
import {pushModal} from "@app/modal"
@@ -28,7 +28,7 @@
const share = () => shareEvent(url, "Link", pin.event)
- const edit = () => pushModal(PinEdit, {url, pin})
+ const edit = () => pushModal(PinSave, {url, pin})
const deletePin = async () => {
try {
diff --git a/src/app/components/PinForm.svelte b/src/app/components/PinSave.svelte
similarity index 71%
rename from src/app/components/PinForm.svelte
rename to src/app/components/PinSave.svelte
index 4896b363..bfed52f6 100644
--- a/src/app/components/PinForm.svelte
+++ b/src/app/components/PinSave.svelte
@@ -1,14 +1,8 @@
-
-
diff --git a/src/app/components/ProfileDetail.svelte b/src/app/components/ProfileDetail.svelte
index 2df78d9c..701b7318 100644
--- a/src/app/components/ProfileDetail.svelte
+++ b/src/app/components/ProfileDetail.svelte
@@ -11,6 +11,7 @@
import Icon from "@lib/components/Icon.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import Button from "@lib/components/Button.svelte"
+ import MenuButton from "@lib/components/MenuButton.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
@@ -100,45 +101,44 @@
-
- {#snippet customActions()}
- {#if !isSelf}
+ {#snippet customActions()}
+ {#if !isSelf}
+
+
+
+ {/if}
+ {#if isBanned}
+ {#if $permissions.addMembers}
-
{/if}
- {#if isBanned}
- {#if $permissions.addMembers}
-
-
-
- Restore Membership
-
-
- {/if}
- {:else}
- {#if $permissions.removeMembers}
-
-
-
- Remove Member
-
-
- {/if}
- {#if $permissions.ban}
-
-
-
- Ban User
-
-
- {/if}
+ {:else}
+ {#if $permissions.removeMembers}
+
+
+
+ Remove Member
+
+
{/if}
- {/snippet}
-
+ {#if $permissions.ban}
+
+
+
+ Ban User
+
+
+ {/if}
+ {/if}
+ {/snippet}
+
diff --git a/src/app/components/ProfileMenu.svelte b/src/app/components/ProfileMenu.svelte
index 258be1c6..7518bb59 100644
--- a/src/app/components/ProfileMenu.svelte
+++ b/src/app/components/ProfileMenu.svelte
@@ -1,34 +1,52 @@
-
-
-
-
-
+
diff --git a/src/app/components/ProfileMenuList.svelte b/src/app/components/ProfileMenuList.svelte
deleted file mode 100644
index 7518bb59..00000000
--- a/src/app/components/ProfileMenuList.svelte
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
diff --git a/src/app/components/ProfilePage.svelte b/src/app/components/ProfilePage.svelte
index de92f697..786440f5 100644
--- a/src/app/components/ProfilePage.svelte
+++ b/src/app/components/ProfilePage.svelte
@@ -11,6 +11,7 @@
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
+ import MenuButton from "@lib/components/MenuButton.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import ProfileAbout from "@app/components/ProfileAbout.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
@@ -170,7 +171,7 @@
{/if}
{/if}
diff --git a/src/app/components/ReportItem.svelte b/src/app/components/ReportItem.svelte
index 63de59e1..36f3f92a 100644
--- a/src/app/components/ReportItem.svelte
+++ b/src/app/components/ReportItem.svelte
@@ -6,6 +6,7 @@
import Card from "@lib/components/Card.svelte"
import Cv from "@lib/components/Cv.svelte"
import Button from "@lib/components/Button.svelte"
+ import MenuButton from "@lib/components/MenuButton.svelte"
import Profile from "@app/components/Profile.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
@@ -48,7 +49,7 @@
{/if}
-
+
{#if event.content}
diff --git a/src/app/components/ReportMenu.svelte b/src/app/components/ReportMenu.svelte
index e27555e9..487f7386 100644
--- a/src/app/components/ReportMenu.svelte
+++ b/src/app/components/ReportMenu.svelte
@@ -1,15 +1,131 @@
-
+
diff --git a/src/app/components/ReportMenuList.svelte b/src/app/components/ReportMenuList.svelte
deleted file mode 100644
index 487f7386..00000000
--- a/src/app/components/ReportMenuList.svelte
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-
diff --git a/src/app/components/Share.svelte b/src/app/components/Share.svelte
index 07186611..c98609c7 100644
--- a/src/app/components/Share.svelte
+++ b/src/app/components/Share.svelte
@@ -13,9 +13,9 @@
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
+ import ChatTitle from "@app/components/ChatTitle.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
- import ProfileName from "@app/components/ProfileName.svelte"
import RelayName from "@app/components/RelayName.svelte"
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
import {chatSearch} from "@app/chats"
@@ -95,19 +95,12 @@
onclick={() => toggle(path)}>
{#if others.length > 1}
-
-
- +{others.length - 1}
-
- {:else if others.length === 1}
-
-
-
-
{:else}
-
- Note to self
+
{/if}
+
+
+
{/each}
diff --git a/src/lib/components/DateTimeInput.svelte b/src/lib/components/DateTimeInput.svelte
index 1b7a03ee..ac58f5ba 100644
--- a/src/lib/components/DateTimeInput.svelte
+++ b/src/lib/components/DateTimeInput.svelte
@@ -3,28 +3,16 @@
import {DatePicker} from "@svelte-plugins/datepicker"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
- import {anchorDatepicker} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
type Props = {
value?: number | undefined
+ end?: number | undefined
+ isRange?: boolean
}
- let {value = $bindable()}: Props = $props()
-
- // The picker works in millisecond timestamps, and we expose unix seconds.
- let element: HTMLElement
- let isOpen = $state(false)
- let startDate: number | undefined = $state(value ? value * 1000 : undefined)
- let startDateTime = $state("")
-
- // With no date the library initializes its time input from the epoch.
- onMount(() => {
- if (!startDate) {
- startDateTime = "12:00"
- }
- })
+ let {value = $bindable(), end = $bindable(), isRange = false}: Props = $props()
const toggle = () => {
isOpen = !isOpen
@@ -38,24 +26,93 @@
const fmt = (ts: number) =>
new Date(ts).toLocaleString([], {dateStyle: "medium", timeStyle: "short"})
- const display = $derived(startDate ? fmt(startDate) : "")
+ // Fixed positioning so the popup escapes scroll-container clipping.
+ const anchor = () => {
+ const reposition = () => {
+ const label = element.querySelector("label")
+ const container = element.querySelector(".calendars-container")
+
+ if (label && container) {
+ const margin = 8
+ const rect = label.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
+
+ element.style.setProperty("--datepicker-container-position", "fixed")
+ element.style.setProperty("--datepicker-container-top", `${top}px`)
+ element.style.setProperty("--datepicker-container-left", `${left}px`)
+ }
+ }
+
+ reposition()
+ addEventListener("scroll", reposition, true)
+ addEventListener("resize", reposition)
+
+ return () => {
+ removeEventListener("scroll", reposition, true)
+ removeEventListener("resize", reposition)
+ }
+ }
+
+ // The picker works in millisecond timestamps, and we expose unix seconds.
+ let element: HTMLElement
+ let isOpen = $state(false)
+ let startDate: number | undefined = $state(value ? value * 1000 : undefined)
+ let endDate: number | undefined = $state(end ? end * 1000 : undefined)
+ let startDateTime = $state("")
+ let endDateTime = $state("")
+
+ const display = $derived(
+ startDate ? (endDate ? `${fmt(startDate)} – ${fmt(endDate)}` : fmt(startDate)) : "",
+ )
$effect(() => {
if (isOpen && element) {
- return anchorDatepicker(element)
+ return anchor()
}
})
$effect(() => {
value = startDate ? Math.round(startDate / 1000) : undefined
})
+
+ $effect(() => {
+ end = endDate ? Math.round(endDate / 1000) : undefined
+ })
+
+ // With no date the library initializes its time inputs from the epoch.
+ onMount(() => {
+ if (!startDate) {
+ startDateTime = "12:00"
+ }
+ if (!endDate) {
+ endDateTime = "13:00"
+ }
+ })
@@ -65,10 +122,10 @@
type="text"
readonly
class="grow cursor-pointer"
- placeholder="Select date"
+ placeholder={isRange ? "Select dates" : "Select date"}
value={display}
onclick={toggle} />
- {#if startDate}
+ {#if startDate && !isRange}
diff --git a/src/lib/components/DateTimeRangeInput.svelte b/src/lib/components/DateTimeRangeInput.svelte
deleted file mode 100644
index c316803e..00000000
--- a/src/lib/components/DateTimeRangeInput.svelte
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/lib/html.ts b/src/lib/html.ts
index dcb59281..60b4739e 100644
--- a/src/lib/html.ts
+++ b/src/lib/html.ts
@@ -26,50 +26,6 @@ export const documentActive = readable(
},
)
-// Fixed positioning so an @svelte-plugins/datepicker popup escapes scroll-container clipping.
-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")
diff --git a/src/routes/spaces/[relay]/library/+page.svelte b/src/routes/spaces/[relay]/library/+page.svelte
index abb7025a..85904fdf 100644
--- a/src/routes/spaces/[relay]/library/+page.svelte
+++ b/src/routes/spaces/[relay]/library/+page.svelte
@@ -18,7 +18,7 @@
import Spinner from "@lib/components/Spinner.svelte"
import SpaceBar from "@app/components/SpaceBar.svelte"
import PinItem from "@app/components/PinItem.svelte"
- import PinAdd from "@app/components/PinAdd.svelte"
+ import PinSave from "@app/components/PinSave.svelte"
import PinboardItem from "@app/components/PinboardItem.svelte"
import PinboardEdit from "@app/components/PinboardEdit.svelte"
import {app, events, network, reader} from "@app/core"
@@ -61,7 +61,7 @@
const createBoard = () => pushModal(PinboardEdit, {url})
- const addLink = () => pushModal(PinAdd, {url, address})
+ const addLink = () => pushModal(PinSave, {url, address})
const selectBoard = (board: PinboardReader) =>
goto(makeLibraryPath(url, board.address()), {replaceState: true, noScroll: true})