34 Commits

Author SHA1 Message Date
5560c42bcc add cross-promo plugin section to READMEs; bump patch (v*.*.+1) 2026-07-10 09:15:39 -03:00
86f40ff6ae drive top-scrollbar layout() from a single ResizeObserver 2026-07-08 10:01:19 -03:00
89292492db empty track (no knob) when board fits, add fit tolerance (v1.5.1) 2026-07-08 09:48:49 -03:00
f0d9974a31 enlarge empty-column drop area when collapsed: min-height 100px -> 250px, still under max; v1.5 2026-07-06 21:11:39 -03:00
1743f9b868 fix top scrollbar freezing after card drag: re-resolve #board-container on rebuild, sync on parent; v1.4.2 2026-07-06 07:46:45 -03:00
045bae1e4c empty columns need 100px for drop 2026-07-04 09:50:45 -03:00
8ae373b4e7 bar from scratch VERSION v1.4 2026-07-01 10:06:22 -03:00
174b6ad31a bar from scratch Template 2026-07-01 10:06:10 -03:00
4ab761c03e bar from scratch plugin 2026-07-01 10:06:01 -03:00
b8308782f8 bar from scratch Assets 2026-07-01 10:05:52 -03:00
5cccbef7d7 title fixed in config 2026-07-01 10:00:33 -03:00
9b78882835 plugin focused on the vertical size and horizontal bars v1.3 2026-07-01 09:45:30 -03:00
51eba0ad5e growth until hit maximum feature bloat, now decoupling into TweakDrag new plugin 2026-07-01 09:42:51 -03:00
54b9fa93fb starting v1.2 with new tweaks 2026-07-01 07:31:52 -03:00
65b4771e51 gitignore PLAN-* files 2026-07-01 07:31:14 -03:00
dbfe98b3c7 drag-scroll checkbox VERSION 1.1 2026-06-28 17:19:42 -03:00
3beaa88fe9 drag-scroll checkbox 2026-06-28 17:19:04 -03:00
dcb672777d drag-scroll behavior new feature 2026-06-28 17:18:53 -03:00
5712d055d3 feature add top horizontal scroll bar done VERSION v1.0 release 2026-06-28 17:12:25 -03:00
5fcf1ffeb1 js feature add top horizontal scroll bar t2 2026-06-28 17:09:56 -03:00
fc961be3c1 new feature checkbox add top horizontal scroll bar 2026-06-28 17:09:22 -03:00
fa23d7a64c Config Controller and Template 2026-06-28 17:03:37 -03:00
e6479c9ed9 menu css and wire 2026-06-28 17:03:17 -03:00
4597c6c61c new version v0.4 with menu 2026-06-28 17:02:50 -03:00
31caec25d0 70 pixels more to test t3 2026-06-28 16:54:48 -03:00
7385f52b95 70 pixels more to test t2 2026-06-28 16:54:33 -03:00
9541ea83e3 70 pixels more to test 2026-06-28 16:54:23 -03:00
7a90d15e82 the min-height was in the way -- bugfix 2026-06-28 16:49:42 -03:00
e9681201c0 more shrink, and a + t2 2026-06-28 16:45:55 -03:00
8b97480e2e more shrink, and a + t1 2026-06-28 16:45:46 -03:00
f7227eaef1 css rules changed 2026-06-28 16:37:36 -03:00
79b4a9a8f6 README first draft 2026-06-28 16:37:10 -03:00
e46a124bad plugin v0.2 2026-06-28 16:36:58 -03:00
248cfe201b reshaping first draft 2026-06-28 16:36:14 -03:00
19 changed files with 577 additions and 61 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
PLAN-*

View File

