Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions assets/js/resizable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
(function () {
"use strict";

function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }

function init(el) {
if (el.__resizableReady) return;

var side = el.dataset.resizableSide || "right";
var key = el.dataset.resizableKey;
if (!key) return;
var storeKey = "resizable:" + key;
var min = parseFloat(el.dataset.resizableMin) || 0;
var max = parseFloat(el.dataset.resizableMax) || Infinity;
var def = parseFloat(el.dataset.resizableDefault) || min || 280;
var handle = el.querySelector(":scope > .resizable__handle");
if (!handle) return;

el.__resizableReady = true;

function setWidth(px) {
var w = clamp(px, min, max);
el.style.setProperty("--resizable-w", w + "px");
handle.setAttribute("aria-valuenow", String(Math.round(w)));
}

function save() {
localStorage.setItem(storeKey, parseFloat(getComputedStyle(el).width));
}

var saved = parseFloat(localStorage.getItem(storeKey));
setWidth(isNaN(saved) ? def : saved);

var startX = 0, startW = 0;

function onMove(e) {
var delta = side === "right" ? e.clientX - startX : startX - e.clientX;
setWidth(startW + delta);
}
function onUp(e) {
// Capture may already be released (e.g. the element was detached); guard it
// so a stray pointercancel can't leave listeners and is-resizing state stuck.
if (handle.hasPointerCapture(e.pointerId)) handle.releasePointerCapture(e.pointerId);
window.removeEventListener("pointermove", onMove);
window.removeEventListener("pointerup", onUp);
window.removeEventListener("pointercancel", onUp);
el.classList.remove("is-resizing");
document.body.classList.remove("is-resizing");
save();
}

handle.addEventListener("pointerdown", function (e) {
e.preventDefault();
startX = e.clientX;
startW = el.getBoundingClientRect().width;
handle.setPointerCapture(e.pointerId);
el.classList.add("is-resizing");
document.body.classList.add("is-resizing");
window.addEventListener("pointermove", onMove);
window.addEventListener("pointerup", onUp);
window.addEventListener("pointercancel", onUp);
});
Comment on lines +6 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Refactor Resizable Initialization for Robustness and Adherence to Style Rules

This refactoring addresses several critical areas of improvement:

  1. Avoid Hardcoded Fallbacks: Resolves CSS custom properties (--resizable-min, --resizable-max, --resizable-default) directly from getComputedStyle(el) instead of hardcoding fallback values in JavaScript, adhering to the general rules.
  2. Safe localStorage Access: Wraps all localStorage reads and writes in try-catch blocks to prevent script crashes in environments where local storage is blocked or unavailable (e.g., private browsing, iframe previews).
  3. Early Return Optimization: Validates el.dataset.resizableKey and handle before setting el.__resizableReady = true to avoid marking invalid elements as ready.
  4. Robust Gesture Handling: Adds a listener for pointercancel to ensure event listeners are cleaned up and resizing state is reset if the gesture is interrupted.
  function init(el) {
    var key = el.dataset.resizableKey;
    var handle = el.querySelector(":scope > .resizable__handle");
    if (!handle || !key) return;

    if (el.__resizableReady) return;
    el.__resizableReady = true;

    var side = el.dataset.resizableSide || "right";
    var storageKey = "resizable:" + key;

    var style = getComputedStyle(el);
    var min = parseFloat(style.getPropertyValue("--resizable-min"));
    var max = parseFloat(style.getPropertyValue("--resizable-max"));
    var def = parseFloat(style.getPropertyValue("--resizable-default"));
    if (isNaN(min) || isNaN(max) || isNaN(def)) return;

    function setWidth(px) {
      el.style.setProperty("--resizable-w", clamp(px, min, max) + "px");
    }
    function save() {
      try {
        localStorage.setItem(storageKey, parseFloat(getComputedStyle(el).width));
      } catch (e) {
        console.warn("localStorage is not available:", e);
      }
    }

    var saved;
    try {
      saved = parseFloat(localStorage.getItem(storageKey));
    } catch (e) {
      saved = NaN;
    }
    setWidth(isNaN(saved) ? def : saved);

    var startX = 0, startW = 0;

    function onMove(e) {
      var delta = side === "right" ? e.clientX - startX : startX - e.clientX;
      setWidth(startW + delta);
    }
    function onUp(e) {
      handle.releasePointerCapture(e.pointerId);
      window.removeEventListener("pointermove", onMove);
      window.removeEventListener("pointerup", onUp);
      window.removeEventListener("pointercancel", onUp);
      el.classList.remove("is-resizing");
      document.body.classList.remove("is-resizing");
      save();
    }

    handle.addEventListener("pointerdown", function (e) {
      e.preventDefault();
      startX = e.clientX;
      startW = el.getBoundingClientRect().width;
      handle.setPointerCapture(e.pointerId);
      el.classList.add("is-resizing");
      document.body.classList.add("is-resizing");
      window.addEventListener("pointermove", onMove);
      window.addEventListener("pointerup", onUp);
      window.addEventListener("pointercancel", onUp);
    });
References
  1. Avoid hardcoding fallback values in JavaScript for CSS custom properties (variables), as this creates multiple sources of truth for UI styling. If the script cannot resolve the CSS variables, it is better to halt execution or handle the error rather than falling back to hardcoded defaults.


handle.addEventListener("dblclick", function () { setWidth(def); save(); });

// Keyboard accessibility
handle.setAttribute("role", "separator");
handle.setAttribute("aria-orientation", "vertical");
handle.setAttribute("aria-label", "Resize " + key + " panel");
handle.setAttribute("aria-valuemin", String(Math.round(min)));
if (isFinite(max)) handle.setAttribute("aria-valuemax", String(Math.round(max)));
handle.setAttribute("tabindex", "0");
handle.addEventListener("keydown", function (e) {
var grow = side === "right" ? "ArrowRight" : "ArrowLeft";
var shrink = side === "right" ? "ArrowLeft" : "ArrowRight";
var step = e.shiftKey ? 40 : 12;
var w = parseFloat(getComputedStyle(el).width);
if (e.key === grow) w += step;
else if (e.key === shrink) w -= step;
else return;
e.preventDefault();
setWidth(w);
save();
});
}

function initAll() {
document.querySelectorAll("[data-resizable]").forEach(init);
}

if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", initAll);
else initAll();
})();
18 changes: 0 additions & 18 deletions assets/js/sidebar-load-width.js

