@@ -2186,7 +2337,65 @@ export default function Player({
)
- const { w: vw, h: vh, ox, oy, bottomInset } = getViewport()
+ const SEGMENTS_PANEL_GAP = 10
+ const SEGMENTS_PANEL_MIN_W = 260
+ const SEGMENTS_PANEL_MAX_W = 340
+
+ const playerOnLeft = miniDesktop
+ ? win.x + win.w / 2 < vw / 2
+ : false
+
+ const segmentPanelAvailableW = playerOnLeft
+ ? vw - (win.x + win.w) - MARGIN - SEGMENTS_PANEL_GAP
+ : win.x - MARGIN - SEGMENTS_PANEL_GAP
+
+ const segmentPanelW = Math.min(
+ SEGMENTS_PANEL_MAX_W,
+ Math.max(SEGMENTS_PANEL_MIN_W, segmentPanelAvailableW)
+ )
+
+ const canRenderSegmentsPanel =
+ miniDesktop &&
+ !isLive &&
+ hasRatingSegments &&
+ !isDragging &&
+ !isResizing &&
+ segmentPanelAvailableW >= SEGMENTS_PANEL_MIN_W
+
+ const segmentsPanelEl = canRenderSegmentsPanel ? (
+
+
+
+ ) : null
const expandedRect = {
left: ox + 16,
@@ -2280,6 +2489,53 @@ export default function Player({
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08);
pointer-events: none;
}
+
+ .player-sidebar-segments {
+ background: rgba(0, 0, 0, 0.3) !important;
+ color: white !important;
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.22);
+ }
+
+ .player-sidebar-segments > * {
+ background: transparent !important;
+ border-color: rgba(255, 255, 255, 0.08) !important;
+ box-shadow: none !important;
+ }
+
+ .player-sidebar-segments [class*="ring-white"],
+ .player-sidebar-segments [class*="border-white"] {
+ --tw-ring-color: rgba(255, 255, 255, 0.08) !important;
+ border-color: rgba(255, 255, 255, 0.08) !important;
+ }
+
+ .player-sidebar-segments [class*="bg-white"] {
+ background-color: rgba(255, 255, 255, 0.06) !important;
+ }
+
+ .player-sidebar-segments [class*="text-gray-"],
+ .player-sidebar-segments [class*="text-slate-"],
+ .player-sidebar-segments [class*="text-zinc-"],
+ .player-sidebar-segments [class*="text-neutral-"],
+ .player-sidebar-segments [class*="text-stone-"],
+ .player-sidebar-segments [class*="text-black"] {
+ color: rgba(255, 255, 255, 0.92) !important;
+ }
+
+ .player-sidebar-segments [class*="text-gray-4"],
+ .player-sidebar-segments [class*="text-gray-5"],
+ .player-sidebar-segments [class*="text-gray-6"],
+ .player-sidebar-segments [class*="text-slate-4"],
+ .player-sidebar-segments [class*="text-slate-5"],
+ .player-sidebar-segments [class*="text-slate-6"],
+ .player-sidebar-segments [class*="text-zinc-4"],
+ .player-sidebar-segments [class*="text-zinc-5"],
+ .player-sidebar-segments [class*="text-zinc-6"] {
+ color: rgba(255, 255, 255, 0.68) !important;
+ }
+
+ .player-sidebar-segments svg {
+ color: rgba(255, 255, 255, 0.85) !important;
+ }
`}
{expanded || miniDesktop ? (
@@ -2297,6 +2553,8 @@ export default function Player({
{cardEl}
+ {segmentsPanelEl}
+
{miniDesktop ? (
diff --git a/frontend/src/components/ui/RatingOverlay.tsx b/frontend/src/components/ui/RatingOverlay.tsx
index db0deaf..2afc4f5 100644
--- a/frontend/src/components/ui/RatingOverlay.tsx
+++ b/frontend/src/components/ui/RatingOverlay.tsx
@@ -21,13 +21,13 @@ type RatingEntry = {
tone?: 'normal' | 'strong' | 'muted'
}
-type SegmentSpritePreview = {
+export type SegmentSpritePreview = {
src: string
frameStyle: CSSProperties
imageStyle: CSSProperties
}
-type RatingSegment = {
+export type RatingSegment = {
key: string
title: string
timeLabel?: string
@@ -825,7 +825,7 @@ function readSegmentArray(metaRaw: unknown): unknown[] {
return []
}
-function readRatingSegments(metaRaw: unknown): RatingSegment[] {
+export function readRatingSegments(metaRaw: unknown): RatingSegment[] {
const rawSegments = readSegmentArray(metaRaw)
const spriteMeta = readPreviewSpriteMeta(metaRaw)
const durationSeconds = readMetaDurationSeconds(metaRaw)
@@ -874,6 +874,178 @@ function readRatingSegments(metaRaw: unknown): RatingSegment[] {
.slice(0, 10) as RatingSegment[]
}
+export function RatingSegmentsPanel({
+ metaRaw,
+ segments: providedSegments,
+ onOpenAt,
+ className = '',
+ maxHeight = 420,
+}: {
+ metaRaw?: unknown
+ segments?: RatingSegment[]
+ onOpenAt?: (seconds: number) => void
+ className?: string
+ maxHeight?: number | string
+}) {
+ const segments = providedSegments ?? readRatingSegments(metaRaw)
+
+ if (segments.length === 0) return null
+
+ const panelMaxHeight =
+ typeof maxHeight === 'number'
+ ? `${maxHeight}px`
+ : maxHeight
+
+ return (
+
+
+
+
+
+ Segmente
+
+
+ AI Rating · {segments.length}
+
+
+
+
+
+
+
+ {segments.map((segment, index) => (
+
+ ))}
+
+
+
+ )
+}
+
function ratingTone(stars: number) {
switch (stars) {
case 1:
diff --git a/frontend/src/components/ui/RecordJobActions.tsx b/frontend/src/components/ui/RecordJobActions.tsx
index 5edbba5..dfbbfbf 100644
--- a/frontend/src/components/ui/RecordJobActions.tsx
+++ b/frontend/src/components/ui/RecordJobActions.tsx
@@ -549,6 +549,7 @@ export default function RecordJobActions({
const MARGIN = 8
const [menuStyle, setMenuStyle] = useState<{ top: number; left: number; maxH: number } | null>(null)
+ const [menuPortalTarget, setMenuPortalTarget] = useState
(null)
useEffect(() => {
if (!menuOpen) return
@@ -578,9 +579,16 @@ export default function RecordJobActions({
useLayoutEffect(() => {
if (!menuOpen) {
setMenuStyle(null)
+ setMenuPortalTarget(null)
return
}
+ const portalTarget =
+ (menuBtnRef.current?.closest('dialog[open]') as HTMLElement | null) ??
+ document.body
+
+ setMenuPortalTarget(portalTarget)
+
const update = () => {
const btn = menuBtnRef.current
if (!btn) return
@@ -589,21 +597,31 @@ export default function RecordJobActions({
const vw = window.innerWidth
const vh = window.innerHeight
- // rechts am Button ausrichten
let left = r.right - MENU_W
left = Math.max(MARGIN, Math.min(left, vw - MENU_W - MARGIN))
let top = r.bottom + GAP
- top = Math.max(MARGIN, Math.min(top, vh - MARGIN))
+ const spaceBelow = vh - top - MARGIN
+ const spaceAbove = r.top - MARGIN
+
+ // Falls unten zu wenig Platz ist, über dem Button öffnen
+ if (spaceBelow < 120 && spaceAbove > spaceBelow) {
+ const maxH = Math.max(120, spaceAbove)
+ top = Math.max(MARGIN, r.top - GAP - Math.min(maxH, 320))
+ setMenuStyle({ top, left, maxH })
+ return
+ }
+
+ top = Math.max(MARGIN, Math.min(top, vh - MARGIN))
const maxH = Math.max(120, vh - top - MARGIN)
+
setMenuStyle({ top, left, maxH })
}
update()
window.addEventListener('resize', update)
- // capture=true, damit auch Scroll in Container-DIVs triggert
window.addEventListener('scroll', update, true)
return () => {
@@ -847,24 +865,25 @@ export default function RecordJobActions({
- {menuOpen && menuStyle && typeof document !== 'undefined'
+ {menuOpen && menuStyle && menuPortalTarget
? createPortal(
e.stopPropagation()}
>
{menuKeys.map(menuItem)}
,
- document.body
+ menuPortalTarget
)
: null}