mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-08 14:03:24 +09:00
enhance(ui): tweak tooltip style and misc fixes (#38524)
Tooltip changes: - center tooltips over their target (default placement `top` instead of `top-start`) - fix the arrow rendering off-center on references narrower than 22px like 16px icons, caused by tippy's hardcoded 3px arrow padding - fix arrowed tooltips overlapping their reference by 1px because the offset distance was derived from the raw arrow option - tweak tooltip colors and reduce content padding - fix a Firefox bug where lazily created tooltips would not show on the first hover, because Firefox skips enter/leave event dispatch when no such listener existed in the window at the time of the pointer crossing Other UI tweaks: - tweak repo sidebar release style - enhance actions back link - center step chevron - add border on workflow graph <img width="92" height="76" alt="Screenshot 2026-07-21 at 18 07 42" src="https://github.com/user-attachments/assets/968a51de-e470-48c0-b592-4a207dfcbb63" /> <img width="94" height="76" alt="Screenshot 2026-07-21 at 18 07 53" src="https://github.com/user-attachments/assets/5e8e0ce3-7536-4689-a6ae-9cfb6e867ddb" /> <img width="203" height="77" alt="Screenshot 2026-07-19 at 00 14 32" src="https://github.com/user-attachments/assets/71c16a54-ad14-4636-8af4-e68b3599f1d0" /> <img width="309" height="290" alt="Screenshot 2026-07-18 at 23 37 21" src="https://github.com/user-attachments/assets/29630504-d69a-4e23-978b-41ae6d0bff92" /> <img width="151" height="110" alt="Screenshot 2026-07-18 at 23 35 47" src="https://github.com/user-attachments/assets/b8107602-12f1-4f9b-a3f6-b930af25c9e7" /> --------- Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -7,15 +7,15 @@
|
|||||||
</a>
|
</a>
|
||||||
<span class="ui small label">{{.NumReleases}}</span>
|
<span class="ui small label">{{.NumReleases}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-relaxed-list">
|
<div>
|
||||||
<div class="flex-text-block">
|
<div class="flex-text-block">
|
||||||
<div>{{svg "octicon-tag"}}</div>
|
<div>{{svg "octicon-tag"}}</div>
|
||||||
<a class="gt-ellipsis muted" href="{{.LatestRelease.Link}}" title="{{.LatestRelease.Title}}">{{.LatestRelease.Title}}</a>
|
<a class="gt-ellipsis muted" href="{{.LatestRelease.Link}}" title="{{.LatestRelease.Title}}"><strong>{{.LatestRelease.Title}}</strong></a>
|
||||||
<div class="tw-shrink-0">
|
<div class="tw-shrink-0">
|
||||||
{{template "repo/release/label" (dict "Release" .LatestRelease "IsLatest" true)}}
|
{{template "repo/release/label" (dict "Release" .LatestRelease "IsLatest" true)}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-ml-[24px]">{{DateUtils.TimeSince .LatestRelease.CreatedUnix}}</div>
|
<div class="tw-ml-[24px] tw-text-12">{{DateUtils.TimeSince .LatestRelease.CreatedUnix}}</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="language-stats">
|
<div class="language-stats">
|
||||||
{{range .LanguageStats}}
|
{{range .LanguageStats}}
|
||||||
<div class="bar" style="width: {{.Percentage}}%; background-color: {{.Color}}" data-tooltip-placement="top" data-tooltip-content="{{.Language}}" data-tooltip-follow-cursor="horizontal"></div>
|
<div class="bar" style="width: {{.Percentage}}%; background-color: {{.Color}}" data-tooltip-content="{{.Language}}" data-tooltip-follow-cursor="horizontal"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="language-stats-details">
|
<div class="language-stats-details">
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
{{svg "octicon-cross-reference"}} {{ctx.Locale.Tr "repo.cite_this_repo"}}
|
{{svg "octicon-cross-reference"}} {{ctx.Locale.Tr "repo.cite_this_repo"}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="flex-text-block muted" {{if not (eq .Repository.Size 0)}}data-tooltip-placement="top" data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}>
|
<div class="flex-text-block muted" {{if not (eq .Repository.Size 0)}}data-tooltip-content="{{.Repository.SizeDetailsString}}"{{end}}>
|
||||||
{{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}}
|
{{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}}
|
||||||
{{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}}
|
{{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}}
|
||||||
{{svg "octicon-database"}} <b>{{ctx.Locale.PrettyNumber (index $fileSizeFields 0)}}</b> {{index $fileSizeFields 1}}
|
{{svg "octicon-database"}} <b>{{ctx.Locale.PrettyNumber (index $fileSizeFields 0)}}</b> {{index $fileSizeFields 1}}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tippy-box[data-theme="tooltip"] .tippy-content {
|
.tippy-box[data-theme="tooltip"] .tippy-content {
|
||||||
padding: 0.5rem 1rem;
|
padding: 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-box[data-theme="tooltip"] .tippy-svg-arrow-inner,
|
.tippy-box[data-theme="tooltip"] .tippy-svg-arrow-inner,
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ gitea-theme-meta-info {
|
|||||||
--color-reaction-hover-bg: var(--color-primary-light-4);
|
--color-reaction-hover-bg: var(--color-primary-light-4);
|
||||||
--color-reaction-active-bg: var(--color-primary-light-5);
|
--color-reaction-active-bg: var(--color-primary-light-5);
|
||||||
--color-tooltip-text: #fafafa;
|
--color-tooltip-text: #fafafa;
|
||||||
--color-tooltip-bg: #0b0b0cf0;
|
--color-tooltip-bg: #2b2e33;
|
||||||
--color-nav-bg: #18191b;
|
--color-nav-bg: #18191b;
|
||||||
--color-nav-hover-bg: var(--color-secondary-light-1);
|
--color-nav-hover-bg: var(--color-secondary-light-1);
|
||||||
--color-nav-text: var(--color-text);
|
--color-nav-text: var(--color-text);
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ gitea-theme-meta-info {
|
|||||||
--color-reaction-hover-bg: var(--color-primary-light-5);
|
--color-reaction-hover-bg: var(--color-primary-light-5);
|
||||||
--color-reaction-active-bg: var(--color-primary-light-6);
|
--color-reaction-active-bg: var(--color-primary-light-6);
|
||||||
--color-tooltip-text: #fbfdff;
|
--color-tooltip-text: #fbfdff;
|
||||||
--color-tooltip-bg: #000017f0;
|
--color-tooltip-bg: #25292e;
|
||||||
--color-nav-bg: #f6f7fa;
|
--color-nav-bg: #f6f7fa;
|
||||||
--color-nav-hover-bg: var(--color-secondary-light-1);
|
--color-nav-hover-bg: var(--color-secondary-light-1);
|
||||||
--color-nav-text: var(--color-text);
|
--color-nav-text: var(--color-text);
|
||||||
|
|||||||
@@ -493,7 +493,7 @@ async function hashChangeListener() {
|
|||||||
<SvgIcon
|
<SvgIcon
|
||||||
v-else
|
v-else
|
||||||
name="octicon-chevron-right"
|
name="octicon-chevron-right"
|
||||||
class="tw-mr-2 step-summary-chevron"
|
class="step-summary-chevron"
|
||||||
:class="{'tw-invisible': !isExpandable(jobStep.status)}"
|
:class="{'tw-invisible': !isExpandable(jobStep.status)}"
|
||||||
/>
|
/>
|
||||||
<ActionStatusIcon :status="jobStep.status" icon-variant="circle-fill"/>
|
<ActionStatusIcon :status="jobStep.status" icon-variant="circle-fill"/>
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ onBeforeUnmount(() => {
|
|||||||
<!-- make the view container full width to make users easier to read logs -->
|
<!-- make the view container full width to make users easier to read logs -->
|
||||||
<div class="ui fluid container">
|
<div class="ui fluid container">
|
||||||
<div class="action-view-header">
|
<div class="action-view-header">
|
||||||
<a v-if="backLink" class="action-view-back silenced" :href="backLink.href">
|
<a v-if="backLink" class="action-view-back" :href="backLink.href">
|
||||||
<SvgIcon name="octicon-arrow-left" :size="14"/>
|
<SvgIcon name="octicon-arrow-left" :size="14"/>
|
||||||
<span>{{ backLink.prefix }} <span class="action-view-back-name">{{ backLink.name }}</span></span>
|
<span>{{ backLink.prefix }} <span class="action-view-back-name">{{ backLink.name }}</span></span>
|
||||||
</a>
|
</a>
|
||||||
@@ -188,7 +188,7 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-text-block tw-pl-[20px]">
|
<div class="flex-text-block tw-pl-[20px]">
|
||||||
<span class="flex-text-inline tw-flex-shrink-0">
|
<span class="flex-text-inline tw-flex-shrink-0">
|
||||||
<ActionStatusIcon :locale-status="locale.status[attempt.status]" :status="attempt.status" :size="14" class="flex-text-block" icon-variant="circle-fill"/>
|
<ActionStatusIcon :locale-status="locale.status[attempt.status]" :status="attempt.status" :size="14" icon-variant="circle-fill"/>
|
||||||
<span>{{ locale.status[attempt.status] }}</span>
|
<span>{{ locale.status[attempt.status] }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
@@ -364,14 +364,10 @@ onBeforeUnmount(() => {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--color-text-light-1);
|
color: var(--color-text-light-1);
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-view-back:hover {
|
.action-view-back:hover {
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-view-back-name {
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -434,6 +434,7 @@ function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) {
|
|||||||
background: var(--color-console-bg);
|
background: var(--color-console-bg);
|
||||||
gap: var(--gap-block);
|
gap: var(--gap-block);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
border-bottom: 1px solid var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-workflow-info {
|
.graph-workflow-info {
|
||||||
|
|||||||
@@ -12,10 +12,21 @@ type TippyOpts = {
|
|||||||
const visibleInstances = new Set<Instance>();
|
const visibleInstances = new Set<Instance>();
|
||||||
const arrowSvg = html`<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
|
const arrowSvg = html`<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
|
||||||
|
|
||||||
|
// shrink tippy's default 3px arrow padding so the arrow can point at the center of
|
||||||
|
// narrow references like 16px icons with "start"/"end" placements
|
||||||
|
function arrowPadding({placement, reference}: {placement: Placement, reference: {width: number, height: number}}): number {
|
||||||
|
const isVertical = placement.startsWith('left') || placement.startsWith('right');
|
||||||
|
const referenceLength = isVertical ? reference.height : reference.width;
|
||||||
|
return Math.max(0, Math.min(3, referenceLength / 2 - 8)); // 8 = half of arrow width
|
||||||
|
}
|
||||||
|
|
||||||
export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||||
// the callback functions should be destructured from opts,
|
// the callback functions should be destructured from opts,
|
||||||
// because we should use our own wrapper functions to handle them, do not let the user override them
|
// because we should use our own wrapper functions to handle them, do not let the user override them
|
||||||
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
|
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
|
||||||
|
// CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare"
|
||||||
|
const resolvedTheme = theme || role || 'default';
|
||||||
|
const resolvedArrow = arrow ?? (resolvedTheme === 'bare' ? false : arrowSvg);
|
||||||
|
|
||||||
const instance: Instance = tippy(target, {
|
const instance: Instance = tippy(target, {
|
||||||
appendTo: document.body,
|
appendTo: document.body,
|
||||||
@@ -44,12 +55,12 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
|||||||
target.setAttribute('aria-controls', instance.popper.id);
|
target.setAttribute('aria-controls', instance.popper.id);
|
||||||
return onShow?.(instance);
|
return onShow?.(instance);
|
||||||
},
|
},
|
||||||
arrow: arrow ?? (theme === 'bare' ? false : arrowSvg),
|
arrow: resolvedArrow,
|
||||||
|
popperOptions: {modifiers: [{name: 'arrow', options: {padding: arrowPadding}}]},
|
||||||
// HTML role attribute, ideally the default role would be "popover" but it does not exist
|
// HTML role attribute, ideally the default role would be "popover" but it does not exist
|
||||||
role: role || 'menu',
|
role: role || 'menu',
|
||||||
// CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare"
|
theme: resolvedTheme,
|
||||||
theme: theme || role || 'default',
|
offset: [0, resolvedArrow ? 10 : 6],
|
||||||
offset: [0, arrow ? 10 : 6],
|
|
||||||
plugins: [followCursor],
|
plugins: [followCursor],
|
||||||
...other,
|
...other,
|
||||||
} satisfies Partial<Props>);
|
} satisfies Partial<Props>);
|
||||||
@@ -87,7 +98,7 @@ function attachTooltip(target: Element, content: Content | null = null): Instanc
|
|||||||
theme: 'tooltip',
|
theme: 'tooltip',
|
||||||
hideOnClick,
|
hideOnClick,
|
||||||
allowHTML: target.getAttribute('data-tooltip-render') === 'html',
|
allowHTML: target.getAttribute('data-tooltip-render') === 'html',
|
||||||
placement: target.getAttribute('data-tooltip-placement') as Placement || 'top-start',
|
placement: target.getAttribute('data-tooltip-placement') as Placement || 'top',
|
||||||
followCursor: target.getAttribute('data-tooltip-follow-cursor') as Props['followCursor'] || false,
|
followCursor: target.getAttribute('data-tooltip-follow-cursor') as Props['followCursor'] || false,
|
||||||
...((target.getAttribute('data-tooltip-interactive') === 'true') && {interactive: true, aria: {content: 'describedby', expanded: false}}),
|
...((target.getAttribute('data-tooltip-interactive') === 'true') && {interactive: true, aria: {content: 'describedby', expanded: false}}),
|
||||||
};
|
};
|
||||||
@@ -115,9 +126,14 @@ function switchTitleToTooltip(target: Element): void {
|
|||||||
* Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
|
* Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)"
|
||||||
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
|
* The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy
|
||||||
*/
|
*/
|
||||||
function lazyTooltipOnMouseHover(this: HTMLElement, e: Event): void {
|
function lazyTooltipOnMouseHover(e: Event): void {
|
||||||
(e.target as HTMLElement).removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
|
const el = e.currentTarget as HTMLElement;
|
||||||
attachTooltip(this);
|
el.removeEventListener('mouseover', lazyTooltipOnMouseHover, true);
|
||||||
|
// Firefox skips enter/leave dispatch when the window had no such listeners at the time of the
|
||||||
|
// pointer crossing, so the new tippy misses its first "mouseenter". Show via a synthetic event,
|
||||||
|
// carrying over the cursor position for "followCursor" tooltips.
|
||||||
|
const {clientX, clientY} = e as MouseEvent;
|
||||||
|
attachTooltip(el)?.reference.dispatchEvent(new MouseEvent('mouseenter', {clientX, clientY}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activate the tooltip for current element.
|
// Activate the tooltip for current element.
|
||||||
|
|||||||
Reference in New Issue
Block a user