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;
|
shortcut?: string;
|
||||||
variant?: "default" | "primary" | "subtle";
|
variant?: "default" | "primary" | "subtle";
|
||||||
title?: string;
|
title?: string;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, label, shortcut, variant = "default", title = "" } = Astro.props;
|
const {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
shortcut,
|
||||||
|
variant = "default",
|
||||||
|
title = "",
|
||||||
|
description = "",
|
||||||
|
} = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class:list={["tui-button", `tui-button--${variant}`]}
|
class:list={["tui-button", `tui-button--${variant}`]}
|
||||||
id={id}
|
id={id}
|
||||||
title={title}
|
data-tooltip-title={title}
|
||||||
|
data-tooltip-desc={description}
|
||||||
>
|
>
|
||||||
{shortcut && <span class="tui-button-shortcut">{shortcut}</span>}
|
{shortcut && <span class="tui-button-shortcut">{shortcut}</span>}
|
||||||
<span class="tui-button-label">{label}</span>
|
<span class="tui-button-label">{label}</span>
|
||||||
|
|||||||
@@ -5,12 +5,25 @@ interface Props {
|
|||||||
options: string[];
|
options: string[];
|
||||||
value?: string;
|
value?: string;
|
||||||
title?: 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>
|
<span class="tui-segment-label">{label}</span>
|
||||||
<div class="tui-segment-options" id={id} data-value={value}>
|
<div class="tui-segment-options" id={id} data-value={value}>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ interface Props {
|
|||||||
step?: number;
|
step?: number;
|
||||||
value?: number;
|
value?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -17,13 +18,19 @@ const {
|
|||||||
step = 0.1,
|
step = 0.1,
|
||||||
value = 1.0,
|
value = 1.0,
|
||||||
title = "",
|
title = "",
|
||||||
|
description = "",
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
// Generate slider visual (12 segments for better resolution)
|
// Generate slider visual (12 segments for better resolution)
|
||||||
const segments = 12;
|
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>
|
<span class="tui-slider-label">{label}</span>
|
||||||
<div class="tui-slider-track-wrapper">
|
<div class="tui-slider-track-wrapper">
|
||||||
<div class="tui-slider-visual">
|
<div class="tui-slider-visual">
|
||||||
@@ -49,7 +56,7 @@ const segments = 12;
|
|||||||
value={value}
|
value={value}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -179,7 +186,7 @@ const segments = 12;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
valueDisplay.textContent = val.toFixed(1);
|
valueDisplay.textContent = val.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
input.addEventListener("input", updateVisual);
|
input.addEventListener("input", updateVisual);
|
||||||
|
|||||||
@@ -4,9 +4,16 @@ interface Props {
|
|||||||
label: string;
|
label: string;
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, label, checked = false, title = "" } = Astro.props;
|
const {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
checked = false,
|
||||||
|
title = "",
|
||||||
|
description = "",
|
||||||
|
} = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -14,7 +21,8 @@ const { id, label, checked = false, title = "" } = Astro.props;
|
|||||||
class:list={["tui-toggle", { active: checked }]}
|
class:list={["tui-toggle", { active: checked }]}
|
||||||
id={id}
|
id={id}
|
||||||
data-checked={checked ? "true" : "false"}
|
data-checked={checked ? "true" : "false"}
|
||||||
title={title}
|
data-tooltip-title={title}
|
||||||
|
data-tooltip-desc={description}
|
||||||
>
|
>
|
||||||
<span class="tui-toggle-label">{label}</span>
|
<span class="tui-toggle-label">{label}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user