feat(ui): add Tooltip component and update TUI controls
This commit is contained in:
136
src/components/Tooltip.astro
Normal file
136
src/components/Tooltip.astro
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user