@hyperframes/studio 0.6.0-alpha.9 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/assets/hyperframes-player-CzwFysqv.js +418 -0
  2. package/dist/assets/index-D1JDq7Gg.css +1 -0
  3. package/dist/assets/index-hYc4aP7M.js +117 -0
  4. package/dist/favicon.svg +14 -0
  5. package/dist/index.html +3 -2
  6. package/package.json +9 -9
  7. package/src/App.tsx +421 -4303
  8. package/src/captions/components/CaptionOverlay.tsx +13 -246
  9. package/src/captions/components/CaptionOverlayUtils.ts +221 -0
  10. package/src/components/AskAgentModal.tsx +120 -0
  11. package/src/components/StudioHeader.tsx +133 -0
  12. package/src/components/StudioLeftSidebar.tsx +125 -0
  13. package/src/components/StudioPreviewArea.tsx +167 -0
  14. package/src/components/StudioRightPanel.tsx +198 -0
  15. package/src/components/TimelineToolbar.tsx +89 -0
  16. package/src/components/editor/DomEditOverlay.tsx +88 -993
  17. package/src/components/editor/EaseCurveEditor.tsx +221 -0
  18. package/src/components/editor/FileTree.tsx +13 -621
  19. package/src/components/editor/FileTreeIcons.tsx +128 -0
  20. package/src/components/editor/FileTreeNodes.tsx +496 -0
  21. package/src/components/editor/MotionPanel.tsx +16 -390
  22. package/src/components/editor/MotionPanelFields.tsx +185 -0
  23. package/src/components/editor/PropertyPanel.test.ts +0 -49
  24. package/src/components/editor/PropertyPanel.tsx +132 -2763
  25. package/src/components/editor/domEditOverlayGeometry.ts +211 -0
  26. package/src/components/editor/domEditOverlayGestures.ts +138 -0
  27. package/src/components/editor/domEditOverlayStartGesture.ts +155 -0
  28. package/src/components/editor/domEditing.ts +44 -1117
  29. package/src/components/editor/domEditingAgentPrompt.ts +97 -0
  30. package/src/components/editor/domEditingDom.ts +266 -0
  31. package/src/components/editor/domEditingElement.ts +329 -0
  32. package/src/components/editor/domEditingLayers.ts +460 -0
  33. package/src/components/editor/domEditingTypes.ts +125 -0
  34. package/src/components/editor/manualEditingAvailability.test.ts +2 -2
  35. package/src/components/editor/manualEditingAvailability.ts +1 -1
  36. package/src/components/editor/manualEdits.ts +84 -1049
  37. package/src/components/editor/manualEditsDom.ts +436 -0
  38. package/src/components/editor/manualEditsParsing.ts +280 -0
  39. package/src/components/editor/manualEditsSnapshot.ts +333 -0
  40. package/src/components/editor/manualEditsTypes.ts +141 -0
  41. package/src/components/editor/propertyPanelColor.tsx +371 -0
  42. package/src/components/editor/propertyPanelFill.tsx +421 -0
  43. package/src/components/editor/propertyPanelFont.tsx +455 -0
  44. package/src/components/editor/propertyPanelHelpers.ts +401 -0
  45. package/src/components/editor/propertyPanelPrimitives.tsx +357 -0
  46. package/src/components/editor/propertyPanelSections.tsx +453 -0
  47. package/src/components/editor/propertyPanelStyleSections.tsx +411 -0
  48. package/src/components/editor/studioMotion.ts +47 -434
  49. package/src/components/editor/studioMotionOps.ts +299 -0
  50. package/src/components/editor/studioMotionTypes.ts +168 -0
  51. package/src/components/editor/useDomEditOverlayGestures.ts +393 -0
  52. package/src/components/editor/useDomEditOverlayRects.ts +207 -0
  53. package/src/components/nle/NLELayout.tsx +68 -155
  54. package/src/components/nle/NLEPreview.tsx +3 -0
  55. package/src/components/nle/useCompositionStack.ts +126 -0
  56. package/src/components/renders/RenderQueue.tsx +102 -31
  57. package/src/components/renders/useRenderQueue.ts +8 -2
  58. package/src/components/sidebar/LeftSidebar.tsx +186 -186
  59. package/src/contexts/DomEditContext.tsx +137 -0
  60. package/src/contexts/FileManagerContext.tsx +110 -0
  61. package/src/contexts/PanelLayoutContext.tsx +68 -0
  62. package/src/contexts/StudioContext.tsx +135 -0
  63. package/src/hooks/useAppHotkeys.ts +326 -0
  64. package/src/hooks/useAskAgentModal.ts +162 -0
  65. package/src/hooks/useCaptionDetection.ts +132 -0
  66. package/src/hooks/useCompositionDimensions.ts +25 -0
  67. package/src/hooks/useConsoleErrorCapture.ts +60 -0
  68. package/src/hooks/useDomEditCommits.ts +437 -0
  69. package/src/hooks/useDomEditSession.ts +342 -0
  70. package/src/hooks/useDomEditTextCommits.ts +330 -0
  71. package/src/hooks/useDomSelection.ts +398 -0
  72. package/src/hooks/useFileManager.ts +431 -0
  73. package/src/hooks/useFrameCapture.ts +77 -0
  74. package/src/hooks/useLintModal.ts +35 -0
  75. package/src/hooks/useManifestPersistence.ts +492 -0
  76. package/src/hooks/usePanelLayout.ts +68 -0
  77. package/src/hooks/usePreviewInteraction.ts +153 -0
  78. package/src/hooks/useRenderClipContent.ts +124 -0
  79. package/src/hooks/useTimelineEditing.ts +472 -0
  80. package/src/hooks/useToast.ts +20 -0
  81. package/src/player/components/Player.tsx +33 -2
  82. package/src/player/components/Timeline.test.ts +0 -8
  83. package/src/player/components/Timeline.tsx +196 -1518
  84. package/src/player/components/TimelineCanvas.tsx +434 -0
  85. package/src/player/components/TimelineClip.tsx +9 -244
  86. package/src/player/components/TimelineEmptyState.tsx +102 -0
  87. package/src/player/components/TimelineRuler.tsx +90 -0
  88. package/src/player/components/timelineIcons.tsx +49 -0
  89. package/src/player/components/timelineLayout.ts +215 -0
  90. package/src/player/components/timelineUtils.ts +211 -0
  91. package/src/player/components/useTimelineClipDrag.ts +388 -0
  92. package/src/player/components/useTimelinePlayhead.ts +200 -0
  93. package/src/player/components/useTimelineRangeSelection.ts +135 -0
  94. package/src/player/hooks/usePlaybackKeyboard.ts +171 -0
  95. package/src/player/hooks/useTimelinePlayer.ts +105 -1371
  96. package/src/player/hooks/useTimelineSyncCallbacks.ts +288 -0
  97. package/src/player/lib/playbackAdapter.ts +145 -0
  98. package/src/player/lib/playbackShortcuts.ts +68 -0
  99. package/src/player/lib/playbackTypes.ts +60 -0
  100. package/src/player/lib/timelineDOM.ts +373 -0
  101. package/src/player/lib/timelineElementHelpers.ts +303 -0
  102. package/src/player/lib/timelineIframeHelpers.ts +269 -0
  103. package/src/utils/domEditHelpers.ts +50 -0
  104. package/src/utils/studioFontHelpers.ts +83 -0
  105. package/src/utils/studioHelpers.ts +214 -0
  106. package/src/utils/studioPreviewHelpers.ts +185 -0
  107. package/src/utils/timelineDiscovery.ts +1 -1
  108. package/dist/assets/hyperframes-player-DjsVzYFP.js +0 -418
  109. package/dist/assets/index-14zH9lqh.css +0 -1
  110. package/dist/assets/index-DYCiFGWQ.js +0 -108
  111. package/src/player/components/TimelineClip.test.ts +0 -92
