mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
parent
31e35e00b8
commit
c0493e8b9f
2 changed files with 154 additions and 161 deletions
|
|
@ -303,15 +303,6 @@
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upcoming-event-row {
|
|
||||||
background-color: transparent;
|
|
||||||
transition: background-color 120ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upcoming-event-row:hover {
|
|
||||||
background-color: var(--gm-bg-pill-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gmail-row-selected {
|
.gmail-row-selected {
|
||||||
background: var(--gm-bg-row-selected);
|
background: var(--gm-bg-row-selected);
|
||||||
box-shadow: inset 2px 0 0 var(--gm-accent);
|
box-shadow: inset 2px 0 0 var(--gm-accent);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
import { Calendar, ChevronDown, Clock, ExternalLink, Loader2, MapPin, Mic, Square, UserRound, UsersRound, Video, X } from 'lucide-react'
|
import { Calendar, ChevronDown, Clock, ExternalLink, Loader2, MapPin, Mic, Square, UserRound, UsersRound, Video, X } from 'lucide-react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
@ -212,6 +213,42 @@ function formatEventTimeRange(event: UpcomingEvent): string {
|
||||||
return `${start} – ${end}`
|
return `${start} – ${end}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compact range for the upcoming list: drops the leading meridiem when both
|
||||||
|
// ends share it ("9:00 – 11:00 AM" instead of "9:00 AM – 11:00 AM").
|
||||||
|
function formatEventTimeRangeCompact(event: UpcomingEvent): string {
|
||||||
|
if (event.isAllDay) return 'All day'
|
||||||
|
const startStr = event.start.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
||||||
|
if (!event.end) return startStr
|
||||||
|
const sameDay = localDateKey(event.start) === localDateKey(event.end)
|
||||||
|
if (!sameDay) return formatEventTimeRange(event)
|
||||||
|
const endStr = event.end.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
||||||
|
const meridiemRe = /\s*[AP]M$/i
|
||||||
|
const startMer = startStr.match(meridiemRe)?.[0]?.trim().toUpperCase()
|
||||||
|
const endMer = endStr.match(meridiemRe)?.[0]?.trim().toUpperCase()
|
||||||
|
if (startMer && endMer && startMer === endMer) {
|
||||||
|
return `${startStr.replace(meridiemRe, '')} – ${endStr}`
|
||||||
|
}
|
||||||
|
return `${startStr} – ${endStr}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether a timed event is happening right now.
|
||||||
|
function isEventNow(event: UpcomingEvent): boolean {
|
||||||
|
if (event.isAllDay) return false
|
||||||
|
const now = Date.now()
|
||||||
|
const start = event.start.getTime()
|
||||||
|
const end = event.end ? event.end.getTime() : start + 30 * 60 * 1000
|
||||||
|
return start <= now && now < end
|
||||||
|
}
|
||||||
|
|
||||||
|
// Human label for the conferencing provider behind an event's join link.
|
||||||
|
function meetingPlatformLabel(link: string | null): string | null {
|
||||||
|
if (!link) return null
|
||||||
|
if (/zoom\.us|zoomgov\.com/i.test(link)) return 'Zoom'
|
||||||
|
if (/teams\.(?:microsoft|live)\.com/i.test(link)) return 'Teams'
|
||||||
|
if (/meet\.google\.com/i.test(link)) return 'Meet'
|
||||||
|
return 'Video call'
|
||||||
|
}
|
||||||
|
|
||||||
function formatEventDetailTime(event: UpcomingEvent): string {
|
function formatEventDetailTime(event: UpcomingEvent): string {
|
||||||
if (!event.isAllDay) {
|
if (!event.isAllDay) {
|
||||||
const date = event.start.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })
|
const date = event.start.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })
|
||||||
|
|
@ -431,8 +468,9 @@ function UpcomingEvents() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Refresh on the hour so day labels and "ended" filtering stay current.
|
// Refresh every minute so the "now" highlight, day labels, and "ended"
|
||||||
const tick = setInterval(() => setRefreshTick((t) => t + 1), 60 * 60 * 1000)
|
// filtering stay current without waiting on a calendar sync.
|
||||||
|
const tick = setInterval(() => setRefreshTick((t) => t + 1), 60 * 1000)
|
||||||
return () => {
|
return () => {
|
||||||
cleanup()
|
cleanup()
|
||||||
clearInterval(tick)
|
clearInterval(tick)
|
||||||
|
|
@ -462,10 +500,7 @@ function UpcomingEvents() {
|
||||||
Coming up
|
Coming up
|
||||||
</h3>
|
</h3>
|
||||||
{loading && events.length === 0 ? null : (
|
{loading && events.length === 0 ? null : (
|
||||||
<span
|
<span className="text-[11px] uppercase tracking-wider text-muted-foreground">
|
||||||
className="text-[11px] uppercase tracking-wider"
|
|
||||||
style={{ color: 'var(--gm-text-faint)' }}
|
|
||||||
>
|
|
||||||
{totalVisible} {totalVisible === 1 ? 'event' : 'events'}
|
{totalVisible} {totalVisible === 1 ? 'event' : 'events'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -491,16 +526,12 @@ function UpcomingEvents() {
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="py-4 text-sm text-muted-foreground">{error}</div>
|
<div className="py-4 text-sm text-muted-foreground">{error}</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="flex flex-col gap-3">
|
||||||
className="overflow-hidden rounded-xl border"
|
{visibleDays.map((day) => (
|
||||||
style={{ borderColor: 'var(--gm-border)', background: 'var(--gm-bg)' }}
|
<UpcomingDayCard
|
||||||
>
|
|
||||||
{visibleDays.map((day, idx) => (
|
|
||||||
<UpcomingDayRow
|
|
||||||
key={day.dateKey}
|
key={day.dateKey}
|
||||||
day={day}
|
day={day}
|
||||||
isToday={day.dateKey === todayKey}
|
isToday={day.dateKey === todayKey}
|
||||||
isLast={idx === visibleDays.length - 1}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -511,63 +542,57 @@ function UpcomingEvents() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpcomingDayRow({ day, isToday, isLast }: { day: DayGroup; isToday: boolean; isLast: boolean }) {
|
function UpcomingDayCard({ day, isToday }: { day: DayGroup; isToday: boolean }) {
|
||||||
const dayNum = day.date.getDate()
|
const dayNum = day.date.getDate()
|
||||||
const month = day.date.toLocaleDateString([], { month: 'short' })
|
const month = day.date.toLocaleDateString([], { month: 'short' })
|
||||||
const weekday = day.date.toLocaleDateString([], { weekday: 'short' })
|
const weekday = day.date.toLocaleDateString([], { weekday: 'short' })
|
||||||
|
const count = day.events.length
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="overflow-hidden rounded-xl border bg-card">
|
||||||
className="grid"
|
<div className="flex items-center justify-between gap-3 border-b bg-muted px-5 py-3.5">
|
||||||
style={{
|
<div className="flex min-w-0 items-baseline gap-2">
|
||||||
gridTemplateColumns: '96px minmax(0, 1fr)',
|
<span className="text-[22px] font-bold leading-none text-foreground">{dayNum}</span>
|
||||||
borderBottom: isLast ? undefined : '1px dashed var(--gm-border-strong)',
|
<span className="truncate text-[13px] text-muted-foreground">
|
||||||
}}
|
{month} · {weekday}
|
||||||
>
|
|
||||||
<div className="flex items-start gap-2 px-4 py-4">
|
|
||||||
<span
|
|
||||||
className="leading-none"
|
|
||||||
style={{ fontSize: 30, fontWeight: 400, color: 'var(--gm-text-strong)' }}
|
|
||||||
>
|
|
||||||
{dayNum}
|
|
||||||
</span>
|
</span>
|
||||||
<span className="flex flex-col leading-tight">
|
|
||||||
<span
|
|
||||||
className="flex items-center gap-1"
|
|
||||||
style={{ fontSize: 12, fontWeight: 600, color: 'var(--gm-text)' }}
|
|
||||||
>
|
|
||||||
{month}
|
|
||||||
{isToday ? (
|
{isToday ? (
|
||||||
<span
|
<span className="shrink-0 rounded-md bg-foreground px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-background">
|
||||||
aria-hidden
|
Today
|
||||||
className="inline-block rounded-full"
|
|
||||||
style={{ width: 5, height: 5, background: 'var(--gm-accent)' }}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</span>
|
</span>
|
||||||
<span style={{ fontSize: 12, color: 'var(--gm-text-faint)' }}>{weekday}</span>
|
) : null}
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
|
{count} {count === 1 ? 'event' : 'events'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 flex-col py-3 pr-3">
|
|
||||||
{day.events.length === 0 ? (
|
{count === 0 ? (
|
||||||
<div
|
<div className="px-5 py-4 text-sm text-muted-foreground">
|
||||||
className="flex w-full items-center gap-3 px-3 py-2 text-sm"
|
{isToday ? 'No events today' : 'No events'}
|
||||||
style={{ color: 'var(--gm-text-faint)', minHeight: 40 }}
|
|
||||||
>
|
|
||||||
<span aria-hidden className="self-stretch shrink-0" style={{ width: 3 }} />
|
|
||||||
<span>{isToday ? 'No events today' : 'No events'}</span>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
day.events.map((ev) => <UpcomingEventItem key={ev.id} event={ev} />)
|
day.events.map((ev, idx) => (
|
||||||
|
<UpcomingEventItem key={ev.id} event={ev} isLast={idx === count - 1} />
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpcomingEventItem({ event }: { event: UpcomingEvent }) {
|
function NowBadge() {
|
||||||
const [open, setOpen] = useState(false)
|
return (
|
||||||
|
<span className="shrink-0 rounded bg-green-600 px-1.5 py-px text-[10px] font-bold uppercase leading-[1.5] tracking-wide text-white">
|
||||||
|
Now
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpcomingEventItem({ event, isLast }: { event: UpcomingEvent; isLast: boolean }) {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const isNow = isEventNow(event)
|
||||||
|
const platform = meetingPlatformLabel(event.conferenceLink)
|
||||||
|
const subtitle = platform ?? event.location
|
||||||
const titleAndLocation = event.location ? `${event.summary} · ${event.location}` : event.summary
|
const titleAndLocation = event.location ? `${event.summary} · ${event.location}` : event.summary
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -578,31 +603,29 @@ function UpcomingEventItem({ event }: { event: UpcomingEvent }) {
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
title={titleAndLocation}
|
title={titleAndLocation}
|
||||||
className={cn(
|
className={cn(
|
||||||
'upcoming-event-row group flex w-full items-center gap-3 px-3 py-2 text-left cursor-pointer',
|
'group flex w-full cursor-pointer items-center gap-4 px-5 py-3 text-left transition-colors',
|
||||||
|
!isLast && 'border-b',
|
||||||
|
isNow ? 'bg-muted' : 'hover:bg-muted/50',
|
||||||
)}
|
)}
|
||||||
style={{ color: 'var(--gm-text)', minHeight: 40 }}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
aria-hidden
|
|
||||||
className="self-stretch rounded-full"
|
|
||||||
style={{ width: 3, background: 'var(--gm-accent)', opacity: 0.55 }}
|
|
||||||
/>
|
|
||||||
<span className="min-w-0 flex-1">
|
|
||||||
<span
|
|
||||||
className="block truncate"
|
|
||||||
style={{ fontSize: 14, fontWeight: 500, color: 'var(--gm-text-strong)' }}
|
|
||||||
>
|
>
|
||||||
|
<span className="shrink-0 text-[13px] tabular-nums text-muted-foreground" style={{ width: 118 }}>
|
||||||
|
{formatEventTimeRangeCompact(event)}
|
||||||
|
</span>
|
||||||
|
<span className="flex min-w-0 flex-1 flex-col">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span className="truncate text-sm font-semibold text-foreground">
|
||||||
{event.summary}
|
{event.summary}
|
||||||
</span>
|
</span>
|
||||||
<span
|
{isNow ? <NowBadge /> : null}
|
||||||
className="mt-0.5 block truncate"
|
|
||||||
style={{ fontSize: 12, color: 'var(--gm-text-muted)' }}
|
|
||||||
>
|
|
||||||
{formatEventTimeRange(event)}
|
|
||||||
{event.location ? <span style={{ color: 'var(--gm-text-faint)' }}> · {event.location}</span> : null}
|
|
||||||
</span>
|
</span>
|
||||||
|
{subtitle ? (
|
||||||
|
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||||
|
{platform ? <Video className="size-3.5 shrink-0" /> : <MapPin className="size-3.5 shrink-0" />}
|
||||||
|
<span className="truncate">{subtitle}</span>
|
||||||
</span>
|
</span>
|
||||||
<div className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100">
|
) : null}
|
||||||
|
</span>
|
||||||
|
<div className="shrink-0">
|
||||||
{event.conferenceLink ? (
|
{event.conferenceLink ? (
|
||||||
<SplitJoinButton
|
<SplitJoinButton
|
||||||
onJoinAndNotes={() => triggerMeetingCapture(event, true)}
|
onJoinAndNotes={() => triggerMeetingCapture(event, true)}
|
||||||
|
|
@ -613,16 +636,9 @@ function UpcomingEventItem({ event }: { event: UpcomingEvent }) {
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => { e.stopPropagation(); triggerMeetingCapture(event, false) }}
|
onClick={(e) => { e.stopPropagation(); triggerMeetingCapture(event, false) }}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs transition-colors"
|
className="inline-flex items-center gap-1.5 rounded-md border bg-background px-2.5 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-accent"
|
||||||
style={{
|
|
||||||
background: 'var(--gm-bg-pill)',
|
|
||||||
color: 'var(--gm-text)',
|
|
||||||
border: '1px solid var(--gm-border)',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--gm-bg-pill-hover)' }}
|
|
||||||
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--gm-bg-pill)' }}
|
|
||||||
>
|
>
|
||||||
<Mic className="size-3" />
|
<Mic className="size-3.5" />
|
||||||
Take notes
|
Take notes
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
@ -650,7 +666,7 @@ function EventDetailsPopover({ event, onClose }: { event: UpcomingEvent; onClose
|
||||||
sideOffset={6}
|
sideOffset={6}
|
||||||
className="w-[min(380px,calc(100vw-32px))] rounded-lg p-0 shadow-xl"
|
className="w-[min(380px,calc(100vw-32px))] rounded-lg p-0 shadow-xl"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--popover, #fff)',
|
backgroundColor: 'var(--muted, #f4f4f5)',
|
||||||
borderColor: 'var(--border, #e4e4e7)',
|
borderColor: 'var(--border, #e4e4e7)',
|
||||||
color: 'var(--popover-foreground, #09090b)',
|
color: 'var(--popover-foreground, #09090b)',
|
||||||
}}
|
}}
|
||||||
|
|
@ -664,7 +680,7 @@ function EventDetailsPopover({ event, onClose }: { event: UpcomingEvent; onClose
|
||||||
style={{ color: 'var(--muted-foreground, #71717a)' }}
|
style={{ color: 'var(--muted-foreground, #71717a)' }}
|
||||||
aria-label="Open in Google Calendar"
|
aria-label="Open in Google Calendar"
|
||||||
title="Open in Google Calendar"
|
title="Open in Google Calendar"
|
||||||
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--muted, #f4f4f5)' }}
|
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--background, #ffffff)' }}
|
||||||
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'transparent' }}
|
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'transparent' }}
|
||||||
>
|
>
|
||||||
<ExternalLink className="size-4" />
|
<ExternalLink className="size-4" />
|
||||||
|
|
@ -677,7 +693,7 @@ function EventDetailsPopover({ event, onClose }: { event: UpcomingEvent; onClose
|
||||||
style={{ color: 'var(--muted-foreground, #71717a)' }}
|
style={{ color: 'var(--muted-foreground, #71717a)' }}
|
||||||
aria-label="Close event details"
|
aria-label="Close event details"
|
||||||
title="Close"
|
title="Close"
|
||||||
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--muted, #f4f4f5)' }}
|
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--background, #ffffff)' }}
|
||||||
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'transparent' }}
|
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'transparent' }}
|
||||||
>
|
>
|
||||||
<X className="size-4" />
|
<X className="size-4" />
|
||||||
|
|
@ -771,41 +787,46 @@ function SplitJoinButton({ onJoinAndNotes, onNotesOnly }: {
|
||||||
onNotesOnly: () => void
|
onNotesOnly: () => void
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
|
// Fixed-position coords for the portaled menu so it isn't clipped by the
|
||||||
|
// calendar card's `overflow-hidden`.
|
||||||
|
const [menuPos, setMenuPos] = useState<{ top: number; right: number } | null>(null)
|
||||||
|
|
||||||
|
const updatePos = useCallback(() => {
|
||||||
|
const rect = containerRef.current?.getBoundingClientRect()
|
||||||
|
if (!rect) return
|
||||||
|
setMenuPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right })
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
|
updatePos()
|
||||||
const handler = (e: MouseEvent) => {
|
const handler = (e: MouseEvent) => {
|
||||||
const target = e.target
|
const target = e.target
|
||||||
if (ref.current && target instanceof globalThis.Node && !ref.current.contains(target)) {
|
if (!(target instanceof globalThis.Node)) return
|
||||||
|
if (containerRef.current?.contains(target) || menuRef.current?.contains(target)) return
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
document.addEventListener('mousedown', handler)
|
document.addEventListener('mousedown', handler)
|
||||||
return () => document.removeEventListener('mousedown', handler)
|
window.addEventListener('resize', updatePos)
|
||||||
}, [open])
|
window.addEventListener('scroll', updatePos, true)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handler)
|
||||||
|
window.removeEventListener('resize', updatePos)
|
||||||
|
window.removeEventListener('scroll', updatePos, true)
|
||||||
|
}
|
||||||
|
}, [open, updatePos])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={containerRef} className="relative inline-flex items-stretch">
|
||||||
ref={ref}
|
|
||||||
style={{ position: 'relative', display: 'inline-flex', alignItems: 'stretch' }}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
onClick={(e) => { e.stopPropagation(); onJoinAndNotes() }}
|
onClick={(e) => { e.stopPropagation(); onJoinAndNotes() }}
|
||||||
className="inline-flex items-center gap-1 px-2 py-1 text-xs transition-colors"
|
className="inline-flex items-center gap-1.5 rounded-l-md border bg-background px-2.5 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-accent"
|
||||||
style={{
|
|
||||||
background: 'var(--gm-bg-pill)',
|
|
||||||
color: 'var(--gm-text)',
|
|
||||||
border: '1px solid var(--gm-border)',
|
|
||||||
borderTopLeftRadius: 6,
|
|
||||||
borderBottomLeftRadius: 6,
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--gm-bg-pill-hover)' }}
|
|
||||||
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--gm-bg-pill)' }}
|
|
||||||
>
|
>
|
||||||
<Video className="size-3" />
|
<Video className="size-3.5" />
|
||||||
Join & take notes
|
Join & take notes
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
@ -813,49 +834,30 @@ function SplitJoinButton({ onJoinAndNotes, onNotesOnly }: {
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
onClick={(e) => { e.stopPropagation(); setOpen((v) => !v) }}
|
onClick={(e) => { e.stopPropagation(); setOpen((v) => !v) }}
|
||||||
aria-label="More meeting options"
|
aria-label="More meeting options"
|
||||||
className="inline-flex items-center justify-center px-1.5 py-1 transition-colors"
|
className="inline-flex items-center justify-center rounded-r-md border border-l-0 bg-background px-1.5 py-1.5 text-foreground transition-colors hover:bg-accent"
|
||||||
style={{
|
|
||||||
background: 'var(--gm-bg-pill)',
|
|
||||||
color: 'var(--gm-text)',
|
|
||||||
border: '1px solid var(--gm-border)',
|
|
||||||
borderLeft: 'none',
|
|
||||||
borderTopRightRadius: 6,
|
|
||||||
borderBottomRightRadius: 6,
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--gm-bg-pill-hover)' }}
|
|
||||||
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--gm-bg-pill)' }}
|
|
||||||
>
|
>
|
||||||
<ChevronDown className="size-3" />
|
<ChevronDown className="size-3" />
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && menuPos
|
||||||
|
? createPortal(
|
||||||
<div
|
<div
|
||||||
style={{
|
ref={menuRef}
|
||||||
position: 'absolute',
|
style={{ position: 'fixed', top: menuPos.top, right: menuPos.right, zIndex: 60 }}
|
||||||
top: 'calc(100% + 4px)',
|
className="min-w-36 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg"
|
||||||
right: 0,
|
|
||||||
zIndex: 50,
|
|
||||||
background: 'var(--gm-bg-card)',
|
|
||||||
border: '1px solid var(--gm-border)',
|
|
||||||
borderRadius: 6,
|
|
||||||
boxShadow: '0 4px 12px rgba(0,0,0,0.12)',
|
|
||||||
minWidth: 144,
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
onClick={(e) => { e.stopPropagation(); setOpen(false); onNotesOnly() }}
|
onClick={(e) => { e.stopPropagation(); setOpen(false); onNotesOnly() }}
|
||||||
className="flex w-full items-center gap-1 px-2 py-1.5 text-xs"
|
className="flex w-full items-center gap-1.5 whitespace-nowrap px-2.5 py-1.5 text-xs transition-colors hover:bg-accent"
|
||||||
style={{ background: 'transparent', color: 'var(--gm-text)', whiteSpace: 'nowrap', border: 'none' }}
|
|
||||||
onMouseEnter={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'var(--gm-bg-row-hover)' }}
|
|
||||||
onMouseLeave={(e) => { (e.currentTarget as HTMLButtonElement).style.background = 'transparent' }}
|
|
||||||
>
|
>
|
||||||
<Mic className="size-3" />
|
<Mic className="size-3" />
|
||||||
Take notes only
|
Take notes only
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>,
|
||||||
)}
|
document.body,
|
||||||
|
)
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue