Bundles a stretch of in-progress work across the SPA tools so the
tree returns to a coherent shippable state ahead of cutting a new
zddc-server stable image:
- landing: substantial rework of the project picker (sortable/filterable
table, presets refactor, ?projects= filter, ?v= channel propagation,
loading/error states)
- archive: presets cleanup, source.js refactor, filtering/url-state
alignment with the landing page
- mdedit: file-system module split, resizer, file-tree improvements,
base/toc styling tweaks
- transmittal/classifier: small template touch-ups for shared chrome
- shared: build-lib.sh helpers, new favicon.svg
- bootstrap, build.sh: pick up the channel-aware install/track zip
generation
- tests: new landing.spec.js, expanded archive/mdedit/build-label specs
- docs: CLAUDE.md picks up the zddc-server section and freshens the
alpha-build exception note
- regenerated artifacts: install.zip, track-{alpha,beta,stable}.zip,
*_alpha.html — these are produced by `sh build.sh` and per project
convention are committed alongside the source changes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
137 lines
4.5 KiB
JavaScript
137 lines
4.5 KiB
JavaScript
/**
|
|
* Pane resizing functionality
|
|
*/
|
|
|
|
/**
|
|
* Make an element resizable by dragging its resizer
|
|
* @param {HTMLElement} resizer - The resizer element
|
|
* @param {HTMLElement} pane - The pane to resize
|
|
*/
|
|
function makeResizable(resizer, pane) {
|
|
const initialWidth = pane.offsetWidth;
|
|
|
|
let x = 0;
|
|
let paneWidth = initialWidth;
|
|
|
|
const mouseDownHandler = function (e) {
|
|
x = e.clientX;
|
|
paneWidth = pane.offsetWidth;
|
|
|
|
document.addEventListener('mousemove', mouseMoveHandler);
|
|
document.addEventListener('mouseup', mouseUpHandler);
|
|
|
|
resizer.classList.add('active');
|
|
document.body.style.cursor = 'col-resize';
|
|
document.body.style.userSelect = 'none';
|
|
};
|
|
|
|
const mouseMoveHandler = function (e) {
|
|
const dx = e.clientX - x;
|
|
const newWidth = Math.max(150, paneWidth + dx);
|
|
|
|
pane.style.width = `${newWidth}px`;
|
|
};
|
|
|
|
const mouseUpHandler = function () {
|
|
document.removeEventListener('mousemove', mouseMoveHandler);
|
|
document.removeEventListener('mouseup', mouseUpHandler);
|
|
|
|
resizer.classList.remove('active');
|
|
document.body.style.cursor = '';
|
|
document.body.style.userSelect = '';
|
|
};
|
|
|
|
resizer.addEventListener('mousedown', mouseDownHandler);
|
|
}
|
|
|
|
/**
|
|
* Make a horizontal split height-adjustable: the resizer drags the height
|
|
* of `topPane` while it remains a sibling of the bottom section inside `container`.
|
|
*
|
|
* @param {HTMLElement} resizer - The horizontal resizer between the panes
|
|
* @param {HTMLElement} topPane - The pane whose height is set
|
|
* @param {HTMLElement} container - The flex column containing both panes
|
|
*/
|
|
function makeHeightResizable(resizer, topPane, container) {
|
|
let y = 0;
|
|
let topHeight = 0;
|
|
let containerHeight = 0;
|
|
|
|
const mouseDownHandler = (e) => {
|
|
y = e.clientY;
|
|
topHeight = topPane.offsetHeight;
|
|
containerHeight = container.offsetHeight;
|
|
document.addEventListener('mousemove', mouseMoveHandler);
|
|
document.addEventListener('mouseup', mouseUpHandler);
|
|
resizer.classList.add('active');
|
|
document.body.style.cursor = 'row-resize';
|
|
document.body.style.userSelect = 'none';
|
|
};
|
|
|
|
const mouseMoveHandler = (e) => {
|
|
const dy = e.clientY - y;
|
|
// Reserve at least 80px for the bottom pane (TOC); cap top at containerHeight - 80.
|
|
const minTop = 60;
|
|
const maxTop = Math.max(minTop, containerHeight - 100);
|
|
const newHeight = Math.max(minTop, Math.min(maxTop, topHeight + dy));
|
|
topPane.style.height = `${newHeight}px`;
|
|
};
|
|
|
|
const mouseUpHandler = () => {
|
|
document.removeEventListener('mousemove', mouseMoveHandler);
|
|
document.removeEventListener('mouseup', mouseUpHandler);
|
|
resizer.classList.remove('active');
|
|
document.body.style.cursor = '';
|
|
document.body.style.userSelect = '';
|
|
};
|
|
|
|
resizer.addEventListener('mousedown', mouseDownHandler);
|
|
}
|
|
|
|
/**
|
|
* Initialize the file navigation pane resizer
|
|
*/
|
|
function initializeFileNavResizer() {
|
|
const fileNavResizer = document.querySelector('.pane-resizer[data-resizer-for="file-nav"]');
|
|
|
|
if (fileNavResizer && !fileNavResizer.hasAttribute('data-resizer-initialized')) {
|
|
fileNavResizer.setAttribute('data-resizer-initialized', 'true');
|
|
|
|
let x = 0;
|
|
let navWidth = 0;
|
|
|
|
const mouseDownHandler = function (e) {
|
|
x = e.clientX;
|
|
|
|
const navPane = document.getElementById('file-nav');
|
|
navWidth = navPane.getBoundingClientRect().width;
|
|
|
|
document.addEventListener('mousemove', mouseMoveHandler);
|
|
document.addEventListener('mouseup', mouseUpHandler);
|
|
|
|
fileNavResizer.classList.add('bg-blue-500');
|
|
};
|
|
|
|
const mouseMoveHandler = function (e) {
|
|
const dx = e.clientX - x;
|
|
|
|
const navPane = document.getElementById('file-nav');
|
|
|
|
const newWidth = navWidth + dx;
|
|
|
|
if (newWidth >= 200) {
|
|
navPane.style.width = `${newWidth}px`;
|
|
}
|
|
};
|
|
|
|
const mouseUpHandler = function () {
|
|
document.removeEventListener('mousemove', mouseMoveHandler);
|
|
document.removeEventListener('mouseup', mouseUpHandler);
|
|
|
|
fileNavResizer.classList.remove('bg-blue-500');
|
|
};
|
|
|
|
fileNavResizer.addEventListener('mousedown', mouseDownHandler);
|
|
}
|
|
}
|
|
|