Added Tooltip Component
This commit is contained in:
@@ -21,6 +21,7 @@ export { default as PasswordField } from "../src/components/PasswordField.astro"
|
|||||||
export { default as ThemeSwitcher } from "../src/components/ThemeSwitcher.astro";
|
export { default as ThemeSwitcher } from "../src/components/ThemeSwitcher.astro";
|
||||||
export { default as TimePicker } from "../src/components/TimePicker.astro";
|
export { default as TimePicker } from "../src/components/TimePicker.astro";
|
||||||
export { default as Drawer } from "../src/components/Drawer.astro";
|
export { default as Drawer } from "../src/components/Drawer.astro";
|
||||||
|
export { default as Tooltip } from "../src/components/Tooltip.astro";
|
||||||
export { cn } from "../src/utils/cn.js";
|
export { cn } from "../src/utils/cn.js";
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export const PasswordField: any;
|
|||||||
export const ThemeSwitcher: any;
|
export const ThemeSwitcher: any;
|
||||||
export const TimePicker: any;
|
export const TimePicker: any;
|
||||||
export const Drawer: any;
|
export const Drawer: any;
|
||||||
|
export const Tooltip: any;
|
||||||
export function cn(...classes: Array<string | number | false | null | undefined>): string;
|
export function cn(...classes: Array<string | number | false | null | undefined>): string;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export interface Props {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
position = "right",
|
position = "right",
|
||||||
size = "320px",
|
size = "384px",
|
||||||
overlay = true,
|
overlay = true,
|
||||||
closeOnOverlayClick = true,
|
closeOnOverlayClick = true,
|
||||||
closeOnEsc = true,
|
closeOnEsc = true,
|
||||||
|
|||||||
@@ -0,0 +1,432 @@
|
|||||||
|
---
|
||||||
|
import type { HTMLAttributes } from "astro/types";
|
||||||
|
|
||||||
|
type Placement =
|
||||||
|
| "bottom-start"
|
||||||
|
| "bottom"
|
||||||
|
| "bottom-end"
|
||||||
|
| "top-start"
|
||||||
|
| "top"
|
||||||
|
| "top-end"
|
||||||
|
| "right-start"
|
||||||
|
| "right"
|
||||||
|
| "right-end"
|
||||||
|
| "left-start"
|
||||||
|
| "left"
|
||||||
|
| "left-end";
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<"div"> {
|
||||||
|
text?: string; // simple string; or use <slot name="content">
|
||||||
|
placement?: Placement; // default: "top"
|
||||||
|
offset?: number; // gap between trigger & tooltip (default 10)
|
||||||
|
shiftPadding?: number; // viewport padding (default 8)
|
||||||
|
showArrow?: boolean; // default true
|
||||||
|
delayOpen?: number; // ms (default 80)
|
||||||
|
delayClose?: number; // ms (default 80)
|
||||||
|
interactive?: boolean; // hoverable panel (default false)
|
||||||
|
disabled?: boolean; // default false
|
||||||
|
maxWidth?: number | string; // e.g. 280 or "20rem" (default "18rem")
|
||||||
|
openOn?: "hover" | "focus" | "hover-focus"; // default "hover-focus"
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
text,
|
||||||
|
placement = "top",
|
||||||
|
offset = 10,
|
||||||
|
shiftPadding = 8,
|
||||||
|
showArrow = true,
|
||||||
|
delayOpen = 80,
|
||||||
|
delayClose = 80,
|
||||||
|
interactive = false,
|
||||||
|
disabled = false,
|
||||||
|
maxWidth = "18rem",
|
||||||
|
openOn = "hover",
|
||||||
|
class: className,
|
||||||
|
...rest
|
||||||
|
}: Props = Astro.props;
|
||||||
|
|
||||||
|
const id = `tt-${Math.random().toString(36).slice(2)}`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
class={["wr:relative wr:inline-flex", className].join(" ")}
|
||||||
|
{...rest}
|
||||||
|
data-tt-root
|
||||||
|
data-placement={placement}
|
||||||
|
data-offset={offset}
|
||||||
|
data-shift-padding={shiftPadding}
|
||||||
|
data-show-arrow={showArrow ? "1" : ""}
|
||||||
|
data-delay-open={delayOpen}
|
||||||
|
data-delay-close={delayClose}
|
||||||
|
data-interactive={interactive ? "1" : ""}
|
||||||
|
data-open-on={openOn}
|
||||||
|
data-disabled={disabled ? "1" : ""}
|
||||||
|
>
|
||||||
|
<!-- Unstyled trigger wrapper (keeps a single trigger, no duplicate UI) -->
|
||||||
|
<span
|
||||||
|
data-tt-trigger
|
||||||
|
class="wr:inline-flex wr:align-middle wr:cursor-help wr:select-none"
|
||||||
|
tabindex={disabled ? undefined : 0}
|
||||||
|
aria-describedby={`${id}-tip`}
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<slot name="trigger" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Portaled tooltip panel (moved to body as fixed) -->
|
||||||
|
<div
|
||||||
|
class="wr:hidden wr:z-50 wr:max-w-[--tt-max] wr:rounded-xl wr:border wr:border-border
|
||||||
|
wr:bg-popover/95 wr:backdrop-blur-md wr:text-popover-foreground
|
||||||
|
wr:px-3 wr:py-2 wr:text-xs wr:leading-5
|
||||||
|
wr:shadow-[0_18px_50px_-15px_rgba(0,0,0,0.35),0_6px_20px_-16px_rgba(0,0,0,0.25)]
|
||||||
|
wr:origin-[var(--tt-origin,center)] wr:translate-y-1 wr:scale-95 wr:opacity-0
|
||||||
|
wr:transition wr:duration-150 wr:ease-out"
|
||||||
|
data-tt-panel
|
||||||
|
style={{
|
||||||
|
"--tt-max":
|
||||||
|
typeof maxWidth === "number"
|
||||||
|
? `${maxWidth}px`
|
||||||
|
: String(maxWidth),
|
||||||
|
}}
|
||||||
|
role="tooltip"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{showArrow && <div data-tt-arrow class="wr:absolute wr:size-2" />}
|
||||||
|
{text ? <div class="wr:whitespace-pre-wrap">{text}</div> : <slot />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
[data-tt-panel] {
|
||||||
|
--ar: 8px;
|
||||||
|
} /* arrow size */
|
||||||
|
|
||||||
|
/* Triangular arrow that points the right way based on side */
|
||||||
|
[data-tt-panel][data-side="bottom"] [data-tt-arrow] {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--ar) * -1);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: var(--ar) solid transparent;
|
||||||
|
border-right: var(--ar) solid transparent;
|
||||||
|
border-bottom: var(--ar) solid hsl(var(--popover)); /* points UP */
|
||||||
|
filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.18));
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tt-panel][data-side="top"] [data-tt-arrow] {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(var(--ar) * -1);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: var(--ar) solid transparent;
|
||||||
|
border-right: var(--ar) solid transparent;
|
||||||
|
border-top: var(--ar) solid hsl(var(--popover)); /* points DOWN */
|
||||||
|
filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.18));
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tt-panel][data-side="right"] [data-tt-arrow] {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(var(--ar) * -1);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: var(--ar) solid transparent;
|
||||||
|
border-bottom: var(--ar) solid transparent;
|
||||||
|
border-right: var(--ar) solid hsl(var(--popover)); /* points LEFT->RIGHT */
|
||||||
|
filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.18));
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-tt-panel][data-side="left"] [data-tt-arrow] {
|
||||||
|
position: absolute;
|
||||||
|
right: calc(var(--ar) * -1);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: var(--ar) solid transparent;
|
||||||
|
border-bottom: var(--ar) solid transparent;
|
||||||
|
border-left: var(--ar) solid hsl(var(--popover)); /* points RIGHT->LEFT */
|
||||||
|
filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.18));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Open state animation stays the same */
|
||||||
|
[data-tt-panel].is-open {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script type="module" is:inline define:vars={{ id }}>
|
||||||
|
(() => {
|
||||||
|
const root = document.getElementById(id);
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
const trigger = root.querySelector("[data-tt-trigger]");
|
||||||
|
const panel = root.querySelector("[data-tt-panel]");
|
||||||
|
const arrowEl = panel?.querySelector("[data-tt-arrow]");
|
||||||
|
|
||||||
|
const disabled = !!root.dataset.disabled;
|
||||||
|
const placement = root.dataset.placement || "top";
|
||||||
|
const offset = Number(root.dataset.offset || 10);
|
||||||
|
const pad = Number(root.dataset.shiftPadding || 8);
|
||||||
|
const showArrow = !!root.dataset.showArrow;
|
||||||
|
const delayOpen = Number(root.dataset.delayOpen || 80);
|
||||||
|
const delayClose = Number(root.dataset.delayClose || 80);
|
||||||
|
const interactive = !!root.dataset.interactive;
|
||||||
|
const openOn = root.dataset.openOn || "hover-focus";
|
||||||
|
|
||||||
|
// Portal (fixed) for robust positioning
|
||||||
|
const portal = document.createElement("div");
|
||||||
|
portal.style.position = "fixed";
|
||||||
|
document.body.appendChild(portal);
|
||||||
|
panel.style.position = "fixed";
|
||||||
|
portal.appendChild(panel);
|
||||||
|
|
||||||
|
let open = false,
|
||||||
|
openT = 0,
|
||||||
|
closeT = 0;
|
||||||
|
|
||||||
|
const sideOf = (p) => p.split("-")[0];
|
||||||
|
const alignOf = (p) => p.split("-")[1] || "center";
|
||||||
|
|
||||||
|
function setOriginFor(side, align) {
|
||||||
|
const mapSide =
|
||||||
|
{ top: "bottom", bottom: "top", left: "right", right: "left" }[
|
||||||
|
side
|
||||||
|
] || "top";
|
||||||
|
const mapAlign =
|
||||||
|
side === "top" || side === "bottom"
|
||||||
|
? align === "start"
|
||||||
|
? "left"
|
||||||
|
: align === "end"
|
||||||
|
? "right"
|
||||||
|
: "center"
|
||||||
|
: align === "start"
|
||||||
|
? "top"
|
||||||
|
: align === "end"
|
||||||
|
? "bottom"
|
||||||
|
: "center";
|
||||||
|
panel.style.setProperty("--tt-origin", `${mapSide} ${mapAlign}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeOnce(pref) {
|
||||||
|
const t = trigger.getBoundingClientRect();
|
||||||
|
const p = panel.getBoundingClientRect();
|
||||||
|
const vw = document.documentElement.clientWidth;
|
||||||
|
const vh = document.documentElement.clientHeight;
|
||||||
|
|
||||||
|
const side = sideOf(pref);
|
||||||
|
const align = alignOf(pref);
|
||||||
|
let x = 0,
|
||||||
|
y = 0;
|
||||||
|
|
||||||
|
if (side === "bottom") y = t.bottom + offset;
|
||||||
|
else if (side === "top") y = t.top - p.height - offset;
|
||||||
|
else if (side === "right") x = t.right + offset;
|
||||||
|
else if (side === "left") x = t.left - p.width - offset;
|
||||||
|
|
||||||
|
if (side === "top" || side === "bottom") {
|
||||||
|
if (align === "start") x = t.left;
|
||||||
|
else if (align === "end") x = t.right - p.width;
|
||||||
|
else x = t.left + (t.width - p.width) / 2;
|
||||||
|
} else {
|
||||||
|
if (align === "start") y = t.top;
|
||||||
|
else if (align === "end") y = t.bottom - p.height;
|
||||||
|
else y = t.top + (t.height - p.height) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = Math.max(pad, Math.min(x, vw - p.width - pad));
|
||||||
|
y = Math.max(pad, Math.min(y, vh - p.height - pad));
|
||||||
|
return { x: Math.round(x), y: Math.round(y) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function smartPosition() {
|
||||||
|
const prefSide = sideOf(placement);
|
||||||
|
const prefAlign = alignOf(placement);
|
||||||
|
const flips = {
|
||||||
|
top: "bottom",
|
||||||
|
bottom: "top",
|
||||||
|
left: "right",
|
||||||
|
right: "left",
|
||||||
|
};
|
||||||
|
const candidates = [
|
||||||
|
placement,
|
||||||
|
`${flips[prefSide]}${prefAlign ? `-${prefAlign}` : ""}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (prefSide === "top" || prefSide === "bottom") {
|
||||||
|
candidates.push(
|
||||||
|
`left-${prefAlign || "start"}`,
|
||||||
|
`right-${prefAlign || "start"}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
candidates.push(
|
||||||
|
`top-${prefAlign || "start"}`,
|
||||||
|
`bottom-${prefAlign || "start"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const vw = document.documentElement.clientWidth;
|
||||||
|
const vh = document.documentElement.clientHeight;
|
||||||
|
let best = null;
|
||||||
|
|
||||||
|
// Temporarily make visible to measure accurately
|
||||||
|
panel.style.visibility = "hidden";
|
||||||
|
panel.classList.remove("wr:hidden");
|
||||||
|
const measure = () => {
|
||||||
|
for (const cand of candidates) {
|
||||||
|
const pos = placeOnce(cand);
|
||||||
|
const p = panel.getBoundingClientRect();
|
||||||
|
const over =
|
||||||
|
Math.max(0, -pos.x) +
|
||||||
|
Math.max(0, -pos.y) +
|
||||||
|
Math.max(0, pos.x + p.width - vw) +
|
||||||
|
Math.max(0, pos.y + p.height - vh);
|
||||||
|
if (!best || over < best.score)
|
||||||
|
best = { cand, ...pos, score: over };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
measure();
|
||||||
|
panel.style.visibility = "";
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionPanel() {
|
||||||
|
if (!open) return;
|
||||||
|
|
||||||
|
const best = smartPosition();
|
||||||
|
panel.style.left = best.x + "px";
|
||||||
|
panel.style.top = best.y + "px";
|
||||||
|
|
||||||
|
const side = sideOf(best.cand);
|
||||||
|
const align = alignOf(best.cand);
|
||||||
|
setOriginFor(side, align);
|
||||||
|
panel.dataset.side = side;
|
||||||
|
|
||||||
|
if (showArrow && arrowEl) {
|
||||||
|
Object.assign(arrowEl.style, {
|
||||||
|
left: "",
|
||||||
|
top: "",
|
||||||
|
right: "",
|
||||||
|
bottom: "",
|
||||||
|
});
|
||||||
|
const t = trigger.getBoundingClientRect();
|
||||||
|
const p = panel.getBoundingClientRect();
|
||||||
|
const aw = arrowEl.getBoundingClientRect().width;
|
||||||
|
const ah = arrowEl.getBoundingClientRect().height;
|
||||||
|
const AR = 7;
|
||||||
|
|
||||||
|
if (side === "bottom") {
|
||||||
|
arrowEl.style.top = -AR + "px";
|
||||||
|
arrowEl.style.left =
|
||||||
|
Math.round(t.left + t.width / 2 - p.left - aw / 2) +
|
||||||
|
"px";
|
||||||
|
} else if (side === "top") {
|
||||||
|
arrowEl.style.bottom = -AR + "px";
|
||||||
|
arrowEl.style.left =
|
||||||
|
Math.round(t.left + t.width / 2 - p.left - aw / 2) +
|
||||||
|
"px";
|
||||||
|
} else if (side === "right") {
|
||||||
|
arrowEl.style.left = -AR + "px";
|
||||||
|
arrowEl.style.top =
|
||||||
|
Math.round(t.top + t.height / 2 - p.top - ah / 2) +
|
||||||
|
"px";
|
||||||
|
} else if (side === "left") {
|
||||||
|
arrowEl.style.right = -AR + "px";
|
||||||
|
arrowEl.style.top =
|
||||||
|
Math.round(t.top + t.height / 2 - p.top - ah / 2) +
|
||||||
|
"px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOpen(v) {
|
||||||
|
if (disabled || open === v) return;
|
||||||
|
open = v;
|
||||||
|
trigger.setAttribute("aria-expanded", String(open));
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
panel.classList.remove("wr:hidden");
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
panel.classList.add("is-open");
|
||||||
|
panel.classList.remove(
|
||||||
|
"wr:translate-y-1",
|
||||||
|
"wr:scale-95",
|
||||||
|
"wr:opacity-0",
|
||||||
|
);
|
||||||
|
positionPanel();
|
||||||
|
});
|
||||||
|
window.addEventListener("resize", positionPanel, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
window.addEventListener("scroll", positionPanel, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
panel.classList.remove("is-open");
|
||||||
|
panel.classList.add(
|
||||||
|
"wr:translate-y-1",
|
||||||
|
"wr:scale-95",
|
||||||
|
"wr:opacity-0",
|
||||||
|
);
|
||||||
|
setTimeout(() => panel.classList.add("wr:hidden"), 140);
|
||||||
|
window.removeEventListener("resize", positionPanel);
|
||||||
|
window.removeEventListener("scroll", positionPanel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openLater = () => {
|
||||||
|
clearTimeout(closeT);
|
||||||
|
clearTimeout(openT);
|
||||||
|
openT = setTimeout(() => setOpen(true), delayOpen);
|
||||||
|
};
|
||||||
|
const closeLater = () => {
|
||||||
|
clearTimeout(openT);
|
||||||
|
clearTimeout(closeT);
|
||||||
|
closeT = setTimeout(() => setOpen(false), delayClose);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wire triggers
|
||||||
|
if (openOn === "hover" || openOn === "hover-focus") {
|
||||||
|
trigger.addEventListener("pointerenter", openLater);
|
||||||
|
trigger.addEventListener("pointerleave", closeLater);
|
||||||
|
if (interactive) {
|
||||||
|
panel.addEventListener("pointerenter", () => {
|
||||||
|
clearTimeout(closeT);
|
||||||
|
});
|
||||||
|
panel.addEventListener("pointerleave", closeLater);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (openOn === "focus" || openOn === "hover-focus") {
|
||||||
|
trigger.addEventListener("focus", openLater);
|
||||||
|
trigger.addEventListener("blur", closeLater);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openOn === "click") {
|
||||||
|
trigger.addEventListener("click", () => setOpen(!open));
|
||||||
|
document.addEventListener("pointerdown", (e) => {
|
||||||
|
if (!root.contains(e.target) && !panel.contains(e.target))
|
||||||
|
setOpen(false);
|
||||||
|
});
|
||||||
|
document.addEventListener(
|
||||||
|
"keydown",
|
||||||
|
(e) => e.key === "Escape" && setOpen(false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close on outside pointerdown
|
||||||
|
document.addEventListener("pointerdown", (e) => {
|
||||||
|
if (!open) return;
|
||||||
|
if (root.contains(e.target) || panel.contains(e.target)) return;
|
||||||
|
setOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup on client navigation
|
||||||
|
document.addEventListener("astro:before-swap", () => {
|
||||||
|
try {
|
||||||
|
panel.remove();
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
portal.remove();
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -14,6 +14,7 @@ export { default as PasswordField } from "../src/components/PasswordField.astro"
|
|||||||
export { default as ThemeSwitcher } from "../src/components/ThemeSwitcher.astro";
|
export { default as ThemeSwitcher } from "../src/components/ThemeSwitcher.astro";
|
||||||
export { default as TimePicker } from "../src/components/TimePicker.astro";
|
export { default as TimePicker } from "../src/components/TimePicker.astro";
|
||||||
export { default as Drawer } from "../src/components/Drawer.astro";
|
export { default as Drawer } from "../src/components/Drawer.astro";
|
||||||
|
export { default as Tooltip } from "../src/components/Tooltip.astro";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
export { cn } from "./utils/cn";
|
export { cn } from "./utils/cn";
|
||||||
@@ -56,6 +56,9 @@ import Base from "./Base.astro";
|
|||||||
<li class="list-item">
|
<li class="list-item">
|
||||||
<a href="/drawer">Drawer</a>
|
<a href="/drawer">Drawer</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="list-item">
|
||||||
|
<a href="/tooltip">Tooltip</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
---
|
||||||
|
import ComponentLayout from "../layouts/ComponentLayout.astro";
|
||||||
|
import Tooltip from "../components/Tooltip.astro";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentLayout>
|
||||||
|
<div class="wr:flex wr:flex-col wr:gap-4">
|
||||||
|
<Tooltip text="Copy to clipboard">
|
||||||
|
<button
|
||||||
|
slot="trigger"
|
||||||
|
class="wr:rounded-md wr:border wr:px-3 wr:py-2">Copy</button
|
||||||
|
>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip interactive={true} placement="bottom" delayOpen={120}>
|
||||||
|
<span slot="trigger">Pricing</span>
|
||||||
|
<div class="wr:flex wr:flex-col wr:gap-1">
|
||||||
|
<div class="wr:font-medium">Pro plan</div>
|
||||||
|
<div class="wr:text-xs wr:text-muted-foreground">
|
||||||
|
Unlimited projects, priority support.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
openOn="hover"
|
||||||
|
placement="right-start"
|
||||||
|
offset={12}
|
||||||
|
showArrow
|
||||||
|
interactive={true}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
slot="trigger"
|
||||||
|
class="wr:inline-flex wr:items-center wr:gap-2 wr:h-9 wr:px-3 wr:rounded-full wr:bg-background/70 wr:backdrop-blur-sm wr:ring-1 wr:ring-border/60 hover:wr:ring-border wr:text-sm"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:eye" class="wr:size-4" />
|
||||||
|
Quick preview
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="wr:w-64 wr:rounded-xl wr:overflow-hidden">
|
||||||
|
<img
|
||||||
|
src="/images/preview.jpg"
|
||||||
|
alt=""
|
||||||
|
class="wr:w-full wr:h-28 wr:object-cover"
|
||||||
|
/>
|
||||||
|
<div class="wr:p-3 wr:space-y-2">
|
||||||
|
<div class="wr:text-sm wr:font-semibold">
|
||||||
|
Starter Template
|
||||||
|
</div>
|
||||||
|
<p class="wr:text-xs wr:text-muted-foreground">
|
||||||
|
Clean, responsive template with auth, dashboard &
|
||||||
|
billing.
|
||||||
|
</p>
|
||||||
|
<div class="wr:flex wr:gap-2">
|
||||||
|
<button
|
||||||
|
class="wr:h-8 wr:px-3 wr:text-xs wr:rounded-md wr:bg-primary wr:text-primary-foreground hover:wr:bg-primary/90"
|
||||||
|
>Use</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="wr:h-8 wr:px-3 wr:text-xs wr:rounded-md wr:border wr:border-border hover:wr:bg-muted/60"
|
||||||
|
>Details</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="wr:block wr:text-sm wr:font-medium wr:mb-1 wr:text-foreground"
|
||||||
|
>
|
||||||
|
Password
|
||||||
|
<Tooltip
|
||||||
|
openOn="click"
|
||||||
|
placement="top-end"
|
||||||
|
offset={10}
|
||||||
|
showArrow
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
slot="trigger"
|
||||||
|
type="button"
|
||||||
|
class="wr:align-middle wr:ml-1 wr:inline-flex wr:items-center wr:justify-center wr:size-5 wr:rounded-full wr:ring-1 wr:ring-border/60 hover:wr:ring-border"
|
||||||
|
aria-label="Password rules"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="mdi:information"
|
||||||
|
class="wr:size-3.5 wr:opacity-80"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="wr:w-64 wr:p-3 wr:text-sm wr:space-y-2">
|
||||||
|
<div class="wr:font-medium">Must include:</div>
|
||||||
|
<ul class="wr:space-y-1 wr:text-xs">
|
||||||
|
<li class="wr:flex wr:items-center wr:gap-2">
|
||||||
|
<span
|
||||||
|
class="wr:size-1.5 wr:rounded-full wr:bg-danger/70"
|
||||||
|
></span> 8+ characters
|
||||||
|
</li>
|
||||||
|
<li class="wr:flex wr:items-center wr:gap-2">
|
||||||
|
<span
|
||||||
|
class="wr:size-1.5 wr:rounded-full wr:bg-danger/70"
|
||||||
|
></span> Upper & lower case
|
||||||
|
</li>
|
||||||
|
<li class="wr:flex wr:items-center wr:gap-2">
|
||||||
|
<span
|
||||||
|
class="wr:size-1.5 wr:rounded-full wr:bg-danger/70"
|
||||||
|
></span> 1+ number or symbol
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="wr:h-11 wr:px-3 wr:w-full wr:rounded-md wr:border wr:border-border wr:bg-background wr:text-sm wr:focus-visible:ring-2 wr:focus-visible:ring-primary/40"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
openOn="contextmenu"
|
||||||
|
placement="bottom-start"
|
||||||
|
offset={8}
|
||||||
|
showArrow
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
slot="trigger"
|
||||||
|
class="wr:rounded-lg wr:border wr:border-border wr:p-4 wr:bg-card wr:text-card-foreground wr:cursor-context-menu"
|
||||||
|
>
|
||||||
|
<div class="wr:font-medium wr:text-sm">Report.pdf</div>
|
||||||
|
<div class="wr:text-xs wr:text-muted-foreground">
|
||||||
|
1.2 MB · Updated today
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wr:w-52 wr:py-1">
|
||||||
|
<button
|
||||||
|
data-dd-item
|
||||||
|
class="wr:w-full wr:flex wr:items-center wr:gap-2 wr:px-3 wr:py-2 wr:text-sm wr:rounded-md hover:wr:bg-muted/60"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:download" class="wr:size-4" /> Download
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-dd-item
|
||||||
|
class="wr:w-full wr:flex wr:items-center wr:gap-2 wr:px-3 wr:py-2 wr:text-sm wr:rounded-md hover:wr:bg-muted/60"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:pencil" class="wr:size-4" /> Rename
|
||||||
|
</button>
|
||||||
|
<div class="wr:h-px wr:bg-border/60 wr:my-1"></div>
|
||||||
|
<button
|
||||||
|
data-dd-item
|
||||||
|
class="wr:w-full wr:flex wr:items-center wr:gap-2 wr:px-3 wr:py-2 wr:text-sm wr:text-danger wr:rounded-md hover:wr:bg-danger/10"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:trash-can" class="wr:size-4" /> Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div class="wr:flex wr:items-center wr:gap-3">
|
||||||
|
<button
|
||||||
|
id="start-tour"
|
||||||
|
class="wr:h-9 wr:px-3 wr:rounded-md wr:bg-primary wr:text-primary-foreground wr:text-sm"
|
||||||
|
>Start tour</button
|
||||||
|
>
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
openOn="click"
|
||||||
|
placement="bottom"
|
||||||
|
offset={10}
|
||||||
|
showArrow
|
||||||
|
initialOpen
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
slot="trigger"
|
||||||
|
id="tour-target"
|
||||||
|
class="wr:h-9 wr:px-3 wr:rounded-md wr:border wr:border-border wr:bg-background wr:text-sm"
|
||||||
|
>
|
||||||
|
Create Project
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="wr:w-72 wr:p-3 wr:space-y-2">
|
||||||
|
<div class="wr:flex wr:items-center wr:justify-between">
|
||||||
|
<span class="wr:text-sm wr:font-medium"
|
||||||
|
>Create your first project</span
|
||||||
|
>
|
||||||
|
<span class="wr:text-[11px] wr:text-muted-foreground"
|
||||||
|
>1 / 3</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p class="wr:text-xs wr:text-muted-foreground">
|
||||||
|
Projects group your tasks, docs and members. You can
|
||||||
|
rename anytime.
|
||||||
|
</p>
|
||||||
|
<div class="wr:flex wr:justify-end wr:gap-2">
|
||||||
|
<button
|
||||||
|
class="wr:h-8 wr:px-3 wr:text-xs wr:rounded-md wr:border wr:border-border hover:wr:bg-muted/60"
|
||||||
|
>Skip</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="wr:h-8 wr:px-3 wr:text-xs wr:rounded-md wr:bg-primary wr:text-primary-foreground hover:wr:bg-primary/90"
|
||||||
|
>Next</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ComponentLayout>
|
||||||
Reference in New Issue
Block a user