diff --git a/apps/desktop/src/components/devtool/seed/shared/organization.ts b/apps/desktop/src/components/devtool/seed/shared/organization.ts index 5ce1fa687e..c988176675 100644 --- a/apps/desktop/src/components/devtool/seed/shared/organization.ts +++ b/apps/desktop/src/components/devtool/seed/shared/organization.ts @@ -9,5 +9,6 @@ export const createOrganization = () => ({ data: { user_id: DEFAULT_USER_ID, name: faker.company.name(), + pinned: false, } satisfies Organization, }); diff --git a/apps/desktop/src/components/main/body/advanced-search/index.tsx b/apps/desktop/src/components/main/body/advanced-search/index.tsx index c863a0d440..89cf4aa1e7 100644 --- a/apps/desktop/src/components/main/body/advanced-search/index.tsx +++ b/apps/desktop/src/components/main/body/advanced-search/index.tsx @@ -68,12 +68,12 @@ function SearchView({ tab }: { tab: Extract }) { } else if (type === "human") { openNew({ type: "contacts", - state: { selectedPerson: id, selectedOrganization: null }, + state: { selected: { type: "person", id } }, }); } else if (type === "organization") { openNew({ type: "contacts", - state: { selectedOrganization: id, selectedPerson: null }, + state: { selected: { type: "organization", id } }, }); } }, diff --git a/apps/desktop/src/components/main/body/calendar/calendar-view.tsx b/apps/desktop/src/components/main/body/calendar/calendar-view.tsx index f76d73401b..02c3ac47d7 100644 --- a/apps/desktop/src/components/main/body/calendar/calendar-view.tsx +++ b/apps/desktop/src/components/main/body/calendar/calendar-view.tsx @@ -1,3 +1,4 @@ +import { platform } from "@tauri-apps/plugin-os"; import { addDays, addMonths, @@ -17,13 +18,33 @@ import { } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@hypr/ui/components/ui/accordion"; import { Button } from "@hypr/ui/components/ui/button"; import { ButtonGroup } from "@hypr/ui/components/ui/button-group"; -import { cn } from "@hypr/utils"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@hypr/ui/components/ui/popover"; +import { safeParseDate } from "@hypr/utils"; +import { cn, TZDate } from "@hypr/utils"; -import { DayCell } from "./day-cell"; -import { useCalendarData, useNow, useWeekStartsOn } from "./hooks"; -import { CalendarSidebarContent } from "./sidebar"; +import { useConfigValue } from "../../../../config/use-config"; +import { useEvent, useIgnoredEvents } from "../../../../hooks/tinybase"; +import { usePermission } from "../../../../hooks/usePermissions"; +import * as main from "../../../../store/tinybase/store/main"; +import { getOrCreateSessionForEventId } from "../../../../store/tinybase/store/sessions"; +import { useTabs } from "../../../../store/zustand/tabs"; +import { AppleCalendarSelection } from "../../../settings/calendar/configure/apple/calendar-selection"; +import { SyncProvider } from "../../../settings/calendar/configure/apple/context"; +import { AccessPermissionRow } from "../../../settings/calendar/configure/apple/permission"; +import { PROVIDERS } from "../../../settings/calendar/shared"; +import { EventDisplay } from "../sessions/outer-header/metadata"; const WEEKDAY_HEADERS_SUN = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const WEEKDAY_HEADERS_MON = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; @@ -56,6 +77,104 @@ function useVisibleCols(ref: React.RefObject) { return cols; } +function useTimezone() { + return useConfigValue("timezone") || undefined; +} + +function toTz(date: Date | string, tz?: string): Date { + const d = typeof date === "string" ? new Date(date) : date; + return tz ? new TZDate(d, tz) : d; +} + +function useNow() { + const tz = useTimezone(); + const [now, setNow] = useState(() => toTz(new Date(), tz)); + + useEffect(() => { + const interval = setInterval(() => { + setNow(toTz(new Date(), tz)); + }, 60000); + return () => clearInterval(interval); + }, [tz]); + + return now; +} + +function getSystemWeekStart(): 0 | 1 { + const locale = navigator.language || "en-US"; + try { + const options = new Intl.Locale(locale); + const info = (options as any).getWeekInfo?.() ?? (options as any).weekInfo; + if (info?.firstDay === 1) return 1; + } catch {} + return 0; +} + +function useWeekStartsOn(): 0 | 1 { + const value = useConfigValue("week_start"); + return useMemo(() => { + if (value === "monday") return 1; + if (value === "sunday") return 0; + return getSystemWeekStart(); + }, [value]); +} + +type CalendarData = { + eventIdsByDate: Record; + sessionIdsByDate: Record; +}; + +function useCalendarData(): CalendarData { + const tz = useTimezone(); + + const eventsTable = main.UI.useResultTable( + main.QUERIES.timelineEvents, + main.STORE_ID, + ); + const sessionsTable = main.UI.useResultTable( + main.QUERIES.timelineSessions, + main.STORE_ID, + ); + const { isIgnored } = useIgnoredEvents(); + + return useMemo(() => { + const eventIdsByDate: Record = {}; + const sessionIdsByDate: Record = {}; + + if (eventsTable) { + for (const [eventId, row] of Object.entries(eventsTable)) { + if (!row.title) continue; + const raw = safeParseDate(row.started_at); + if (!raw) continue; + const day = format(toTz(raw, tz), "yyyy-MM-dd"); + if (isIgnored(row.tracking_id_event, row.recurrence_series_id, day)) + continue; + (eventIdsByDate[day] ??= []).push(eventId); + } + + for (const ids of Object.values(eventIdsByDate)) { + ids.sort((a, b) => { + const aAllDay = eventsTable[a]?.is_all_day ? 0 : 1; + const bAllDay = eventsTable[b]?.is_all_day ? 0 : 1; + return aAllDay - bAllDay; + }); + } + } + + if (sessionsTable) { + for (const [sessionId, row] of Object.entries(sessionsTable)) { + if (row.event_json || !row.title) continue; + const raw = safeParseDate(row.created_at); + if (!raw) continue; + const key = format(toTz(raw, tz), "yyyy-MM-dd"); + (sessionIdsByDate[key] ??= []).push(sessionId); + } + } + + return { eventIdsByDate, sessionIdsByDate }; + }, [eventsTable, sessionsTable, tz, isIgnored]); +} + export function CalendarView() { const now = useNow(); const weekStartsOn = useWeekStartsOn(); @@ -233,3 +352,407 @@ export function CalendarView() { ); } + +function useVisibleItemCount( + ref: React.RefObject, + totalItems: number, +) { + const [maxVisible, setMaxVisible] = useState(totalItems); + + useEffect(() => { + const el = ref.current; + if (!el || totalItems === 0) return; + + const compute = () => { + const available = el.clientHeight; + const children = Array.from(el.children) as HTMLElement[]; + if (children.length === 0 || available <= 0) return; + + const chipH = children[0].offsetHeight; + if (chipH === 0) return; + + const gap = parseFloat(getComputedStyle(el).rowGap) || 0; + + const allH = totalItems * chipH + Math.max(0, totalItems - 1) * gap; + if (allH <= available) { + setMaxVisible((prev) => (prev === totalItems ? prev : totalItems)); + return; + } + + const overflowH = chipH; + let count = 0; + let used = 0; + + while (count < totalItems) { + const next = chipH + (count > 0 ? gap : 0); + const remaining = totalItems - count - 1; + const moreSpace = remaining > 0 ? overflowH + gap : 0; + if (used + next + moreSpace > available) break; + used += next; + count++; + } + + const result = Math.max(1, count); + setMaxVisible((prev) => (prev === result ? prev : result)); + }; + + compute(); + const observer = new ResizeObserver(compute); + observer.observe(el); + return () => observer.disconnect(); + }, [ref, totalItems]); + + return maxVisible; +} + +function DayCell({ + day, + isCurrentMonth, + calendarData, +}: { + day: Date; + isCurrentMonth: boolean; + calendarData: CalendarData; +}) { + const dateKey = format(day, "yyyy-MM-dd"); + const eventIds = calendarData.eventIdsByDate[dateKey] ?? []; + const sessionIds = calendarData.sessionIdsByDate[dateKey] ?? []; + + const now = useNow(); + const itemsRef = useRef(null); + const totalItems = eventIds.length + sessionIds.length; + const maxVisible = useVisibleItemCount(itemsRef, totalItems); + const today = format(day, "yyyy-MM-dd") === format(now, "yyyy-MM-dd"); + + const visibleEvents = eventIds.slice(0, maxVisible); + const remainingSlots = Math.max(0, maxVisible - visibleEvents.length); + const visibleSessions = sessionIds.slice(0, remainingSlots); + const shownCount = visibleEvents.length + visibleSessions.length; + const overflow = totalItems - shownCount; + + return ( +
+
+
+ {format(day, "d")} +
+
+
+ {visibleEvents.map((eventId) => ( + + ))} + {visibleSessions.map((sessionId) => ( + + ))} + {overflow > 0 && ( + + + + + e.stopPropagation()} + > +
+ {format(day, "MMM d, yyyy")} +
+
+ {eventIds.map((eventId) => ( + + ))} + {sessionIds.map((sessionId) => ( + + ))} +
+
+
+ )} +
+
+ ); +} + +function useCalendarColor(calendarId: string | null): string | null { + const calendar = main.UI.useRow("calendars", calendarId ?? "", main.STORE_ID); + if (!calendarId) return null; + return calendar?.color ? String(calendar.color) : null; +} + +function EventChip({ eventId }: { eventId: string }) { + const tz = useTimezone(); + const event = main.UI.useResultRow( + main.QUERIES.timelineEvents, + eventId, + main.STORE_ID, + ); + const calendarColor = useCalendarColor( + (event?.calendar_id as string) ?? null, + ); + + if (!event || !event.title) { + return null; + } + + const isAllDay = !!event.is_all_day; + const color = calendarColor ?? "#888"; + + const startedAt = event.started_at + ? format(toTz(event.started_at as string, tz), "h:mm a") + : null; + + return ( + + + {isAllDay ? ( + + ) : ( + + )} + + e.stopPropagation()} + > + + + + ); +} + +function EventPopoverContent({ eventId }: { eventId: string }) { + const event = useEvent(eventId); + const store = main.UI.useStore(main.STORE_ID); + const openNew = useTabs((state) => state.openNew); + const tz = useTimezone(); + + const eventRow = main.UI.useResultRow( + main.QUERIES.timelineEvents, + eventId, + main.STORE_ID, + ); + + const handleOpen = useCallback(() => { + if (!store) return; + const title = (eventRow?.title as string) || "Untitled"; + const sessionId = getOrCreateSessionForEventId(store, eventId, title, tz); + openNew({ type: "sessions", id: sessionId }); + }, [store, eventId, eventRow?.title, openNew, tz]); + + if (!event) { + return null; + } + + return ( +
+ + +
+ ); +} + +function SessionChip({ sessionId }: { sessionId: string }) { + const tz = useTimezone(); + const session = main.UI.useResultRow( + main.QUERIES.timelineSessions, + sessionId, + main.STORE_ID, + ); + + if (!session || !session.title) { + return null; + } + + const createdAt = session.created_at + ? format(toTz(session.created_at as string, tz), "h:mm a") + : null; + + return ( + + + + + e.stopPropagation()} + > + + + + ); +} + +function SessionPopoverContent({ sessionId }: { sessionId: string }) { + const session = main.UI.useResultRow( + main.QUERIES.timelineSessions, + sessionId, + main.STORE_ID, + ); + const openNew = useTabs((state) => state.openNew); + const tz = useTimezone(); + + const handleOpen = useCallback(() => { + openNew({ type: "sessions", id: sessionId }); + }, [openNew, sessionId]); + + if (!session) { + return null; + } + + const createdAt = session.created_at + ? format(toTz(session.created_at as string, tz), "MMM d, yyyy h:mm a") + : null; + + return ( +
+
+ {session.title as string} +
+
+ {createdAt &&
{createdAt}
} + +
+ ); +} + +function CalendarSidebarContent() { + const isMacos = platform() === "macos"; + const calendar = usePermission("calendar"); + + const visibleProviders = PROVIDERS.filter( + (p) => p.platform === "all" || (p.platform === "macos" && isMacos), + ); + + return ( + + {visibleProviders.map((provider) => + provider.disabled ? ( +
+ {provider.icon} + {provider.displayName} + {provider.badge && ( + + {provider.badge} + + )} +
+ ) : ( + + +
+ {provider.icon} + + {provider.displayName} + + {provider.badge && ( + + {provider.badge} + + )} +
+
+ + {provider.id === "apple" && ( +
+ {calendar.status !== "authorized" && ( +
+ +
+ )} + {calendar.status === "authorized" && ( + + + + )} +
+ )} +
+
+ ), + )} +
+ ); +} diff --git a/apps/desktop/src/components/main/body/contacts/contacts-list.tsx b/apps/desktop/src/components/main/body/contacts/contacts-list.tsx new file mode 100644 index 0000000000..fc334b17d0 --- /dev/null +++ b/apps/desktop/src/components/main/body/contacts/contacts-list.tsx @@ -0,0 +1,601 @@ +import { Building2, CornerDownLeft, Pin } from "lucide-react"; +import { Reorder } from "motion/react"; +import React, { useCallback, useMemo, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +import type { ContactsSelection } from "@hypr/plugin-windows"; +import { cn } from "@hypr/utils"; + +import * as main from "../../../../store/tinybase/store/main"; +import { ColumnHeader, getInitials, type SortOption } from "./shared"; + +type ContactItem = + | { kind: "person"; id: string } + | { kind: "organization"; id: string }; + +export function ContactsListColumn({ + selected, + setSelected, +}: { + selected: ContactsSelection | null; + setSelected: (value: ContactsSelection | null) => void; +}) { + const [showNewPerson, setShowNewPerson] = useState(false); + const [searchValue, setSearchValue] = useState(""); + const [sortOption, setSortOption] = useState("alphabetical"); + const [showSearch, setShowSearch] = useState(false); + + useHotkeys( + "mod+f", + () => setShowSearch(true), + { preventDefault: true, enableOnFormTags: true }, + [setShowSearch], + ); + + const allHumans = main.UI.useTable("humans", main.STORE_ID); + const allOrgs = main.UI.useTable("organizations", main.STORE_ID); + const store = main.UI.useStore(main.STORE_ID); + + const alphabeticalHumanIds = main.UI.useResultSortedRowIds( + main.QUERIES.visibleHumans, + "name", + false, + 0, + undefined, + main.STORE_ID, + ); + const reverseAlphabeticalHumanIds = main.UI.useResultSortedRowIds( + main.QUERIES.visibleHumans, + "name", + true, + 0, + undefined, + main.STORE_ID, + ); + const newestHumanIds = main.UI.useResultSortedRowIds( + main.QUERIES.visibleHumans, + "created_at", + true, + 0, + undefined, + main.STORE_ID, + ); + const oldestHumanIds = main.UI.useResultSortedRowIds( + main.QUERIES.visibleHumans, + "created_at", + false, + 0, + undefined, + main.STORE_ID, + ); + + const alphabeticalOrgIds = main.UI.useResultSortedRowIds( + main.QUERIES.visibleOrganizations, + "name", + false, + 0, + undefined, + main.STORE_ID, + ); + const reverseAlphabeticalOrgIds = main.UI.useResultSortedRowIds( + main.QUERIES.visibleOrganizations, + "name", + true, + 0, + undefined, + main.STORE_ID, + ); + const newestOrgIds = main.UI.useResultSortedRowIds( + main.QUERIES.visibleOrganizations, + "created_at", + true, + 0, + undefined, + main.STORE_ID, + ); + const oldestOrgIds = main.UI.useResultSortedRowIds( + main.QUERIES.visibleOrganizations, + "created_at", + false, + 0, + undefined, + main.STORE_ID, + ); + + const sortedHumanIds = + sortOption === "alphabetical" + ? alphabeticalHumanIds + : sortOption === "reverse-alphabetical" + ? reverseAlphabeticalHumanIds + : sortOption === "newest" + ? newestHumanIds + : oldestHumanIds; + + const sortedOrgIds = + sortOption === "alphabetical" + ? alphabeticalOrgIds + : sortOption === "reverse-alphabetical" + ? reverseAlphabeticalOrgIds + : sortOption === "newest" + ? newestOrgIds + : oldestOrgIds; + + const { pinnedHumanIds, unpinnedHumanIds } = useMemo(() => { + const pinned = sortedHumanIds.filter((id) => allHumans[id]?.pinned); + const unpinned = sortedHumanIds.filter((id) => !allHumans[id]?.pinned); + + const sortedPinned = [...pinned].sort((a, b) => { + const orderA = + (allHumans[a]?.pin_order as number | undefined) ?? Infinity; + const orderB = + (allHumans[b]?.pin_order as number | undefined) ?? Infinity; + return orderA - orderB; + }); + + return { pinnedHumanIds: sortedPinned, unpinnedHumanIds: unpinned }; + }, [sortedHumanIds, allHumans]); + + const { pinnedOrgIds, unpinnedOrgIds } = useMemo(() => { + const pinned = sortedOrgIds.filter((id) => allOrgs[id]?.pinned); + const unpinned = sortedOrgIds.filter((id) => !allOrgs[id]?.pinned); + + const sortedPinned = [...pinned].sort((a, b) => { + const orderA = (allOrgs[a]?.pin_order as number | undefined) ?? Infinity; + const orderB = (allOrgs[b]?.pin_order as number | undefined) ?? Infinity; + return orderA - orderB; + }); + + return { pinnedOrgIds: sortedPinned, unpinnedOrgIds: unpinned }; + }, [sortedOrgIds, allOrgs]); + + const { pinnedItems, nonPinnedItems } = useMemo(() => { + const q = searchValue.toLowerCase().trim(); + + const filterHuman = (id: string) => { + if (!q) return true; + const human = allHumans[id]; + const name = (human?.name ?? "").toLowerCase(); + const email = (human?.email ?? "").toLowerCase(); + return name.includes(q) || email.includes(q); + }; + + const filterOrg = (id: string) => { + if (!q) return true; + const name = (allOrgs[id]?.name ?? "").toLowerCase(); + return name.includes(q); + }; + + const allPinned = [ + ...pinnedHumanIds.filter(filterHuman).map((id) => ({ + kind: "person" as const, + id, + pin_order: (allHumans[id]?.pin_order as number | undefined) ?? Infinity, + })), + ...pinnedOrgIds.filter(filterOrg).map((id) => ({ + kind: "organization" as const, + id, + pin_order: (allOrgs[id]?.pin_order as number | undefined) ?? Infinity, + })), + ] + .sort((a, b) => a.pin_order - b.pin_order) + .map(({ kind, id }) => ({ kind, id })); + + const unpinnedOrgs: ContactItem[] = unpinnedOrgIds + .filter(filterOrg) + .map((id) => ({ kind: "organization" as const, id })); + + const unpinnedPeople: ContactItem[] = unpinnedHumanIds + .filter(filterHuman) + .map((id) => ({ kind: "person" as const, id })); + + return { + pinnedItems: allPinned, + nonPinnedItems: [...unpinnedOrgs, ...unpinnedPeople], + }; + }, [ + pinnedHumanIds, + unpinnedHumanIds, + pinnedOrgIds, + unpinnedOrgIds, + allOrgs, + allHumans, + searchValue, + ]); + + const handleReorderPinned = useCallback( + (newOrder: string[]) => { + if (!store) return; + store.transaction(() => { + newOrder.forEach((id, index) => { + const item = pinnedItems.find((i) => i.id === id); + if (item?.kind === "person") { + store.setCell("humans", id, "pin_order", index); + } else if (item?.kind === "organization") { + store.setCell("organizations", id, "pin_order", index); + } + }); + }); + }, + [store, pinnedItems], + ); + + const handleAdd = useCallback(() => { + setShowNewPerson(true); + }, []); + + const isActive = (item: ContactItem) => { + if (!selected) return false; + return selected.type === item.kind && selected.id === item.id; + }; + + return ( +
+ +
+
+ {showNewPerson && ( + { + setShowNewPerson(false); + setSelected({ type: "person", id: humanId }); + }} + onCancel={() => setShowNewPerson(false)} + /> + )} + {pinnedItems.length > 0 && !searchValue.trim() && ( + i.id)} + onReorder={handleReorderPinned} + className="flex flex-col" + > + {pinnedItems.map((item) => ( + + {item.kind === "person" ? ( + + setSelected({ type: "person", id: item.id }) + } + /> + ) : ( + + setSelected({ type: "organization", id: item.id }) + } + /> + )} + + ))} + + )} + {pinnedItems.length > 0 && searchValue.trim() && ( +
+ {pinnedItems.map((item) => + item.kind === "person" ? ( + setSelected({ type: "person", id: item.id })} + /> + ) : ( + + setSelected({ type: "organization", id: item.id }) + } + /> + ), + )} +
+ )} + {pinnedItems.length > 0 && nonPinnedItems.length > 0 && ( +
+ )} + {nonPinnedItems.map((item) => + item.kind === "person" ? ( + setSelected({ type: "person", id: item.id })} + /> + ) : ( + + setSelected({ type: "organization", id: item.id }) + } + /> + ), + )} +
+
+
+ ); +} + +function PersonItem({ + humanId, + active, + onClick, +}: { + humanId: string; + active: boolean; + onClick: () => void; +}) { + const person = main.UI.useRow("humans", humanId, main.STORE_ID); + const isPinned = Boolean(person.pinned); + const personName = String(person.name ?? ""); + const personEmail = String(person.email ?? ""); + + const store = main.UI.useStore(main.STORE_ID); + + const handleTogglePin = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + if (!store) return; + + const currentPinned = store.getCell("humans", humanId, "pinned"); + if (currentPinned) { + store.setPartialRow("humans", humanId, { + pinned: false, + pin_order: 0, + }); + } else { + const allHumans = store.getTable("humans"); + const allOrgs = store.getTable("organizations"); + const maxHumanOrder = Object.values(allHumans).reduce((max, h) => { + const order = (h.pin_order as number | undefined) ?? 0; + return Math.max(max, order); + }, 0); + const maxOrgOrder = Object.values(allOrgs).reduce((max, o) => { + const order = (o.pin_order as number | undefined) ?? 0; + return Math.max(max, order); + }, 0); + store.setPartialRow("humans", humanId, { + pinned: true, + pin_order: Math.max(maxHumanOrder, maxOrgOrder) + 1, + }); + } + }, + [store, humanId], + ); + + return ( +
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick(); + } + }} + className={cn([ + "group w-full text-left px-3 py-2 rounded-md text-sm border hover:bg-neutral-100 transition-colors flex items-center gap-2 bg-white overflow-hidden", + active ? "border-neutral-500 bg-neutral-100" : "border-transparent", + ])} + > +
+ + {getInitials(personName || personEmail)} + +
+
+
+ {personName || personEmail || "Unnamed"} +
+ {personEmail && personName && ( +
{personEmail}
+ )} +
+ +
+ ); +} + +function OrganizationItem({ + organizationId, + active, + onClick, +}: { + organizationId: string; + active: boolean; + onClick: () => void; +}) { + const organization = main.UI.useRow( + "organizations", + organizationId, + main.STORE_ID, + ); + const isPinned = Boolean(organization.pinned); + const store = main.UI.useStore(main.STORE_ID); + + const handleTogglePin = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + if (!store) return; + + const currentPinned = store.getCell( + "organizations", + organizationId, + "pinned", + ); + if (currentPinned) { + store.setPartialRow("organizations", organizationId, { + pinned: false, + pin_order: 0, + }); + } else { + const allOrgs = store.getTable("organizations"); + const allHumans = store.getTable("humans"); + const maxOrgOrder = Object.values(allOrgs).reduce((max, o) => { + const order = (o.pin_order as number | undefined) ?? 0; + return Math.max(max, order); + }, 0); + const maxHumanOrder = Object.values(allHumans).reduce((max, h) => { + const order = (h.pin_order as number | undefined) ?? 0; + return Math.max(max, order); + }, 0); + store.setPartialRow("organizations", organizationId, { + pinned: true, + pin_order: Math.max(maxOrgOrder, maxHumanOrder) + 1, + }); + } + }, + [store, organizationId], + ); + + if (!organization) { + return null; + } + + return ( +
{ + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick(); + } + }} + className={cn([ + "group w-full text-left px-3 py-2 rounded-md text-sm border hover:bg-neutral-100 transition-colors flex items-center gap-2 overflow-hidden", + active ? "border-neutral-500 bg-neutral-100" : "border-transparent", + ])} + > +
+ +
+
+
{organization.name}
+
+ +
+ ); +} + +function NewPersonForm({ + onSave, + onCancel, +}: { + onSave: (humanId: string) => void; + onCancel: () => void; +}) { + const [name, setName] = useState(""); + const userId = main.UI.useValue("user_id", main.STORE_ID); + + const createHuman = main.UI.useSetRowCallback( + "humans", + (p: { name: string; humanId: string }) => p.humanId, + (p: { name: string; humanId: string }) => ({ + user_id: userId || "", + created_at: new Date().toISOString(), + name: p.name, + email: "", + org_id: "", + job_title: "", + linkedin_username: "", + memo: "", + pinned: false, + }), + [userId], + main.STORE_ID, + ); + + const handleAdd = () => { + const humanId = crypto.randomUUID(); + createHuman({ humanId, name: name.trim() }); + setName(""); + onSave(humanId); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (name.trim()) { + handleAdd(); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + if (name.trim()) { + handleAdd(); + } + } + if (e.key === "Escape") { + onCancel(); + } + }; + + return ( +
+
+
+ setName(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Add person" + className="w-full bg-transparent text-sm focus:outline-hidden placeholder:text-neutral-400" + autoFocus + /> + {name.trim() && ( + + )} +
+
+
+ ); +} diff --git a/apps/desktop/src/components/main/body/contacts/details.tsx b/apps/desktop/src/components/main/body/contacts/details.tsx index 18c5f34d0f..a4cd184855 100644 --- a/apps/desktop/src/components/main/body/contacts/details.tsx +++ b/apps/desktop/src/components/main/body/contacts/details.tsx @@ -3,7 +3,7 @@ import { Building2, CircleMinus, FileText, - Pin, + Plus, SearchIcon, } from "lucide-react"; import React, { useCallback, useState } from "react"; @@ -33,8 +33,6 @@ export function DetailsColumn({ selectedHumanId ?? "", main.STORE_ID, ); - const isPinned = selectedPersonData.pinned as boolean | undefined; - const mappingIdsByHuman = main.UI.useSliceRowIds( main.INDEXES.sessionsByHuman, selectedHumanId ?? "", @@ -110,28 +108,6 @@ export function DetailsColumn({ const store = main.UI.useStore(main.STORE_ID); - const handleTogglePin = useCallback(() => { - if (!store || !selectedHumanId) return; - - const currentPinned = store.getCell("humans", selectedHumanId, "pinned"); - if (currentPinned) { - store.setPartialRow("humans", selectedHumanId, { - pinned: false, - pin_order: undefined, - }); - } else { - const allHumans = store.getTable("humans"); - const maxOrder = Object.values(allHumans).reduce((max, h) => { - const order = (h.pin_order as number | undefined) ?? 0; - return Math.max(max, order); - }, 0); - store.setPartialRow("humans", selectedHumanId, { - pinned: true, - pin_order: maxOrder + 1, - }); - } - }, [store, selectedHumanId]); - const handleMergeContacts = useCallback( (duplicateId: string) => { if (!store || !selectedHumanId) return; @@ -209,41 +185,18 @@ export function DetailsColumn({
{selectedPersonData && selectedHumanId ? ( <> -
-
-
- -
-
-
-
- - -
-
-
+
+
+
@@ -300,6 +253,12 @@ export function DetailsColumn({ )}
+
+
Name
+
+ +
+
@@ -432,7 +391,7 @@ function EditablePersonNameField({ personId }: { personId: string }) { value={(value as string) || ""} onChange={handleChange} placeholder="Name" - className="border-none shadow-none p-0 h-8 text-lg font-semibold focus-visible:ring-0 focus-visible:ring-offset-0" + className="border-none shadow-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0" /> ); } @@ -544,7 +503,7 @@ function EditablePersonMemoField({ personId }: { personId: string }) { value={(value as string) || ""} onChange={handleChange} placeholder="Add notes about this contact..." - className="border-none shadow-none p-2 min-h-[80px] text-base focus-visible:ring-0 focus-visible:ring-offset-0 resize-none" + className="border-none shadow-none px-0 py-2 min-h-[80px] text-base focus-visible:ring-0 focus-visible:ring-offset-0 resize-none" rows={3} />
@@ -579,8 +538,8 @@ function EditPersonOrganizationSelector({ personId }: { personId: string }) { return ( -
- {organization ? ( +
+ {organization?.name ? (
{organization.name} @@ -594,8 +553,9 @@ function EditPersonOrganizationSelector({ personId }: { personId: string }) {
) : ( - - Select organization + + + Add organization )}
diff --git a/apps/desktop/src/components/main/body/contacts/index.tsx b/apps/desktop/src/components/main/body/contacts/index.tsx index f7ae11489f..7568e1e713 100644 --- a/apps/desktop/src/components/main/body/contacts/index.tsx +++ b/apps/desktop/src/components/main/body/contacts/index.tsx @@ -2,6 +2,7 @@ import { Contact2Icon } from "lucide-react"; import { useCallback, useEffect } from "react"; import { useShallow } from "zustand/shallow"; +import type { ContactsSelection } from "@hypr/plugin-windows"; import { ResizableHandle, ResizablePanel, @@ -12,10 +13,9 @@ import * as main from "../../../../store/tinybase/store/main"; import { type Tab, useTabs } from "../../../../store/zustand/tabs"; import { StandardTabWrapper } from "../index"; import { type TabItem, TabItemBase } from "../shared"; +import { ContactsListColumn } from "./contacts-list"; import { DetailsColumn } from "./details"; import { OrganizationDetailsColumn } from "./organization-details"; -import { OrganizationsColumn } from "./organizations"; -import { PeopleColumn, useSortedHumanIds } from "./people"; export const TabItemContact: TabItem> = ({ tab, @@ -67,26 +67,11 @@ function ContactView({ tab }: { tab: Extract }) { })), ); - const { selectedOrganization, selectedPerson } = tab.state; + const selected = tab.state.selected; - const setSelectedOrganization = useCallback( - (value: string | null) => { - updateContactsTabState(tab, { - ...tab.state, - selectedOrganization: value, - // Clear selected person when selecting an organization - selectedPerson: value ? null : tab.state.selectedPerson, - }); - }, - [updateContactsTabState, tab], - ); - - const setSelectedPerson = useCallback( - (value: string | null) => { - updateContactsTabState(tab, { - ...tab.state, - selectedPerson: value, - }); + const setSelected = useCallback( + (value: ContactsSelection | null) => { + updateContactsTabState(tab, { selected: value }); }, [updateContactsTabState, tab], ); @@ -108,8 +93,9 @@ function ContactView({ tab }: { tab: Extract }) { (id: string) => { invalidateResource("humans", id); deletePersonFromStore(id); + setSelected(null); }, - [invalidateResource, deletePersonFromStore], + [invalidateResource, deletePersonFromStore, setSelected], ); const deleteOrganizationFromStore = main.UI.useDelRowCallback( @@ -122,52 +108,57 @@ function ContactView({ tab }: { tab: Extract }) { (id: string) => { invalidateResource("organizations" as const, id); deleteOrganizationFromStore(id); + setSelected(null); }, - [invalidateResource, deleteOrganizationFromStore], + [invalidateResource, deleteOrganizationFromStore, setSelected], + ); + + const allHumanIds = main.UI.useResultSortedRowIds( + main.QUERIES.visibleHumans, + "name", + false, + 0, + undefined, + main.STORE_ID, ); - // Get the list of humanIds to auto-select the first person (only when no org is selected) - const { humanIds } = useSortedHumanIds(selectedOrganization); + const allOrgIds = main.UI.useResultSortedRowIds( + main.QUERIES.visibleOrganizations, + "name", + false, + 0, + undefined, + main.STORE_ID, + ); - // Auto-select first person on load if no person is selected and no org is selected useEffect(() => { - if (!selectedOrganization && !selectedPerson && humanIds.length > 0) { - setSelectedPerson(humanIds[0]); + if (!selected) { + if (allHumanIds.length > 0) { + setSelected({ type: "person", id: allHumanIds[0] }); + } else if (allOrgIds.length > 0) { + setSelected({ type: "organization", id: allOrgIds[0] }); + } } - }, [humanIds, selectedPerson, selectedOrganization, setSelectedPerson]); - - const isViewingOrgDetails = selectedOrganization && !selectedPerson; + }, [allHumanIds, allOrgIds, selected, setSelected]); return ( - - - - - - + + - - {selectedOrganization && !selectedPerson ? ( - // Show organization details when org is selected but no person is selected + + {selected?.type === "organization" ? ( + setSelected({ type: "person", id: personId }) + } /> ) : ( - // Show person details when a person is selected or no org is selected diff --git a/apps/desktop/src/components/main/body/contacts/organization-details.tsx b/apps/desktop/src/components/main/body/contacts/organization-details.tsx index 352f763cef..bc80b5d00f 100644 --- a/apps/desktop/src/components/main/body/contacts/organization-details.tsx +++ b/apps/desktop/src/components/main/body/contacts/organization-details.tsx @@ -35,28 +35,31 @@ export function OrganizationDetailsColumn({
{selectedOrgData && selectedOrganizationId ? ( <> -
-
-
- +
+
+ +
+
+ +
+
+
+
Name
+
+ +
-
-
-
- -

- {peopleInOrg?.length ?? 0}{" "} - {(peopleInOrg?.length ?? 0) === 1 ? "person" : "people"} -

-
+
+
People
+
+ {peopleInOrg?.length ?? 0}{" "} + {(peopleInOrg?.length ?? 0) === 1 ? "person" : "people"}
-
-

People @@ -220,7 +223,7 @@ function EditableOrganizationNameField({ value={(value as string) || ""} onChange={handleChange} placeholder="Organization name" - className="border-none shadow-none p-0 h-8 text-lg font-semibold focus-visible:ring-0 focus-visible:ring-offset-0" + className="border-none shadow-none p-0 h-7 text-base focus-visible:ring-0 focus-visible:ring-offset-0" /> ); } diff --git a/apps/desktop/src/components/main/body/contacts/organizations.tsx b/apps/desktop/src/components/main/body/contacts/organizations.tsx deleted file mode 100644 index c9690810fa..0000000000 --- a/apps/desktop/src/components/main/body/contacts/organizations.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { Building2, CornerDownLeft, User } from "lucide-react"; -import React, { useState } from "react"; - -import { cn } from "@hypr/utils"; - -import * as main from "../../../../store/tinybase/store/main"; -import { ColumnHeader, type SortOption } from "./shared"; - -export function OrganizationsColumn({ - selectedOrganization, - setSelectedOrganization, - isViewingOrgDetails, -}: { - selectedOrganization: string | null; - setSelectedOrganization: (id: string | null) => void; - isViewingOrgDetails: boolean; -}) { - const [showNewOrg, setShowNewOrg] = useState(false); - const [searchValue, setSearchValue] = useState(""); - const { organizationIds, sortOption, setSortOption } = - useSortedOrganizationIds(); - - const allOrgs = main.UI.useTable("organizations", main.STORE_ID); - - const filteredOrganizationIds = React.useMemo(() => { - if (!searchValue.trim()) { - return organizationIds; - } - - return organizationIds.filter((id) => { - const org = allOrgs[id]; - const nameLower = (org?.name ?? "").toLowerCase(); - return nameLower.includes(searchValue.toLowerCase()); - }); - }, [organizationIds, searchValue, allOrgs]); - - return ( -
- setShowNewOrg(true)} - searchValue={searchValue} - onSearchChange={setSearchValue} - /> -
-
- - {showNewOrg && ( - setShowNewOrg(false)} - onCancel={() => setShowNewOrg(false)} - /> - )} - {filteredOrganizationIds.map((orgId) => ( - - ))} -
-
-
- ); -} - -function useSortedOrganizationIds() { - const [sortOption, setSortOption] = useState("alphabetical"); - - const alphabeticalIds = main.UI.useResultSortedRowIds( - main.QUERIES.visibleOrganizations, - "name", - false, - 0, - undefined, - main.STORE_ID, - ); - const reverseAlphabeticalIds = main.UI.useResultSortedRowIds( - main.QUERIES.visibleOrganizations, - "name", - true, - 0, - undefined, - main.STORE_ID, - ); - const newestIds = main.UI.useResultSortedRowIds( - main.QUERIES.visibleOrganizations, - "created_at", - true, - 0, - undefined, - main.STORE_ID, - ); - const oldestIds = main.UI.useResultSortedRowIds( - main.QUERIES.visibleOrganizations, - "created_at", - false, - 0, - undefined, - main.STORE_ID, - ); - - const organizationIds = - sortOption === "alphabetical" - ? alphabeticalIds - : sortOption === "reverse-alphabetical" - ? reverseAlphabeticalIds - : sortOption === "newest" - ? newestIds - : oldestIds; - - return { organizationIds, sortOption, setSortOption }; -} - -function OrganizationItem({ - organizationId, - isSelected, - isViewingDetails, - setSelectedOrganization, -}: { - organizationId: string; - isSelected: boolean; - isViewingDetails: boolean; - setSelectedOrganization: (id: string | null) => void; -}) { - const organization = main.UI.useRow( - "organizations", - organizationId, - main.STORE_ID, - ); - if (!organization) { - return null; - } - - return ( -
- -
- ); -} - -function NewOrganizationForm({ - onSave, - onCancel, -}: { - onSave: () => void; - onCancel: () => void; -}) { - const [name, setName] = useState(""); - const userId = main.UI.useValue("user_id", main.STORE_ID); - - const handleAdd = main.UI.useAddRowCallback( - "organizations", - () => ({ - user_id: userId || "", - name: name.trim(), - created_at: new Date().toISOString(), - }), - [name, userId], - main.STORE_ID, - () => { - setName(""); - onSave(); - }, - ); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (name.trim()) { - handleAdd(); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - if (name.trim()) { - handleAdd(); - } - } - if (e.key === "Escape") { - onCancel(); - } - }; - - return ( -
-
-
- setName(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Add organization" - className="w-full bg-transparent text-sm focus:outline-hidden placeholder:text-neutral-400" - autoFocus - /> - {name.trim() && ( - - )} -
-
-
- ); -} diff --git a/apps/desktop/src/components/main/body/contacts/people.tsx b/apps/desktop/src/components/main/body/contacts/people.tsx deleted file mode 100644 index 1d282f75fd..0000000000 --- a/apps/desktop/src/components/main/body/contacts/people.tsx +++ /dev/null @@ -1,369 +0,0 @@ -import { Facehash } from "facehash"; -import { CornerDownLeft, Pin } from "lucide-react"; -import { Reorder } from "motion/react"; -import React, { useCallback, useMemo, useState } from "react"; - -import { cn } from "@hypr/utils"; - -import * as main from "../../../../store/tinybase/store/main"; -import { ColumnHeader, type SortOption } from "./shared"; - -export function PeopleColumn({ - currentOrgId, - currentHumanId, - setSelectedPerson, -}: { - currentOrgId?: string | null; - currentHumanId?: string | null; - setSelectedPerson: (id: string | null) => void; -}) { - const [showNewPerson, setShowNewPerson] = useState(false); - const [searchValue, setSearchValue] = useState(""); - const { humanIds, pinnedIds, unpinnedIds, sortOption, setSortOption } = - useSortedHumanIds(currentOrgId); - - const allHumans = main.UI.useTable("humans", main.STORE_ID); - const store = main.UI.useStore(main.STORE_ID); - - const filteredHumanIds = useMemo(() => { - if (!searchValue.trim()) { - return humanIds; - } - - return humanIds.filter((id) => { - const human = allHumans[id]; - const q = searchValue.toLowerCase(); - const name = (human?.name ?? "").toLowerCase(); - const email = (human?.email ?? "").toLowerCase(); - return name.includes(q) || email.includes(q); - }); - }, [humanIds, searchValue, allHumans]); - - const filteredPinnedIds = useMemo(() => { - if (!searchValue.trim()) { - return pinnedIds; - } - return pinnedIds.filter((id) => filteredHumanIds.includes(id)); - }, [pinnedIds, filteredHumanIds, searchValue]); - - const filteredUnpinnedIds = useMemo(() => { - if (!searchValue.trim()) { - return unpinnedIds; - } - return unpinnedIds.filter((id) => filteredHumanIds.includes(id)); - }, [unpinnedIds, filteredHumanIds, searchValue]); - - const handleReorderPinned = useCallback( - (newOrder: string[]) => { - if (!store) return; - store.transaction(() => { - newOrder.forEach((id, index) => { - store.setCell("humans", id, "pin_order", index); - }); - }); - }, - [store], - ); - - return ( -
- setShowNewPerson(true)} - searchValue={searchValue} - onSearchChange={setSearchValue} - /> -
-
- {showNewPerson && ( - { - setShowNewPerson(false); - setSelectedPerson(humanId); - }} - onCancel={() => setShowNewPerson(false)} - /> - )} - {filteredPinnedIds.length > 0 && ( - - {filteredPinnedIds.map((humanId) => ( - - - - ))} - - )} - {filteredUnpinnedIds.map((humanId) => ( - - ))} -
-
-
- ); -} - -export function useSortedHumanIds(currentOrgId?: string | null) { - const [sortOption, setSortOption] = useState("alphabetical"); - - const allAlphabeticalIds = main.UI.useResultSortedRowIds( - main.QUERIES.visibleHumans, - "name", - false, - 0, - undefined, - main.STORE_ID, - ); - const allReverseAlphabeticalIds = main.UI.useResultSortedRowIds( - main.QUERIES.visibleHumans, - "name", - true, - 0, - undefined, - main.STORE_ID, - ); - const allNewestIds = main.UI.useResultSortedRowIds( - main.QUERIES.visibleHumans, - "created_at", - true, - 0, - undefined, - main.STORE_ID, - ); - const allOldestIds = main.UI.useResultSortedRowIds( - main.QUERIES.visibleHumans, - "created_at", - false, - 0, - undefined, - main.STORE_ID, - ); - - const thisOrgHumanIds = main.UI.useSliceRowIds( - main.INDEXES.humansByOrg, - currentOrgId ?? "", - main.STORE_ID, - ); - - const allHumans = main.UI.useTable("humans", main.STORE_ID); - - const sortedIds = currentOrgId - ? (sortOption === "alphabetical" - ? allAlphabeticalIds - : sortOption === "reverse-alphabetical" - ? allReverseAlphabeticalIds - : sortOption === "newest" - ? allNewestIds - : allOldestIds - ).filter((id) => thisOrgHumanIds.includes(id)) - : sortOption === "alphabetical" - ? allAlphabeticalIds - : sortOption === "reverse-alphabetical" - ? allReverseAlphabeticalIds - : sortOption === "newest" - ? allNewestIds - : allOldestIds; - - const { humanIds, pinnedIds, unpinnedIds } = useMemo(() => { - const pinned = sortedIds.filter((id) => allHumans[id]?.pinned); - const unpinned = sortedIds.filter((id) => !allHumans[id]?.pinned); - - const sortedPinned = [...pinned].sort((a, b) => { - const orderA = - (allHumans[a]?.pin_order as number | undefined) ?? Infinity; - const orderB = - (allHumans[b]?.pin_order as number | undefined) ?? Infinity; - return orderA - orderB; - }); - - return { - humanIds: [...sortedPinned, ...unpinned], - pinnedIds: sortedPinned, - unpinnedIds: unpinned, - }; - }, [sortedIds, allHumans]); - - return { humanIds, pinnedIds, unpinnedIds, sortOption, setSortOption }; -} - -function PersonItem({ - humanId, - active, - setSelectedPerson, -}: { - humanId: string; - active: boolean; - setSelectedPerson: (id: string | null) => void; -}) { - const person = main.UI.useRow("humans", humanId, main.STORE_ID); - const isPinned = Boolean(person.pinned); - const personName = String(person.name ?? ""); - const personEmail = String(person.email ?? ""); - - const store = main.UI.useStore(main.STORE_ID); - - const handleTogglePin = useCallback( - (e: React.MouseEvent) => { - e.stopPropagation(); - if (!store) return; - - const currentPinned = store.getCell("humans", humanId, "pinned"); - if (currentPinned) { - store.setPartialRow("humans", humanId, { - pinned: false, - pin_order: undefined, - }); - } else { - const allHumans = store.getTable("humans"); - const maxOrder = Object.values(allHumans).reduce((max, h) => { - const order = (h.pin_order as number | undefined) ?? 0; - return Math.max(max, order); - }, 0); - store.setPartialRow("humans", humanId, { - pinned: true, - pin_order: maxOrder + 1, - }); - } - }, - [store, humanId], - ); - - return ( - - - ); -} - -function NewPersonForm({ - currentOrgId, - onSave, - onCancel, -}: { - currentOrgId?: string | null; - onSave: (humanId: string) => void; - onCancel: () => void; -}) { - const [name, setName] = useState(""); - const userId = main.UI.useValue("user_id", main.STORE_ID); - - const createHuman = main.UI.useSetRowCallback( - "humans", - (p: { name: string; humanId: string }) => p.humanId, - (p: { name: string; humanId: string }) => ({ - user_id: userId || "", - created_at: new Date().toISOString(), - name: p.name, - email: "", - org_id: currentOrgId || "", - job_title: "", - linkedin_username: "", - memo: "", - pinned: false, - }), - [userId, currentOrgId], - main.STORE_ID, - ); - - const handleAdd = () => { - const humanId = crypto.randomUUID(); - createHuman({ humanId, name: name.trim() }); - setName(""); - onSave(humanId); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (name.trim()) { - handleAdd(); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.preventDefault(); - if (name.trim()) { - handleAdd(); - } - } - if (e.key === "Escape") { - onCancel(); - } - }; - - return ( -
-
-
- setName(e.target.value)} - onKeyDown={handleKeyDown} - placeholder="Add person" - className="w-full bg-transparent text-sm focus:outline-hidden placeholder:text-neutral-400" - autoFocus - /> - {name.trim() && ( - - )} -
-
-
- ); -} diff --git a/apps/desktop/src/components/main/body/contacts/shared.tsx b/apps/desktop/src/components/main/body/contacts/shared.tsx index 16cb5b0369..ef84b372e0 100644 --- a/apps/desktop/src/components/main/body/contacts/shared.tsx +++ b/apps/desktop/src/components/main/body/contacts/shared.tsx @@ -3,11 +3,12 @@ import { useState } from "react"; import { Button } from "@hypr/ui/components/ui/button"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, -} from "@hypr/ui/components/ui/select"; + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@hypr/ui/components/ui/dropdown-menu"; export const getInitials = (name?: string | null) => { if (!name) { @@ -35,28 +36,44 @@ export function SortDropdown({ setSortOption: (option: SortOption) => void; }) { return ( - + + + + + + setSortOption(value as SortOption)} + > + + A-Z + + + Z-A + + + Oldest + + + Newest + + + + ); } @@ -67,6 +84,8 @@ export function ColumnHeader({ onAdd, searchValue, onSearchChange, + showSearch: showSearchProp, + onShowSearchChange, }: { title: string; sortOption?: SortOption; @@ -74,8 +93,12 @@ export function ColumnHeader({ onAdd: () => void; searchValue?: string; onSearchChange?: (value: string) => void; + showSearch?: boolean; + onShowSearchChange?: (show: boolean) => void; }) { - const [showSearch, setShowSearch] = useState(false); + const [showSearchInternal, setShowSearchInternal] = useState(false); + const showSearch = showSearchProp ?? showSearchInternal; + const setShowSearch = onShowSearchChange ?? setShowSearchInternal; const handleSearchToggle = () => { if (showSearch) { diff --git a/apps/desktop/src/components/main/body/empty/index.tsx b/apps/desktop/src/components/main/body/empty/index.tsx index 82a645f016..e4b0c7e511 100644 --- a/apps/desktop/src/components/main/body/empty/index.tsx +++ b/apps/desktop/src/components/main/body/empty/index.tsx @@ -1,5 +1,6 @@ import { AppWindowIcon } from "lucide-react"; -import { useCallback, useState } from "react"; +import { AnimatePresence, motion } from "motion/react"; +import { useCallback, useEffect, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { Kbd } from "@hypr/ui/components/ui/kbd"; @@ -51,6 +52,57 @@ export function TabContentEmpty({ ); } +const TIPS = [ + { text: "Press ⌘⇧N to create a new note and start listening immediately" }, + { text: "Use ⌘K to quickly search across all your notes" }, + { + text: "Hyprnote works fully offline — set up Ollama or LM Studio in AI Settings", + }, + { + text: "Press ⌘⇧J to open AI Chat and ask follow-up questions about your notes", + }, + { + text: "Use templates to get structured summaries tailored to your meeting type", + }, + { text: "Press ⌘⇧T to reopen the last tab you closed" }, + { + text: "Connect your Apple Calendar to automatically see upcoming meetings", + }, +]; + +function RotatingTip() { + const [index, setIndex] = useState(() => + Math.floor(Math.random() * TIPS.length), + ); + + useEffect(() => { + const interval = setInterval(() => { + setIndex((prev) => (prev + 1) % TIPS.length); + }, 5000); + return () => clearInterval(interval); + }, []); + + return ( +
+ Did you know? +
+ + + {TIPS[index].text} + + +
+
+ ); +} + function EmptyView() { const newNote = useNewNote({ behavior: "current" }); const openCurrent = useTabs((state) => state.openCurrent); @@ -85,7 +137,7 @@ function EmptyView() { ); return ( -
+
+
+ +
openNew({ type: "contacts", - state: { selectedOrganization: null, selectedPerson: null }, + state: { selected: null }, }), { preventDefault: true, diff --git a/apps/desktop/src/components/main/body/search.tsx b/apps/desktop/src/components/main/body/search.tsx index 87b1df446c..33cbf1e4d0 100644 --- a/apps/desktop/src/components/main/body/search.tsx +++ b/apps/desktop/src/components/main/body/search.tsx @@ -168,16 +168,14 @@ function ExpandedSearch({ onBlur }: { onBlur?: () => void }) { openNew({ type: "contacts", state: { - selectedPerson: item.id, - selectedOrganization: null, + selected: { type: "person", id: item.id }, }, }); } else if (item.type === "organization") { openNew({ type: "contacts", state: { - selectedOrganization: item.id, - selectedPerson: null, + selected: { type: "organization", id: item.id }, }, }); } diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/chip.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/chip.tsx index 07d65e98f6..6ed1ca65af 100644 --- a/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/chip.tsx +++ b/apps/desktop/src/components/main/body/sessions/outer-header/metadata/participants/chip.tsx @@ -29,7 +29,7 @@ export function ParticipantChip({ mappingId }: { mappingId: string }) { if (assignedHumanId) { useTabs.getState().openNew({ type: "contacts", - state: { selectedOrganization: null, selectedPerson: assignedHumanId }, + state: { selected: { type: "person", id: assignedHumanId } }, }); } }, [assignedHumanId]); diff --git a/apps/desktop/src/components/main/sidebar/profile/index.tsx b/apps/desktop/src/components/main/sidebar/profile/index.tsx index c6c4439e95..6af5227f19 100644 --- a/apps/desktop/src/components/main/sidebar/profile/index.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/index.tsx @@ -106,8 +106,7 @@ export function ProfileSection({ onExpandChange }: ProfileSectionProps = {}) { openNew({ type: "contacts", state: { - selectedOrganization: null, - selectedPerson: null, + selected: null, }, }); closeMenu(); diff --git a/apps/desktop/src/components/main/sidebar/search/item.tsx b/apps/desktop/src/components/main/sidebar/search/item.tsx index 8e2fdd9d6c..2b6447acaf 100644 --- a/apps/desktop/src/components/main/sidebar/search/item.tsx +++ b/apps/desktop/src/components/main/sidebar/search/item.tsx @@ -282,13 +282,13 @@ function getTab(result: SearchResult): TabInput | null { if (result.type === "human") { return { type: "contacts", - state: { selectedPerson: result.id, selectedOrganization: null }, + state: { selected: { type: "person", id: result.id } }, }; } if (result.type === "organization") { return { type: "contacts", - state: { selectedOrganization: result.id, selectedPerson: null }, + state: { selected: { type: "organization", id: result.id } }, }; } diff --git a/apps/desktop/src/components/settings/general/permissions.tsx b/apps/desktop/src/components/settings/general/permissions.tsx index 4159503f5f..3900001e23 100644 --- a/apps/desktop/src/components/settings/general/permissions.tsx +++ b/apps/desktop/src/components/settings/general/permissions.tsx @@ -132,6 +132,7 @@ export function Permissions() { const mic = usePermission("microphone"); const systemAudio = usePermission("systemAudio"); const accessibility = usePermission("accessibility"); + const contacts = usePermission("contacts"); return (
@@ -164,6 +165,15 @@ export function Permissions() { onReset={accessibility.reset} onOpen={accessibility.open} /> +
); diff --git a/apps/desktop/src/store/tinybase/persister/human/transform.test.ts b/apps/desktop/src/store/tinybase/persister/human/transform.test.ts index 2bdbf80f86..735a93d539 100644 --- a/apps/desktop/src/store/tinybase/persister/human/transform.test.ts +++ b/apps/desktop/src/store/tinybase/persister/human/transform.test.ts @@ -59,6 +59,7 @@ describe("frontmatterToHuman", () => { ); expect(result).toEqual({ user_id: "user-1", + created_at: undefined, name: "John Doe", email: "john@example.com", org_id: "org-1", @@ -66,6 +67,7 @@ describe("frontmatterToHuman", () => { linkedin_username: "johndoe", memo: "Notes", pinned: false, + pin_order: undefined, }); }); }); @@ -81,6 +83,7 @@ describe("humanToFrontmatter", () => { linkedin_username: "", memo: "", pinned: false, + created_at: "", }); expect(result.frontmatter.emails).toEqual([ "a@example.com", @@ -98,6 +101,7 @@ describe("humanToFrontmatter", () => { linkedin_username: "", memo: "", pinned: false, + created_at: "", }); expect(result.frontmatter.emails).toEqual([]); }); @@ -112,6 +116,7 @@ describe("humanToFrontmatter", () => { linkedin_username: "", memo: "", pinned: false, + created_at: "", }); expect(result.frontmatter.emails).toEqual([ "a@example.com", @@ -129,6 +134,7 @@ describe("humanToFrontmatter", () => { linkedin_username: "", memo: "", pinned: false, + created_at: "", }); expect(result.frontmatter.emails).toEqual(["a@example.com"]); }); @@ -143,6 +149,7 @@ describe("humanToFrontmatter", () => { linkedin_username: "", memo: "Some notes", pinned: false, + created_at: "", }); expect(result.body).toBe("Some notes"); }); @@ -150,6 +157,7 @@ describe("humanToFrontmatter", () => { test("converts all fields correctly", () => { const result = humanToFrontmatter({ user_id: "user-1", + created_at: "2024-01-01T00:00:00Z", name: "John Doe", email: "john@example.com", org_id: "org-1", @@ -161,12 +169,14 @@ describe("humanToFrontmatter", () => { expect(result).toEqual({ frontmatter: { user_id: "user-1", + created_at: "2024-01-01T00:00:00Z", name: "John Doe", emails: ["john@example.com"], org_id: "org-1", job_title: "Engineer", linkedin_username: "johndoe", pinned: false, + pin_order: 0, }, body: "Notes", }); diff --git a/apps/desktop/src/store/tinybase/persister/human/transform.ts b/apps/desktop/src/store/tinybase/persister/human/transform.ts index 1c4f9dc4fa..7c5acde1b1 100644 --- a/apps/desktop/src/store/tinybase/persister/human/transform.ts +++ b/apps/desktop/src/store/tinybase/persister/human/transform.ts @@ -27,12 +27,17 @@ function frontmatterToStore( ): HumanFrontmatter { return { user_id: String(frontmatter.user_id ?? ""), + created_at: frontmatter.created_at + ? String(frontmatter.created_at) + : undefined, name: String(frontmatter.name ?? ""), email: emailsToStore(frontmatter), org_id: String(frontmatter.org_id ?? ""), job_title: String(frontmatter.job_title ?? ""), linkedin_username: String(frontmatter.linkedin_username ?? ""), pinned: Boolean(frontmatter.pinned ?? false), + pin_order: + frontmatter.pin_order != null ? Number(frontmatter.pin_order) : undefined, }; } @@ -41,12 +46,14 @@ function storeToFrontmatter( ): Record { return { user_id: store.user_id ?? "", + created_at: store.created_at ?? "", name: store.name ?? "", emails: emailToFrontmatter(store.email), org_id: store.org_id ?? "", job_title: store.job_title ?? "", linkedin_username: store.linkedin_username ?? "", pinned: store.pinned ?? false, + pin_order: store.pin_order ?? 0, }; } diff --git a/apps/desktop/src/store/tinybase/persister/organization/transform.test.ts b/apps/desktop/src/store/tinybase/persister/organization/transform.test.ts index 8d3e2109fe..e85b40d966 100644 --- a/apps/desktop/src/store/tinybase/persister/organization/transform.test.ts +++ b/apps/desktop/src/store/tinybase/persister/organization/transform.test.ts @@ -10,13 +10,17 @@ describe("frontmatterToOrganization", () => { const result = frontmatterToOrganization( { user_id: "user-1", + created_at: "2024-01-01T00:00:00Z", name: "Acme Corp", }, "", ); expect(result).toEqual({ user_id: "user-1", + created_at: "2024-01-01T00:00:00Z", name: "Acme Corp", + pinned: false, + pin_order: undefined, }); }); @@ -24,21 +28,65 @@ describe("frontmatterToOrganization", () => { const result = frontmatterToOrganization({}, ""); expect(result).toEqual({ user_id: "", + created_at: undefined, name: "", + pinned: false, + pin_order: undefined, + }); + }); + + test("preserves pinned state", () => { + const result = frontmatterToOrganization( + { + user_id: "user-1", + name: "Acme Corp", + pinned: true, + }, + "", + ); + expect(result).toEqual({ + user_id: "user-1", + created_at: undefined, + name: "Acme Corp", + pinned: true, + pin_order: undefined, }); }); }); describe("organizationToFrontmatter", () => { test("converts organization storage to frontmatter", () => { + const result = organizationToFrontmatter({ + user_id: "user-1", + created_at: "2024-01-01T00:00:00Z", + name: "Acme Corp", + pinned: false, + }); + expect(result).toEqual({ + frontmatter: { + user_id: "user-1", + created_at: "2024-01-01T00:00:00Z", + name: "Acme Corp", + pinned: false, + pin_order: 0, + }, + body: "", + }); + }); + + test("converts pinned organization to frontmatter", () => { const result = organizationToFrontmatter({ user_id: "user-1", name: "Acme Corp", + pinned: true, }); expect(result).toEqual({ frontmatter: { user_id: "user-1", + created_at: "", name: "Acme Corp", + pinned: true, + pin_order: 0, }, body: "", }); diff --git a/apps/desktop/src/store/tinybase/persister/organization/transform.ts b/apps/desktop/src/store/tinybase/persister/organization/transform.ts index 372064f2a0..d7ba1e2003 100644 --- a/apps/desktop/src/store/tinybase/persister/organization/transform.ts +++ b/apps/desktop/src/store/tinybase/persister/organization/transform.ts @@ -7,7 +7,13 @@ export function frontmatterToOrganization( ): OrganizationStorage { return { user_id: String(frontmatter.user_id ?? ""), + created_at: frontmatter.created_at + ? String(frontmatter.created_at) + : undefined, name: String(frontmatter.name ?? ""), + pinned: Boolean(frontmatter.pinned ?? false), + pin_order: + frontmatter.pin_order != null ? Number(frontmatter.pin_order) : undefined, }; } @@ -17,8 +23,11 @@ export function organizationToFrontmatter(org: OrganizationStorage): { } { return { frontmatter: { - name: org.name ?? "", user_id: org.user_id ?? "", + created_at: org.created_at ?? "", + name: org.name ?? "", + pinned: org.pinned ?? false, + pin_order: org.pin_order ?? 0, }, body: "", }; diff --git a/apps/desktop/src/store/tinybase/store/main.ts b/apps/desktop/src/store/tinybase/store/main.ts index 946cf84928..33d9a9f1dc 100644 --- a/apps/desktop/src/store/tinybase/store/main.ts +++ b/apps/desktop/src/store/tinybase/store/main.ts @@ -112,6 +112,7 @@ export const StoreComponent = () => { }, ) .setQueryDefinition(QUERIES.visibleHumans, "humans", ({ select }) => { + select("created_at"); select("name"); select("email"); select("org_id"); @@ -124,7 +125,10 @@ export const StoreComponent = () => { QUERIES.visibleOrganizations, "organizations", ({ select }) => { + select("created_at"); select("name"); + select("pinned"); + select("pin_order"); }, ) .setQueryDefinition( @@ -403,6 +407,7 @@ interface _QueryResultRows { folder_id: string; }; visibleHumans: { + created_at: string; name: string; email: string; org_id: string; @@ -412,7 +417,10 @@ interface _QueryResultRows { pin_order: number; }; visibleOrganizations: { + created_at: string; name: string; + pinned: boolean; + pin_order: number; }; visibleTemplates: { title: string; diff --git a/apps/desktop/src/store/zustand/tabs/schema.ts b/apps/desktop/src/store/zustand/tabs/schema.ts index bd4849016e..339770d187 100644 --- a/apps/desktop/src/store/zustand/tabs/schema.ts +++ b/apps/desktop/src/store/zustand/tabs/schema.ts @@ -4,6 +4,7 @@ import type { ChangelogState, ChatShortcutsState, ChatState, + ContactsSelection, ContactsState, EditorView, ExtensionsState, @@ -20,6 +21,7 @@ export type { ChangelogState, ChatShortcutsState, ChatState, + ContactsSelection, ContactsState, EditorView, ExtensionsState, @@ -130,8 +132,7 @@ export const getDefaultState = (tab: TabInput): Tab => { ...base, type: "contacts", state: tab.state ?? { - selectedOrganization: null, - selectedPerson: null, + selected: null, }, }; case "templates": diff --git a/apps/desktop/src/store/zustand/tabs/state.test.ts b/apps/desktop/src/store/zustand/tabs/state.test.ts index 0e842a3c51..a559aa18e3 100644 --- a/apps/desktop/src/store/zustand/tabs/state.test.ts +++ b/apps/desktop/src/store/zustand/tabs/state.test.ts @@ -84,9 +84,8 @@ describe("State Updater Actions", () => { describe("updateContactsTabState", () => { const newContactsState = { - selectedOrganization: "org-1", - selectedPerson: "person-1", - } as const; + selected: { type: "person" as const, id: "person-1" }, + }; test("updates contacts tab and current tab state", () => { const contacts = createContactsTab({ active: true }); diff --git a/apps/desktop/src/store/zustand/tabs/test-utils.ts b/apps/desktop/src/store/zustand/tabs/test-utils.ts index 02800b6593..ad8f869c0d 100644 --- a/apps/desktop/src/store/zustand/tabs/test-utils.ts +++ b/apps/desktop/src/store/zustand/tabs/test-utils.ts @@ -36,8 +36,7 @@ export const createContactsTab = ( pinned: overrides.pinned ?? false, slotId: id(), state: { - selectedOrganization: null, - selectedPerson: null, + selected: null, ...overrides.state, }, }); diff --git a/package.json b/package.json index ae01cf032f..55d55edcec 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,11 @@ "turbo": "2.8.3", "typescript-eslint": "^8.55.0" }, + "pnpm": { + "overrides": { + "@slack/web-api": "^7.14.0" + } + }, "packageManager": "pnpm@10.30.0", "engines": { "node": ">=22" diff --git a/packages/store/src/tinybase.ts b/packages/store/src/tinybase.ts index f5c4d6c84f..a62eb5997e 100644 --- a/packages/store/src/tinybase.ts +++ b/packages/store/src/tinybase.ts @@ -41,6 +41,7 @@ export const tableSchemaForTinybase = { } as const satisfies InferTinyBaseSchema, humans: { user_id: { type: "string" }, + created_at: { type: "string" }, name: { type: "string" }, email: { type: "string" }, org_id: { type: "string" }, @@ -52,7 +53,10 @@ export const tableSchemaForTinybase = { } as const satisfies InferTinyBaseSchema, organizations: { user_id: { type: "string" }, + created_at: { type: "string" }, name: { type: "string" }, + pinned: { type: "boolean" }, + pin_order: { type: "number" }, } as const satisfies InferTinyBaseSchema, calendars: { user_id: { type: "string" }, diff --git a/packages/store/src/zod.ts b/packages/store/src/zod.ts index 0cdc806a4f..5758ab30ae 100644 --- a/packages/store/src/zod.ts +++ b/packages/store/src/zod.ts @@ -4,6 +4,7 @@ import { jsonObject, type ToStorageType } from "./shared"; export const humanSchema = z.object({ user_id: z.string(), + created_at: z.preprocess((val) => val ?? undefined, z.string().optional()), name: z.string(), email: z.string(), org_id: z.string(), @@ -89,7 +90,10 @@ export const calendarSchema = z.object({ export const organizationSchema = z.object({ user_id: z.string(), + created_at: z.preprocess((val) => val ?? undefined, z.string().optional()), name: z.string(), + pinned: z.preprocess((val) => val ?? false, z.boolean()), + pin_order: z.preprocess((val) => val ?? undefined, z.number().optional()), }); export const sessionSchema = z.object({ diff --git a/plugins/windows/js/bindings.gen.ts b/plugins/windows/js/bindings.gen.ts index 18a08372da..dfa3de0724 100644 --- a/plugins/windows/js/bindings.gen.ts +++ b/plugins/windows/js/bindings.gen.ts @@ -75,7 +75,8 @@ export type AppWindow = { type: "main" } | { type: "control" } export type ChangelogState = { previous: string | null; current: string } export type ChatShortcutsState = { isWebMode: boolean | null; selectedMineId: string | null; selectedWebIndex: number | null } export type ChatState = { groupId: string | null; initialMessage: string | null } -export type ContactsState = { selectedOrganization: string | null; selectedPerson: string | null } +export type ContactsSelection = { type: "person"; id: string } | { type: "organization"; id: string } +export type ContactsState = { selected: ContactsSelection | null } export type EditorView = { type: "raw" } | { type: "transcript" } | { type: "enhanced"; id: string } | { type: "attachments" } export type ExtensionsState = { selectedExtension: string | null } export type JsonValue = null | boolean | number | string | JsonValue[] | Partial<{ [key in string]: JsonValue }> diff --git a/plugins/windows/src/tab/state.rs b/plugins/windows/src/tab/state.rs index bfb80b4acc..0c89bd0a5c 100644 --- a/plugins/windows/src/tab/state.rs +++ b/plugins/windows/src/tab/state.rs @@ -19,10 +19,19 @@ crate::common_derives! { } } +crate::common_derives! { + #[serde(tag = "type")] + pub enum ContactsSelection { + #[serde(rename = "person")] + Person { id: String }, + #[serde(rename = "organization")] + Organization { id: String }, + } +} + crate::common_derives! { pub struct ContactsState { - pub selected_organization: Option, - pub selected_person: Option, + pub selected: Option, } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c52b67c488..4d2b8789aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@slack/web-api': ^7.14.0 + importers: .: @@ -15356,6 +15359,7 @@ packages: tar@7.5.7: resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me tauri-plugin-keygen-api@https://codeload.github.com/bagindo/tauri-plugin-keygen/tar.gz/e0de03a76a5c82c36adb347e43e4bff4f78c87de: resolution: {tarball: https://codeload.github.com/bagindo/tauri-plugin-keygen/tar.gz/e0de03a76a5c82c36adb347e43e4bff4f78c87de} @@ -16849,14 +16853,14 @@ snapshots: '@apm-js-collab/tracing-hooks@0.3.1': dependencies: '@apm-js-collab/code-transformer': 0.8.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) module-details-from-path: 1.0.4 transitivePeerDependencies: - supports-color '@argos-ci/api-client@0.16.0': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) openapi-fetch: 0.15.2 transitivePeerDependencies: - supports-color @@ -16868,7 +16872,7 @@ snapshots: '@argos-ci/api-client': 0.16.0 '@argos-ci/util': 3.2.0 convict: 6.2.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) fast-glob: 3.3.3 mime-types: 3.0.2 sharp: 0.34.5 @@ -16882,7 +16886,7 @@ snapshots: '@argos-ci/core': 5.1.0 '@argos-ci/util': 3.2.0 chalk: 5.6.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -16943,7 +16947,7 @@ snapshots: '@astrojs/telemetry@3.3.0': dependencies: ci-info: 4.4.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) dlv: 1.1.3 dset: 3.1.4 is-docker: 3.0.0 @@ -16996,7 +17000,7 @@ snapshots: '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -17090,7 +17094,7 @@ snapshots: '@babel/parser': 7.29.0 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -17634,7 +17638,7 @@ snapshots: '@esbuild-plugins/node-resolve@0.2.2(esbuild@0.25.12)': dependencies: '@types/resolve': 1.20.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) esbuild: 0.25.12 escape-string-regexp: 4.0.0 resolve: 1.22.11 @@ -17954,7 +17958,7 @@ snapshots: '@eslint/config-array@0.21.1': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -17970,7 +17974,7 @@ snapshots: '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -18440,7 +18444,7 @@ snapshots: '@jest/pattern@30.0.1': dependencies: - '@types/node': 20.19.33 + '@types/node': 24.10.13 jest-regex-util: 30.0.1 '@jest/schemas@30.0.5': @@ -18453,7 +18457,7 @@ snapshots: '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.19.33 + '@types/node': 24.10.13 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -18820,19 +18824,6 @@ snapshots: '@lukeed/ms@2.0.2': {} - '@mapbox/node-pre-gyp@2.0.3': - dependencies: - consola: 3.4.2 - detect-libc: 2.1.2 - https-proxy-agent: 7.0.6 - node-fetch: 2.7.0 - nopt: 8.1.0 - semver: 7.6.3 - tar: 7.5.7 - transitivePeerDependencies: - - encoding - - supports-color - '@mapbox/node-pre-gyp@2.0.3(supports-color@10.2.2)': dependencies: consola: 3.4.2 @@ -19026,14 +19017,6 @@ snapshots: '@netlify/dev-utils': 4.3.0 '@netlify/runtime-utils': 2.2.0 - '@netlify/blobs@10.6.0': - dependencies: - '@netlify/dev-utils': 4.3.3 - '@netlify/otel': 5.1.1 - '@netlify/runtime-utils': 2.3.0 - transitivePeerDependencies: - - supports-color - '@netlify/blobs@10.6.0(supports-color@10.2.2)': dependencies: '@netlify/dev-utils': 4.3.3 @@ -19059,7 +19042,7 @@ snapshots: '@bugsnag/js': 8.8.1 '@netlify/blobs': 10.6.0(supports-color@10.2.2) '@netlify/cache-utils': 6.0.4 - '@netlify/config': 24.3.0 + '@netlify/config': 24.3.1 '@netlify/edge-bundler': 14.9.5 '@netlify/functions-utils': 6.2.21(rollup@4.57.1)(supports-color@10.2.2) '@netlify/git-utils': 6.0.3 @@ -19135,7 +19118,7 @@ snapshots: '@netlify/config@24.3.0': dependencies: '@iarna/toml': 2.2.5 - '@netlify/api': 14.0.13 + '@netlify/api': 14.0.14 '@netlify/headers-parser': 9.0.2 '@netlify/redirect-parser': 15.0.3 chalk: 5.6.2 @@ -19245,7 +19228,7 @@ snapshots: '@netlify/dev@4.10.0(@netlify/api@14.0.14)(aws4fetch@1.0.20)(ioredis@5.9.2)(rollup@4.57.1)': dependencies: '@netlify/ai': 0.3.6(@netlify/api@14.0.14) - '@netlify/blobs': 10.6.0 + '@netlify/blobs': 10.6.0(supports-color@10.2.2) '@netlify/config': 24.3.1 '@netlify/dev-utils': 4.3.3 '@netlify/edge-functions-dev': 1.0.10 @@ -19347,10 +19330,10 @@ snapshots: '@netlify/functions-dev@1.1.10(rollup@4.57.1)': dependencies: - '@netlify/blobs': 10.6.0 + '@netlify/blobs': 10.6.0(supports-color@10.2.2) '@netlify/dev-utils': 4.3.3 '@netlify/functions': 5.1.2 - '@netlify/zip-it-and-ship-it': 14.3.1(rollup@4.57.1) + '@netlify/zip-it-and-ship-it': 14.3.1(rollup@4.57.1)(supports-color@10.2.2) cron-parser: 4.9.0 decache: 4.6.2 extract-zip: 2.0.1 @@ -19508,16 +19491,6 @@ snapshots: dependencies: '@opentelemetry/api': 1.8.0 - '@netlify/otel@5.1.1': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.203.0(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - '@netlify/otel@5.1.1(supports-color@10.2.2)': dependencies: '@opentelemetry/api': 1.9.0 @@ -19555,7 +19528,7 @@ snapshots: '@netlify/runtime@4.1.14': dependencies: - '@netlify/blobs': 10.6.0 + '@netlify/blobs': 10.6.0(supports-color@10.2.2) '@netlify/cache': 3.3.5 '@netlify/runtime-utils': 2.3.0 '@netlify/types': 2.3.0 @@ -19636,47 +19609,6 @@ snapshots: - supports-color - uploadthing - '@netlify/zip-it-and-ship-it@14.3.1(rollup@4.57.1)': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@netlify/binary-info': 1.0.0 - '@netlify/serverless-functions-api': 2.8.3 - '@vercel/nft': 0.29.4(rollup@4.57.1) - archiver: 7.0.1 - common-path-prefix: 3.0.0 - copy-file: 11.1.0 - es-module-lexer: 1.7.0 - esbuild: 0.27.2 - execa: 8.0.1 - fast-glob: 3.3.3 - filter-obj: 6.1.0 - find-up: 7.0.0 - is-path-inside: 4.0.0 - junk: 4.0.1 - locate-path: 7.2.0 - merge-options: 3.0.4 - minimatch: 9.0.5 - normalize-path: 3.0.0 - p-map: 7.0.3 - path-exists: 5.0.0 - precinct: 12.2.0 - require-package-name: 2.0.1 - resolve: 2.0.0-next.5 - semver: 7.6.3 - tmp-promise: 3.0.3 - toml: 3.0.0 - unixify: 1.0.0 - urlpattern-polyfill: 8.0.2 - yargs: 17.7.2 - zod: 3.25.76 - transitivePeerDependencies: - - bare-abort-controller - - encoding - - react-native-b4a - - rollup - - supports-color - '@netlify/zip-it-and-ship-it@14.3.1(rollup@4.57.1)(supports-color@10.2.2)': dependencies: '@babel/parser': 7.29.0 @@ -20445,15 +20377,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.203.0 - import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 - transitivePeerDependencies: - - supports-color - '@opentelemetry/instrumentation@0.203.0(@opentelemetry/api@1.9.0)(supports-color@10.2.2)': dependencies: '@opentelemetry/api': 1.9.0 @@ -20487,7 +20410,7 @@ snapshots: '@opentelemetry/api-logs': 0.53.0 '@types/shimmer': 1.2.0 import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 + require-in-the-middle: 7.5.2(supports-color@10.2.2) semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: @@ -20499,7 +20422,7 @@ snapshots: '@opentelemetry/api-logs': 0.57.1 '@types/shimmer': 1.2.0 import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 + require-in-the-middle: 7.5.2(supports-color@10.2.2) semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: @@ -20511,7 +20434,7 @@ snapshots: '@opentelemetry/api-logs': 0.57.2 '@types/shimmer': 1.2.0 import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 + require-in-the-middle: 7.5.2(supports-color@10.2.2) semver: 7.6.3 shimmer: 1.2.1 transitivePeerDependencies: @@ -20903,7 +20826,7 @@ snapshots: '@pnpm/tabtab@0.5.4': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) enquirer: 2.4.1 minimist: 1.2.8 untildify: 4.0.0 @@ -20998,7 +20921,7 @@ snapshots: '@puppeteer/browsers@2.12.0': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 @@ -21013,7 +20936,7 @@ snapshots: '@puppeteer/browsers@2.3.0': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) extract-zip: 2.0.1 progress: 2.0.3 proxy-agent: 6.5.0 @@ -22437,14 +22360,14 @@ snapshots: '@slack/logger@4.0.0': dependencies: - '@types/node': 25.2.3 + '@types/node': 24.10.13 '@slack/oauth@3.0.4': dependencies: '@slack/logger': 4.0.0 '@slack/web-api': 7.14.0 '@types/jsonwebtoken': 9.0.10 - '@types/node': 25.2.3 + '@types/node': 24.10.13 jsonwebtoken: 9.0.3 transitivePeerDependencies: - debug @@ -22453,7 +22376,7 @@ snapshots: dependencies: '@slack/logger': 4.0.0 '@slack/web-api': 7.14.0 - '@types/node': 25.2.3 + '@types/node': 24.10.13 '@types/ws': 8.18.1 eventemitter3: 5.0.4 ws: 8.19.0 @@ -22468,7 +22391,7 @@ snapshots: dependencies: '@slack/logger': 4.0.0 '@slack/types': 2.20.0 - '@types/node': 25.2.3 + '@types/node': 24.10.13 '@types/retry': 0.12.0 axios: 1.13.5 eventemitter3: 5.0.4 @@ -23741,7 +23664,7 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.19.33 + '@types/node': 24.10.13 '@types/btoa-lite@1.0.2': {} @@ -23758,11 +23681,11 @@ snapshots: '@types/connect@3.4.36': dependencies: - '@types/node': 20.19.33 + '@types/node': 24.10.13 '@types/connect@3.4.38': dependencies: - '@types/node': 20.19.33 + '@types/node': 24.10.13 '@types/d3-array@3.2.2': {} @@ -23897,7 +23820,7 @@ snapshots: '@types/express-serve-static-core@5.1.1': dependencies: - '@types/node': 20.19.33 + '@types/node': 24.10.13 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 1.2.1 @@ -23925,7 +23848,7 @@ snapshots: '@types/http-proxy@1.17.17': dependencies: - '@types/node': 22.19.11 + '@types/node': 24.10.13 '@types/istanbul-lib-coverage@2.0.6': {} @@ -23950,7 +23873,7 @@ snapshots: '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 2.1.0 - '@types/node': 25.2.3 + '@types/node': 24.10.13 '@types/katex@0.16.8': {} @@ -23984,11 +23907,11 @@ snapshots: '@types/mysql@2.15.26': dependencies: - '@types/node': 20.19.33 + '@types/node': 24.10.13 '@types/mysql@2.15.27': dependencies: - '@types/node': 25.2.3 + '@types/node': 24.10.13 '@types/nlcst@2.0.3': dependencies: @@ -24015,6 +23938,7 @@ snapshots: '@types/node@25.2.3': dependencies: undici-types: 7.16.0 + optional: true '@types/normalize-package-data@2.4.4': {} @@ -24022,27 +23946,27 @@ snapshots: '@types/pg-pool@2.0.6': dependencies: - '@types/pg': 8.6.1 + '@types/pg': 8.16.0 '@types/pg-pool@2.0.7': dependencies: - '@types/pg': 8.15.6 + '@types/pg': 8.16.0 '@types/pg@8.15.6': dependencies: - '@types/node': 25.2.3 + '@types/node': 24.10.13 pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/pg@8.16.0': dependencies: - '@types/node': 25.2.3 + '@types/node': 24.10.13 pg-protocol: 1.11.0 pg-types: 2.2.0 '@types/pg@8.6.1': dependencies: - '@types/node': 20.19.33 + '@types/node': 24.10.13 pg-protocol: 1.11.0 pg-types: 2.2.0 @@ -24072,18 +23996,18 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 22.19.11 + '@types/node': 24.10.13 '@types/semver@7.7.1': {} '@types/send@1.2.1': dependencies: - '@types/node': 20.19.33 + '@types/node': 24.10.13 '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 20.19.33 + '@types/node': 24.10.13 '@types/shimmer@1.2.0': {} @@ -24095,7 +24019,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 25.2.3 + '@types/node': 24.10.13 '@types/triple-beam@1.3.5': {} @@ -24127,7 +24051,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 22.19.11 + '@types/node': 24.10.13 optional: true '@typescript-eslint/eslint-plugin@8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': @@ -24150,9 +24074,9 @@ snapshots: dependencies: '@typescript-eslint/scope-manager': 8.55.0 '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.55.0(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.55.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -24167,15 +24091,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.55.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 - debug: 4.4.3(supports-color@8.1.1) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/scope-manager@8.55.0': dependencies: '@typescript-eslint/types': 8.55.0 @@ -24188,9 +24103,9 @@ snapshots: '@typescript-eslint/type-utils@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.55.0(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) eslint: 9.39.2(jiti@2.6.1) ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 @@ -24214,27 +24129,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.55.0(typescript@5.9.3)': - dependencies: - '@typescript-eslint/project-service': 8.55.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.9.3) - '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/visitor-keys': 8.55.0 - debug: 4.4.3(supports-color@8.1.1) - minimatch: 9.0.5 - semver: 7.7.4 - tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.55.0 '@typescript-eslint/types': 8.55.0 - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.55.0(supports-color@10.2.2)(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: @@ -24247,7 +24147,7 @@ snapshots: '@typescript/vfs@1.6.2(typescript@5.9.3)': dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -24335,25 +24235,6 @@ snapshots: '@use-gesture/core': 10.3.1 react: 19.2.4 - '@vercel/nft@0.29.4(rollup@4.57.1)': - dependencies: - '@mapbox/node-pre-gyp': 2.0.3 - '@rollup/pluginutils': 5.3.0(rollup@4.57.1) - acorn: 8.15.0 - acorn-import-attributes: 1.9.5(acorn@8.15.0) - async-sema: 3.1.1 - bindings: 1.5.0 - estree-walker: 2.0.2 - glob: 10.5.0 - graceful-fs: 4.2.11 - node-gyp-build: 4.8.4 - picomatch: 4.0.3 - resolve-from: 5.0.0 - transitivePeerDependencies: - - encoding - - rollup - - supports-color - '@vercel/nft@0.29.4(rollup@4.57.1)(supports-color@10.2.2)': dependencies: '@mapbox/node-pre-gyp': 2.0.3(supports-color@10.2.2) @@ -24857,7 +24738,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -25107,7 +24988,7 @@ snapshots: common-ancestor-path: 1.0.1 cookie: 1.1.1 cssesc: 3.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) deterministic-object-hash: 2.0.2 devalue: 5.6.2 diff: 8.0.3 @@ -25366,7 +25247,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 @@ -25438,7 +25319,7 @@ snapshots: bun-types@1.3.9: dependencies: - '@types/node': 25.2.3 + '@types/node': 24.10.13 bundle-name@4.1.0: dependencies: @@ -26385,15 +26266,6 @@ snapshots: transitivePeerDependencies: - supports-color - detective-typescript@14.0.0(typescript@5.9.3): - dependencies: - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) - ast-module-types: 6.0.1 - node-source-walk: 7.0.1 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - detective-vue2@2.2.0(supports-color@10.2.2)(typescript@5.9.3): dependencies: '@dependents/detective-less': 5.0.1 @@ -26407,19 +26279,6 @@ snapshots: transitivePeerDependencies: - supports-color - detective-vue2@2.2.0(typescript@5.9.3): - dependencies: - '@dependents/detective-less': 5.0.1 - '@vue/compiler-sfc': 3.5.28 - detective-es6: 5.0.1 - detective-sass: 6.0.1 - detective-scss: 5.0.1 - detective-stylus: 5.0.1 - detective-typescript: 14.0.0(typescript@5.9.3) - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - deterministic-object-hash@2.0.2: dependencies: base-64: 1.0.0 @@ -26550,7 +26409,7 @@ snapshots: edge-paths: 3.0.5 fast-xml-parser: 5.3.5 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) which: 6.0.1 transitivePeerDependencies: - supports-color @@ -26682,7 +26541,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.25.12): dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) esbuild: 0.25.12 transitivePeerDependencies: - supports-color @@ -26848,7 +26707,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -27066,7 +26925,7 @@ snapshots: content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.2.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -27115,7 +26974,7 @@ snapshots: extract-zip@2.0.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) get-stream: 5.2.0 yauzl: 2.10.0 optionalDependencies: @@ -27278,7 +27137,7 @@ snapshots: finalhandler@2.1.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -27341,7 +27200,7 @@ snapshots: follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) fontace@0.4.1: dependencies: @@ -27415,7 +27274,7 @@ snapshots: gaxios@7.1.3: dependencies: extend: 3.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) node-fetch: 3.3.2 rimraf: 5.0.10 transitivePeerDependencies: @@ -27435,7 +27294,7 @@ snapshots: '@zip.js/zip.js': 2.8.19 decamelize: 6.0.1 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) modern-tar: 0.7.3 transitivePeerDependencies: - supports-color @@ -27504,7 +27363,7 @@ snapshots: dependencies: basic-ftp: 5.1.0 data-uri-to-buffer: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -27996,14 +27855,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color http-proxy-middleware@3.0.5: dependencies: '@types/http-proxy': 1.17.17 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) http-proxy: 1.18.1(debug@4.4.3) is-glob: 4.0.3 is-plain-object: 5.0.0 @@ -28029,14 +27888,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) - transitivePeerDependencies: - - supports-color - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -28189,7 +28041,7 @@ snapshots: dependencies: '@ioredis/commands': 1.5.0 cluster-key-slot: 1.1.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) denque: 2.1.0 lodash.defaults: 4.2.0 lodash.isarguments: 3.1.0 @@ -28499,7 +28351,7 @@ snapshots: jest-mock@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 20.19.33 + '@types/node': 24.10.13 jest-util: 30.2.0 jest-regex-util@30.0.1: {} @@ -28507,7 +28359,7 @@ snapshots: jest-util@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 20.19.33 + '@types/node': 24.10.13 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 @@ -28558,7 +28410,7 @@ snapshots: decimal.js: 10.6.0 html-encoding-sniffer: 6.0.0(@noble/hashes@1.8.0) http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) is-potential-custom-element-name: 1.0.1 parse5: 8.0.0 saxes: 6.0.0 @@ -28740,7 +28592,7 @@ snapshots: chalk: 4.1.2 console-table-printer: 2.15.0 p-queue: 6.6.2 - semver: 7.7.4 + semver: 7.6.3 uuid: 10.0.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -28804,7 +28656,7 @@ snapshots: light-my-request@6.6.0: dependencies: - cookie: 1.0.2 + cookie: 1.1.1 process-warning: 4.0.1 set-cookie-parser: 2.7.2 @@ -30034,7 +29886,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) decode-named-character-reference: 1.3.0 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -30057,7 +29909,7 @@ snapshots: micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -30268,7 +30120,7 @@ snapshots: '@netlify/images': 1.2.5(@netlify/blobs@10.1.0)(aws4fetch@1.0.20)(ioredis@5.9.2) '@netlify/local-functions-proxy': 2.0.3 '@netlify/redirect-parser': 15.0.3 - '@netlify/zip-it-and-ship-it': 14.3.1(rollup@4.57.1) + '@netlify/zip-it-and-ship-it': 14.3.1(rollup@4.57.1)(supports-color@10.2.2) '@octokit/rest': 22.0.0 '@opentelemetry/api': 1.8.0 '@pnpm/tabtab': 0.5.4 @@ -30286,7 +30138,7 @@ snapshots: content-type: 1.0.5 cookie: 1.0.2 cron-parser: 4.9.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) decache: 4.6.2 dot-prop: 10.1.0 dotenv: 17.2.3 @@ -30308,7 +30160,7 @@ snapshots: gitconfiglocal: 2.1.0 http-proxy: 1.18.1(debug@4.4.3) http-proxy-middleware: 3.0.5 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) inquirer: 8.2.7(@types/node@22.19.11) inquirer-autocomplete-prompt: 1.4.0(inquirer@8.2.7(@types/node@22.19.11)) is-docker: 3.0.0 @@ -30803,10 +30655,10 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) get-uri: 6.0.5 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) pac-resolver: 7.0.1 socks-proxy-agent: 8.0.5 transitivePeerDependencies: @@ -31251,26 +31103,6 @@ snapshots: preact@10.28.3: {} - precinct@12.2.0: - dependencies: - '@dependents/detective-less': 5.0.1 - commander: 12.1.0 - detective-amd: 6.0.1 - detective-cjs: 6.0.1 - detective-es6: 5.0.1 - detective-postcss: 7.0.1(postcss@8.5.6) - detective-sass: 6.0.1 - detective-scss: 5.0.1 - detective-stylus: 5.0.1 - detective-typescript: 14.0.0(typescript@5.9.3) - detective-vue2: 2.2.0(typescript@5.9.3) - module-definition: 6.0.1 - node-source-walk: 7.0.1 - postcss: 8.5.6 - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - precinct@12.2.0(supports-color@10.2.2): dependencies: '@dependents/detective-less': 5.0.1 @@ -31511,7 +31343,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 25.2.3 + '@types/node': 24.10.13 long: 5.3.2 proxy-addr@2.0.7: @@ -31522,9 +31354,9 @@ snapshots: proxy-agent@6.5.0: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) lru-cache: 7.18.3 pac-proxy-agent: 7.2.0 proxy-from-env: 1.1.0 @@ -31558,7 +31390,7 @@ snapshots: dependencies: '@puppeteer/browsers': 2.3.0 chromium-bidi: 0.6.3(devtools-protocol@0.0.1312386) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) devtools-protocol: 0.0.1312386 ws: 8.19.0 transitivePeerDependencies: @@ -32282,14 +32114,6 @@ snapshots: require-from-string@2.0.2: {} - require-in-the-middle@7.5.2: - dependencies: - debug: 4.4.3(supports-color@8.1.1) - module-details-from-path: 1.0.4 - resolve: 1.22.11 - transitivePeerDependencies: - - supports-color - require-in-the-middle@7.5.2(supports-color@10.2.2): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -32300,7 +32124,7 @@ snapshots: require-in-the-middle@8.0.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) module-details-from-path: 1.0.4 transitivePeerDependencies: - supports-color @@ -32457,7 +32281,7 @@ snapshots: router@2.2.0: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) depd: 2.0.0 is-promise: 4.0.0 parseurl: 1.3.3 @@ -32551,7 +32375,7 @@ snapshots: send@1.2.1: dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -32746,7 +32570,7 @@ snapshots: socks-proxy-agent@8.0.5: dependencies: agent-base: 7.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) socks: 2.8.7 transitivePeerDependencies: - supports-color @@ -33376,7 +33200,7 @@ snapshots: dependencies: '@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.55.0(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.55.0(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/utils': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 @@ -33601,7 +33425,7 @@ snapshots: ofetch: 1.5.1 ufo: 1.6.3 optionalDependencies: - '@netlify/blobs': 10.6.0 + '@netlify/blobs': 10.6.0(supports-color@10.2.2) aws4fetch: 1.0.20 ioredis: 5.9.2 @@ -33765,7 +33589,7 @@ snapshots: vite-node@2.1.9(@types/node@20.19.33)(lightningcss@1.30.2): dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 1.1.2 vite: 5.4.21(@types/node@20.19.33)(lightningcss@1.30.2) @@ -33783,7 +33607,7 @@ snapshots: vite-node@3.2.4(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) @@ -33804,7 +33628,7 @@ snapshots: vite-node@3.2.4(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) @@ -33825,7 +33649,7 @@ snapshots: vite-node@3.2.4(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2) @@ -33845,7 +33669,7 @@ snapshots: vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.11)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)): dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: @@ -33946,7 +33770,7 @@ snapshots: '@vitest/spy': 2.1.9 '@vitest/utils': 2.1.9 chai: 5.3.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) expect-type: 1.3.0 magic-string: 0.30.21 pathe: 1.1.2 @@ -33984,7 +33808,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.3.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) expect-type: 1.3.0 magic-string: 0.30.21 pathe: 2.0.3 @@ -34028,7 +33852,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.3.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) expect-type: 1.3.0 magic-string: 0.30.21 pathe: 2.0.3 @@ -34072,7 +33896,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.3.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) expect-type: 1.3.0 magic-string: 0.30.21 pathe: 2.0.3 @@ -34132,7 +33956,7 @@ snapshots: dependencies: chalk: 4.1.2 commander: 9.5.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -34158,7 +33982,7 @@ snapshots: '@wdio/types': 9.24.0 '@wdio/utils': 9.24.0 deepmerge-ts: 7.1.5 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@10.2.2) undici: 6.23.0 ws: 8.19.0 transitivePeerDependencies: