diff --git a/apps/desktop/package.json b/apps/desktop/package.json index b0050cc29c..f6e4f74d77 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -32,6 +32,7 @@ "@hypr/plugin-windows": "workspace:*", "@hypr/tiptap": "workspace:^", "@hypr/ui": "workspace:^", + "@hypr/utils": "workspace:^", "@iconify-icon/react": "^3.0.1", "@lobehub/icons": "^2.43.1", "@orama/highlight": "^0.1.9", diff --git a/apps/desktop/src/components/chat/body/index.tsx b/apps/desktop/src/components/chat/body/index.tsx index 03becb48d1..73c5b94c2d 100644 --- a/apps/desktop/src/components/chat/body/index.tsx +++ b/apps/desktop/src/components/chat/body/index.tsx @@ -1,7 +1,7 @@ import type { ChatStatus } from "ai"; import { useEffect, useRef } from "react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import type { HyprUIMessage } from "../../../chat/types"; import { useShell } from "../../../contexts/shell"; import { ChatBodyEmpty } from "./empty"; diff --git a/apps/desktop/src/components/chat/header.tsx b/apps/desktop/src/components/chat/header.tsx index a5342ceca2..43b22b7105 100644 --- a/apps/desktop/src/components/chat/header.tsx +++ b/apps/desktop/src/components/chat/header.tsx @@ -1,10 +1,10 @@ -import { formatDistanceToNow } from "date-fns"; +import { formatDistanceToNow } from "@hypr/utils"; import { ChevronDown, MessageCircle, PanelRightIcon, PictureInPicture2Icon, Plus, X } from "lucide-react"; import { useState } from "react"; import { Button } from "@hypr/ui/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "@hypr/ui/components/ui/dropdown-menu"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { useShell } from "../../contexts/shell"; import * as persisted from "../../store/tinybase/persisted"; diff --git a/apps/desktop/src/components/chat/input.tsx b/apps/desktop/src/components/chat/input.tsx index e4e4c330b6..1e3a5c553f 100644 --- a/apps/desktop/src/components/chat/input.tsx +++ b/apps/desktop/src/components/chat/input.tsx @@ -1,7 +1,7 @@ import Editor from "@hypr/tiptap/editor"; import { Button } from "@hypr/ui/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { MicIcon, PaperclipIcon, SendIcon } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; diff --git a/apps/desktop/src/components/chat/interactive.tsx b/apps/desktop/src/components/chat/interactive.tsx index 404dcd0e15..270dedf9a4 100644 --- a/apps/desktop/src/components/chat/interactive.tsx +++ b/apps/desktop/src/components/chat/interactive.tsx @@ -1,7 +1,7 @@ import { Resizable } from "re-resizable"; import { type ReactNode, useState } from "react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; export function InteractiveContainer( { diff --git a/apps/desktop/src/components/chat/message/normal.tsx b/apps/desktop/src/components/chat/message/normal.tsx index f72fb72ef8..92da98d49e 100644 --- a/apps/desktop/src/components/chat/message/normal.tsx +++ b/apps/desktop/src/components/chat/message/normal.tsx @@ -1,4 +1,4 @@ -import { formatDistanceToNow } from "date-fns"; +import { formatDistanceToNow } from "@hypr/utils"; import { BrainIcon, RotateCcw } from "lucide-react"; import { Streamdown } from "streamdown"; diff --git a/apps/desktop/src/components/chat/message/shared.tsx b/apps/desktop/src/components/chat/message/shared.tsx index 5d9fed28a4..05fca0b6d3 100644 --- a/apps/desktop/src/components/chat/message/shared.tsx +++ b/apps/desktop/src/components/chat/message/shared.tsx @@ -1,7 +1,7 @@ import { ChevronRight, Loader2 } from "lucide-react"; import { type ReactNode } from "react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; export function MessageContainer({ align = "start", diff --git a/apps/desktop/src/components/chat/trigger.tsx b/apps/desktop/src/components/chat/trigger.tsx index 5a553baf0f..99a345d372 100644 --- a/apps/desktop/src/components/chat/trigger.tsx +++ b/apps/desktop/src/components/chat/trigger.tsx @@ -1,4 +1,4 @@ -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; export function ChatTrigger({ onClick }: { onClick: () => void }) { return ( diff --git a/apps/desktop/src/components/main/body/calendars.tsx b/apps/desktop/src/components/main/body/calendars.tsx index 486a4af51c..6997d30f69 100644 --- a/apps/desktop/src/components/main/body/calendars.tsx +++ b/apps/desktop/src/components/main/body/calendars.tsx @@ -1,9 +1,9 @@ +import { addMonths, eachDayOfInterval, endOfMonth, format, getDay, isSameMonth, startOfMonth } from "@hypr/utils"; import { clsx } from "clsx"; -import { addMonths, eachDayOfInterval, endOfMonth, format, getDay, isSameMonth, startOfMonth } from "date-fns"; -import { CalendarIcon, FileTextIcon, Pen } from "lucide-react"; +import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon, FileTextIcon, Pen } from "lucide-react"; import { useState } from "react"; -import { CalendarStructure } from "@hypr/ui/components/block/calendar-structure"; +import { Button } from "@hypr/ui/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; import * as persisted from "../../../store/tinybase/persisted"; import { type Tab, useTabs } from "../../../store/zustand/tabs"; @@ -43,6 +43,7 @@ export function TabContentCalendar({ tab }: { tab: Tab }) { const days = eachDayOfInterval({ start: monthStart, end: monthEnd }).map((day) => format(day, "yyyy-MM-dd")); const startDayOfWeek = getDay(monthStart); const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const monthLabel = format(tab.month, "MMMM yyyy"); const handlePreviousMonth = () => { openCurrent({ ...tab, month: addMonths(tab.month, -1) }); @@ -58,18 +59,56 @@ export function TabContentCalendar({ tab }: { tab: Tab }) { return ( - - {days.map((day) => ( - - ))} - +
+
+
{monthLabel}
+
+ + + + + +
+
+
+
+ {weekDays.map((day) => ( +
+ {day} +
+ ))} +
+
+ {Array.from({ length: startDayOfWeek }).map((_, i) => ( +
+ ))} + {days.map((day) => ( + + ))} +
+
+
); } diff --git a/apps/desktop/src/components/main/body/contacts/organizations.tsx b/apps/desktop/src/components/main/body/contacts/organizations.tsx index d97e098565..4ddb275df0 100644 --- a/apps/desktop/src/components/main/body/contacts/organizations.tsx +++ b/apps/desktop/src/components/main/body/contacts/organizations.tsx @@ -1,7 +1,7 @@ import { Building2, CornerDownLeft, Pencil, User } from "lucide-react"; import React, { useState } from "react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import * as persisted from "../../../../store/tinybase/persisted"; import { ColumnHeader, type SortOption } from "./shared"; diff --git a/apps/desktop/src/components/main/body/contacts/people.tsx b/apps/desktop/src/components/main/body/contacts/people.tsx index 93ef88f8dd..b074dff91d 100644 --- a/apps/desktop/src/components/main/body/contacts/people.tsx +++ b/apps/desktop/src/components/main/body/contacts/people.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import * as persisted from "../../../../store/tinybase/persisted"; import { ColumnHeader, getInitials, type SortOption } from "./shared"; diff --git a/apps/desktop/src/components/main/body/index.tsx b/apps/desktop/src/components/main/body/index.tsx index e1d46c0824..8ae75e23ee 100644 --- a/apps/desktop/src/components/main/body/index.tsx +++ b/apps/desktop/src/components/main/body/index.tsx @@ -1,5 +1,5 @@ import { Button } from "@hypr/ui/components/ui/button"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { useRouteContext } from "@tanstack/react-router"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; diff --git a/apps/desktop/src/components/main/body/search.tsx b/apps/desktop/src/components/main/body/search.tsx index 9419ab4c96..a842107e14 100644 --- a/apps/desktop/src/components/main/body/search.tsx +++ b/apps/desktop/src/components/main/body/search.tsx @@ -2,7 +2,7 @@ import { Loader2Icon, SearchIcon, XIcon } from "lucide-react"; import { useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { useSearch } from "../../../contexts/search/ui"; export function Search() { diff --git a/apps/desktop/src/components/main/body/sessions/floating/generate.tsx b/apps/desktop/src/components/main/body/sessions/floating/generate.tsx index 2efc6c447b..4851e27240 100644 --- a/apps/desktop/src/components/main/body/sessions/floating/generate.tsx +++ b/apps/desktop/src/components/main/body/sessions/floating/generate.tsx @@ -1,7 +1,7 @@ import { SparklesIcon } from "lucide-react"; import { useState } from "react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import * as persisted from "../../../../../store/tinybase/persisted"; import { FloatingButton } from "./shared"; diff --git a/apps/desktop/src/components/main/body/sessions/floating/listen.tsx b/apps/desktop/src/components/main/body/sessions/floating/listen.tsx index d46de5f115..897d326709 100644 --- a/apps/desktop/src/components/main/body/sessions/floating/listen.tsx +++ b/apps/desktop/src/components/main/body/sessions/floating/listen.tsx @@ -1,8 +1,8 @@ import { Icon } from "@iconify-icon/react"; import useMediaQuery from "beautiful-react-hooks/useMediaQuery"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; -import { SoundIndicator } from "@hypr/ui/components/block/sound-indicator"; +import { DancingSticks } from "@hypr/ui/components/ui/dancing-sticks"; import { Spinner } from "@hypr/ui/components/ui/spinner"; import { useListener } from "../../../../../contexts/listener"; import { useSTTConnection } from "../../../../../hooks/useSTTConnection"; @@ -88,6 +88,20 @@ function BeforeMeeingButton({ tab }: { tab: Extract } ); } +function SoundIndicator({ value, color }: { value: number | Array; color?: string }) { + const [amplitude, setAmplitude] = useState(0); + + const u16max = 65535; + useEffect(() => { + const sample = Array.isArray(value) + ? (value.reduce((sum, v) => sum + v, 0) / value.length) / u16max + : value / u16max; + setAmplitude(Math.min(sample, 1)); + }, [value]); + + return ; +} + function DuringMeetingButton() { const stop = useListener((state) => state.stop); const { amplitude, seconds } = useListener(({ amplitude, seconds }) => ({ amplitude, seconds })); diff --git a/apps/desktop/src/components/main/body/sessions/note-input/index.tsx b/apps/desktop/src/components/main/body/sessions/note-input/index.tsx index 2950fcd5ed..00327b18c5 100644 --- a/apps/desktop/src/components/main/body/sessions/note-input/index.tsx +++ b/apps/desktop/src/components/main/body/sessions/note-input/index.tsx @@ -1,7 +1,7 @@ import { useRef } from "react"; import type { TiptapEditor } from "@hypr/tiptap/editor"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { useListener } from "../../../../../contexts/listener"; import * as persisted from "../../../../../store/tinybase/persisted"; import { type Tab, useTabs } from "../../../../../store/zustand/tabs"; diff --git a/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx b/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx index 3f6cb17fb9..5a930bc25c 100644 --- a/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx +++ b/apps/desktop/src/components/main/body/sessions/note-input/raw.tsx @@ -6,7 +6,7 @@ import { Effect, pipe } from "effect"; import { forwardRef, useCallback } from "react"; import type { PlaceholderFunction } from "@hypr/tiptap/shared"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import * as persisted from "../../../../../store/tinybase/persisted"; import { commands } from "../../../../../types/tauri.gen"; diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/event.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/event.tsx deleted file mode 100644 index 11d64260c6..0000000000 --- a/apps/desktop/src/components/main/body/sessions/outer-header/event.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useCallback, useState } from "react"; - -import { type Event, EventChip } from "@hypr/ui/components/block/event-chip"; -import { useQuery } from "../../../../../hooks/useQuery"; -import * as persisted from "../../../../../store/tinybase/persisted"; - -export function SessionEvent({ - sessionId, -}: { - sessionId: string; -}) { - const sessionRow = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID); - const [eventSearchQuery, setEventSearchQuery] = useState(""); - - const store = persisted.UI.useStore(persisted.STORE_ID); - - const eventRow = persisted.UI.useRow( - "events", - sessionRow.event_id || "dummy-event-id", - persisted.STORE_ID, - ); - - const event: Event | null = sessionRow.event_id && eventRow && eventRow.started_at && eventRow.ended_at - ? { - id: sessionRow.event_id, - name: eventRow.title ?? "", - start_date: eventRow.started_at, - end_date: eventRow.ended_at, - calendar_id: eventRow.calendar_id ?? undefined, - } - : null; - - const eventSearch = useQuery({ - enabled: !!store, - deps: [store, eventSearchQuery] as const, - queryFn: async (store, query) => { - const results: Event[] = []; - const now = new Date(); - - store!.forEachRow("events", (rowId, forEachCell) => { - let title: string | undefined; - let started_at: string | undefined; - let ended_at: string | undefined; - let calendar_id: string | undefined; - - forEachCell((cellId, cell) => { - if (cellId === "title") { - title = cell as string; - } else if (cellId === "started_at") { - started_at = cell as string; - } else if (cellId === "ended_at") { - ended_at = cell as string; - } else if (cellId === "calendar_id") { - calendar_id = cell as string; - } - }); - - if (!started_at || !ended_at) { - return; - } - - const eventEndDate = new Date(ended_at); - - if (eventEndDate >= now) { - return; - } - - if ( - query && title - && !title.toLowerCase().includes(query.toLowerCase()) - ) { - return; - } - - results.push({ - id: rowId, - name: title ?? "", - start_date: started_at, - end_date: ended_at, - calendar_id: calendar_id, - }); - }); - - results.sort((a, b) => new Date(b.start_date).getTime() - new Date(a.start_date).getTime()); - return results.slice(0, 20); - }, - }); - - const handleEventSelect = useCallback((eventId: string) => { - if (store) { - store.setCell("sessions", sessionId, "event_id", eventId); - } - }, [store, sessionId]); - - const handleEventDetach = useCallback(() => { - if (store) { - store.delCell("sessions", sessionId, "event_id"); - } - }, [store, sessionId]); - - const handleDateChange = useCallback((date: Date) => { - if (store) { - store.setCell("sessions", sessionId, "created_at", date.toISOString()); - } - }, [store, sessionId]); - - const handleJoinMeeting = useCallback((meetingLink: string) => { - window.open(meetingLink, "_blank"); - }, []); - - const handleViewInCalendar = useCallback(() => { - }, []); - - return ( - - ); -} diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/metadata.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/metadata.tsx index 107e598322..9e3b98ade5 100644 --- a/apps/desktop/src/components/main/body/sessions/outer-header/metadata.tsx +++ b/apps/desktop/src/components/main/body/sessions/outer-header/metadata.tsx @@ -1,9 +1,19 @@ -import { - MeetingMetadata, - MeetingMetadataChip, - MeetingParticipant, -} from "@hypr/ui/components/block/meeting-metadata-chip"; +import { LinkedInIcon } from "@hypr/ui/components/icons/linkedin"; +import { Avatar, AvatarFallback } from "@hypr/ui/components/ui/avatar"; +import { Button } from "@hypr/ui/components/ui/button"; +import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; +import { cn, formatDateRange, getMeetingDomain } from "@hypr/utils"; +import { + CalendarIcon, + CircleMinus, + CornerDownLeft, + ExternalLinkIcon, + MailIcon, + MapPinIcon, + SearchIcon, + VideoIcon, +} from "lucide-react"; import { useCallback, useMemo, useState } from "react"; import { useQuery } from "../../../../../hooks/useQuery"; @@ -11,6 +21,236 @@ import * as internal from "../../../../../store/tinybase/internal"; import * as persisted from "../../../../../store/tinybase/persisted"; import { useTabs } from "../../../../../store/zustand/tabs"; +interface MeetingParticipant { + id: string; + full_name?: string | null; + email?: string | null; + job_title?: string | null; + linkedin_username?: string | null; + organization?: { + id: string; + name: string; + } | null; +} + +interface MeetingMetadata { + id: string; + title: string; + started_at: string; + ended_at: string; + location?: string | null; + meeting_link?: string | null; + description?: string | null; + participants: MeetingParticipant[]; +} + +interface ParticipantChipProps { + participant: MeetingParticipant; + currentUserId?: string; + onClick?: () => void; + onRemove?: () => void; +} + +function ParticipantChip({ participant, currentUserId, onClick, onRemove }: ParticipantChipProps) { + const getInitials = (name: string) => { + return name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); + }; + + const displayName = participant.full_name + || (participant.id === currentUserId ? "You" : "Unknown"); + + return ( +
+
+ + + {participant.full_name ? getInitials(participant.full_name) : "?"} + + + + {displayName} + +
+ +
+ {participant.email && ( + e.stopPropagation()} + className="text-neutral-500 transition-colors hover:text-neutral-700 opacity-0 group-hover:opacity-100" + > + + + )} + {participant.linkedin_username && ( + e.stopPropagation()} + target="_blank" + rel="noopener noreferrer" + className="text-neutral-500 transition-colors hover:text-neutral-700 opacity-0 group-hover:opacity-100" + > + + + )} + +
+
+ ); +} + +interface ParticipantsSectionProps { + participants: MeetingParticipant[]; + searchQuery: string; + searchResults: MeetingParticipant[]; + onSearchChange?: (query: string) => void; + onParticipantAdd?: (participantId: string) => void; + onParticipantClick?: (participant: MeetingParticipant) => void; + onParticipantRemove?: (participantId: string) => void; + currentUserId?: string; +} + +function ParticipantsSection({ + participants, + searchQuery, + searchResults, + onSearchChange, + onParticipantAdd, + onParticipantClick, + onParticipantRemove, + currentUserId, +}: ParticipantsSectionProps) { + const [isFocused, setIsFocused] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(-1); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!searchQuery.trim() || searchResults.length === 0) { + return; + } + + if (e.key === "ArrowDown") { + e.preventDefault(); + setSelectedIndex((prev) => (prev < searchResults.length - 1 ? prev + 1 : prev)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setSelectedIndex((prev) => (prev > 0 ? prev - 1 : 0)); + } else if (e.key === "Enter") { + e.preventDefault(); + if (selectedIndex >= 0 && selectedIndex < searchResults.length) { + handleSelectParticipant(searchResults[selectedIndex].id); + } + } else if (e.key === "Escape") { + e.preventDefault(); + setIsFocused(false); + onSearchChange?.(""); + } + }; + + const handleSelectParticipant = (participantId: string) => { + onParticipantAdd?.(participantId); + onSearchChange?.(""); + setSelectedIndex(-1); + setIsFocused(true); + }; + + return ( +
+
Participants
+ {participants.length > 0 && ( +
+ {participants.map((participant) => ( + onParticipantClick?.(participant)} + onRemove={() => onParticipantRemove?.(participant.id)} + /> + ))} +
+ )} + +
+
+ + { + onSearchChange?.(e.target.value); + setSelectedIndex(-1); + }} + onKeyDown={handleKeyDown} + onFocus={() => setIsFocused(true)} + onBlur={() => { + setTimeout(() => setIsFocused(false), 200); + }} + placeholder="Add participant" + className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-500" + /> + {searchQuery.trim() && ( + + )} +
+ + {isFocused && searchQuery.trim() && ( +
+ {searchResults.length > 0 + ? ( + searchResults.map((participant, index) => ( + + )) + ) + : ( +
+ No matching participants found +
+ )} +
+ )} +
+
+ ); +} + export function SessionMetadata({ sessionId, currentUserId, @@ -19,6 +259,7 @@ export function SessionMetadata({ currentUserId: string | undefined; }) { const [participantSearchQuery, setParticipantSearchQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); const { openNew } = useTabs(); const { user_id } = internal.UI.useValues(internal.STORE_ID); @@ -207,17 +448,106 @@ export function SessionMetadata({ } }, [store, participantMappingIds]); + if (!meetingMetadata) { + return ( + + ); + } + return ( - + + + + + + +
+
{meetingMetadata.title}
+ +
+ + {meetingMetadata.location && ( + <> +
+ + + {meetingMetadata.location} + +
+
+ + )} + + {meetingMetadata.meeting_link && ( + <> +
+
+ + + {getMeetingDomain(meetingMetadata.meeting_link)} + +
+ +
+
+ + )} + +

+ {formatDateRange(meetingMetadata.started_at, meetingMetadata.ended_at)} +

+ +
+ + + + {meetingMetadata.description && ( + <> +
+
+ {meetingMetadata.description} +
+ + )} +
+ + ); } diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/participant.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/participant.tsx deleted file mode 100644 index ca4a611337..0000000000 --- a/apps/desktop/src/components/main/body/sessions/outer-header/participant.tsx +++ /dev/null @@ -1,271 +0,0 @@ -import { useCallback, useMemo, useState } from "react"; - -import * as internal from "../../../../../store/tinybase/internal"; -import * as persisted from "../../../../../store/tinybase/persisted"; - -import { Participant, ParticipantGroup, ParticipantsChip } from "@hypr/ui/components/block/participants-chip"; -import { useQuery } from "../../../../../hooks/useQuery"; -import { useTabs } from "../../../../../store/zustand/tabs"; - -export function SessionParticipants({ - sessionId, - currentUserId, -}: { - sessionId: string; - currentUserId: string | undefined; -}) { - const [participantSearchQuery, setParticipantSearchQuery] = useState(""); - const { openNew } = useTabs(); - const { user_id } = internal.UI.useValues(internal.STORE_ID); - - const store = persisted.UI.useStore(persisted.STORE_ID); - const indexes = persisted.UI.useIndexes(persisted.STORE_ID); - - const participantMappingIds = persisted.UI.useSliceRowIds( - persisted.INDEXES.sessionParticipantsBySession, - sessionId, - persisted.STORE_ID, - ); - - const orgSliceIds = persisted.UI.useSliceIds( - persisted.INDEXES.humansByOrg, - persisted.STORE_ID, - ); - - const participantGroups = useSessionParticipantGroups(sessionId, participantMappingIds, orgSliceIds); - - const participantSearch = useQuery({ - enabled: !!store && !!indexes && !!participantSearchQuery.trim(), - deps: [store, indexes, participantSearchQuery, sessionId] as const, - queryFn: async (store, indexes, query, sessionId) => { - const results: Participant[] = []; - const existingParticipantIds = new Set(); - - const participantMappings = indexes!.getSliceRowIds( - persisted.INDEXES.sessionParticipantsBySession, - sessionId, - ); - participantMappings?.forEach((mappingId: string) => { - const humanId = store!.getCell("mapping_session_participant", mappingId, "human_id") as string | undefined; - if (humanId) { - existingParticipantIds.add(humanId); - } - }); - - const normalizedQuery = query.toLowerCase(); - - store!.forEachRow("humans", (rowId, forEachCell) => { - if (existingParticipantIds.has(rowId)) { - return; - } - - let name: string | undefined; - let email: string | undefined; - let job_title: string | undefined; - let linkedin_username: string | undefined; - let org_id: string | undefined; - - forEachCell((cellId, cell) => { - if (cellId === "name") { - name = cell as string; - } else if (cellId === "email") { - email = cell as string; - } else if (cellId === "job_title") { - job_title = cell as string; - } else if (cellId === "linkedin_username") { - linkedin_username = cell as string; - } else if (cellId === "org_id") { - org_id = cell as string; - } - }); - - if ( - name && !name.toLowerCase().includes(normalizedQuery) - && (!email || !email.toLowerCase().includes(normalizedQuery)) - ) { - return; - } - - const org = org_id ? store!.getRow("organizations", org_id) : null; - - results.push({ - id: rowId, - full_name: name || null, - email: email || null, - job_title: job_title || null, - linkedin_username: linkedin_username || null, - organization: org - ? { - id: org_id!, - name: org.name as string, - } - : null, - }); - }); - - return results.slice(0, 10); - }, - }); - - const handleRemove = useCallback((participantId: string) => { - if (!store || !participantMappingIds) { - return; - } - - const mappingId = participantMappingIds.find((id) => { - const humanId = store.getCell("mapping_session_participant", id, "human_id"); - return humanId === participantId; - }); - - if (mappingId) { - store.delRow("mapping_session_participant", mappingId); - } - }, [store, participantMappingIds]); - - const handleSelect = useCallback((participantId: string) => { - if (!store) { - return; - } - - const mappingId = crypto.randomUUID(); - - store.setRow("mapping_session_participant", mappingId, { - user_id, - session_id: sessionId, - human_id: participantId, - created_at: new Date().toISOString(), - }); - - setParticipantSearchQuery(""); - }, [store, sessionId]); - - const handleAdd = useCallback((query: string) => { - if (!store) { - return; - } - - const humanId = crypto.randomUUID(); - - let orgId: string | undefined; - - store.forEachRow("organizations", (rowId, forEachCell) => { - let name: string | undefined; - forEachCell((cellId, cell) => { - if (cellId === "name") { - name = cell as string; - } - }); - if (name === "No organization") { - orgId = rowId; - } - }); - - if (!orgId) { - orgId = crypto.randomUUID(); - store.setRow("organizations", orgId, { - user_id, - name: "No organization", - created_at: new Date().toISOString(), - }); - } - - store.setRow("humans", humanId, { - user_id, - name: query, - email: "", - org_id: orgId, - created_at: new Date().toISOString(), - }); - - const mappingId = crypto.randomUUID(); - store.setRow("mapping_session_participant", mappingId, { - user_id, - session_id: sessionId, - human_id: humanId, - created_at: new Date().toISOString(), - }); - - setParticipantSearchQuery(""); - }, [store, sessionId, user_id]); - - return ( - { - openNew({ - type: "contacts", - active: true, - state: { - selectedPerson: participant.id, - selectedOrganization: null, - }, - }); - }} - onParticipantRemove={handleRemove} - onParticipantAdd={handleAdd} - onParticipantSelect={handleSelect} - searchQuery={participantSearchQuery} - onSearchChange={setParticipantSearchQuery} - searchResults={participantSearch.data ?? []} - allowMutate={true} - /> - ); -} - -function useSessionParticipantGroups( - sessionId: string, - participantMappingIds: string[] | undefined, - orgSliceIds: string[] | undefined, -): ParticipantGroup[] { - const store = persisted.UI.useStore(persisted.STORE_ID); - const indexes = persisted.UI.useIndexes(persisted.STORE_ID); - - return useMemo(() => { - if (!store || !indexes || !participantMappingIds) { - return []; - } - - const participantHumanIds = new Set(); - participantMappingIds.forEach((mappingId) => { - const humanId = store.getCell("mapping_session_participant", mappingId, "human_id") as string | undefined; - if (humanId) { - participantHumanIds.add(humanId); - } - }); - - const groups: ParticipantGroup[] = []; - - orgSliceIds?.forEach((orgId) => { - const humansInOrg = indexes.getSliceRowIds(persisted.INDEXES.humansByOrg, orgId); - const participantsInOrg = humansInOrg?.filter((humanId: string) => participantHumanIds.has(humanId)) ?? []; - - if (participantsInOrg.length === 0) { - return; - } - - const org = orgId ? store.getRow("organizations", orgId) : null; - - const participants = participantsInOrg.map((humanId: string) => { - const humanRow = store.getRow("humans", humanId); - return { - id: humanId, - full_name: humanRow?.name as string | null, - email: humanRow?.email as string | null, - job_title: humanRow?.job_title as string | null, - linkedin_username: humanRow?.linkedin_username as string | null, - organization: org && orgId ? { id: orgId, name: org.name as string } : null, - }; - }); - - groups.push({ - organization: org && orgId ? { id: orgId, name: org.name as string } : null, - participants, - }); - }); - - return groups; - }, [store, indexes, participantMappingIds, orgSliceIds, sessionId]); -} diff --git a/apps/desktop/src/components/main/body/sessions/title-input.tsx b/apps/desktop/src/components/main/body/sessions/title-input.tsx index eb9109b88e..a7dbb34f19 100644 --- a/apps/desktop/src/components/main/body/sessions/title-input.tsx +++ b/apps/desktop/src/components/main/body/sessions/title-input.tsx @@ -1,4 +1,4 @@ -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import * as persisted from "../../../../store/tinybase/persisted"; import { type Tab } from "../../../../store/zustand/tabs"; diff --git a/apps/desktop/src/components/main/sidebar/profile/banner.tsx b/apps/desktop/src/components/main/sidebar/profile/banner.tsx index e85ce20337..75bdb1d3cb 100644 --- a/apps/desktop/src/components/main/sidebar/profile/banner.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/banner.tsx @@ -2,7 +2,7 @@ import { X } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import { Button } from "@hypr/ui/components/ui/button"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; export function TryProBanner({ isDismissed, onDismiss }: { isDismissed: boolean; onDismiss: () => void }) { const handleDismiss = () => { diff --git a/apps/desktop/src/components/main/sidebar/profile/notification.tsx b/apps/desktop/src/components/main/sidebar/profile/notification.tsx index b57c1092ae..3441d6a3ea 100644 --- a/apps/desktop/src/components/main/sidebar/profile/notification.tsx +++ b/apps/desktop/src/components/main/sidebar/profile/notification.tsx @@ -3,7 +3,7 @@ import { Button } from "@hypr/ui/components/ui/button"; import { clsx } from "clsx"; import { ArrowLeft, ArrowRight, Bell, CheckCheck, MessageSquare } from "lucide-react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { MenuItem } from "./shared"; diff --git a/apps/desktop/src/components/main/sidebar/search/group.tsx b/apps/desktop/src/components/main/sidebar/search/group.tsx index 5e48503162..3758d3afa0 100644 --- a/apps/desktop/src/components/main/sidebar/search/group.tsx +++ b/apps/desktop/src/components/main/sidebar/search/group.tsx @@ -1,7 +1,7 @@ import { ChevronDownIcon } from "lucide-react"; import { useState } from "react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { type SearchGroup } from "../../../../contexts/search/ui"; import { SearchResultItem } from "./item"; diff --git a/apps/desktop/src/components/main/sidebar/search/index.tsx b/apps/desktop/src/components/main/sidebar/search/index.tsx index e68df541d7..312735f83a 100644 --- a/apps/desktop/src/components/main/sidebar/search/index.tsx +++ b/apps/desktop/src/components/main/sidebar/search/index.tsx @@ -1,6 +1,6 @@ import { SearchXIcon } from "lucide-react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { type GroupedSearchResults, useSearch } from "../../../../contexts/search/ui"; import { SearchResultGroup } from "./group"; diff --git a/apps/desktop/src/components/main/sidebar/search/item.tsx b/apps/desktop/src/components/main/sidebar/search/item.tsx index 1fad37b594..5ef9bdd4c6 100644 --- a/apps/desktop/src/components/main/sidebar/search/item.tsx +++ b/apps/desktop/src/components/main/sidebar/search/item.tsx @@ -1,7 +1,7 @@ import DOMPurify from "dompurify"; import { useCallback, useMemo } from "react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { type SearchResult } from "../../../../contexts/search/ui"; import * as persisted from "../../../../store/tinybase/persisted"; import { Tab, useTabs } from "../../../../store/zustand/tabs"; diff --git a/apps/desktop/src/components/main/sidebar/timeline/index.tsx b/apps/desktop/src/components/main/sidebar/timeline/index.tsx index 08a6d7be13..7aec271d5c 100644 --- a/apps/desktop/src/components/main/sidebar/timeline/index.tsx +++ b/apps/desktop/src/components/main/sidebar/timeline/index.tsx @@ -1,5 +1,5 @@ import { Button } from "@hypr/ui/components/ui/button"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { clsx } from "clsx"; import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; diff --git a/apps/desktop/src/components/main/sidebar/timeline/item.tsx b/apps/desktop/src/components/main/sidebar/timeline/item.tsx index 58c922448d..90e5155eeb 100644 --- a/apps/desktop/src/components/main/sidebar/timeline/item.tsx +++ b/apps/desktop/src/components/main/sidebar/timeline/item.tsx @@ -2,7 +2,7 @@ import { ExternalLink, SquareArrowOutUpRight, Trash2 } from "lucide-react"; import { useCallback, useMemo } from "react"; import { ContextMenuItem, ContextMenuShortcut } from "@hypr/ui/components/ui/context-menu"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import * as persisted from "../../../../store/tinybase/persisted"; import { Tab, useTabs } from "../../../../store/zustand/tabs"; import { id } from "../../../../utils"; diff --git a/apps/desktop/src/components/settings/ai/llm/configure.tsx b/apps/desktop/src/components/settings/ai/llm/configure.tsx index 70efb4c25d..e16d754483 100644 --- a/apps/desktop/src/components/settings/ai/llm/configure.tsx +++ b/apps/desktop/src/components/settings/ai/llm/configure.tsx @@ -2,7 +2,7 @@ import { useForm } from "@tanstack/react-form"; import { Streamdown } from "streamdown"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@hypr/ui/components/ui/accordion"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { aiProviderSchema } from "../../../../store/tinybase/internal"; import * as internal from "../../../../store/tinybase/internal"; import { FormField, useProvider } from "../shared"; diff --git a/apps/desktop/src/components/settings/ai/llm/select.tsx b/apps/desktop/src/components/settings/ai/llm/select.tsx index 0fdb93a3ae..a666f84693 100644 --- a/apps/desktop/src/components/settings/ai/llm/select.tsx +++ b/apps/desktop/src/components/settings/ai/llm/select.tsx @@ -2,7 +2,7 @@ import { useForm } from "@tanstack/react-form"; import { useMemo } from "react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import * as internal from "../../../../store/tinybase/internal"; import { ModelCombobox, openaiCompatibleListModels } from "../shared/model-combobox"; import { PROVIDERS } from "./shared"; diff --git a/apps/desktop/src/components/settings/ai/shared/model-combobox.tsx b/apps/desktop/src/components/settings/ai/shared/model-combobox.tsx index 3696e329b3..7e84549da3 100644 --- a/apps/desktop/src/components/settings/ai/shared/model-combobox.tsx +++ b/apps/desktop/src/components/settings/ai/shared/model-combobox.tsx @@ -12,7 +12,7 @@ import { CommandList, } from "@hypr/ui/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; export function ModelCombobox({ providerId, diff --git a/apps/desktop/src/components/settings/ai/stt/configure.tsx b/apps/desktop/src/components/settings/ai/stt/configure.tsx index 6adbc3d779..77d79a0c6a 100644 --- a/apps/desktop/src/components/settings/ai/stt/configure.tsx +++ b/apps/desktop/src/components/settings/ai/stt/configure.tsx @@ -10,7 +10,7 @@ import { commands as localSttCommands } from "@hypr/plugin-local-stt"; import type { SupportedSttModel } from "@hypr/plugin-local-stt"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@hypr/ui/components/ui/accordion"; import { Button } from "@hypr/ui/components/ui/button"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import * as internal from "../../../../store/tinybase/internal"; import { aiProviderSchema } from "../../../../store/tinybase/internal"; import { diff --git a/apps/desktop/src/components/settings/ai/stt/select.tsx b/apps/desktop/src/components/settings/ai/stt/select.tsx index 1ecdddf8fd..4650f49cac 100644 --- a/apps/desktop/src/components/settings/ai/stt/select.tsx +++ b/apps/desktop/src/components/settings/ai/stt/select.tsx @@ -2,7 +2,7 @@ import { useForm } from "@tanstack/react-form"; import { useQueries } from "@tanstack/react-query"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import * as internal from "../../../../store/tinybase/internal"; import { displayModelId, type ProviderId, PROVIDERS, sttModelQueries } from "./shared"; diff --git a/apps/desktop/src/contexts/audio-player/timeline.tsx b/apps/desktop/src/contexts/audio-player/timeline.tsx index ae1074a0d6..30ab6e575c 100644 --- a/apps/desktop/src/contexts/audio-player/timeline.tsx +++ b/apps/desktop/src/contexts/audio-player/timeline.tsx @@ -1,6 +1,6 @@ import { Pause, Play } from "lucide-react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { useAudioPlayer } from "./provider"; export function Timeline() { diff --git a/apps/desktop/src/devtool/index.tsx b/apps/desktop/src/devtool/index.tsx index 03eb535794..9234f2a475 100644 --- a/apps/desktop/src/devtool/index.tsx +++ b/apps/desktop/src/devtool/index.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from "react"; import { useStores } from "tinybase/ui-react"; import { commands as windowsCommands } from "@hypr/plugin-windows"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; import { useAutoCloser } from "../hooks/useAutoCloser"; import { type Store as PersistedStore, STORE_ID as STORE_ID_PERSISTED } from "../store/tinybase/persisted"; import { SeedDefinition, seeds } from "./seed/index"; diff --git a/apps/desktop/src/devtool/tinytick.tsx b/apps/desktop/src/devtool/tinytick.tsx index a19efb19b4..060510f9ee 100644 --- a/apps/desktop/src/devtool/tinytick.tsx +++ b/apps/desktop/src/devtool/tinytick.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useManager } from "tinytick/ui-react"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; interface TaskInfo { taskId: string; diff --git a/apps/desktop/src/routes/app/onboarding.tsx b/apps/desktop/src/routes/app/onboarding.tsx index 7d28cadb9b..a1d03aa8b0 100644 --- a/apps/desktop/src/routes/app/onboarding.tsx +++ b/apps/desktop/src/routes/app/onboarding.tsx @@ -10,10 +10,9 @@ import { z } from "zod"; import { commands as listenerCommands } from "@hypr/plugin-listener"; import { commands as windowsCommands } from "@hypr/plugin-windows"; import { Button } from "@hypr/ui/components/ui/button"; -import PushableButton from "@hypr/ui/components/ui/pushable-button"; import { Spinner } from "@hypr/ui/components/ui/spinner"; import { TextAnimate } from "@hypr/ui/components/ui/text-animate"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; const STEPS = ["welcome", "calendars", "permissions"] as const; @@ -77,12 +76,12 @@ function Welcome() { Where Conversations Stay Yours - goNext({ local: false })} className="mb-4 w-full max-w-sm hover:underline decoration-gray-100" > Get Started - +
{allPermissionsGranted && ( - goNext()} className="w-full"> + )} {!allPermissionsGranted && ( diff --git a/apps/desktop/src/routes/app/settings/_layout.tsx b/apps/desktop/src/routes/app/settings/_layout.tsx index 9697d51c15..6359ed8e2d 100644 --- a/apps/desktop/src/routes/app/settings/_layout.tsx +++ b/apps/desktop/src/routes/app/settings/_layout.tsx @@ -11,7 +11,7 @@ import { } from "lucide-react"; import { z } from "zod"; -import { cn } from "@hypr/ui/lib/utils"; +import { cn } from "@hypr/utils"; const TABS = [ "general", diff --git a/apps/desktop/src/store/tinybase/persisted.ts b/apps/desktop/src/store/tinybase/persisted.ts index 221606597d..a043d0df2b 100644 --- a/apps/desktop/src/store/tinybase/persisted.ts +++ b/apps/desktop/src/store/tinybase/persisted.ts @@ -1,4 +1,4 @@ -import { format } from "date-fns"; +import { format } from "@hypr/utils"; import * as _UI from "tinybase/ui-react/with-schemas"; import { createIndexes, diff --git a/apps/desktop/src/utils/timeline.ts b/apps/desktop/src/utils/timeline.ts index 5c4ffa35f7..7ba6a45237 100644 --- a/apps/desktop/src/utils/timeline.ts +++ b/apps/desktop/src/utils/timeline.ts @@ -1,6 +1,4 @@ -import { differenceInCalendarMonths, differenceInDays, isPast, startOfDay } from "date-fns"; - -import { format } from "date-fns"; +import { differenceInCalendarMonths, differenceInDays, format, isPast, startOfDay } from "@hypr/utils"; import type * as persisted from "../store/tinybase/persisted"; export type TimelineEventRow = { diff --git a/packages/ui/package.json b/packages/ui/package.json index c2464ac70f..53b43901e7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,6 +16,7 @@ "build": "tailwindcss -i ./src/styles/globals.css -o ./dist/globals.css --minify" }, "dependencies": { + "@hypr/utils": "workspace:^", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.10", diff --git a/packages/ui/src/components/block/calendar-structure.tsx b/packages/ui/src/components/block/calendar-structure.tsx deleted file mode 100644 index aa030746d0..0000000000 --- a/packages/ui/src/components/block/calendar-structure.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; -import { Button } from "../ui/button"; - -interface CalendarStructureProps { - monthLabel: string; - weekDays: string[]; - startDayOfWeek: number; - onPreviousMonth: () => void; - onNextMonth: () => void; - onToday: () => void; - children: React.ReactNode; -} - -export const CalendarStructure = ({ - monthLabel, - weekDays, - startDayOfWeek, - onPreviousMonth, - onNextMonth, - onToday, - children, -}: CalendarStructureProps) => { - return ( -
-
-
{monthLabel}
-
- - - - - -
-
-
-
- {weekDays.map((day) => ( -
- {day} -
- ))} -
-
- {Array.from({ length: startDayOfWeek }).map((_, i) => ( -
- ))} - {children} -
-
-
- ); -}; diff --git a/packages/ui/src/components/block/chat-panel-button.tsx b/packages/ui/src/components/block/chat-panel-button.tsx deleted file mode 100644 index c8190585bf..0000000000 --- a/packages/ui/src/components/block/chat-panel-button.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { MessageCircleMore } from "lucide-react"; -import { useEffect } from "react"; - -import { Button } from "@hypr/ui/components/ui/button"; -import { cn } from "@hypr/ui/lib/utils"; - -export function ChatPanelButton({ isExpanded, togglePanel }: { isExpanded: boolean; togglePanel: () => void }) { - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === "j" && (event.metaKey || event.ctrlKey)) { - event.preventDefault(); - togglePanel(); - } - }; - - document.addEventListener("keydown", handleKeyDown); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - }; - }, [togglePanel]); - - const handleClick = () => { - togglePanel(); - }; - - return ( - - ); -} diff --git a/packages/ui/src/components/block/event-chip.tsx b/packages/ui/src/components/block/event-chip.tsx deleted file mode 100644 index 423353653d..0000000000 --- a/packages/ui/src/components/block/event-chip.tsx +++ /dev/null @@ -1,340 +0,0 @@ -import { CalendarIcon, SearchIcon, SpeechIcon, VideoIcon, XIcon } from "lucide-react"; -import { useState } from "react"; - -import { cn } from "../../lib/utils"; -import { Button } from "../ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; - -const formatDate = (date: Date, format: string): string => { - const pad = (n: number) => n.toString().padStart(2, "0"); - - const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - - const replacements: Record = { - "yyyy": date.getFullYear().toString(), - "MMM": months[date.getMonth()], - "MM": pad(date.getMonth() + 1), - "d": date.getDate().toString(), - "dd": pad(date.getDate()), - "EEE": days[date.getDay()], - "h": (date.getHours() % 12 || 12).toString(), - "mm": pad(date.getMinutes()), - "a": date.getHours() >= 12 ? "PM" : "AM", - "p": `${date.getHours() % 12 || 12}:${pad(date.getMinutes())} ${date.getHours() >= 12 ? "PM" : "AM"}`, - }; - - return format.replace(/yyyy|MMM|MM|dd|EEE|h|mm|a|p|d/g, (token) => replacements[token]); -}; - -const isSameDay = (date1: Date, date2: Date): boolean => { - return date1.getFullYear() === date2.getFullYear() - && date1.getMonth() === date2.getMonth() - && date1.getDate() === date2.getDate(); -}; - -export interface Event { - id: string; - name: string; - start_date: string; - end_date: string; - note?: string; - meetingLink?: string | null; - calendar_id?: string; -} - -export interface EventChipProps { - event?: Event | null; - date: string; - isVeryNarrow?: boolean; - isNarrow?: boolean; - onEventSelect?: (eventId: string) => void; - onEventDetach?: () => void; - onDateChange?: (date: Date) => void; - onJoinMeeting?: (meetingLink: string) => void; - onViewInCalendar?: () => void; - searchQuery?: string; - onSearchChange?: (query: string) => void; - searchResults?: Event[]; - formatRelativeDate?: (date: string) => string; -} - -export function EventChip({ - event, - date, - isVeryNarrow = false, - onEventSelect, - onEventDetach, - onDateChange, - onJoinMeeting, - onViewInCalendar, - searchQuery = "", - onSearchChange, - searchResults = [], - formatRelativeDate = (d) => formatDate(new Date(d), "MMM d"), -}: EventChipProps) { - const [isOpen, setIsOpen] = useState(false); - const [activeTab, setActiveTab] = useState<"event" | "date">("event"); - - const getIcon = () => { - if (event?.meetingLink) { - return ; - } - if (event) { - return ; - } - return ; - }; - - const handleEventSelect = (eventId: string) => { - onEventSelect?.(eventId); - setIsOpen(false); - }; - - const handleEventDetach = () => { - onEventDetach?.(); - setIsOpen(false); - }; - - const handleDateChange = (date: Date) => { - onDateChange?.(date); - setIsOpen(false); - }; - - return ( - - -
{ - e.stopPropagation(); - }} - onMouseDown={(e) => { - e.stopPropagation(); - }} - title={formatDate(new Date(event?.start_date || date), "EEE, MMM d, yyyy") + " at " - + formatDate(new Date(event?.start_date || date), "h:mm a")} - > - {getIcon()} - {!isVeryNarrow && ( -

- {formatRelativeDate(event?.start_date || date)} -

- )} -
-
- - - {event - ? ( - - ) - : ( - setActiveTab(v as "event" | "date")}> - - Add Event - Change Date - - - - - - - - - - - )} - -
- ); -} - -function EventDetails({ - event, - onDetach, - onJoinMeeting, - onViewInCalendar, - formatRelativeDate, -}: { - event: Event; - onDetach?: () => void; - onJoinMeeting?: (meetingLink: string) => void; - onViewInCalendar?: () => void; - formatRelativeDate: (date: string) => string; -}) { - const startDate = new Date(event.start_date); - const endDate = new Date(event.end_date); - - const getDateString = () => { - const formattedStart = formatRelativeDate(event.start_date); - const startTime = formatDate(startDate, "p"); - const endTime = formatDate(endDate, "p"); - - if (isSameDay(startDate, endDate)) { - return `${formattedStart}, ${startTime} - ${endTime}`; - } else { - const formattedEnd = formatRelativeDate(event.end_date); - return `${formattedStart}, ${startTime} - ${formattedEnd}, ${endTime}`; - } - }; - - return ( -
- {onDetach && ( - - )} - -
{event.name}
-
{getDateString()}
- -
- {event.meetingLink && onJoinMeeting && ( - - )} - - {onViewInCalendar && ( - - )} -
- - {event.note && ( -
- {event.note} -
- )} -
- ); -} - -function EventSearch({ - searchQuery, - onSearchChange, - searchResults, - onEventSelect, -}: { - searchQuery: string; - onSearchChange?: (query: string) => void; - searchResults: Event[]; - onEventSelect?: (eventId: string) => void; -}) { - return ( -
-
- - onSearchChange?.(e.target.value)} - className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-500" - /> -
- - {searchResults.length === 0 - ? ( -
- {searchQuery ? "No matching events found." : "No past events available."} -
- ) - : ( -
- {searchResults.map((event) => ( - - ))} -
- )} -
- ); -} - -function DatePicker({ - currentDate, - onDateChange, -}: { - currentDate: string; - onDateChange?: (date: Date) => void; -}) { - const [selectedDate, setSelectedDate] = useState(currentDate); - - const handleDateChange = (e: React.ChangeEvent) => { - const dateStr = e.target.value; - if (dateStr) { - setSelectedDate(dateStr); - } - }; - - const handleSave = () => { - const newDate = new Date(selectedDate); - if (!isNaN(newDate.getTime())) { - onDateChange?.(newDate); - } - }; - - return ( -
- - - -
- ); -} diff --git a/packages/ui/src/components/block/listen-button.tsx b/packages/ui/src/components/block/listen-button.tsx deleted file mode 100644 index e14b7a6372..0000000000 --- a/packages/ui/src/components/block/listen-button.tsx +++ /dev/null @@ -1,473 +0,0 @@ -import { - CheckIcon, - ChevronDownIcon, - MicIcon, - MicOffIcon, - PlayIcon, - StopCircleIcon, - Volume2Icon, - VolumeOffIcon, -} from "lucide-react"; -import { useState } from "react"; - -import { SoundIndicator } from "@hypr/ui/components/block/sound-indicator"; -import { Button } from "@hypr/ui/components/ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; -import ShinyButton from "@hypr/ui/components/ui/shiny-button"; -import { Spinner } from "@hypr/ui/components/ui/spinner"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@hypr/ui/components/ui/tooltip"; -import { cn } from "@hypr/ui/lib/utils"; - -export type ListenButtonState = - | "loading" - | "inactive_meeting_not_ended" - | "inactive_meeting_ended" - | "running_active_this_session" - | "running_active_other_session"; - -export function ListenButton( - { - state, - isOnboarding, - disabled, - isCompact = false, - handleStartSession, - handleStopSession, - muted, - amplitude, - setMicMuted, - setSpeakerMuted, - currentDevice, - availableDevices, - handleSelectDevice, - handleOpenMicSelectorPopover, - }: { - state: ListenButtonState; - isOnboarding: boolean; - disabled: boolean; - isCompact?: boolean; - handleStartSession: () => void; - handleStopSession: () => void; - muted: { mic: boolean; speaker: boolean }; - amplitude: { mic: number; speaker: number }; - setMicMuted: () => void; - setSpeakerMuted: () => void; - currentDevice?: string; - availableDevices?: string[]; - handleSelectDevice: (device: string) => void; - handleOpenMicSelectorPopover?: () => void; - }, -) { - switch (state) { - case "loading": - return ( -
- -
- ); - - case "inactive_meeting_not_ended": - return isOnboarding - ? - : ; - - case "inactive_meeting_ended": - return isOnboarding - ? - : ; - - case "running_active_this_session": - return ( - - ); - - case "running_active_other_session": - return null; - } -} - -function WhenInactiveAndMeetingNotEnded({ disabled, onClick }: { disabled: boolean; onClick: () => void }) { - return ( - - - - - - Start recording - - - ); -} - -function WhenInactiveAndMeetingEnded( - { disabled, onClick, isCompact = false }: { disabled: boolean; onClick: () => void; isCompact?: boolean }, -) { - const [isHovered, setIsHovered] = useState(false); - - return ( - - ); -} - -function WhenInactiveAndMeetingNotEndedOnboarding({ disabled, onClick }: { disabled: boolean; onClick: () => void }) { - return ( - - - {disabled ? "Wait..." : "Play video"} - - ); -} - -function WhenInactiveAndMeetingEndedOnboarding({ disabled, onClick }: { disabled: boolean; onClick: () => void }) { - return ( - - ); -} - -function WhenActive( - { - disabled, - handleStopSession, - amplitude, - muted, - setMicMuted, - setSpeakerMuted, - currentDevice, - availableDevices, - handleSelectDevice, - handleOpenMicSelectorPopover, - }: { - disabled: boolean; - handleStopSession: () => void; - amplitude: { mic: number; speaker: number }; - muted: { mic: boolean; speaker: boolean }; - setMicMuted: () => void; - setSpeakerMuted: () => void; - currentDevice?: string; - availableDevices?: string[]; - handleSelectDevice: (device: string) => void; - handleOpenMicSelectorPopover?: () => void; - }, -) { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const handleStopSessionWrapper = () => { - setIsPopoverOpen(false); - handleStopSession(); - }; - - return ( - - - - - - - - - ); -} - -function RecordingControls({ - disabled, - micMuted, - speakerMuted, - setMicMuted, - setSpeakerMuted, - amplitude, - onStop, - currentDevice, - availableDevices, - handleSelectDevice, - handleOpenMicSelectorPopover, -}: { - disabled: boolean; - micMuted: boolean; - speakerMuted: boolean; - setMicMuted: () => void; - setSpeakerMuted: () => void; - amplitude: { mic: number; speaker: number }; - onStop: () => void; - currentDevice?: string; - availableDevices?: string[]; - handleSelectDevice: (device: string) => void; - handleOpenMicSelectorPopover?: () => void; -}) { - return ( - <> -
- - -
- - - - ); -} - -function StopButton({ onStop }: { onStop: (templateId: string | null) => void }) { - return ( - - ); -} - -function MicrophoneSelector({ - isMuted, - amplitude, - onToggleMuted, - disabled, - currentDevice, - availableDevices, - handleSelectDevice, - handleOpenPopover, -}: { - isMuted?: boolean; - amplitude: number; - onToggleMuted: () => void; - disabled?: boolean; - currentDevice?: string; - availableDevices?: string[]; - handleSelectDevice: (device: string) => void; - handleOpenPopover?: () => void; -}) { - const [isOpen, setIsOpen] = useState(false); - - const Icon = isMuted ? MicOffIcon : MicIcon; - - const handleOpenChange = (open: boolean) => { - setIsOpen(open); - if (open && handleOpenPopover) { - handleOpenPopover(); - } - }; - - return ( -
- -
- - -
- - - - -
- - -
-
- Microphone -
- - {!availableDevices - ? ( -
-
-

Loading devices...

-
- ) - : availableDevices?.length === 0 - ? ( -
-

No microphones found

-
- ) - : ( -
- {availableDevices?.map((device) => { - const isSelected = device === currentDevice; - return ( - - ); - })} -
- )} -
-
- -
- ); -} - -function SpeakerButton({ - isMuted, - onClick, - disabled, - amplitude, -}: { - isMuted?: boolean; - onClick: () => void; - disabled?: boolean; - amplitude: number; -}) { - const Icon = isMuted ? VolumeOffIcon : Volume2Icon; - - return ( -
- -
- ); -} diff --git a/packages/ui/src/components/block/meeting-metadata-chip.tsx b/packages/ui/src/components/block/meeting-metadata-chip.tsx deleted file mode 100644 index dff36c8fe5..0000000000 --- a/packages/ui/src/components/block/meeting-metadata-chip.tsx +++ /dev/null @@ -1,435 +0,0 @@ -import { - CalendarIcon, - CircleMinus, - CornerDownLeft, - ExternalLinkIcon, - MailIcon, - MapPinIcon, - SearchIcon, - VideoIcon, -} from "lucide-react"; -import { useState } from "react"; - -import { cn } from "../../lib/utils"; -import { LinkedInIcon } from "../icons/linkedin"; -import { Avatar, AvatarFallback } from "../ui/avatar"; -import { Button } from "../ui/button"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; - -const formatDate = (date: Date, format: string): string => { - const pad = (n: number) => n.toString().padStart(2, "0"); - - const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - - const replacements: Record = { - "yyyy": date.getFullYear().toString(), - "MMM": months[date.getMonth()], - "MM": pad(date.getMonth() + 1), - "d": date.getDate().toString(), - "dd": pad(date.getDate()), - "EEE": days[date.getDay()], - "h": (date.getHours() % 12 || 12).toString(), - "mm": pad(date.getMinutes()), - "a": date.getHours() >= 12 ? "PM" : "AM", - "p": `${date.getHours() % 12 || 12}:${pad(date.getMinutes())} ${date.getHours() >= 12 ? "PM" : "AM"}`, - }; - - return format.replace(/yyyy|MMM|MM|dd|EEE|h|mm|a|p|d/g, (token) => replacements[token]); -}; - -const isSameDay = (date1: Date, date2: Date): boolean => { - return date1.getFullYear() === date2.getFullYear() - && date1.getMonth() === date2.getMonth() - && date1.getDate() === date2.getDate(); -}; - -export interface MeetingMetadata { - id: string; - title: string; - started_at: string; - ended_at: string; - location?: string | null; - meeting_link?: string | null; - description?: string | null; - participants: MeetingParticipant[]; -} - -export interface MeetingParticipant { - id: string; - full_name?: string | null; - email?: string | null; - job_title?: string | null; - linkedin_username?: string | null; - organization?: { - id: string; - name: string; - } | null; -} - -export interface MeetingMetadataChipProps { - metadata?: MeetingMetadata | null; - isVeryNarrow?: boolean; - isNarrow?: boolean; - onJoinMeeting?: (meetingLink: string) => void; - onParticipantClick?: (participant: MeetingParticipant) => void; - onParticipantAdd?: (participantId: string) => void; - onParticipantRemove?: (participantId: string) => void; - participantSearchQuery?: string; - onParticipantSearchChange?: (query: string) => void; - participantSearchResults?: MeetingParticipant[]; - currentUserId?: string; - formatRelativeDate?: (date: string) => string; -} - -export function MeetingMetadataChip({ - metadata, - onJoinMeeting, - onParticipantClick, - onParticipantAdd, - onParticipantRemove, - participantSearchQuery = "", - onParticipantSearchChange, - participantSearchResults = [], - currentUserId, -}: MeetingMetadataChipProps) { - const [isOpen, setIsOpen] = useState(false); - - const getMeetingDomain = (url: string): string => { - try { - const urlObj = new URL(url); - return urlObj.hostname; - } catch { - return url; - } - }; - - if (!metadata) { - return ( - - ); - } - - return ( - - - - - - -
-
{metadata.title}
- -
- - {metadata.location && ( - <> -
- - - {metadata.location} - -
-
- - )} - - {metadata.meeting_link && ( - <> -
-
- - - {getMeetingDomain(metadata.meeting_link)} - -
- -
-
- - )} - -

- {formatDateRange(metadata.started_at, metadata.ended_at)} -

- -
- - - - {metadata.description && ( - <> -
-
- {metadata.description} -
- - )} -
- - - ); -} - -function formatDateRange(startDate: string, endDate: string): string { - const start = new Date(startDate); - const end = new Date(endDate); - - const formatTime = (date: Date) => formatDate(date, "p"); - const formatFullDate = (date: Date) => formatDate(date, "MMM d, yyyy"); - - if (isSameDay(start, end)) { - return `${formatFullDate(start)} ${formatTime(start)} to ${formatTime(end)}`; - } else { - return `${formatFullDate(start)} ${formatTime(start)} to ${formatFullDate(end)} ${formatTime(end)}`; - } -} - -interface ParticipantsSectionProps { - participants: MeetingParticipant[]; - searchQuery: string; - searchResults: MeetingParticipant[]; - onSearchChange?: (query: string) => void; - onParticipantAdd?: (participantId: string) => void; - onParticipantClick?: (participant: MeetingParticipant) => void; - onParticipantRemove?: (participantId: string) => void; - currentUserId?: string; -} - -function ParticipantsSection({ - participants, - searchQuery, - searchResults, - onSearchChange, - onParticipantAdd, - onParticipantClick, - onParticipantRemove, - currentUserId, -}: ParticipantsSectionProps) { - const [isFocused, setIsFocused] = useState(false); - const [selectedIndex, setSelectedIndex] = useState(-1); - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (!searchQuery.trim() || searchResults.length === 0) { - return; - } - - if (e.key === "ArrowDown") { - e.preventDefault(); - setSelectedIndex((prev) => (prev < searchResults.length - 1 ? prev + 1 : prev)); - } else if (e.key === "ArrowUp") { - e.preventDefault(); - setSelectedIndex((prev) => (prev > 0 ? prev - 1 : 0)); - } else if (e.key === "Enter") { - e.preventDefault(); - if (selectedIndex >= 0 && selectedIndex < searchResults.length) { - handleSelectParticipant(searchResults[selectedIndex].id); - } - } else if (e.key === "Escape") { - e.preventDefault(); - setIsFocused(false); - onSearchChange?.(""); - } - }; - - const handleSelectParticipant = (participantId: string) => { - onParticipantAdd?.(participantId); - onSearchChange?.(""); - setSelectedIndex(-1); - setIsFocused(true); // Keep focus on input - }; - - return ( -
-
Participants
- - {/* Existing Participants Chips */} - {participants.length > 0 && ( -
- {participants.map((participant) => ( - onParticipantClick?.(participant)} - onRemove={() => onParticipantRemove?.(participant.id)} - /> - ))} -
- )} - - {/* Search Input */} -
-
- - { - onSearchChange?.(e.target.value); - setSelectedIndex(-1); - }} - onKeyDown={handleKeyDown} - onFocus={() => setIsFocused(true)} - onBlur={() => { - // Delay to allow click on results - setTimeout(() => setIsFocused(false), 200); - }} - placeholder="Add participant" - className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-500" - /> - {searchQuery.trim() && ( - - )} -
- - {/* Search Results Dropdown */} - {isFocused && searchQuery.trim() && ( -
- {searchResults.length > 0 - ? ( - searchResults.map((participant, index) => ( - - )) - ) - : ( -
- No matching participants found -
- )} -
- )} -
-
- ); -} - -interface ParticipantChipProps { - participant: MeetingParticipant; - currentUserId?: string; - onClick?: () => void; - onRemove?: () => void; -} - -function ParticipantChip({ participant, currentUserId, onClick, onRemove }: ParticipantChipProps) { - const getInitials = (name: string) => { - return name - .split(" ") - .map((n) => n[0]) - .join("") - .toUpperCase() - .slice(0, 2); - }; - - const displayName = participant.full_name - || (participant.id === currentUserId ? "You" : "Unknown"); - - return ( -
-
- - - {participant.full_name ? getInitials(participant.full_name) : "?"} - - - - {displayName} - -
- -
- {participant.email && ( - e.stopPropagation()} - className="text-neutral-500 transition-colors hover:text-neutral-700 opacity-0 group-hover:opacity-100" - > - - - )} - {participant.linkedin_username && ( - e.stopPropagation()} - target="_blank" - rel="noopener noreferrer" - className="text-neutral-500 transition-colors hover:text-neutral-700 opacity-0 group-hover:opacity-100" - > - - - )} - -
-
- ); -} diff --git a/packages/ui/src/components/block/participants-chip.tsx b/packages/ui/src/components/block/participants-chip.tsx deleted file mode 100644 index 01b7d26d9e..0000000000 --- a/packages/ui/src/components/block/participants-chip.tsx +++ /dev/null @@ -1,325 +0,0 @@ -import { CircleMinus, CornerDownLeft, MailIcon, SearchIcon, Users2Icon } from "lucide-react"; -import { cn } from "../../lib/utils"; -import { LinkedInIcon } from "../icons/linkedin"; -import { Avatar, AvatarFallback } from "../ui/avatar"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; - -export interface Participant { - id: string; - full_name?: string | null; - email?: string | null; - job_title?: string | null; - linkedin_username?: string | null; - organization?: { - id: string; - name: string; - } | null; -} - -export interface ParticipantGroup { - organization: { id: string; name: string } | null; - participants: Participant[]; -} - -interface ParticipantsChipProps { - participants: ParticipantGroup[]; - currentUserId?: string; - isVeryNarrow?: boolean; - isNarrow?: boolean; - onParticipantClick?: (participant: Participant) => void; - onParticipantRemove?: (participantId: string) => void; - onParticipantAdd?: (query: string) => void; - onParticipantSelect?: (participantId: string) => void; - searchQuery?: string; - onSearchChange?: (query: string) => void; - searchResults?: Participant[]; - allowMutate?: boolean; -} - -export function ParticipantsChip({ - participants, - currentUserId, - isVeryNarrow = false, - isNarrow = false, - onParticipantClick, - onParticipantRemove, - onParticipantAdd, - onParticipantSelect, - searchQuery = "", - onSearchChange, - searchResults = [], - allowMutate = true, -}: ParticipantsChipProps) { - const count = participants.reduce((acc, group) => acc + group.participants.length, 0); - - const getButtonText = () => { - if (count === 0) { - return isVeryNarrow ? "Add" : isNarrow ? "Add people" : "Add participants"; - } - - if (isVeryNarrow || isNarrow) { - return count.toString(); - } - - const firstParticipant = participants.find(g => g.participants.length > 0)?.participants[0]; - if (!firstParticipant) { - return "Add participants"; - } - - if (firstParticipant.id === currentUserId && !firstParticipant.full_name) { - return "You"; - } - - return firstParticipant.full_name || "??"; - }; - - return ( - - -
- - {getButtonText()} - {count > 1 && !isVeryNarrow && !isNarrow && + {count - 1}} -
-
- - - {participants.length === 0 && allowMutate - ? ( - - ) - : ( -
- - {allowMutate && ( - - )} -
- )} -
-
- ); -} - -function ParticipantList({ - participants, - currentUserId, - onParticipantClick, - onParticipantRemove, - allowMutate, -}: { - participants: ParticipantGroup[]; - currentUserId?: string; - onParticipantClick?: (participant: Participant) => void; - onParticipantRemove?: (participantId: string) => void; - allowMutate?: boolean; -}) { - return ( -
- {participants.map((group, index) => ( -
-
- {group.organization?.name || "No organization"} -
-
- {group.participants.map((participant) => ( - onParticipantClick?.(participant)} - onRemove={() => - onParticipantRemove?.(participant.id)} - allowRemove={allowMutate} - /> - ))} -
-
- ))} -
- ); -} - -function ParticipantItem({ - participant, - currentUserId, - onClick, - onRemove, - allowRemove, -}: { - participant: Participant; - currentUserId?: string; - onClick?: () => void; - onRemove?: () => void; - allowRemove?: boolean; -}) { - const getInitials = (name: string) => { - return name - .split(" ") - .map((n) => n[0]) - .join("") - .toUpperCase() - .slice(0, 2); - }; - - return ( -
-
-
-
- - - {participant.full_name ? getInitials(participant.full_name) : "?"} - - -
- {allowRemove && ( - - )} -
-
- - {participant.full_name || (participant.id === currentUserId ? "You" : "Unknown")} - - {participant.job_title && {participant.job_title}} -
-
- - -
- ); -} - -function AddParticipantInput({ - value, - onChange, - onSubmit, - searchResults, - onSelectResult, -}: { - value: string; - onChange?: (value: string) => void; - onSubmit?: (value: string) => void; - searchResults?: Participant[]; - onSelectResult?: (participantId: string) => void; -}) { - return ( -
{ - e.preventDefault(); - if (value.trim()) { - onSubmit?.(value.trim()); - } - }} - > -
-
- - onChange?.(e.target.value)} - placeholder="Find person" - className="w-full bg-transparent text-sm focus:outline-none placeholder:text-neutral-500" - /> - {value.trim() && ( - - )} -
- - {value.trim() && searchResults && ( -
- {searchResults.map((participant) => ( - - ))} - {searchResults.length === 0 && ( - - )} -
- )} -
-
- ); -} diff --git a/packages/ui/src/components/block/sound-indicator.tsx b/packages/ui/src/components/block/sound-indicator.tsx deleted file mode 100644 index 11f014b20e..0000000000 --- a/packages/ui/src/components/block/sound-indicator.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { DancingSticks } from "@hypr/ui/components/ui/dancing-sticks"; -import { useEffect, useState } from "react"; - -export function SoundIndicator({ value, color }: { value: number | Array; color?: string }) { - const [amplitude, setAmplitude] = useState(0); - - const u16max = 65535; - useEffect(() => { - const sample = Array.isArray(value) - ? (value.reduce((sum, v) => sum + v, 0) / value.length) / u16max - : value / u16max; - setAmplitude(Math.min(sample, 1)); - }, [value]); - - return ; -} diff --git a/packages/ui/src/components/block/tab-header.tsx b/packages/ui/src/components/block/tab-header.tsx deleted file mode 100644 index 725d3ad9b2..0000000000 --- a/packages/ui/src/components/block/tab-header.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { cn } from "@hypr/ui/lib/utils"; -import { useEffect } from "react"; - -interface TabHeaderProps { - isEnhancing?: boolean; - onVisibilityChange?: (isVisible: boolean) => void; - currentTab: "raw" | "enhanced" | "transcript"; - onTabChange: (tab: "raw" | "enhanced" | "transcript") => void; - isCurrentlyRecording: boolean; - shouldShowTab: boolean; - shouldShowEnhancedTab: boolean; -} - -export const TabHeader = ({ - isEnhancing, - onVisibilityChange, - onTabChange, - currentTab, - isCurrentlyRecording, - shouldShowTab, - shouldShowEnhancedTab, -}: TabHeaderProps) => { - useEffect(() => { - // when enhancement starts (immediately after recording ends) -> switch to enhanced note - if (isEnhancing) { - onTabChange("enhanced"); - } - }, [isEnhancing]); - - // set default tab to 'raw' for blank notes (no meeting session) - useEffect(() => { - if (!shouldShowTab) { - onTabChange("raw"); - } - }, [shouldShowTab, onTabChange]); - - // notify parent when visibility changes - useEffect(() => { - if (onVisibilityChange) { - onVisibilityChange(shouldShowTab ?? false); - } - }, [shouldShowTab, onVisibilityChange]); - - // don't render tabs at all for blank notes (no meeting session) - if (!shouldShowTab) { - return null; - } - - return ( -
- {/* Tab container */} -
-
-
- {/* Raw Note Tab */} - - {/* Enhanced Note Tab - show when session ended OR transcript exists OR enhanced memo exists */} - {shouldShowEnhancedTab && ( - - )} - - - - {/* Transcript Tab - always show */} - -
-
-
-
- ); -}; diff --git a/packages/ui/src/components/block/title-input.tsx b/packages/ui/src/components/block/title-input.tsx deleted file mode 100644 index 0fb256a099..0000000000 --- a/packages/ui/src/components/block/title-input.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { type ChangeEvent, type KeyboardEvent, useEffect, useRef } from "react"; - -interface TitleInputProps { - value: string; - onChange: (e: ChangeEvent) => void; - onNavigateToEditor?: () => void; - editable?: boolean; - isGenerating?: boolean; - autoFocus?: boolean; -} - -export default function TitleInput({ - value, - onChange, - onNavigateToEditor, - editable, - isGenerating = false, - autoFocus = false, -}: TitleInputProps) { - const inputRef = useRef(null); - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter" || e.key === "Tab") { - e.preventDefault(); - onNavigateToEditor?.(); - } - }; - - const getPlaceholder = () => { - if (isGenerating) { - return "Generating title..."; - } - return "Untitled"; - }; - - useEffect(() => { - if (autoFocus && editable && !isGenerating && inputRef.current) { - const timeoutId = setTimeout(() => { - inputRef.current?.focus(); - }, 200); - - return () => clearTimeout(timeoutId); - } - }, [autoFocus, editable, isGenerating]); - - return ( - - ); -} diff --git a/packages/ui/src/components/ui/accordion.tsx b/packages/ui/src/components/ui/accordion.tsx index 9500dcb31d..9b688387e6 100644 --- a/packages/ui/src/components/ui/accordion.tsx +++ b/packages/ui/src/components/ui/accordion.tsx @@ -1,9 +1,9 @@ +import { cn } from "@hypr/utils"; + import * as AccordionPrimitive from "@radix-ui/react-accordion"; import { ChevronDown } from "lucide-react"; import * as React from "react"; -import { cn } from "@hypr/ui/lib/utils"; - const Accordion = AccordionPrimitive.Root; const AccordionItem = React.forwardRef< diff --git a/packages/ui/src/components/ui/avatar.tsx b/packages/ui/src/components/ui/avatar.tsx index 6c3aa87675..2298161adf 100644 --- a/packages/ui/src/components/ui/avatar.tsx +++ b/packages/ui/src/components/ui/avatar.tsx @@ -1,8 +1,8 @@ +import { cn } from "@hypr/utils"; + import * as AvatarPrimitive from "@radix-ui/react-avatar"; import * as React from "react"; -import { cn } from "../../lib/utils"; - interface AvatarProps extends React.ComponentPropsWithoutRef { variant?: "rounded" | "circle"; } diff --git a/packages/ui/src/components/ui/badge.tsx b/packages/ui/src/components/ui/badge.tsx index e98e08a092..f7493dae82 100644 --- a/packages/ui/src/components/ui/badge.tsx +++ b/packages/ui/src/components/ui/badge.tsx @@ -1,8 +1,8 @@ +import { cn } from "@hypr/utils"; + import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; -import { cn } from "../../lib/utils"; - const badgeVariants = cva( "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { diff --git a/packages/ui/src/components/ui/bottom-sheet.tsx b/packages/ui/src/components/ui/bottom-sheet.tsx index 6e86f3131d..e57cfcb909 100644 --- a/packages/ui/src/components/ui/bottom-sheet.tsx +++ b/packages/ui/src/components/ui/bottom-sheet.tsx @@ -1,8 +1,8 @@ +import { cn } from "@hypr/utils"; + import { AnimatePresence, motion } from "motion/react"; import * as React from "react"; -import { cn } from "../../lib/utils"; - interface BottomSheetProps { open: boolean; onClose: () => void; diff --git a/packages/ui/src/components/ui/breadcrumb.tsx b/packages/ui/src/components/ui/breadcrumb.tsx index f607c474d2..5fdc0e018a 100644 --- a/packages/ui/src/components/ui/breadcrumb.tsx +++ b/packages/ui/src/components/ui/breadcrumb.tsx @@ -1,9 +1,9 @@ +import { cn } from "@hypr/utils"; + import { Slot } from "@radix-ui/react-slot"; import { ChevronRight, MoreHorizontal } from "lucide-react"; import * as React from "react"; -import { cn } from "../../lib/utils"; - const Breadcrumb = React.forwardRef< HTMLElement, React.ComponentPropsWithoutRef<"nav"> & { diff --git a/packages/ui/src/components/ui/button.tsx b/packages/ui/src/components/ui/button.tsx index 0802f22a89..a6cd8ff3ba 100644 --- a/packages/ui/src/components/ui/button.tsx +++ b/packages/ui/src/components/ui/button.tsx @@ -1,9 +1,9 @@ +import { cn } from "@hypr/utils"; + import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; -import { cn } from "@hypr/ui/lib/utils"; - const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { diff --git a/packages/ui/src/components/ui/card.tsx b/packages/ui/src/components/ui/card.tsx index 691e8102c3..e644ca0d42 100644 --- a/packages/ui/src/components/ui/card.tsx +++ b/packages/ui/src/components/ui/card.tsx @@ -1,6 +1,6 @@ -import { forwardRef } from "react"; +import { cn } from "@hypr/utils"; -import { cn } from "../../lib/utils"; +import { forwardRef } from "react"; interface CardProps extends React.HTMLAttributes { variant?: "default" | "outline" | "ghost"; diff --git a/packages/ui/src/components/ui/carousel.tsx b/packages/ui/src/components/ui/carousel.tsx index 481df459ab..74ca58bb33 100644 --- a/packages/ui/src/components/ui/carousel.tsx +++ b/packages/ui/src/components/ui/carousel.tsx @@ -1,8 +1,9 @@ +import { cn } from "@hypr/utils"; + import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react"; import { ArrowLeft, ArrowRight } from "lucide-react"; import * as React from "react"; -import { cn } from "../../lib/utils"; import { Button } from "./button"; type CarouselApi = UseEmblaCarouselType[1]; diff --git a/packages/ui/src/components/ui/checkbox.tsx b/packages/ui/src/components/ui/checkbox.tsx index 5dedb0ea12..cf8e6c504a 100644 --- a/packages/ui/src/components/ui/checkbox.tsx +++ b/packages/ui/src/components/ui/checkbox.tsx @@ -1,9 +1,9 @@ +import { cn } from "@hypr/utils"; + import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; import { Check } from "lucide-react"; import * as React from "react"; -import { cn } from "@hypr/ui/lib/utils"; - const Checkbox = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/packages/ui/src/components/ui/command.tsx b/packages/ui/src/components/ui/command.tsx index a3743175e6..45cf440f42 100644 --- a/packages/ui/src/components/ui/command.tsx +++ b/packages/ui/src/components/ui/command.tsx @@ -1,11 +1,11 @@ +import { Dialog, DialogContent } from "@hypr/ui/components/ui/dialog"; +import { cn } from "@hypr/utils"; + import { type DialogProps } from "@radix-ui/react-dialog"; import { Command as CommandPrimitive } from "cmdk"; import { Search } from "lucide-react"; import * as React from "react"; -import { Dialog, DialogContent } from "@hypr/ui/components/ui/dialog"; -import { cn } from "@hypr/ui/lib/utils"; - const Command = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/packages/ui/src/components/ui/context-menu.tsx b/packages/ui/src/components/ui/context-menu.tsx index 5f591262cb..49f402ab83 100644 --- a/packages/ui/src/components/ui/context-menu.tsx +++ b/packages/ui/src/components/ui/context-menu.tsx @@ -1,19 +1,14 @@ +import { cn } from "@hypr/utils"; + import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"; import { Check, ChevronRight, Circle } from "lucide-react"; import * as React from "react"; -import { cn } from "../../lib/utils"; - const ContextMenu = ContextMenuPrimitive.Root; - const ContextMenuTrigger = ContextMenuPrimitive.Trigger; - const ContextMenuGroup = ContextMenuPrimitive.Group; - const ContextMenuPortal = ContextMenuPrimitive.Portal; - const ContextMenuSub = ContextMenuPrimitive.Sub; - const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; const ContextMenuSubTrigger = React.forwardRef< diff --git a/packages/ui/src/components/ui/cursor-tooltip.tsx b/packages/ui/src/components/ui/cursor-tooltip.tsx deleted file mode 100644 index bf95c2ac38..0000000000 --- a/packages/ui/src/components/ui/cursor-tooltip.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { ReactNode, useCallback, useState } from "react"; -import ReactDOM from "react-dom"; - -interface CursorFollowTooltipProps { - children: React.ReactElement; - tooltipContent: ReactNode; - offset?: { x: number; y: number }; - disabled?: boolean; -} - -const CursorFollowTooltip: React.FC = ({ - children, - tooltipContent, - offset = { x: 15, y: 15 }, // Default offset from cursor - disabled = false, -}) => { - const [visible, setVisible] = useState(false); - const [position, setPosition] = useState({ x: 0, y: 0 }); - - const handleMouseMove = useCallback((e: React.MouseEvent) => { - setPosition({ x: e.clientX + offset.x, y: e.clientY + offset.y }); - }, [offset]); - - const handleMouseEnter = useCallback(() => { - if (!disabled) { - setVisible(true); - } - }, [disabled]); - - const handleMouseLeave = useCallback(() => { - setVisible(false); - }, []); - - // Check if document is defined (for SSR compatibility) - if (typeof document === "undefined") { - return children; - } - - const childWithMouseEvents = React.cloneElement(children, { - ...children.props, // Preserve existing props - onMouseMove: (e: React.MouseEvent) => { - handleMouseMove(e); - if (children.props.onMouseMove) { - children.props.onMouseMove(e); - } - }, - onMouseEnter: (e: React.MouseEvent) => { - handleMouseEnter(); - if (children.props.onMouseEnter) { - children.props.onMouseEnter(e); - } - }, - onMouseLeave: (e: React.MouseEvent) => { - handleMouseLeave(); - if (children.props.onMouseLeave) { - children.props.onMouseLeave(e); - } - }, - }); - - return ( - <> - {childWithMouseEvents} - {visible && !disabled && ReactDOM.createPortal( -
- {tooltipContent} -
, - document.body, - )} - - ); -}; - -export default CursorFollowTooltip; diff --git a/packages/ui/src/components/ui/dialog.tsx b/packages/ui/src/components/ui/dialog.tsx index 433b7f165f..d7bf61c528 100644 --- a/packages/ui/src/components/ui/dialog.tsx +++ b/packages/ui/src/components/ui/dialog.tsx @@ -1,15 +1,12 @@ +import { cn } from "@hypr/utils"; + import * as DialogPrimitive from "@radix-ui/react-dialog"; import { X } from "lucide-react"; import * as React from "react"; -import { cn } from "@hypr/ui/lib/utils"; - const Dialog = DialogPrimitive.Root; - const DialogTrigger = DialogPrimitive.Trigger; - const DialogPortal = DialogPrimitive.Portal; - const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< diff --git a/packages/ui/src/components/ui/dropdown-menu.tsx b/packages/ui/src/components/ui/dropdown-menu.tsx index 060c81424a..0773fe2f3b 100644 --- a/packages/ui/src/components/ui/dropdown-menu.tsx +++ b/packages/ui/src/components/ui/dropdown-menu.tsx @@ -1,19 +1,14 @@ +import { cn } from "@hypr/utils"; + import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import { Check, ChevronRight, Circle } from "lucide-react"; import * as React from "react"; -import { cn } from "../../lib/utils"; - const DropdownMenu = DropdownMenuPrimitive.Root; - const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; - const DropdownMenuGroup = DropdownMenuPrimitive.Group; - const DropdownMenuPortal = DropdownMenuPrimitive.Portal; - const DropdownMenuSub = DropdownMenuPrimitive.Sub; - const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; const DropdownMenuSubTrigger = React.forwardRef< diff --git a/packages/ui/src/components/ui/form.tsx b/packages/ui/src/components/ui/form.tsx index b5cad285f3..37e5fbcb49 100644 --- a/packages/ui/src/components/ui/form.tsx +++ b/packages/ui/src/components/ui/form.tsx @@ -1,9 +1,10 @@ +import { cn } from "@hypr/utils"; + import * as LabelPrimitive from "@radix-ui/react-label"; import { Slot } from "@radix-ui/react-slot"; import * as React from "react"; import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form"; -import { cn } from "../../lib/utils"; import { Label } from "./label"; const Form = FormProvider; diff --git a/packages/ui/src/components/ui/input-group.tsx b/packages/ui/src/components/ui/input-group.tsx index f82f50b2fe..2cafa16c6c 100644 --- a/packages/ui/src/components/ui/input-group.tsx +++ b/packages/ui/src/components/ui/input-group.tsx @@ -1,10 +1,11 @@ +import { cn } from "@hypr/utils"; + import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; -import { Button } from "@hypr/ui/components/ui/button"; -import { Input } from "@hypr/ui/components/ui/input"; -import { Textarea } from "@hypr/ui/components/ui/textarea"; -import { cn } from "@hypr/ui/lib/utils"; +import { Button } from "./button"; +import { Input } from "./input"; +import { Textarea } from "./textarea"; function InputGroup({ className, ...props }: React.ComponentProps<"div">) { return ( diff --git a/packages/ui/src/components/ui/input.tsx b/packages/ui/src/components/ui/input.tsx index 29a6dc7330..bd8386f0d7 100644 --- a/packages/ui/src/components/ui/input.tsx +++ b/packages/ui/src/components/ui/input.tsx @@ -1,6 +1,6 @@ -import * as React from "react"; +import { cn } from "@hypr/utils"; -import { cn } from "@hypr/ui/lib/utils"; +import * as React from "react"; const Input = React.forwardRef>( ({ className, type, ...props }, ref) => { diff --git a/packages/ui/src/components/ui/label.tsx b/packages/ui/src/components/ui/label.tsx index 8fd11ee94e..f8b55343b0 100644 --- a/packages/ui/src/components/ui/label.tsx +++ b/packages/ui/src/components/ui/label.tsx @@ -1,9 +1,9 @@ +import { cn } from "@hypr/utils"; + import * as LabelPrimitive from "@radix-ui/react-label"; import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; -import { cn } from "../../lib/utils"; - const labelVariants = cva( "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", ); diff --git a/packages/ui/src/components/ui/modal.tsx b/packages/ui/src/components/ui/modal.tsx index bf97396dfc..c45f7c6f9f 100644 --- a/packages/ui/src/components/ui/modal.tsx +++ b/packages/ui/src/components/ui/modal.tsx @@ -1,6 +1,6 @@ -import * as React from "react"; +import { cn } from "@hypr/utils"; -import { cn } from "../../lib/utils"; +import * as React from "react"; interface ModalProps { open: boolean; diff --git a/packages/ui/src/components/ui/particles.tsx b/packages/ui/src/components/ui/particles.tsx deleted file mode 100644 index b78c7766a7..0000000000 --- a/packages/ui/src/components/ui/particles.tsx +++ /dev/null @@ -1,304 +0,0 @@ -import React, { ComponentPropsWithoutRef, useEffect, useRef, useState } from "react"; - -import { cn } from "../../lib/utils"; - -interface MousePosition { - x: number; - y: number; -} - -function MousePosition(): MousePosition { - const [mousePosition, setMousePosition] = useState({ - x: 0, - y: 0, - }); - - useEffect(() => { - const handleMouseMove = (event: MouseEvent) => { - setMousePosition({ x: event.clientX, y: event.clientY }); - }; - - window.addEventListener("mousemove", handleMouseMove); - - return () => { - window.removeEventListener("mousemove", handleMouseMove); - }; - }, []); - - return mousePosition; -} - -interface ParticlesProps extends ComponentPropsWithoutRef<"div"> { - className?: string; - quantity?: number; - staticity?: number; - ease?: number; - size?: number; - refresh?: boolean; - color?: string; - vx?: number; - vy?: number; -} - -function hexToRgb(hex: string): number[] { - hex = hex.replace("#", ""); - - if (hex.length === 3) { - hex = hex - .split("") - .map((char) => char + char) - .join(""); - } - - const hexInt = parseInt(hex, 16); - const red = (hexInt >> 16) & 255; - const green = (hexInt >> 8) & 255; - const blue = hexInt & 255; - return [red, green, blue]; -} - -type Circle = { - x: number; - y: number; - translateX: number; - translateY: number; - size: number; - alpha: number; - targetAlpha: number; - dx: number; - dy: number; - magnetism: number; -}; - -export const Particles: React.FC = ({ - className = "", - quantity = 100, - staticity = 50, - ease = 50, - size = 0.4, - refresh = false, - color = "#ffffff", - vx = 0, - vy = 0, - ...props -}) => { - const canvasRef = useRef(null); - const canvasContainerRef = useRef(null); - const context = useRef(null); - const circles = useRef([]); - const mousePosition = MousePosition(); - const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 }); - const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }); - const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1; - const rafID = useRef(null); - const resizeTimeout = useRef | null>(null); - - useEffect(() => { - if (canvasRef.current) { - context.current = canvasRef.current.getContext("2d"); - } - initCanvas(); - animate(); - - const handleResize = () => { - if (resizeTimeout.current) { - clearTimeout(resizeTimeout.current); - } - resizeTimeout.current = setTimeout(() => { - initCanvas(); - }, 200); - }; - - window.addEventListener("resize", handleResize); - - return () => { - if (rafID.current != null) { - window.cancelAnimationFrame(rafID.current); - } - if (resizeTimeout.current) { - clearTimeout(resizeTimeout.current); - } - window.removeEventListener("resize", handleResize); - }; - }, [color]); - - useEffect(() => { - onMouseMove(); - }, [mousePosition.x, mousePosition.y]); - - useEffect(() => { - initCanvas(); - }, [refresh]); - - const initCanvas = () => { - resizeCanvas(); - drawParticles(); - }; - - const onMouseMove = () => { - if (canvasRef.current) { - const rect = canvasRef.current.getBoundingClientRect(); - const { w, h } = canvasSize.current; - const x = mousePosition.x - rect.left - w / 2; - const y = mousePosition.y - rect.top - h / 2; - const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2; - if (inside) { - mouse.current.x = x; - mouse.current.y = y; - } - } - }; - - const resizeCanvas = () => { - if (canvasContainerRef.current && canvasRef.current && context.current) { - canvasSize.current.w = canvasContainerRef.current.offsetWidth; - canvasSize.current.h = canvasContainerRef.current.offsetHeight; - - canvasRef.current.width = canvasSize.current.w * dpr; - canvasRef.current.height = canvasSize.current.h * dpr; - canvasRef.current.style.width = `${canvasSize.current.w}px`; - canvasRef.current.style.height = `${canvasSize.current.h}px`; - context.current.scale(dpr, dpr); - - // Clear existing particles and create new ones with exact quantity - circles.current = []; - for (let i = 0; i < quantity; i++) { - const circle = circleParams(); - drawCircle(circle); - } - } - }; - - const circleParams = (): Circle => { - const x = Math.floor(Math.random() * canvasSize.current.w); - const y = Math.floor(Math.random() * canvasSize.current.h); - const translateX = 0; - const translateY = 0; - const pSize = Math.floor(Math.random() * 2) + size; - const alpha = 0; - const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)); - const dx = (Math.random() - 0.5) * 0.1; - const dy = (Math.random() - 0.5) * 0.1; - const magnetism = 0.1 + Math.random() * 4; - return { - x, - y, - translateX, - translateY, - size: pSize, - alpha, - targetAlpha, - dx, - dy, - magnetism, - }; - }; - - const rgb = hexToRgb(color); - - const drawCircle = (circle: Circle, update = false) => { - if (context.current) { - const { x, y, translateX, translateY, size, alpha } = circle; - context.current.translate(translateX, translateY); - context.current.beginPath(); - context.current.arc(x, y, size, 0, 2 * Math.PI); - context.current.fillStyle = `rgba(${rgb.join(", ")}, ${alpha})`; - context.current.fill(); - context.current.setTransform(dpr, 0, 0, dpr, 0, 0); - - if (!update) { - circles.current.push(circle); - } - } - }; - - const clearContext = () => { - if (context.current) { - context.current.clearRect( - 0, - 0, - canvasSize.current.w, - canvasSize.current.h, - ); - } - }; - - const drawParticles = () => { - clearContext(); - const particleCount = quantity; - for (let i = 0; i < particleCount; i++) { - const circle = circleParams(); - drawCircle(circle); - } - }; - - const remapValue = ( - value: number, - start1: number, - end1: number, - start2: number, - end2: number, - ): number => { - const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2; - return remapped > 0 ? remapped : 0; - }; - - const animate = () => { - clearContext(); - circles.current.forEach((circle: Circle, i: number) => { - // Handle the alpha value - const edge = [ - circle.x + circle.translateX - circle.size, // distance from left edge - canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge - circle.y + circle.translateY - circle.size, // distance from top edge - canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge - ]; - const closestEdge = edge.reduce((a, b) => Math.min(a, b)); - const remapClosestEdge = parseFloat( - remapValue(closestEdge, 0, 20, 0, 1).toFixed(2), - ); - if (remapClosestEdge > 1) { - circle.alpha += 0.02; - if (circle.alpha > circle.targetAlpha) { - circle.alpha = circle.targetAlpha; - } - } else { - circle.alpha = circle.targetAlpha * remapClosestEdge; - } - circle.x += circle.dx + vx; - circle.y += circle.dy + vy; - circle.translateX += (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) - / ease; - circle.translateY += (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) - / ease; - - drawCircle(circle, true); - - // circle gets out of the canvas - if ( - circle.x < -circle.size - || circle.x > canvasSize.current.w + circle.size - || circle.y < -circle.size - || circle.y > canvasSize.current.h + circle.size - ) { - // remove the circle from the array - circles.current.splice(i, 1); - // create a new circle - const newCircle = circleParams(); - drawCircle(newCircle); - } - }); - rafID.current = window.requestAnimationFrame(animate); - }; - - return ( - - ); -}; diff --git a/packages/ui/src/components/ui/popover.tsx b/packages/ui/src/components/ui/popover.tsx index e2585727ca..1aafbcdff0 100644 --- a/packages/ui/src/components/ui/popover.tsx +++ b/packages/ui/src/components/ui/popover.tsx @@ -1,12 +1,10 @@ +import { cn } from "@hypr/utils"; + import * as PopoverPrimitive from "@radix-ui/react-popover"; import * as React from "react"; -import { cn } from "@hypr/ui/lib/utils"; - const Popover = PopoverPrimitive.Root; - const PopoverTrigger = PopoverPrimitive.Trigger; - const PopoverAnchor = PopoverPrimitive.Anchor; const PopoverContent = React.forwardRef< diff --git a/packages/ui/src/components/ui/progress.tsx b/packages/ui/src/components/ui/progress.tsx index b65627a8b9..eedad16f4d 100644 --- a/packages/ui/src/components/ui/progress.tsx +++ b/packages/ui/src/components/ui/progress.tsx @@ -1,8 +1,8 @@ +import { cn } from "@hypr/utils"; + import * as ProgressPrimitive from "@radix-ui/react-progress"; import * as React from "react"; -import { cn } from "../../lib/utils"; - const Progress = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/packages/ui/src/components/ui/pushable-button.tsx b/packages/ui/src/components/ui/pushable-button.tsx deleted file mode 100644 index e4001c1fa8..0000000000 --- a/packages/ui/src/components/ui/pushable-button.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import "../../styles/pushable.css"; - -import { ButtonHTMLAttributes, type ReactNode } from "react"; - -interface PushableButtonProps extends ButtonHTMLAttributes { - children: ReactNode; -} - -export default function PushableButton({ - children, - className, - ...props -}: PushableButtonProps) { - return ( - - ); -} diff --git a/packages/ui/src/components/ui/radio-group.tsx b/packages/ui/src/components/ui/radio-group.tsx index c4abe83196..738b84ad70 100644 --- a/packages/ui/src/components/ui/radio-group.tsx +++ b/packages/ui/src/components/ui/radio-group.tsx @@ -1,11 +1,9 @@ -"use client"; +import { cn } from "@hypr/utils"; import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; import { Circle } from "lucide-react"; import * as React from "react"; -import { cn } from "@hypr/ui/lib/utils"; - const RadioGroup = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/packages/ui/src/components/ui/resizable.tsx b/packages/ui/src/components/ui/resizable.tsx index 60d8c6b9b8..43e5880c1e 100644 --- a/packages/ui/src/components/ui/resizable.tsx +++ b/packages/ui/src/components/ui/resizable.tsx @@ -1,8 +1,8 @@ +import { cn } from "@hypr/utils"; + import { GripVertical } from "lucide-react"; import * as ResizablePrimitive from "react-resizable-panels"; -import { cn } from "../../lib/utils"; - const ResizablePanelGroup = ({ className, ...props diff --git a/packages/ui/src/components/ui/scroll-area.tsx b/packages/ui/src/components/ui/scroll-area.tsx deleted file mode 100644 index 5611742611..0000000000 --- a/packages/ui/src/components/ui/scroll-area.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; -import * as React from "react"; - -import { cn } from "@hypr/ui/lib/utils"; - -const ScrollArea = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - {children} - - - - -)); -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; - -const ScrollBar = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef ->(({ className, orientation = "vertical", ...props }, ref) => ( - - - -)); -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; - -export { ScrollArea, ScrollBar }; diff --git a/packages/ui/src/components/ui/select.tsx b/packages/ui/src/components/ui/select.tsx index 2121dd6b73..6fa54ade9b 100644 --- a/packages/ui/src/components/ui/select.tsx +++ b/packages/ui/src/components/ui/select.tsx @@ -1,13 +1,11 @@ +import { cn } from "@hypr/utils"; + import * as SelectPrimitive from "@radix-ui/react-select"; import { Check, ChevronDown, ChevronUp } from "lucide-react"; import * as React from "react"; -import { cn } from "@hypr/ui/lib/utils"; - const Select = SelectPrimitive.Root; - const SelectGroup = SelectPrimitive.Group; - const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< diff --git a/packages/ui/src/components/ui/separator.tsx b/packages/ui/src/components/ui/separator.tsx index e1e51f19c5..9e363cc218 100644 --- a/packages/ui/src/components/ui/separator.tsx +++ b/packages/ui/src/components/ui/separator.tsx @@ -1,10 +1,8 @@ -"use client"; +import { cn } from "@hypr/utils"; import * as SeparatorPrimitive from "@radix-ui/react-separator"; import * as React from "react"; -import { cn } from "@hypr/ui/lib/utils"; - const Separator = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef diff --git a/packages/ui/src/components/ui/shimmer-button.tsx b/packages/ui/src/components/ui/shimmer-button.tsx deleted file mode 100644 index e2c50acb63..0000000000 --- a/packages/ui/src/components/ui/shimmer-button.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// https://magicui.design/docs/components/shimmer-button - -import React, { CSSProperties } from "react"; - -import { cn } from "../../lib/utils"; - -export interface ShimmerButtonProps extends React.ButtonHTMLAttributes { - shimmerColor?: string; - shimmerSize?: string; - borderRadius?: string; - shimmerDuration?: string; - background?: string; - className?: string; - children?: React.ReactNode; -} - -const ShimmerButton = React.forwardRef( - ( - { - shimmerColor = "#ffffff", - shimmerSize = "0.05em", - shimmerDuration = "3s", - background = "rgba(0, 0, 0, 1)", - className, - children, - ...props - }, - ref, - ) => { - return ( - - ); - }, -); - -ShimmerButton.displayName = "ShimmerButton"; - -export default ShimmerButton; diff --git a/packages/ui/src/components/ui/shiny-button.tsx b/packages/ui/src/components/ui/shiny-button.tsx deleted file mode 100644 index ef62a08e84..0000000000 --- a/packages/ui/src/components/ui/shiny-button.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { cn } from "@hypr/ui/lib/utils"; -import { type AnimationProps, motion, type MotionProps } from "motion/react"; - -interface ShinyButtonProps extends Omit, keyof MotionProps>, MotionProps { - children: React.ReactNode; - className?: string; - onClick?: () => void; - disabled?: boolean; -} - -const animationProps = { - initial: { "--x": "100%" }, - animate: { "--x": "-100%" }, - whileTap: { scale: 0.95 }, - transition: { - repeat: Infinity, - repeatType: "loop", - repeatDelay: 1, - duration: 1.5, - }, -} as AnimationProps; - -export default function ShinyButton({ children, className, onClick, disabled, ...props }: ShinyButtonProps) { - return ( - - - {children} - - - - - ); -} diff --git a/packages/ui/src/components/ui/spinner.tsx b/packages/ui/src/components/ui/spinner.tsx index 7d89ca45af..729a13bccb 100644 --- a/packages/ui/src/components/ui/spinner.tsx +++ b/packages/ui/src/components/ui/spinner.tsx @@ -1,6 +1,6 @@ -import React from "react"; +import { cn } from "@hypr/utils"; -import { cn } from "../../lib/utils"; +import React from "react"; interface SpinnerProps extends React.HTMLAttributes { size?: number; diff --git a/packages/ui/src/components/ui/splash.tsx b/packages/ui/src/components/ui/splash.tsx index 302822f462..5c14d3c03f 100644 --- a/packages/ui/src/components/ui/splash.tsx +++ b/packages/ui/src/components/ui/splash.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useRef, useState } from "react"; +import { cn } from "@hypr/utils"; -import { cn } from "../../lib/utils"; +import React, { useEffect, useRef, useState } from "react"; const DURATION = 1500; const MAX_WORM_LENGTH = 0.6; diff --git a/packages/ui/src/components/ui/switch.tsx b/packages/ui/src/components/ui/switch.tsx index 18e72d2269..2d0bbd9386 100644 --- a/packages/ui/src/components/ui/switch.tsx +++ b/packages/ui/src/components/ui/switch.tsx @@ -1,9 +1,9 @@ +import { cn } from "@hypr/utils"; + import * as SwitchPrimitives from "@radix-ui/react-switch"; import { cva, type VariantProps } from "class-variance-authority"; import * as React from "react"; -import { cn } from "../../lib/utils"; - const switchVariants = cva( "peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input", { diff --git a/packages/ui/src/components/ui/tabs.tsx b/packages/ui/src/components/ui/tabs.tsx index 026c47c24f..e2ece5c453 100644 --- a/packages/ui/src/components/ui/tabs.tsx +++ b/packages/ui/src/components/ui/tabs.tsx @@ -1,8 +1,8 @@ +import { cn } from "@hypr/utils"; + import * as TabsPrimitive from "@radix-ui/react-tabs"; import * as React from "react"; -import { cn } from "@hypr/ui/lib/utils"; - const Tabs = TabsPrimitive.Root; const TabsList = React.forwardRef< diff --git a/packages/ui/src/components/ui/text-animate.tsx b/packages/ui/src/components/ui/text-animate.tsx index b68fdb479f..801cb23bff 100644 --- a/packages/ui/src/components/ui/text-animate.tsx +++ b/packages/ui/src/components/ui/text-animate.tsx @@ -1,8 +1,8 @@ +import { cn } from "@hypr/utils"; + import { AnimatePresence, motion, MotionProps, Variants } from "motion/react"; import { ElementType } from "react"; -import { cn } from "../../lib/utils"; - type AnimationType = "text" | "word" | "character" | "line"; type AnimationVariant = | "fadeIn" diff --git a/packages/ui/src/components/ui/textarea.tsx b/packages/ui/src/components/ui/textarea.tsx index b774552a5c..bd7ef166a2 100644 --- a/packages/ui/src/components/ui/textarea.tsx +++ b/packages/ui/src/components/ui/textarea.tsx @@ -1,6 +1,6 @@ -import * as React from "react"; +import { cn } from "@hypr/utils"; -import { cn } from "@hypr/ui/lib/utils"; +import * as React from "react"; const Textarea = React.forwardRef< HTMLTextAreaElement, diff --git a/packages/ui/src/components/ui/tooltip.tsx b/packages/ui/src/components/ui/tooltip.tsx index 48ec118d07..5d932ddb64 100644 --- a/packages/ui/src/components/ui/tooltip.tsx +++ b/packages/ui/src/components/ui/tooltip.tsx @@ -1,12 +1,10 @@ +import { cn } from "@hypr/utils"; + import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as React from "react"; -import { cn } from "../../lib/utils"; - const TooltipProvider = TooltipPrimitive.Provider; - const Tooltip = TooltipPrimitive.Root; - const TooltipTrigger = TooltipPrimitive.Trigger; const TooltipContent = React.forwardRef< diff --git a/packages/ui/src/lib/utils.ts b/packages/ui/src/lib/utils.ts deleted file mode 100644 index 365058cebd..0000000000 --- a/packages/ui/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/packages/utils/package.json b/packages/utils/package.json index 2bdcfb3959..a35fe740c7 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -13,10 +13,12 @@ "@tauri-apps/api": "^2.8.0", "@tauri-apps/plugin-http": "^2.5.2", "ai": "^5.0.76", + "clsx": "^2.1.1", "date-fns": "^4.1.0", "mutative": "^1.3.0", "p-debounce": "^4.0.0", "react-hotkeys-hook": "^4.6.2", + "tailwind-merge": "^2.6.0", "tauri-plugin-keygen-api": "github:bagindo/tauri-plugin-keygen#v2", "zustand": "^5.0.8" }, diff --git a/packages/utils/src/cn.ts b/packages/utils/src/cn.ts new file mode 100644 index 0000000000..c834356dc4 --- /dev/null +++ b/packages/utils/src/cn.ts @@ -0,0 +1,22 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +/** + * Combines multiple class names using clsx and merges Tailwind CSS classes intelligently. + * + * This utility function is essential for conditional className composition in React components, + * especially when working with Tailwind CSS where class conflicts need to be resolved. + * + * @param inputs - Class values that can be strings, objects, arrays, or other types accepted by clsx + * @returns A merged string of class names with Tailwind conflicts resolved + * + * @example + * ```tsx + * cn("px-2 py-1", "px-4") // => "py-1 px-4" (px-4 overrides px-2) + * cn("text-red-500", condition && "text-blue-500") // Conditional classes + * cn({ "bg-gray-100": isActive, "bg-white": !isActive }) // Object syntax + * ``` + */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/packages/utils/src/date.ts b/packages/utils/src/date.ts new file mode 100644 index 0000000000..72b7a2f061 --- /dev/null +++ b/packages/utils/src/date.ts @@ -0,0 +1,91 @@ +/** + * Centralized date utilities + * + * This module provides date manipulation and formatting utilities. + * It re-exports ALL date-fns functions and adds custom helpers where needed. + */ + +// Re-export ALL date-fns functions so users can import any date-fns function from @hypr/utils +export * from "date-fns"; + +// Import only what we need for our custom functions +import { isSameDay } from "date-fns"; + +/** + * Formats a date according to a custom format string. + * + * This is a lightweight alternative to date-fns format for simple cases. + * For complex formatting, prefer using date-fns format function. + * + * @param date - The date to format + * @param formatString - Format string with tokens: + * - yyyy: 4-digit year + * - MMM: Short month name (Jan, Feb, etc.) + * - MM: 2-digit month (01-12) + * - dd: 2-digit day (01-31) + * - d: Day without leading zero + * - EEE: Short day name (Sun, Mon, etc.) + * - h: Hour in 12-hour format + * - mm: 2-digit minutes + * - a: AM/PM + * - p: Complete time string (e.g., "3:45 PM") + * @returns Formatted date string + */ +export const formatDate = (date: Date, formatString: string): string => { + const pad = (n: number) => n.toString().padStart(2, "0"); + + const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; + const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + const replacements: Record = { + "yyyy": date.getFullYear().toString(), + "MMM": months[date.getMonth()], + "MM": pad(date.getMonth() + 1), + "d": date.getDate().toString(), + "dd": pad(date.getDate()), + "EEE": days[date.getDay()], + "h": (date.getHours() % 12 || 12).toString(), + "mm": pad(date.getMinutes()), + "a": date.getHours() >= 12 ? "PM" : "AM", + "p": `${date.getHours() % 12 || 12}:${pad(date.getMinutes())} ${date.getHours() >= 12 ? "PM" : "AM"}`, + }; + + return formatString.replace(/yyyy|MMM|MM|dd|EEE|h|mm|a|p|d/g, (token) => replacements[token]); +}; + +/** + * Formats a date range with intelligent formatting based on whether the dates are on the same day. + * Uses date-fns isSameDay for comparison. + * + * @param startDate - ISO date string for the start of the range + * @param endDate - ISO date string for the end of the range + * @returns Formatted date range string (e.g., "Jan 15, 2024 9:00 AM to 10:30 AM") + */ +export const formatDateRange = (startDate: string, endDate: string): string => { + const start = new Date(startDate); + const end = new Date(endDate); + + const formatTime = (date: Date) => formatDate(date, "p"); + const formatFullDate = (date: Date) => formatDate(date, "MMM d, yyyy"); + + if (isSameDay(start, end)) { + return `${formatFullDate(start)} ${formatTime(start)} to ${formatTime(end)}`; + } else { + return `${formatFullDate(start)} ${formatTime(start)} to ${formatFullDate(end)} ${formatTime(end)}`; + } +}; + +/** + * Extracts the domain/hostname from a URL string. + * + * @param url - The URL to parse + * @returns The hostname of the URL, or the original string if parsing fails + */ +export const getMeetingDomain = (url: string): string => { + try { + const urlObj = new URL(url); + return urlObj.hostname; + } catch { + return url; + } +}; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 9898400f43..4d98359375 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1 +1,3 @@ +export * from "./cn"; +export * from "./date"; export * from "./fetch"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5e5cdaa12..0fec736dfc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: '@hypr/ui': specifier: workspace:^ version: link:../../packages/ui + '@hypr/utils': + specifier: workspace:^ + version: link:../../packages/utils '@iconify-icon/react': specifier: ^3.0.1 version: 3.0.1(react@19.2.0) @@ -564,6 +567,9 @@ importers: '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.65.0(react@19.2.0)) + '@hypr/utils': + specifier: workspace:^ + version: link:../utils '@radix-ui/react-accordion': specifier: ^1.2.12 version: 1.2.12(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -718,6 +724,9 @@ importers: ai: specifier: ^5.0.76 version: 5.0.76(zod@4.1.12) + clsx: + specifier: ^2.1.1 + version: 2.1.1 date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -733,6 +742,9 @@ importers: react-hotkeys-hook: specifier: ^4.6.2 version: 4.6.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 tauri-plugin-keygen-api: specifier: github:bagindo/tauri-plugin-keygen#v2 version: https://codeload.github.com/bagindo/tauri-plugin-keygen/tar.gz/e0de03a76a5c82c36adb347e43e4bff4f78c87de