This file was deleted.

113 changes: 0 additions & 113 deletions assets/js/sidebar-resizer.js

This file was deleted.

42 changes: 42 additions & 0 deletions assets/scss/_content_project.scss
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,45 @@ th {
}
}
}

// Shared ecosystem callout box (emitted by the `ecosystem-box` shortcode).
$ecosystem-box-glow: inset 0 0em 4em #ebc01766, 0 0 0 2px #ebc01766, 0.3em 0.3em 1em #ebc01733;

%ecosystem-box-base {
display: flex;
align-items: center;
font-style: italic;
gap: 0.5rem;
padding: 1rem;
margin: auto -1rem;
transition: box-shadow 0.3s ease;
text-decoration: none;
color: inherit;

img {
height: 65px;
width: 65px;
border: 0;
background: transparent;
}
}

.highlight-box {
@extend %ecosystem-box-base;
box-shadow: $ecosystem-box-glow;
}

.hidden-highlight-box {
@extend %ecosystem-box-base;
box-shadow: none;

&:hover {
box-shadow: $ecosystem-box-glow;
}

/* Hide the sibling highlight-box's shadow when this one is hovered/focused */
&:hover ~ .highlight-box,
&:focus ~ .highlight-box {
box-shadow: none;
}
}
86 changes: 86 additions & 0 deletions assets/scss/_resizable-bootstrap-overrides.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* resizable-bootstrap-overrides.scss — load ONLY in Bootstrap/Docsy consumers.
Reason: Bootstrap applies `.row > * { width:100%; max-width:100%; padding-inline }`
to every direct row child. Since .resizable is now a direct child of .row,
that rule overrides the component's own width. These selectors are 0,2,0,
so they win over `.row > *` (0,1,0) without !important.
Do NOT fold these into resizable.scss — they assume a Bootstrap `.row`. */

.row > .resizable {
flex: 0 0 auto;
width: var(--resizable-w, 260px);
max-width: var(--resizable-max, 480px);
min-width: 0;
padding-inline: 0; // clear Bootstrap's gutter padding on the wrapper
}

/* Sidebar(s) are now fixed-px wrappers, not percentage columns, so main must
absorb the remaining space instead of holding col-xl-8's 66.66% max-width. */
.row > main {
flex: 1 1 0;
max-width: none;
width: auto;
}

/* ── Sidebar wrapper inherits the sticky + full-height role ──
.td-sidebar was sticky as a direct .row child. Now .resizable is the
direct .row child, so the wrapper must carry sticky/height. Values match
.td-sidebar's originals (top: 5.5rem, height: calc(100vh - 5.5rem)). */
[data-resizable-key="sidebar"] {
position: sticky;
top: 5.5rem;
height: calc(100vh - 5.5rem);
align-self: flex-start; // stick instead of stretching to row height
overflow: hidden; // wrapper clips; the aside scrolls internally
}

[data-resizable-key="sidebar"] .resizable__inner {
height: 100%;
overflow: hidden;
}

/* Demote the aside: the wrapper is sticky now, so .td-sidebar becomes a
plain full-height scroll panel. Keeps its gradient + padding; only the
positioning role moves up to the wrapper. */
[data-resizable-key="sidebar"] .td-sidebar {
position: static;
height: 100%; // fill wrapper, not 100vh
top: auto;
// background-image, padding-top, overflow-y: auto, overflow-x: hidden
// all remain from the base rule and still apply.
}

@media screen and (max-width: 768px) {
/* Break the nowrap row so children can stack on their own lines. */
.row.flex-xl-nowrap {
flex-wrap: wrap;
}

/* Sidebar: its own full-width row, on top, not resizable. */
.row > .resizable[data-resizable-key="sidebar"] {
position: relative; /* NOT static — keeps handle anchored if shown; prevents overlap */
flex: 1 1 100%; /* full row */
top: auto;
width: 100%;
max-width: none;
height: auto; /* was fit-content — auto is safer for stacked flow */
overflow: visible;
order: 0; /* ensure it comes before main */
box-sizing: border-box; /* padding counts inside the 100%, not added to it */
padding-inline: 0; /* clear Bootstrap's gutter that returns at mobile */
}

/* Inner + aside follow the auto height instead of the desktop 100vh. */
[data-resizable-key="sidebar"] .resizable__inner { height: auto; overflow: visible; }
[data-resizable-key="sidebar"] .td-sidebar { height: auto; }

.row > main {
flex: 1 1 100%;
max-width: none;
order: 1; /* after the sidebar */
}

.resizable .resizable__handle {
display: none;
pointer-events: none;
}
}
Loading
Loading