@@ -0,0 +1,434 @@
1
+ import { memo, type ReactNode } from "react";
2
+ import { TimelineClip } from "./TimelineClip";
3
+ import { TimelineRuler } from "./TimelineRuler";
4
+ import {
5
+ getTimelineEditCapabilities,
6
+ resolveBlockedTimelineEditIntent,
7
+ type TimelineRangeSelection,
8
+ } from "./timelineEditing";
9
+ import { getRenderedTimelineElement, type TimelineTheme } from "./timelineTheme";
10
+ import { GUTTER, TRACK_H, RULER_H, CLIP_Y, CLIP_HANDLE_W } from "./timelineLayout";
11
+ import type { TimelineElement } from "../store/playerStore";
12
+ import type { DraggedClipState, ResizingClipState, BlockedClipState } from "./useTimelineClipDrag";
13
+ import { formatTime } from "../lib/time";
14
+ import type { TrackVisualStyle } from "./timelineIcons";
15
+
16
+ interface TimelineCanvasProps {
17
+ major: number[];
18
+ minor: number[];
19
+ pps: number;
20
+ trackContentWidth: number;
21
+ totalH: number;
22
+ effectiveDuration: number;
23
+ majorTickInterval: number;
24
+ shiftHeld: boolean;
25
+ rangeSelection: TimelineRangeSelection | null;
26
+ theme: TimelineTheme;
27
+ displayTrackOrder: number[];
28
+ trackOrder: number[];
29
+ tracks: [number, TimelineElement[]][];
30
+ trackStyles: Map<number, TrackVisualStyle>;
31
+ selectedElementId: string | null;
32
+ hoveredClip: string | null;
33
+ draggedClip: DraggedClipState | null;
34
+ resizingClip: ResizingClipState | null;
35
+ blockedClipRef: React.RefObject<BlockedClipState | null>;
36
+ suppressClickRef: React.RefObject<boolean>;
37
+ scrollRef: React.RefObject<HTMLDivElement | null>;
38
+ renderClipContent?: (
39
+ element: TimelineElement,
40
+ style: { clip: string; label: string },
41
+ ) => ReactNode;
42
+ renderClipOverlay?: (element: TimelineElement) => ReactNode;
43
+ playheadRef: React.RefObject<HTMLDivElement | null>;
44
+ onResizeElement?: unknown;
45
+ onMoveElement?: unknown;
46
+ onDrillDown?: (element: TimelineElement) => void;
47
+ onSelectElement?: (element: TimelineElement | null) => void;
48
+ setHoveredClip: (key: string | null) => void;
49
+ setShowPopover: (v: boolean) => void;
50
+ setRangeSelection: (v: null) => void;
51
+ setResizingClip: (v: ResizingClipState | null) => void;
52
+ setDraggedClip: (v: DraggedClipState | null) => void;
53
+ setSelectedElementId: (id: string | null) => void;
54
+ syncClipDragAutoScroll: (x: number, y: number) => void;
55
+ shiftClickClipRef: React.RefObject<{
56
+ element: TimelineElement;
57
+ anchorX: number;
58
+ anchorY: number;
59
+ } | null>;
60
+ getPreviewElement: (element: TimelineElement) => TimelineElement;
61
+ getTrackStyle: (tag: string) => TrackVisualStyle;
62
+ }
63
+
64
+ export const TimelineCanvas = memo(function TimelineCanvas({
65
+ major,
66
+ minor,
67
+ pps,
68
+ trackContentWidth,
69
+ totalH,
70
+ effectiveDuration,
71
+ majorTickInterval,
72
+ shiftHeld,
73
+ rangeSelection,
74
+ theme,
75
+ displayTrackOrder,
76
+ trackOrder,
77
+ tracks,
78
+ trackStyles,
79
+ selectedElementId,
80
+ hoveredClip,
81
+ draggedClip,
82
+ resizingClip: _resizingClip,
83
+ blockedClipRef,
84
+ suppressClickRef,
85
+ scrollRef,
86
+ renderClipContent,
87
+ renderClipOverlay,
88
+ playheadRef,
89
+ onResizeElement,
90
+ onMoveElement,
91
+ onDrillDown,
92
+ onSelectElement,
93
+ setHoveredClip,
94
+ setShowPopover,
95
+ setRangeSelection,
96
+ setResizingClip,
97
+ setDraggedClip,
98
+ setSelectedElementId,
99
+ syncClipDragAutoScroll,
100
+ shiftClickClipRef,
101
+ getPreviewElement,
102
+ getTrackStyle,
103
+ }: TimelineCanvasProps) {
104
+ const draggedElement = draggedClip?.element ?? null;
105
+ const activeDraggedElement =
106
+ draggedClip?.started === true && draggedElement
107
+ ? getRenderedTimelineElement({
108
+ element: draggedElement,
109
+ draggedElementId: draggedElement.key ?? draggedElement.id,
110
+ previewStart: draggedClip.previewStart,
111
+ previewTrack: draggedClip.previewTrack,
112
+ })
113
+ : null;
114
+ const activeDraggedPosition =
115
+ draggedClip?.started === true && activeDraggedElement && scrollRef.current
116
+ ? {
117
+ left:
118
+ draggedClip.pointerClientX -
119
+ scrollRef.current.getBoundingClientRect().left +
120
+ scrollRef.current.scrollLeft -
121
+ draggedClip.pointerOffsetX,
122
+ top:
123
+ draggedClip.pointerClientY -
124
+ scrollRef.current.getBoundingClientRect().top +
125
+ scrollRef.current.scrollTop -
126
+ draggedClip.pointerOffsetY,
127
+ }
128
+ : null;
129
+
130
+ const renderClipChildren = (element: TimelineElement, clipStyle: TrackVisualStyle) => (
131
+ <>
132
+ {renderClipOverlay?.(element)}
133
+ <div
134
+ className={
135
+ renderClipContent
136
+ ? "absolute inset-0 overflow-hidden"
137
+ : "flex flex-col justify-center overflow-hidden flex-1 min-w-0 px-6"
138
+ }
139
+ >
140
+ {renderClipContent?.(element, clipStyle) ?? (
141
+ <div className="flex h-full min-h-0 flex-col justify-between py-3">
142
+ <span
143
+ className="max-w-full truncate rounded-md px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-[0.08em] leading-none"
144
+ style={{
145
+ color: clipStyle.label,
146
+ background: `${clipStyle.accent}26`,
147
+ boxShadow: `inset 0 0 0 1px ${clipStyle.accent}33`,
148
+ }}
149
+ >
150
+ {element.tag}
151
+ </span>
152
+ <span
153
+ className="max-w-full truncate rounded-md px-1.5 py-0.5 text-[10px] font-medium tabular-nums leading-none"
154
+ style={{ color: theme.textSecondary, background: "rgba(255,255,255,0.04)" }}
155
+ >
156
+ {formatTime(element.start)} {"→"} {formatTime(element.start + element.duration)}
157
+ </span>
158
+ </div>
159
+ )}
160
+ </div>
161
+ </>
162
+ );
163
+
164
+ return (
165
+ <div className="relative" style={{ height: totalH, width: GUTTER + trackContentWidth }}>
166
+ <TimelineRuler
167
+ major={major}
168
+ minor={minor}
169
+ pps={pps}
170
+ trackContentWidth={trackContentWidth}
171
+ totalH={totalH}
172
+ effectiveDuration={effectiveDuration}
173
+ majorTickInterval={majorTickInterval}
174
+ shiftHeld={shiftHeld}
175
+ rangeSelection={rangeSelection}
176
+ theme={theme}
177
+ />
178
+
179
+ {displayTrackOrder.map((trackNum) => {
180
+ const els = tracks.find(([t]) => t === trackNum)?.[1] ?? [];
181
+ const ts = trackStyles.get(trackNum) ?? getTrackStyle("");
182
+ const isPendingTrack =
183
+ draggedClip?.started === true && !trackOrder.includes(trackNum) && els.length === 0;
184
+ return (
185
+ <div
186
+ key={trackNum}
187
+ className="relative flex"
188
+ style={{
189
+ height: TRACK_H,
190
+ background: theme.rowBackground,
191
+ borderBottom: `1px solid ${theme.rowBorder}`,
192
+ }}
193
+ >
194
+ <div
195
+ className="flex-shrink-0 flex items-center justify-center"
196
+ style={{
197
+ width: GUTTER,
198
+ background: theme.gutterBackground,
199
+ borderRight: `1px solid ${theme.gutterBorder}`,
200
+ }}
201
+ >
202
+ <div
203
+ className="flex items-center justify-center"
204
+ style={{
205
+ width: 18,
206
+ height: 18,
207
+ borderRadius: 6,
208
+ backgroundColor: ts.iconBackground,
209
+ border: `1px solid ${theme.gutterBorder}`,
210
+ color: "#fff",
211
+ }}
212
+ >
213
+ {ts.icon}
214
+ </div>
215
+ </div>
216
+ <div style={{ width: trackContentWidth }} className="relative">
217
+ {isPendingTrack && (
218
+ <div
219
+ className="absolute inset-0 flex items-center"
220
+ style={{
221
+ paddingLeft: 16,
222
+ color: ts.label,
223
+ fontSize: 11,
224
+ letterSpacing: "0.08em",
225
+ textTransform: "uppercase",
226
+ background: `linear-gradient(90deg, ${ts.accent}14, transparent 28%)`,
227
+ boxShadow: `inset 0 0 0 1px ${ts.accent}24`,
228
+ }}
229
+ >
230
+ New track
231
+ </div>
232
+ )}
233
+ {els.map((el, i) => {
234
+ const clipStyle = getTrackStyle(el.tag);
235
+ const elementKey = el.key ?? el.id;
236
+ const capabilities = getTimelineEditCapabilities(el);
237
+ const isSelected = selectedElementId === elementKey;
238
+ const isComposition = !!el.compositionSrc;
239
+ const clipKey = `${elementKey}-${i}`;
240
+ const isDraggingClip =
241
+ draggedClip?.started === true &&
242
+ (draggedElement?.key ?? draggedElement?.id) === elementKey;
243
+ if (isDraggingClip) return null;
244
+ const previewElement = getPreviewElement(el);
245
+ return (
246
+ <TimelineClip
247
+ key={clipKey}
248
+ el={previewElement}
249
+ pps={pps}
250
+ clipY={CLIP_Y}
251
+ isSelected={isSelected}
252
+ isHovered={hoveredClip === clipKey}
253
+ isDragging={false}
254
+ hasCustomContent={!!renderClipContent}
255
+ theme={theme}
256
+ trackStyle={clipStyle}
257
+ isComposition={isComposition}
258
+ onHoverStart={() => setHoveredClip(clipKey)}
259
+ onHoverEnd={() => setHoveredClip(null)}
260
+ onResizeStart={(edge, e) => {
261
+ if (e.button !== 0 || e.shiftKey || !onResizeElement) return;
262
+ if (edge === "start" && !capabilities.canTrimStart) return;
263
+ if (edge === "end" && !capabilities.canTrimEnd) return;
264
+ e.stopPropagation();
265
+ blockedClipRef.current = null;
266
+ setShowPopover(false);
267
+ setRangeSelection(null);
268
+ setResizingClip({
269
+ element: el,
270
+ edge,
271
+ originClientX: e.clientX,
272
+ previewStart: el.start,
273
+ previewDuration: el.duration,
274
+ previewPlaybackStart: el.playbackStart,
275
+ started: false,
276
+ });
277
+ }}
278
+ onPointerDown={(e) => {
279
+ if (e.button !== 0) return;
280
+ if (e.shiftKey) {
281
+ shiftClickClipRef.current = {
282
+ element: el,
283
+ anchorX: e.clientX,
284
+ anchorY: e.clientY,
285
+ };
286
+ return;
287
+ }
288
+ const target = e.currentTarget as HTMLElement;
289
+ const rect = target.getBoundingClientRect();
290
+ const blockedIntent = resolveBlockedTimelineEditIntent({
291
+ width: rect.width,
292
+ offsetX: e.clientX - rect.left,
293
+ handleWidth: CLIP_HANDLE_W,
294
+ capabilities,
295
+ });
296
+ if (
297
+ blockedIntent &&
298
+ ((blockedIntent === "move" && onMoveElement) ||
299
+ (blockedIntent !== "move" && onResizeElement))
300
+ ) {
301
+ blockedClipRef.current = {
302
+ element: el,
303
+ intent: blockedIntent,
304
+ originClientX: e.clientX,
305
+ originClientY: e.clientY,
306
+ started: false,
307
+ };
308
+ return;
309
+ }
310
+ if (!onMoveElement || !capabilities.canMove) return;
311
+ blockedClipRef.current = null;
312
+ setShowPopover(false);
313
+ setRangeSelection(null);
314
+ setDraggedClip({
315
+ element: el,
316
+ originClientX: e.clientX,
317
+ originClientY: e.clientY,
318
+ originScrollLeft: scrollRef.current?.scrollLeft ?? 0,
319
+ originScrollTop: scrollRef.current?.scrollTop ?? 0,
320
+ pointerClientX: e.clientX,
321
+ pointerClientY: e.clientY,
322
+ pointerOffsetX: e.clientX - rect.left,
323
+ pointerOffsetY: e.clientY - rect.top,
324
+ previewStart: el.start,
325
+ previewTrack: el.track,
326
+ started: false,
327
+ });
328
+ syncClipDragAutoScroll(e.clientX, e.clientY);
329
+ }}
330
+ onClick={(e) => {
331
+ e.stopPropagation();
332
+ if (suppressClickRef.current) return;
333
+ const nextElement = isSelected ? null : el;
334
+ setSelectedElementId(nextElement ? elementKey : null);
335
+ onSelectElement?.(nextElement);
336
+ }}
337
+ onDoubleClick={(e) => {
338
+ e.stopPropagation();
339
+ if (suppressClickRef.current) return;
340
+ if (isComposition && onDrillDown) onDrillDown(el);
341
+ }}
342
+ >
343
+ {renderClipChildren(previewElement, clipStyle)}
344
+ </TimelineClip>
345
+ );
346
+ })}
347
+ </div>
348
+ </div>
349
+ );
350
+ })}
351
+
352
+ {/* Drag ghost */}
353
+ {activeDraggedElement && activeDraggedPosition && (
354
+ <div
355
+ className="absolute pointer-events-none"
356
+ style={{
357
+ top: activeDraggedPosition.top,
358
+ left: activeDraggedPosition.left,
359
+ width: Math.max(activeDraggedElement.duration * pps, 4),
360
+ height: TRACK_H - CLIP_Y * 2,
361
+ zIndex: 40,
362
+ }}
363
+ >
364
+ <TimelineClip
365
+ el={{ ...activeDraggedElement, start: 0 }}
366
+ pps={pps}
367
+ clipY={0}
368
+ isSelected={selectedElementId === (activeDraggedElement.key ?? activeDraggedElement.id)}
369
+ isHovered={false}
370
+ isDragging={true}
371
+ hasCustomContent={!!renderClipContent}
372
+ theme={theme}
373
+ trackStyle={getTrackStyle(activeDraggedElement.tag)}
374
+ isComposition={!!activeDraggedElement.compositionSrc}
375
+ onHoverStart={() => {}}
376
+ onHoverEnd={() => {}}
377
+ onResizeStart={() => {}}
378
+ onClick={() => {}}
379
+ onDoubleClick={() => {}}
380
+ >
381
+ {renderClipChildren(activeDraggedElement, getTrackStyle(activeDraggedElement.tag))}
382
+ </TimelineClip>
383
+ </div>
384
+ )}
385
+
386
+ {/* Range highlight */}
387
+ {rangeSelection && (
388
+ <div
389
+ className="absolute pointer-events-none"
390
+ style={{
391
+ left: GUTTER + Math.min(rangeSelection.start, rangeSelection.end) * pps,
392
+ width: Math.abs(rangeSelection.end - rangeSelection.start) * pps,
393
+ top: RULER_H,
394
+ bottom: 0,
395
+ backgroundColor: "rgba(59, 130, 246, 0.12)",
396
+ borderLeft: "1px solid rgba(59, 130, 246, 0.4)",
397
+ borderRight: "1px solid rgba(59, 130, 246, 0.4)",
398
+ zIndex: 50,
399
+ }}
400
+ />
401
+ )}
402
+
403
+ {/* Playhead */}
404
+ <div
405
+ ref={playheadRef}
406
+ className="absolute top-0 bottom-0 pointer-events-none"
407
+ style={{ left: `${GUTTER}px`, zIndex: 100 }}
408
+ >
409
+ <div
410
+ className="absolute top-0 bottom-0"
411
+ style={{
412
+ left: "50%",
413
+ width: 2,
414
+ marginLeft: -1,
415
+ background: "var(--hf-accent, #3CE6AC)",
416
+ boxShadow: "0 0 8px rgba(60,230,172,0.5)",
417
+ }}
418
+ />
419
+ <div className="absolute" style={{ left: "50%", top: 0, transform: "translateX(-50%)" }}>
420
+ <div
421
+ style={{
422
+ width: 0,
423
+ height: 0,
424
+ borderLeft: "6px solid transparent",
425
+ borderRight: "6px solid transparent",
426
+ borderTop: "8px solid var(--hf-accent, #3CE6AC)",
427
+ filter: "drop-shadow(0 1px 3px rgba(0,0,0,0.6))",
428
+ }}
429
+ />
430
+ </div>
431
+ </div>
432
+ </div>
433
+ );
434
+ });