@@ -0,0 +1,81 @@
/*
* ShrinkVertically -- keep the board's horizontal scrollbar visible when columns
* are vertically collapsed (gear menu "collapse vertically" / shortcut, which sets
* localStorage vertical_scroll=0 and adds the class .board-task-list-compact).
*
* Kanboard core sets: .board-task-list-compact { max-height: 90vh; }
* That 90vh is measured from the top of the viewport and ignores the page header
* above the board, so column + header together exceed the viewport and push the
* horizontal scrollbar below the fold.
*
* We instead reserve a fixed pixel budget for the chrome above and below the task
* list (page header + board column header + the horizontal scrollbar + a little
* breathing room). A fixed-px budget is more reliable than a proportional vh value
* because that chrome is a fixed height regardless of screen size.
*
* The pixel budget is the --sv-shrink-offset CSS variable. It is injected in the
* page head from the plugin setting (Settings -> Shrink Vertically); when unset it
* falls back to 240px below. Increase it if the scrollbar is still clipped, or
* decrease it to make the columns taller.
*
* The selector is intentionally more specific than core's single-class rule, and
* uses !important, so this override wins even alongside a theme (e.g. Essential).
*
* The min-height override is essential: Kanboard's drag-and-drop sets an INLINE
* min-height on each column equal to its full content height
* (BoardDragAndDrop.js: css("min-height", parent().height())). In CSS, min-height
* beats max-height, so that inline value keeps tall columns from shrinking and our
* max-height alone does nothing. A stylesheet !important overrides the inline
* (non-important) min-height so max-height can win.
*
* We override it to a NON-ZERO value, not 0: Kanboard relies on that same inline
* min-height to keep EMPTY columns tall enough to be a drop target, so zeroing it makes
* you unable to drop a card into an empty collapsed column. 250px gives a comfortable drop
* target (well above Kanboard's 70px .draggable-placeholder) while staying below the
* collapsed max-height, so the board never overflows the viewport (which would re-hide the
* horizontal scrollbar). Non-empty columns are unaffected -- their content is taller -- and
* short columns simply get this min-height, which is only cosmetic (no scrollbar).
*/
#board td .board-task-list.board-task-list-compact {
max-height: calc(100vh - var(--sv-shrink-offset, 240px)) !important;
min-height: 250px !important;
}
/*
* Optional custom top horizontal scrollbar (built by Asset/js/top-scrollbar.js when the
* "Add a top horizontal scrollbar" setting is enabled). It is a track + draggable thumb,
* not a native scrollbar, so it renders and can be sized identically in every browser
* (Chrome, Firefox, Safari) and on touch. Thickness follows the --sv-bar-size variable
* (Settings -> Shrink Vertically), injected in the page head; falls back to 25px.
* touch-action: none lets a finger drag the thumb without scrolling the page.
*/
.sv-top-scroll {
position: relative;
width: 100%;
height: var(--sv-bar-size, 25px);
background: rgba(0, 0, 0, 0.06);
border-radius: 4px;
overflow: hidden;
touch-action: none;
}
.sv-top-scroll-thumb {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 100%;
background: rgba(0, 0, 0, 0.35);
border-radius: 4px;
cursor: grab;
touch-action: none;
}
.sv-top-scroll.sv-dragging .sv-top-scroll-thumb {
cursor: grabbing;
background: rgba(0, 0, 0, 0.5);
}
.sv-dragging {
user-select: none;
}

View File

@@ -1 +0,0 @@
/* Skeleton plugin styles -- add CSS here. Loaded via template:layout:css. */

View File

