|
1 | 1 | <script lang="ts"> |
| 2 | + import { portal } from '@gitbutler/ui/utils/portal'; |
2 | 3 | import { pxToRem } from '@gitbutler/ui/utils/pxToRem'; |
3 | 4 |
|
4 | 5 | interface Props { |
|
9 | 10 | } |
10 | 11 |
|
11 | 12 | const { hovered, activated, advertize, yOffsetPx = 0 }: Props = $props(); |
| 13 | +
|
| 14 | + let containerElement = $state<HTMLDivElement>(); |
| 15 | + let indicatorElement = $state<HTMLDivElement>(); |
| 16 | + let indicatorRect = $state<{ top: number; left: number; width: number; height: number }>(); |
| 17 | + let stackViewElement = $state<Element | null>(null); |
| 18 | +
|
| 19 | + function updatePosition() { |
| 20 | + if (!indicatorElement) return; |
| 21 | +
|
| 22 | + const rect = indicatorElement.getBoundingClientRect(); |
| 23 | + const stackView = indicatorElement.closest('.stack-view'); |
| 24 | + if (!stackView) return; |
| 25 | +
|
| 26 | + stackViewElement = stackView; |
| 27 | +
|
| 28 | + const stackRect = stackView.getBoundingClientRect(); |
| 29 | + const scrollTop = stackView.scrollTop || 0; |
| 30 | +
|
| 31 | + // Calculate position relative to .stack-view container including scroll offset |
| 32 | + indicatorRect = { |
| 33 | + top: rect.top - stackRect.top + scrollTop, |
| 34 | + left: rect.left - stackRect.left, |
| 35 | + width: rect.width, |
| 36 | + height: rect.height |
| 37 | + }; |
| 38 | + } |
| 39 | +
|
| 40 | + $effect(() => { |
| 41 | + if (containerElement && indicatorElement && activated) { |
| 42 | + updatePosition(); |
| 43 | + } |
| 44 | + }); |
12 | 45 | </script> |
13 | 46 |
|
14 | 47 | <div |
| 48 | + bind:this={containerElement} |
15 | 49 | class="dropzone-target container" |
16 | 50 | class:activated |
17 | 51 | class:advertize |
18 | 52 | class:hovered |
19 | 53 | style:--y-offset="{pxToRem(yOffsetPx)}rem" |
20 | 54 | > |
21 | | - <div class="indicator"></div> |
| 55 | + <div bind:this={indicatorElement} class="indicator-placeholder"></div> |
22 | 56 | </div> |
23 | 57 |
|
| 58 | +{#if activated && indicatorRect && stackViewElement} |
| 59 | + <div |
| 60 | + class="indicator-portal" |
| 61 | + class:hovered |
| 62 | + class:advertize |
| 63 | + use:portal={stackViewElement} |
| 64 | + style:top="{indicatorRect.top}px" |
| 65 | + style:left="{indicatorRect.left}px" |
| 66 | + style:width="{indicatorRect.width}px" |
| 67 | + style:height="{indicatorRect.height}px" |
| 68 | + > |
| 69 | + <div class="indicator"></div> |
| 70 | + </div> |
| 71 | +{/if} |
| 72 | + |
24 | 73 | <style lang="postcss"> |
25 | 74 | .container { |
26 | | - --dropzone-overlap: calc(var(--dropzone-height) / 2); |
| 75 | + --dropzone-overlap: calc(var(--dropzone-height) / -2); |
27 | 76 | --dropzone-height: 24px; |
28 | 77 |
|
29 | 78 | display: flex; |
30 | | -
|
31 | | - z-index: var(--z-floating); |
32 | | -
|
| 79 | + z-index: var(--z-modal); |
33 | 80 | position: absolute; |
34 | 81 | top: var(--y-offset); |
35 | 82 | align-items: center; |
36 | 83 | width: 100%; |
37 | | -
|
38 | 84 | height: var(--dropzone-height); |
39 | | - margin-top: calc(var(--dropzone-overlap) * -1); |
40 | | - transition: background-color 0.3s ease-in-out; |
41 | | -
|
42 | | - /* It is very important that all children are pointer-events: none */ |
43 | | - /* https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element */ |
44 | | - & * { |
45 | | - pointer-events: none; |
46 | | - } |
| 85 | + margin-top: var(--dropzone-overlap); |
| 86 | + /* For debugging */ |
| 87 | + /* background-color: rgba(238, 130, 238, 0.319); */ |
47 | 88 |
|
48 | 89 | &:not(.activated) { |
49 | 90 | display: none; |
50 | 91 | } |
51 | 92 |
|
52 | | - &.hovered { |
53 | | - & .indicator { |
54 | | - background-color: var(--clr-theme-pop-element); |
55 | | - } |
| 93 | + & > * { |
| 94 | + pointer-events: none; /* Block all nested elements */ |
56 | 95 | } |
| 96 | + } |
| 97 | +
|
| 98 | + .indicator-placeholder { |
| 99 | + width: 100%; |
| 100 | + height: 3px; |
| 101 | + margin-top: 1px; |
| 102 | + background-color: transparent; |
| 103 | + } |
| 104 | +
|
| 105 | + .indicator-portal { |
| 106 | + display: flex; |
| 107 | + z-index: var(--z-blocker); |
| 108 | + position: absolute; |
| 109 | + align-items: center; |
| 110 | + pointer-events: none; |
57 | 111 |
|
58 | | - &:not(.hovered).advertize { |
| 112 | + &.hovered { |
59 | 113 | & .indicator { |
60 | | - background-color: var(--clr-theme-pop-soft-hover); |
| 114 | + background-color: var(--clr-theme-pop-element); |
61 | 115 | } |
62 | 116 | } |
63 | 117 | } |
64 | 118 |
|
65 | 119 | .indicator { |
66 | 120 | width: 100%; |
67 | 121 | height: 3px; |
68 | | - margin-top: 1px; |
69 | 122 | background-color: transparent; |
70 | | - transition: opacity 0.1s; |
| 123 | + transition: background-color var(--transition-fast); |
71 | 124 | } |
72 | 125 | </style> |
0 commit comments