feat(ui): add Tooltip component and update TUI controls

This commit is contained in:
syntaxbullet
2026-02-09 22:33:40 +01:00
parent 658f4ab841
commit 961383b402
5 changed files with 182 additions and 9 deletions

View File

@@ -0,0 +1,136 @@
---
---
<div id="tui-tooltip" class="tui-tooltip">
<div class="tooltip-header">
<span class="tooltip-title"></span>
</div>
<div class="tooltip-body">
<span class="tooltip-desc"></span>
</div>
</div>
<style>
.tui-tooltip {
position: fixed;
display: none;
pointer-events: none;
z-index: 1000;
background: rgba(10, 10, 10, 0.95);
border: 1px solid var(--text-color, #ff6700);
padding: 8px 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
min-width: 200px;
max-width: 300px;
backdrop-filter: blur(4px);
font-family: var(--font-mono, monospace);
will-change: transform, display;
}
.tooltip-header {
margin-bottom: 4px;
border-bottom: 1px solid rgba(255, 103, 0, 0.3);
padding-bottom: 4px;
}
.tooltip-title {
font-size: 11px;
font-weight: bold;
text-transform: uppercase;
color: var(--text-color, #ff6700);
letter-spacing: 1px;
}
.tooltip-desc {
font-size: 10px;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
</style>
<script>
const tooltip = document.getElementById("tui-tooltip");
const titleEl = tooltip?.querySelector(".tooltip-title");
const descEl = tooltip?.querySelector(".tooltip-desc");
const OFFSET_X = 15;
const OFFSET_Y = 15;
if (tooltip && titleEl && descEl) {
let isVisible = false;
const updatePosition = (e: MouseEvent) => {
if (!isVisible) return;
const rect = tooltip.getBoundingClientRect();
const winW = window.innerWidth;
const winH = window.innerHeight;
// Calculate potential position
let x = e.clientX + OFFSET_X;
let y = e.clientY + OFFSET_Y;
// Flip horizontally if toolip goes off right edge
if (x + rect.width > winW) {
x = e.clientX - rect.width - OFFSET_X;
}
// Flip vertically if tooltip goes off bottom edge
if (y + rect.height > winH) {
y = e.clientY - rect.height - OFFSET_Y;
}
// Ensure it doesn't go off top/left
x = Math.max(0, x);
y = Math.max(0, y);
tooltip.style.left = `${x}px`;
tooltip.style.top = `${y}px`;
};
const showTooltip = (target: Element, e: MouseEvent) => {
const title = target.getAttribute("data-tooltip-title");
const desc = target.getAttribute("data-tooltip-desc");
if (title) {
titleEl.textContent = title;
descEl.textContent = desc || "";
tooltip.style.display = "block";
isVisible = true;
updatePosition(e);
}
};
const hideTooltip = () => {
tooltip.style.display = "none";
isVisible = false;
};
// Event delegation
document.addEventListener("mouseover", (e) => {
const target = (e.target as HTMLElement).closest(
"[data-tooltip-title]",
);
if (target) {
showTooltip(target, e as MouseEvent);
}
});
document.addEventListener("mouseout", (e) => {
const target = (e.target as HTMLElement).closest(
"[data-tooltip-title]",
);
if (target) {
const related = e.relatedTarget as HTMLElement;
if (related && target.contains(related)) return;
hideTooltip();
}
});
document.addEventListener("mousemove", (e) => {
if (isVisible) {
updatePosition(e as MouseEvent);
}
});
}
</script>

View File

@@ -5,16 +5,25 @@ interface Props {
shortcut?: string;
variant?: "default" | "primary" | "subtle";
title?: string;
description?: string;
}
const { id, label, shortcut, variant = "default", title = "" } = Astro.props;
const {
id,
label,
shortcut,
variant = "default",
title = "",
description = "",
} = Astro.props;
---
<button
type="button"
class:list={["tui-button", `tui-button--${variant}`]}
id={id}
title={title}
data-tooltip-title={title}
data-tooltip-desc={description}
>
{shortcut && <span class="tui-button-shortcut">{shortcut}</span>}
<span class="tui-button-label">{label}</span>

View File

@@ -5,12 +5,25 @@ interface Props {
options: string[];
value?: string;
title?: string;
description?: string;
}
const { id, label, options, value = options[0], title = "" } = Astro.props;
const {
id,
label,
options,
value = options[0],
title = "",
description = "",
} = Astro.props;
---
<div class="tui-segment" data-segment-id={id} title={title}>
<div
class="tui-segment"
data-segment-id={id}
data-tooltip-title={title}
data-tooltip-desc={description}
>
<span class="tui-segment-label">{label}</span>
<div class="tui-segment-options" id={id} data-value={value}>
{

View File

@@ -7,6 +7,7 @@ interface Props {
step?: number;
value?: number;
title?: string;
description?: string;
}
const {
@@ -17,13 +18,19 @@ const {
step = 0.1,
value = 1.0,
title = "",
description = "",
} = Astro.props;
// Generate slider visual (12 segments for better resolution)
const segments = 12;
---
<div class="tui-slider" data-slider-id={id} title={title}>
<div
class="tui-slider"
data-slider-id={id}
data-tooltip-title={title}
data-tooltip-desc={description}
>
<span class="tui-slider-label">{label}</span>
<div class="tui-slider-track-wrapper">
<div class="tui-slider-visual">
@@ -49,7 +56,7 @@ const segments = 12;
value={value}
/>
</div>
<span class="tui-slider-value" id={`val-${id}`}>{value.toFixed(1)}</span>
<span class="tui-slider-value" id={`val-${id}`}>{value.toFixed(2)}</span>
</div>
<style>
@@ -179,7 +186,7 @@ const segments = 12;
}
});
valueDisplay.textContent = val.toFixed(1);
valueDisplay.textContent = val.toFixed(2);
}
input.addEventListener("input", updateVisual);

View File

@@ -4,9 +4,16 @@ interface Props {
label: string;
checked?: boolean;
title?: string;
description?: string;
}
const { id, label, checked = false, title = "" } = Astro.props;
const {
id,
label,
checked = false,
title = "",
description = "",
} = Astro.props;
---
<button
@@ -14,7 +21,8 @@ const { id, label, checked = false, title = "" } = Astro.props;
class:list={["tui-toggle", { active: checked }]}
id={id}
data-checked={checked ? "true" : "false"}
title={title}
data-tooltip-title={title}
data-tooltip-desc={description}
>
<span class="tui-toggle-label">{label}</span>
</button>