@@ -0,0 +1,34 @@
/*
* ShrinkVertically -- mark the two vertical-collapse menu items so it is obvious the
* behaviour comes from this plugin and not from Kanboard core.
*
* Core renders these labels in app/Template/project_header/dropdown.php:
* .filter-vert-collapse .filter-vert-toggle-collapse -> "Collapse vertically"
* .filter-vert-expand .filter-vert-toggle-collapse -> "Expand vertically"
*
* We append a "+" to each so they read "Collapse vertically+" / "Expand vertically+".
* This is locale-independent (it edits the rendered text, not the translation table).
*/
(function () {
"use strict";
function addPlus() {
var links = document.querySelectorAll(
".filter-vert-collapse .filter-vert-toggle-collapse, " +
".filter-vert-expand .filter-vert-toggle-collapse"
);
for (var i = 0; i < links.length; i++) {
var text = links[i].textContent;
if (text.charAt(text.length - 1) !== "+") {
links[i].textContent = text + "+";
}
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", addPlus);
} else {
addPlus();
}
})();

View File

@@ -1 +0,0 @@
// Skeleton plugin script -- add JS here. Loaded via template:layout:js.

238
Asset/js/top-scrollbar.js Normal file
View File

@@ -0,0 +1,238 @@
/*
* ShrinkVertically -- optional custom horizontal scrollbar above the board.
*
* Kanboard's board scrolls horizontally inside #board-container (overflow-x: auto), so the
* only native horizontal scrollbar sits at the very bottom of the board. On a wide board
* you have to scroll all the way down to reach it. This adds a second scrollbar just above
* the columns and keeps it in sync with the board.
*
* Unlike a native scrollbar (whose thickness cannot be sized in Firefox), this is a custom
* track + thumb widget: it renders and behaves the same in every browser (Chrome, Firefox,
* Safari) and on touch, and its thickness follows the --sv-bar-size setting.
*
* Resilience note: after a card drag Kanboard rebuilds the board with
* `$("#board-container").replaceWith(data)` (BoardDragAndDrop.refresh), so the element we
* started with is destroyed and replaced. We therefore never cache the container: we
* re-resolve it on every use, listen for its scroll on `document` in the capture phase (so
* the listener survives the swap), and observe the STABLE parent (which is not replaced) so
* the widget keeps working -- previously the observer and scroll listener died with the old
* element, which froze the bar and lost sync after each move.
*
* Loaded only when the "Add a top horizontal scrollbar" setting is enabled.
*/
(function () {
"use strict";
// Keep the thumb wide enough to grab, even on a very wide board.
var MIN_THUMB = 24;
// Ignore a few px of residual overflow (padding / sub-pixel rounding) so a board that visually
// fits is treated as "no horizontal scroll" instead of showing an immovable near-full thumb.
var FIT_TOLERANCE = 4;
function getContainer() {
return document.getElementById("board-container");
}
function init() {
var container = getContainer();
// Only on the board, and never add it twice.
if (!container || document.querySelector(".sv-top-scroll")) {
return;
}
// The parent survives the board refresh (only #board-container is replaced).
var boardParent = container.parentNode;
var track = document.createElement("div");
track.className = "sv-top-scroll";
var thumb = document.createElement("div");
thumb.className = "sv-top-scroll-thumb";
track.appendChild(thumb);
boardParent.insertBefore(track, container);
var dragging = false;
var dragStartX = 0;
var dragStartScrollLeft = 0;
function metrics() {
var c = getContainer();
if (!c) {
return null;
}
return {
c: c,
viewport: c.clientWidth,
content: c.scrollWidth,
trackW: track.clientWidth
};
}
function positionThumb(m, thumbW) {
var maxScroll = m.content - m.viewport;
var maxThumb = m.trackW - thumbW;
var left = maxScroll > 0 ? (m.c.scrollLeft / maxScroll) * maxThumb : 0;
thumb.style.transform = "translateX(" + Math.round(left) + "px)";
}
// The board needs scrolling only if its content exceeds the viewport by more than the
// tolerance (absorbs a residual px or two so a fitting board reads as "no scroll").
function needsScroll(m) {
return m && m.content - m.viewport > FIT_TOLERANCE;
}
// Recompute the thumb. The (light) track stays visible for a STABLE height; when the board
// fits, just drop the knob (thumb width 0) so it reads as "nothing to scroll" without the
// layout jump that hiding the whole bar would cause.
function layout() {
var m = metrics();
if (!m || m.trackW <= 0) {
return;
}
track.style.display = "";
if (!needsScroll(m)) {
thumb.style.width = "0px";
return;
}
var thumbW = Math.round(m.trackW * m.viewport / m.content);
if (thumbW < MIN_THUMB) { thumbW = MIN_THUMB; }
if (thumbW > m.trackW) { thumbW = m.trackW; }
thumb.style.width = thumbW + "px";
positionThumb(m, thumbW);
}
// Keep the track directly before the (current) container after a board refresh.
function reattachTrack() {
var c = getContainer();
if (c && c.parentNode && track.nextSibling !== c) {
c.parentNode.insertBefore(track, c);
}
observeSizes();
}
// Board scrolled by any other means -> keep the thumb in sync. Capture-phase on
// document catches #board-container's (non-bubbling) scroll and survives the swap.
document.addEventListener("scroll", function (e) {
if (dragging) { return; }
if (e.target !== getContainer()) { return; }
var m = metrics();
if (!needsScroll(m)) { return; }
positionThumb(m, thumb.offsetWidth);
}, true);
// Drag the thumb (mouse + touch via Pointer Events).
thumb.addEventListener("pointerdown", function (e) {
var c = getContainer();
if (!c) { return; }
e.preventDefault();
e.stopPropagation();
dragging = true;
dragStartX = e.clientX;
dragStartScrollLeft = c.scrollLeft;
track.classList.add("sv-dragging");
if (thumb.setPointerCapture) {
thumb.setPointerCapture(e.pointerId);
}
});
thumb.addEventListener("pointermove", function (e) {
if (!dragging) { return; }
var m = metrics();
if (!m) { return; }
var maxThumb = m.trackW - thumb.offsetWidth;
var maxScroll = m.content - m.viewport;
if (maxThumb <= 0) { return; }
m.c.scrollLeft = dragStartScrollLeft + (e.clientX - dragStartX) * (maxScroll / maxThumb);
positionThumb(m, thumb.offsetWidth);
});
function endDrag(e) {
if (!dragging) { return; }
dragging = false;
track.classList.remove("sv-dragging");
if (thumb.releasePointerCapture && e && e.pointerId != null) {
try { thumb.releasePointerCapture(e.pointerId); } catch (err) { /* ignore */ }
}
}
thumb.addEventListener("pointerup", endDrag);
thumb.addEventListener("pointercancel", endDrag);
// Click on the empty track -> jump so the thumb centers under the pointer.
track.addEventListener("pointerdown", function (e) {
if (e.target === thumb || dragging) { return; }
var m = metrics();
if (!needsScroll(m)) { return; }
var thumbW = thumb.offsetWidth;
var maxThumb = m.trackW - thumbW;
var maxScroll = m.content - m.viewport;
if (maxThumb <= 0) { return; }
var clickX = e.clientX - track.getBoundingClientRect().left;
var targetLeft = clickX - thumbW / 2;
if (targetLeft < 0) { targetLeft = 0; }
if (targetLeft > maxThumb) { targetLeft = maxThumb; }
m.c.scrollLeft = (targetLeft / maxThumb) * maxScroll;
positionThumb(m, thumbW);
});
// Single source of truth for recomputing the thumb: a ResizeObserver on the container
// (viewport) and #board (content). It fires exactly when the width settles/changes -- the
// right moment -- which also fixes the stale thumb from the first layout() running before the
// board finished sizing. Both are replaced on a board refresh, so re-observe the current ones
// (from reattachTrack). Without ResizeObserver, fall back to a window resize listener.
var sizeObserver = window.ResizeObserver ? new ResizeObserver(function () { layout(); }) : null;
var observedContainer = null;
var observedBoard = null;
function observeSizes() {
if (!sizeObserver) {
layout();
return;
}
var c = getContainer();
var b = document.getElementById("board");
if (c && c !== observedContainer) {
if (observedContainer) { sizeObserver.unobserve(observedContainer); }
sizeObserver.observe(c);
observedContainer = c;
}
if (b && b !== observedBoard) {
if (observedBoard) { sizeObserver.unobserve(observedBoard); }
sizeObserver.observe(b);
observedBoard = b;
}
}
if (!sizeObserver) {
window.addEventListener("resize", layout);
}
// Re-seat the track and re-observe after #board-container is replaced by a card move / AJAX
// polling (we watch the STABLE parent, since the container itself is swapped out).
if (window.MutationObserver) {
new MutationObserver(reattachTrack).observe(boardParent, { childList: true, subtree: true });
}
observeSizes();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();

View File

View File

@@ -0,0 +1,62 @@
<?php
namespace Kanboard\Plugin\ShrinkVertically\Controller;
use Kanboard\Controller\BaseController;
/**
* Settings page for the ShrinkVertically plugin: configure how many pixels are
* reserved around the vertically collapsed board columns.
*/
class ConfigController extends BaseController
{
const DEFAULT_OFFSET = 240;
const MIN_OFFSET = 0;
const MAX_OFFSET = 2000;
const DEFAULT_BAR_SIZE = 25;
const MIN_BAR_SIZE = 0;
const MAX_BAR_SIZE = 200;
public function show()
{
$offset = (int) $this->configModel->get('shrink_vertically_offset', self::DEFAULT_OFFSET);
$barSize = (int) $this->configModel->get('shrink_vertically_bar_size', self::DEFAULT_BAR_SIZE);
$topScrollbar = (int) $this->configModel->get('shrink_vertically_top_scrollbar', 1);
$this->response->html($this->helper->layout->config('shrinkVertically:config/show', array(
'title' => t('Settings').' &gt; '.t('Shrink Vertically'),
'values' => array(
'shrink_vertically_offset' => $offset,
'shrink_vertically_bar_size' => $barSize,
'shrink_vertically_top_scrollbar' => $topScrollbar,
),
'errors' => array(),
)));
}
public function save()
{
$values = $this->request->getValues();
$offset = isset($values['shrink_vertically_offset']) ? (int) $values['shrink_vertically_offset'] : self::DEFAULT_OFFSET;
$offset = max(self::MIN_OFFSET, min(self::MAX_OFFSET, $offset));
$barSize = isset($values['shrink_vertically_bar_size']) ? (int) $values['shrink_vertically_bar_size'] : self::DEFAULT_BAR_SIZE;
$barSize = max(self::MIN_BAR_SIZE, min(self::MAX_BAR_SIZE, $barSize));
$topScrollbar = isset($values['shrink_vertically_top_scrollbar']) ? 1 : 0;
if ($this->configModel->save(array(
'shrink_vertically_offset' => $offset,
'shrink_vertically_bar_size' => $barSize,
'shrink_vertically_top_scrollbar' => $topScrollbar,
))) {
$this->flash->success(t('Settings saved successfully.'));
} else {
$this->flash->failure(t('Unable to save your settings.'));
}
$this->response->redirect($this->helper->url->to('ConfigController', 'show', array('plugin' => 'ShrinkVertically')));
}
}

View File

View File

View File

@@ -1,6 +1,6 @@
<?php <?php
namespace Kanboard\Plugin\Skeleton; namespace Kanboard\Plugin\ShrinkVertically;
use Kanboard\Core\Plugin\Base; use Kanboard\Core\Plugin\Base;
@@ -8,28 +8,40 @@ class Plugin extends Base
{ {
public function initialize() public function initialize()
{ {
// 1. Render a visible word at the top of every page (the demo output). // Inject a small CSS override that reduces the height of vertically
$this->template->hook->attach('template:layout:top', 'skeleton:layout/header'); // collapsed board columns so the horizontal scrollbar stays visible.
// 2. Load the plugin stylesheet (currently empty -- proves the CSS hook fires).
$this->hook->on('template:layout:css', array( $this->hook->on('template:layout:css', array(
'template' => 'plugins/Skeleton/Asset/css/skeleton.css', 'template' => 'plugins/ShrinkVertically/Asset/css/shrink-vertically.css',
)); ));
// 3. Load the plugin script (currently empty -- proves the JS hook fires). // Inject the configured pixel offset as a CSS variable in the page head.
$this->template->hook->attach('template:layout:head', 'shrinkVertically:layout/variable');
// Append a "+" to the two vertical-collapse menu labels so it is visible
// that the behaviour is modified by this plugin.
$this->hook->on('template:layout:js', array( $this->hook->on('template:layout:js', array(
'template' => 'plugins/Skeleton/Asset/js/skeleton.js', 'template' => 'plugins/ShrinkVertically/Asset/js/shrink-vertically.js',
)); ));
// Add a "Shrink Vertically" entry to the Settings sidebar.
$this->template->hook->attach('template:config:sidebar', 'shrinkVertically:config/sidebar');
// Optional: a second, mirrored horizontal scrollbar above the board columns.
if ((int) $this->configModel->get('shrink_vertically_top_scrollbar', 1) === 1) {
$this->hook->on('template:layout:js', array(
'template' => 'plugins/ShrinkVertically/Asset/js/top-scrollbar.js',
));
}
} }
public function getPluginName() public function getPluginName()
{ {
return 'Skeleton'; return 'ShrinkVertically';
} }
public function getPluginDescription() public function getPluginDescription()
{ {
return t('Reusable skeleton/template for building Kanboard plugins.'); return t('Shrink vertically-collapsed board columns so the horizontal scrollbar stays visible.');
} }
public function getPluginAuthor() public function getPluginAuthor()
@@ -39,12 +51,12 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '0.1.0'; return '1.5.2';
} }
public function getPluginHomepage() public function getPluginHomepage()
{ {
return 'https://code.beco.cc/beco/kanboard-plugin-skeleton'; return 'https://code.beco.cc/beco/shrink-vertically';
} }
public function getCompatibleVersion() public function getCompatibleVersion()

138
README.md
View File

@@ -1,19 +1,58 @@
# Skeleton -- a Kanboard plugin template # ShrinkVertically -- keep the board horizontal scrollbar visible
A minimal, working Kanboard plugin that you copy and rename as the starting point for a A small Kanboard plugin focused on the vertically-collapsed board view. It fixes the
real plugin. By itself it does only one trivial thing: it renders the word "Skeleton" at collapse so the bottom horizontal scrollbar stays on screen, and can optionally add a
the top of every page. It changes no data and runs no database migration. second mirrored scrollbar above the columns. Configurable under
"Settings -> Shrink Vertically".
## What it demonstrates Board drag/touch and column-gap options that used to live here have moved to the separate
`TweakDrag` plugin.
- A complete `Plugin.php` registration class with all the metadata Kanboard shows in ## The problem
Settings -> Plugins (name, description, author, version, homepage, compatible version).
- A template hook (`template:layout:top`) that injects a template into the page. Kanboard's board gear menu has an "expand vertically / collapse vertically" toggle. When
- Asset hooks (`template:layout:css` and `template:layout:js`) that load a stylesheet and collapsed, each column gets a limited height so the horizontal scrollbar sits closer.
a script. They are empty for now but prove the injection path works -- handy when a real Core implements this with a single rule:
plugin needs custom CSS or JS.
- The standard plugin directory layout, with stub folders (`Controller/`, `Model/`, ```css
`Schema/`, `Locale/`, `Test/`) ready to grow into. .board-task-list-compact { max-height: 90vh; }
```
That `90vh` is measured from the top of the viewport and does not account for the page
header above the board. So even when collapsed, the column plus the header together
exceed the viewport height and the horizontal scrollbar is pushed below the fold.
## The fix
This plugin injects one CSS rule (via the `template:layout:css` hook) that replaces the
proportional `90vh` with a fixed-pixel budget for the chrome above and below the task
list:
```css
#board td .board-task-list.board-task-list-compact {
max-height: calc(100vh - var(--sv-shrink-offset, 240px)) !important;
min-height: 0 !important;
}
```
The offset reserves room for the page header, the board column header, the horizontal
scrollbar, and a little breathing space. A fixed-pixel budget is more reliable than a
proportional value because that chrome is a fixed height regardless of screen size. It
defaults to `240px` and is configurable (see Settings below).
The `min-height: 0` is essential: Kanboard's drag-and-drop sets an inline `min-height` on
each column equal to its full content height, and in CSS `min-height` beats `max-height`,
so without this reset the tall columns would never shrink.
The override only applies while columns are collapsed (when core adds the
`board-task-list-compact` class). It changes no data and runs no database migration.
## Menu labels
So it is obvious the collapsed behaviour comes from this plugin and not from core, a
small script (via `template:layout:js`) appends a `+` to the two gear-menu items, so they
read "Collapse vertically+" and "Expand vertically+". It only edits the rendered link
text, so it works in any locale.
## Requirements ## Requirements
@@ -21,46 +60,53 @@ the top of every page. It changes no data and runs no database migration.
## Installation ## Installation
No build step and no dependencies. Copy this folder into your Kanboard installation as `plugins/ShrinkVertically/`. The
directory name must be exactly `ShrinkVertically` (Kanboard derives the plugin namespace
from the folder name). Reload the board; no build step, no configuration.
1. Copy this folder into your Kanboard installation as `plugins/Skeleton/`. ## Top horizontal scrollbar (optional)
2. Reload any page. The word "Skeleton" appears at the top.
3. Confirm it under Settings -> Plugins.
To uninstall, delete `plugins/Skeleton/`. Nothing else is left behind. Kanboard's board scrolls horizontally inside `#board-container`, so its only horizontal
scrollbar is at the very bottom of the board -- on a wide board you must scroll all the
way down to move sideways. Enable "Add a top horizontal scrollbar" in the settings to get
a second scrollbar just above the columns.
## Directory layout It is a custom track + draggable thumb (`Asset/js/top-scrollbar.js`, loaded only when the
option is on), not a native scrollbar, so it renders and can be sized the same in every
browser (Chrome, Firefox, Safari) and on touch. Drag the thumb or click the track to move
the board; the thumb tracks the board when you scroll it by other means. Its thickness
follows the "Top Horizontal Scrollbar thickness" setting.
``` ## Settings
Skeleton/
Plugin.php Registration and hook wiring (the only required file).
README.md
LICENSE AGPL-3.0.
Template/
layout/header.php The visible "Skeleton" word.
Asset/
css/skeleton.css Loaded via template:layout:css.
js/skeleton.js Loaded via template:layout:js.
Controller/ Stub for future request handlers.
Model/ Stub for future business logic / DB access.
Schema/ Stub for future database migrations.
Locale/ Stub for future translations (e.g. pt_BR/, fr_FR/).
Test/ Stub for future unit tests.
```
## How to fork this into a new plugin These settings are global (per Kanboard instance) and admin-only. Go to
"Settings -> Shrink Vertically":
1. Copy the folder and rename it, e.g. `plugins/MyPlugin/`. The folder name must match the - **Vertical offset in pixels** -- pixels reserved around the collapsed columns. Injected
namespace and start with a capital letter. in the page head as the `--sv-shrink-offset` CSS variable. Increase it if the scrollbar
2. In `Plugin.php`, change the namespace from `Kanboard\Plugin\Skeleton` to is still clipped (for example with a taller theme header); decrease it to make the
`Kanboard\Plugin\MyPlugin`. collapsed columns taller. Default: 240.
3. Update the metadata methods (`getPluginName`, `getPluginDescription`, `getPluginAuthor`, - **Top Horizontal Scrollbar thickness in pixels** -- thickness of the custom top
`getPluginVersion`, `getPluginHomepage`, `getCompatibleVersion`). scrollbar (all browsers). Injected as the `--sv-bar-size` CSS variable. Default: 25.
4. Update the hook target paths: the lowercase prefix in `'skeleton:layout/header'` and the - **Add a top horizontal scrollbar to the board** -- toggles the mirrored top scrollbar
`plugins/Skeleton/Asset/...` asset paths must match the new plugin name. described above. Default: on.
5. Replace `Template/layout/header.php` with your real template, or attach to a different
hook. See the Kanboard plugin hooks documentation for the full list of hook points.
## License ## License
AGPL-3.0. See LICENSE. AGPL-3.0. See LICENSE.
## More Kanboard plugins by Ruben (drbeco)
All free and AGPL-3.0, at [code.beco.cc](https://code.beco.cc/beco):
- **[RecoReco](https://code.beco.cc/beco/RecoReco)** -- calendar-scheduled recurring cards (yearly,
monthly, weekly, daily), driven by the card due date.
- **[FinanceBuddy](https://code.beco.cc/beco/FinanceBuddy)** -- attach a money value (debit/credit
and installments) to cards, shown on the card and totalled per column.
- **[OrganonTweaks](https://code.beco.cc/beco/OrganonTweaks)** -- an umbrella of small
quality-of-life board tweaks: remove an empty column, always show the comment icon, emphasize due
dates, extra search keywords and shared board filters, and more.
- **[BulkMoveTasks](https://code.beco.cc/beco/BulkMoveTasks)** -- move every task from one board
column to another in a single action.
- **[TweakDrag](https://code.beco.cc/beco/TweakDrag)** -- board drag and touch niceties:
drag-to-scroll, a wider column gap, and smoother card dragging.

View File

34
Template/config/show.php Normal file
View File

@@ -0,0 +1,34 @@
<div class="page-header">
<h2><?= t('Shrink Vertically') ?></h2>
</div>
<form method="post" action="<?= $this->url->href('ConfigController', 'save', array('plugin' => 'ShrinkVertically')) ?>" autocomplete="off">
<?= $this->form->csrf() ?>
<fieldset>
<?= $this->form->label(t('Vertical offset in pixels'), 'shrink_vertically_offset') ?>
<?= $this->form->number('shrink_vertically_offset', $values, $errors) ?>
<p class="form-help">
<?= t('Pixels reserved above and below the vertically collapsed board columns (page header, column header and the horizontal scrollbar). A larger value makes the columns shorter. Default: 240.') ?>
</p>
</fieldset>
<fieldset>
<?= $this->form->label(t('Top Horizontal Scrollbar thickness in pixels'), 'shrink_vertically_bar_size') ?>
<?= $this->form->number('shrink_vertically_bar_size', $values, $errors) ?>
<p class="form-help">
<?= t('Thickness of the top horizontal scrollbar. Default: 25.') ?>
</p>
</fieldset>
<fieldset>
<?= $this->form->checkbox('shrink_vertically_top_scrollbar', t('Add a top horizontal scrollbar to the board'), 1, isset($values['shrink_vertically_top_scrollbar']) && $values['shrink_vertically_top_scrollbar'] == 1) ?>
<p class="form-help">
<?= t('Adds a second horizontal scrollbar above the columns, mirrored with the board scrollbar, so you do not have to scroll to the bottom to move sideways.') ?>
</p>
</fieldset>
<div class="form-actions">
<button type="submit" class="btn btn-blue"><?= t('Save') ?></button>
</div>
</form>

View File

@@ -0,0 +1,3 @@
<li <?= $this->app->checkMenuSelection('ConfigController', 'show', 'ShrinkVertically') ?>>
<?= $this->url->link(t('Shrink Vertically'), 'ConfigController', 'show', array('plugin' => 'ShrinkVertically')) ?>
</li>

View File

@@ -1 +0,0 @@
<div class="skeleton-plugin-marker">Skeleton</div>

View File

@@ -0,0 +1,6 @@
<style type="text/css">
:root {
--sv-shrink-offset: <?= (int) $this->app->config('shrink_vertically_offset', 240) ?>px;
--sv-bar-size: <?= (int) $this->app->config('shrink_vertically_bar_size', 25) ?>px;
}
</style>

View File

1
VERSION Normal file
View File

@@ -0,0 +1 @@
ShrinkVertically v1.5.2