diff --git a/src/components/ColorSwitcher.astro b/src/components/ColorSwitcher.astro index 26d2199..b068712 100644 --- a/src/components/ColorSwitcher.astro +++ b/src/components/ColorSwitcher.astro @@ -27,6 +27,8 @@ const { { key: "sunset", label: "Sunset" }, { key: "grape", label: "Grape" }, { key: "slate", label: "Slate" }, + { key: "gray", label: "Gray" }, + { key: "neutral", label: "Neutral" }, { key: "mint", label: "Mint" }, { key: "amber", label: "Amber" }, { key: "crimson", label: "Crimson" }, diff --git a/src/components/Dropdown.astro b/src/components/Dropdown.astro index b5cd673..6d0eb75 100644 --- a/src/components/Dropdown.astro +++ b/src/components/Dropdown.astro @@ -197,13 +197,18 @@ const id = `dd-${Math.random().toString(36).slice(2)}`; const side = sideOf(pref); const align = alignOf(pref); + + const AR = showArrow && arrowEl ? 8 : 0; // arrow size + const SAFE = Math.max(shiftPadding, 8); // viewport padding + const GAP = offset + AR; // desired gap + arrow + 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 === "bottom") y = t.bottom + GAP; + else if (side === "top") y = t.top - p.height - GAP; + else if (side === "right") x = t.right + GAP; + else if (side === "left") x = t.left - p.width - GAP; if (side === "top" || side === "bottom") { if (align === "start") x = t.left; @@ -215,116 +220,99 @@ const id = `dd-${Math.random().toString(36).slice(2)}`; else y = t.top + (t.height - p.height) / 2; } - const pad = shiftPadding; - x = Math.max(pad, Math.min(x, vw - p.width - pad)); - y = Math.max(pad, Math.min(y, vh - p.height - pad)); - return { x, y }; // no Math.round - } + // Clamp into viewport + x = Math.max(SAFE, Math.min(x, vw - p.width - SAFE)); + y = Math.max(SAFE, Math.min(y, vh - p.height - SAFE)); - function positionPanel() { - if (!open) return; - if (matchTriggerWidth) { - const w = Math.max(192, trigger.getBoundingClientRect().width); - panel.style.minWidth = `${w}px`; - } - const best = smartPosition(); - panel.style.left = `${best.x}px`; - panel.style.top = `${best.y}px`; + // Return overflow score (lower is better) + const over = + Math.max(0, SAFE - x) + + Math.max(0, SAFE - y) + + Math.max(0, x + p.width - (vw - SAFE)) + + Math.max(0, y + p.height - (vh - SAFE)); - const side = sideOf(best.cand), - 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`; - const cx = t.left + t.width / 2; - arrowEl.style.left = `${cx - p.left - aw / 2}px`; - } else if (side === "top") { - arrowEl.style.bottom = `${-AR}px`; - const cx = t.left + t.width / 2; - arrowEl.style.left = `${cx - p.left - aw / 2}px`; - } else if (side === "right") { - arrowEl.style.left = `${-AR}px`; - const cy = t.top + t.height / 2; - arrowEl.style.top = `${cy - p.top - ah / 2}px`; - } else if (side === "left") { - arrowEl.style.right = `${-AR}px`; - const cy = t.top + t.height / 2; - arrowEl.style.top = `${cy - p.top - ah / 2}px`; - } - } + return { x: Math.round(x), y: Math.round(y), over }; } function smartPosition() { - const s = sideOf(placement), - a = alignOf(placement); - const flips = { + const s = sideOf(placement); + const a = alignOf(placement); + + const flipSide = { top: "bottom", bottom: "top", left: "right", right: "left", - }; - const candidates = [placement, `${flips[s]}${a ? `-${a}` : ""}`]; - if (s === "top" || s === "bottom") - candidates.push( - `left-${a || "start"}`, - `right-${a || "start"}`, - ); - else - candidates.push( - `top-${a || "start"}`, - `bottom-${a || "start"}`, - ); + }[s]; - const vw = document.documentElement.clientWidth; - const vh = document.documentElement.clientHeight; + // Try: requested → side flipped → align flipped → both flipped → orthogonal sides + const aligns = a + ? [a, a === "start" ? "end" : "start", "center"] + : ["center", "start", "end"]; + const candidates = []; + + aligns.forEach((al) => + candidates.push(`${s}${al === "center" ? "" : "-" + al}`), + ); + aligns.forEach((al) => + candidates.push( + `${flipSide}${al === "center" ? "" : "-" + al}`, + ), + ); + + if (s === "top" || s === "bottom") { + aligns.forEach((al) => + candidates.push(`left-${al === "center" ? "start" : al}`), + ); + aligns.forEach((al) => + candidates.push(`right-${al === "center" ? "start" : al}`), + ); + } else { + aligns.forEach((al) => + candidates.push(`top-${al === "center" ? "start" : al}`), + ); + aligns.forEach((al) => + candidates.push(`bottom-${al === "center" ? "start" : al}`), + ); + } let best = null; for (const cand of candidates) { const pos = placeOnce(cand); - const p = panel.getBoundingClientRect(); - const w = p.width, - h = p.height; - const over = - Math.max(0, -pos.x) + - Math.max(0, -pos.y) + - Math.max(0, pos.x + w - vw) + - Math.max(0, pos.y + h - vh); - if (!best || over < best.score) - best = { cand, ...pos, score: over }; + if (!best || pos.over < best.over) best = { cand, ...pos }; + if (pos.over === 0) break; // perfect fit } return best; } function positionPanel() { if (!open) return; + + // Constrain size so the panel never forces overflow + const vw = document.documentElement.clientWidth; + const vh = document.documentElement.clientHeight; + const PAD = Math.max(shiftPadding, 8); + + // Optional: match trigger width if (matchTriggerWidth) { const w = Math.max(192, trigger.getBoundingClientRect().width); panel.style.minWidth = `${Math.round(w)}px`; } + + panel.style.maxWidth = `${vw - PAD * 2}px`; + panel.style.maxHeight = `${vh - PAD * 2}px`; + panel.style.overflow = "auto"; // scroll when needed + const best = smartPosition(); panel.style.left = best.x + "px"; panel.style.top = best.y + "px"; - const side = sideOf(best.cand), - align = alignOf(best.cand); + const side = sideOf(best.cand); + const align = alignOf(best.cand); setOriginFor(side, align); panel.dataset.side = side; + // Arrow if (showArrow && arrowEl) { Object.assign(arrowEl.style, { left: "", @@ -336,26 +324,60 @@ const id = `dd-${Math.random().toString(36).slice(2)}`; const p = panel.getBoundingClientRect(); const aw = arrowEl.getBoundingClientRect().width; const ah = arrowEl.getBoundingClientRect().height; - const AR = 7; + const AR = 8; if (side === "bottom") { arrowEl.style.top = -AR + "px"; const cx = t.left + t.width / 2; arrowEl.style.left = - Math.round(cx - p.left - aw / 2) + "px"; + Math.round( + Math.max( + PAD, + Math.min( + cx - p.left - aw / 2, + p.width - PAD - aw, + ), + ), + ) + "px"; } else if (side === "top") { arrowEl.style.bottom = -AR + "px"; const cx = t.left + t.width / 2; arrowEl.style.left = - Math.round(cx - p.left - aw / 2) + "px"; + Math.round( + Math.max( + PAD, + Math.min( + cx - p.left - aw / 2, + p.width - PAD - aw, + ), + ), + ) + "px"; } else if (side === "right") { arrowEl.style.left = -AR + "px"; const cy = t.top + t.height / 2; - arrowEl.style.top = Math.round(cy - p.top - ah / 2) + "px"; + arrowEl.style.top = + Math.round( + Math.max( + PAD, + Math.min( + cy - p.top - ah / 2, + p.height - PAD - ah, + ), + ), + ) + "px"; } else if (side === "left") { arrowEl.style.right = -AR + "px"; const cy = t.top + t.height / 2; - arrowEl.style.top = Math.round(cy - p.top - ah / 2) + "px"; + arrowEl.style.top = + Math.round( + Math.max( + PAD, + Math.min( + cy - p.top - ah / 2, + p.height - PAD - ah, + ), + ), + ) + "px"; } } } diff --git a/src/components/Field.astro b/src/components/Field.astro index 70cab19..71ffead 100644 --- a/src/components/Field.astro +++ b/src/components/Field.astro @@ -53,7 +53,7 @@ const descId = p.description ? `${fieldId}-desc` : undefined; const errId = `${fieldId}-err`; const inputBase = cva( - "wr:py-2.5 wr:sm:py-3 wr:px-4 wr:block wr:w-full wr:border-gray-200 wr:rounded-lg wr:sm:text-sm wr:focus:border-blue-500 wr:focus:ring-blue-500 wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-neutral-900 wr:dark:border-neutral-700 wr:dark:text-neutral-400 wr:dark:placeholder-neutral-500 wr:dark:focus:ring-neutral-600", + "wr:py-2.5 wr:sm:py-3 wr:px-4 wr:block wr:w-full wr:border-border wr:rounded-lg wr:sm:text-sm wr:focus:border-border wr:focus:ring-blue-500 wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-input wr:dark:border-border wr:dark:text-neutral-400 wr:dark:placeholder-neutral-500 wr:dark:focus:ring-neutral-600", { variants: { size: { @@ -68,7 +68,7 @@ const inputBase = cva( ); const selectBase = cva( - "wr:py-3 wr:px-4 wr:pe-9 wr:block wr:w-full wr:border-gray-200 wr:rounded-lg wr:text-sm wr:focus:border-blue-500 wr:focus:ring-blue-500 wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-neutral-900 wr:dark:border-neutral-700 wr:dark:text-neutral-400 wr:dark:placeholder-neutral-500 wr:dark:focus:ring-neutral-600", + "wr:py-3 wr:px-4 wr:pe-9 wr:block wr:w-full wr:border-border wr:rounded-lg wr:text-sm wr:focus:border-border wr:focus:ring-blue-500 wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-input wr:dark:border-border wr:dark:text-neutral-400 wr:dark:placeholder-neutral-500 wr:dark:focus:ring-neutral-600", { variants: { size: { @@ -90,7 +90,7 @@ const helpCls = "wr:mt-2 wr:text-sm wr:text-gray-500 wr:dark:text-neutral-500"; const errCls = "wr:text-sm wr:text-red-600 wr:mt-2"; const groupCls = "wr:relative wr:flex wr:items-stretch"; const addonBase = - "wr:px-4 wr:inline-flex wr:items-center wr:min-w-fit wr:rounded-md wr:border wr:border-gray-200 wr:bg-gray-50 wr:text-sm wr:text-gray-500 wr:dark:bg-neutral-700 wr:dark:border-neutral-700 wr:dark:text-neutral-400"; + "wr:px-4 wr:inline-flex wr:items-center wr:min-w-fit wr:rounded-md wr:border wr:border-border wr:bg-gray-200 wr:text-sm wr:text-gray-500 wr:dark:bg-card wr:dark:border-border wr:dark:text-neutral-400"; const controlWrap = "wr:relative"; --- @@ -220,7 +220,7 @@ const controlWrap = "wr:relative"; name={p.name} type={(p.kind as any) ?? "text"} class={cn( - "wr:block wr:w-full wr:border wr:border-gray-200 wr:shadow-sm wr:rounded-lg wr:text-sm wr:focus:z-10 wr:focus:border-blue-500 wr:focus:ring-blue-500 wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-neutral-900 wr:dark:border-neutral-700 wr:dark:text-neutral-400 wr:file:bg-gray-50 wr:file:border-0 wr:file:me-4 wr:file:py-3 wr:file:px-4 wr:dark:file:bg-neutral-700 wr:dark:file:text-neutral-400", + "wr:block wr:w-full wr:border wr:border-border wr:shadow-sm wr:rounded-lg wr:text-sm wr:focus:z-10 wr:focus:border-border wr:focus:ring-blue-500 wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-input wr:dark:border-border wr:dark:text-neutral-400 wr:file:bg-gray-50 wr:file:border-0 wr:file:me-4 wr:file:py-3 wr:file:px-4 wr:dark:file:bg-input wr:dark:file:text-neutral-400", p.prefixText && "wr:rounded-l-none", p.suffixText && "wr:rounded-r-none", p.error && @@ -281,7 +281,7 @@ const controlWrap = "wr:relative"; .join(" ") || undefined } class={cn( - "wr:py-2.5 wr:sm:py-3 wr:px-4 wr:block wr:w-full wr:border-gray-200 wr:rounded-lg wr:sm:text-sm wr:focus:border-blue-500 wr:focus:ring-blue-500 wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-neutral-900 wr:dark:border-neutral-700 wr:dark:text-neutral-400 wr:dark:placeholder-neutral-500 wr:dark:focus:ring-neutral-600", + "wr:py-2.5 wr:sm:py-3 wr:px-4 wr:block wr:w-full wr:border-border wr:rounded-lg wr:sm:text-sm wr:focus:border-border wr:focus:ring-blue-500 wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-input wr:dark:border-border wr:dark:text-neutral-400 wr:dark:placeholder-neutral-500 wr:dark:focus:ring-neutral-600", p.size === "sm" ? "wr:text-sm wr:p-3" : p.size === "lg" @@ -352,7 +352,7 @@ const controlWrap = "wr:relative"; id={fieldId} name={p.name} type="checkbox" - class="wr:shrink-0 wr:mt-0.5 wr:border-gray-200 wr:rounded-sm wr:text-blue-600 wr:focus:ring-blue-500 wr:checked:border-blue-500 wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-neutral-800 wr:dark:border-neutral-700 wr:dark:checked:bg-blue-500 wr:dark:checked:border-blue-500 wr:dark:focus:ring-offset-gray-800" + class="wr:shrink-0 wr:mt-0.5 wr:border-border wr:rounded-sm wr:text-blue-600 wr:focus:ring-blue-500 wr:checked:border-border wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-input wr:dark:border-border wr:dark:checked:bg-blue-500 wr:dark:checked:border-border wr:dark:focus:ring-offset-gray-800" checked={!!p.value} disabled={p.disabled} data-control @@ -395,7 +395,7 @@ const controlWrap = "wr:relative"; name={p.name} value={opt.value} checked={String(p.value) === opt.value} - class="wr:shrink-0 wr:mt-0.5 wr:border-gray-200 wr:rounded-full wr:text-blue-600 wr:focus:ring-blue-500 wr:checked:border-blue-500 wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-neutral-800 wr:dark:border-neutral-700 wr:dark:checked:bg-blue-500 wr:dark:checked:border-blue-500 wr:dark:focus:ring-offset-gray-800" + class="wr:shrink-0 wr:mt-0.5 wr:border-border wr:rounded-full wr:text-blue-600 wr:focus:ring-blue-500 wr:checked:border-border wr:disabled:opacity-50 wr:disabled:pointer-events-none wr:dark:bg-input wr:dark:border-border wr:dark:checked:bg-blue-500 wr:dark:checked:border-border wr:dark:focus:ring-offset-gray-800" disabled={opt.disabled || p.disabled} data-control /> @@ -422,8 +422,8 @@ const controlWrap = "wr:relative"; disabled={p.disabled} data-control /> - - + + {p.label && (