super-buckets: split tab click (activate) from caret click (dropdown)
Clicking the super-bucket tab now activates the last-shown child directly. Only the caret arrow on the right opens the dropdown popover. Caret has its own hit area with accent hover highlight. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
04b52ecc19
commit
527305355c
5 files changed
+64
-16
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
define('APP_NAME', 'hyprcrd');
|
define('APP_NAME', 'hyprcrd');
|
||||||
define('APP_VERSION', '1.2.0');
|
define('APP_VERSION', '1.3.0');
|
||||||
|
|
||||||
// Database path — writable directory, outside webroot
|
// Database path — writable directory, outside webroot
|
||||||
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
// DB_PATH / UPLOAD_DIR can be pre-defined by the Electron router for app isolation
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hyprcrd",
|
"name": "hyprcrd",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": "hyprcrd — self-hosted card manager",
|
"description": "hyprcrd — self-hosted card manager",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -821,6 +821,24 @@ body.anim-slow .board-card.card-pulse-local {
|
|||||||
/* Super-bucket dropdown popover. Positioned via inline styles by
|
/* Super-bucket dropdown popover. Positioned via inline styles by
|
||||||
openSuperDropdown() in buckets.js. Lives on document.body (not
|
openSuperDropdown() in buckets.js. Lives on document.body (not
|
||||||
inside #bucket-bar) so it can escape the bar's overflow:auto. */
|
inside #bucket-bar) so it can escape the bar's overflow:auto. */
|
||||||
|
.super-caret {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 2px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.15s, background 0.15s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.super-caret:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background: color-mix(in srgb, var(--t-accent) 25%, transparent);
|
||||||
|
}
|
||||||
|
.bucket-tab.active .super-caret:hover {
|
||||||
|
background: rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
#bucket-super-popover {
|
#bucket-super-popover {
|
||||||
z-index: 220;
|
z-index: 220;
|
||||||
background: var(--t-surface);
|
background: var(--t-surface);
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ function renderBar() {
|
|||||||
const shown = tileFor(b);
|
const shown = tileFor(b);
|
||||||
const c = shown.color || '#64748b';
|
const c = shown.color || '#64748b';
|
||||||
const groupActive = state.activeBucket == b.id || kids.some(k => k.id == state.activeBucket);
|
const groupActive = state.activeBucket == b.id || kids.some(k => k.id == state.activeBucket);
|
||||||
return `<button class="${tab(groupActive)} bucket-super" data-bucket-super="${b.id}" data-super-shown="${shown.id}" draggable="true" data-bucket-drag="${b.id}" style="border-top:3px solid ${c};${groupActive ? `background:${c};color:var(--t-bg);border-color:${c}` : ''}"><i class="ph ph-stack text-sm"></i><span class="font-pixel text-[10px]">${esc(shown.name)}</span><i class="ph ph-caret-down text-[10px] ml-0.5 opacity-70"></i></button>`;
|
return `<button class="${tab(groupActive)} bucket-super" data-bucket-super="${b.id}" data-super-shown="${shown.id}" draggable="true" data-bucket-drag="${b.id}" style="border-top:3px solid ${c};${groupActive ? `background:${c};color:var(--t-bg);border-color:${c}` : ''}"><i class="ph ph-stack text-sm"></i><span class="font-pixel text-[10px]">${esc(shown.name)}</span><span class="super-caret" data-super-caret="${b.id}"><i class="ph ph-caret-down text-[10px]"></i></span></button>`;
|
||||||
}
|
}
|
||||||
const c = b.color || '#64748b';
|
const c = b.color || '#64748b';
|
||||||
const active = state.activeBucket == b.id;
|
const active = state.activeBucket == b.id;
|
||||||
@@ -462,11 +462,21 @@ function renderBar() {
|
|||||||
const isUser = isSuperTile || !SPECIAL.includes(bid);
|
const isUser = isSuperTile || !SPECIAL.includes(bid);
|
||||||
|
|
||||||
if (isSuperTile) {
|
if (isSuperTile) {
|
||||||
// Click opens the dropdown popover instead of activating the
|
// Caret arrow opens the dropdown; clicking the rest of the
|
||||||
// parent directly. The dropdown lists parent + every child.
|
// tab activates the last-shown child (or parent).
|
||||||
t.addEventListener('click', (e) => {
|
const caret = t.querySelector('[data-super-caret]');
|
||||||
e.stopPropagation();
|
if (caret) {
|
||||||
openSuperDropdown(t, parseInt(bid, 10));
|
caret.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openSuperDropdown(t, parseInt(bid, 10));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
t.addEventListener('click', () => {
|
||||||
|
const shownId = t.dataset.superShown;
|
||||||
|
setActiveBucket(shownId || bid);
|
||||||
|
state.activeGroup = null;
|
||||||
|
renderBar();
|
||||||
|
window._renderBoard();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
t.addEventListener('click', () => { setActiveBucket(bid); state.activeGroup = null; renderBar(); window._renderBoard(); });
|
t.addEventListener('click', () => { setActiveBucket(bid); state.activeGroup = null; renderBar(); window._renderBoard(); });
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ describe('Super-buckets: Bar rendering', () => {
|
|||||||
expect(superTile.textContent).toContain('__sb_active_child__');
|
expect(superTile.textContent).toContain('__sb_active_child__');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking the super-tile opens a popover with parent + child entries', async () => {
|
it('clicking the caret opens a popover with parent + child entries', async () => {
|
||||||
const parent = await makeBucket('__sb_pop_parent__');
|
const parent = await makeBucket('__sb_pop_parent__');
|
||||||
const child1 = await makeBucket('__sb_pop_child1__');
|
const child1 = await makeBucket('__sb_pop_child1__');
|
||||||
const child2 = await makeBucket('__sb_pop_child2__');
|
const child2 = await makeBucket('__sb_pop_child2__');
|
||||||
@@ -191,15 +191,15 @@ describe('Super-buckets: Bar rendering', () => {
|
|||||||
|
|
||||||
const superTile = document.querySelector(`#bucket-bar [data-bucket-super="${parent.id}"]`);
|
const superTile = document.querySelector(`#bucket-bar [data-bucket-super="${parent.id}"]`);
|
||||||
if (!superTile) return;
|
if (!superTile) return;
|
||||||
click(superTile);
|
const caret = superTile.querySelector('[data-super-caret]');
|
||||||
|
expect(caret).toExist();
|
||||||
|
click(caret);
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
|
|
||||||
const popover = document.getElementById('bucket-super-popover');
|
const popover = document.getElementById('bucket-super-popover');
|
||||||
expect(popover).toExist();
|
expect(popover).toExist();
|
||||||
// Parent + 2 children = 3 entries
|
|
||||||
const entries = popover.querySelectorAll('[data-bucket]');
|
const entries = popover.querySelectorAll('[data-bucket]');
|
||||||
expect(entries.length).toBe(3);
|
expect(entries.length).toBe(3);
|
||||||
// All three names appear
|
|
||||||
const text = popover.textContent;
|
const text = popover.textContent;
|
||||||
expect(text).toContain('__sb_pop_parent__');
|
expect(text).toContain('__sb_pop_parent__');
|
||||||
expect(text).toContain('__sb_pop_child1__');
|
expect(text).toContain('__sb_pop_child1__');
|
||||||
@@ -215,7 +215,8 @@ describe('Super-buckets: Bar rendering', () => {
|
|||||||
|
|
||||||
const superTile = document.querySelector(`#bucket-bar [data-bucket-super="${parent.id}"]`);
|
const superTile = document.querySelector(`#bucket-bar [data-bucket-super="${parent.id}"]`);
|
||||||
if (!superTile) return;
|
if (!superTile) return;
|
||||||
click(superTile);
|
const caret = superTile.querySelector('[data-super-caret]');
|
||||||
|
click(caret);
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
|
|
||||||
const childEntry = document.querySelector(`#bucket-super-popover [data-bucket="${child.id}"]`);
|
const childEntry = document.querySelector(`#bucket-super-popover [data-bucket="${child.id}"]`);
|
||||||
@@ -226,7 +227,25 @@ describe('Super-buckets: Bar rendering', () => {
|
|||||||
expect(String(state.activeBucket)).toBe(String(child.id));
|
expect(String(state.activeBucket)).toBe(String(child.id));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clicking the super-tile twice toggles the popover closed', async () => {
|
it('clicking the super-tile activates the shown bucket', async () => {
|
||||||
|
const parent = await makeBucket('__sb_direct_parent__');
|
||||||
|
const child = await makeBucket('__sb_direct_child__');
|
||||||
|
await patchBucket(child.id, { parent_id: parent.id });
|
||||||
|
await window._reloadAll?.();
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
state.activeBucket = 'all';
|
||||||
|
const superTile = document.querySelector(`#bucket-bar [data-bucket-super="${parent.id}"]`);
|
||||||
|
if (!superTile) return;
|
||||||
|
click(superTile);
|
||||||
|
await sleep(150);
|
||||||
|
|
||||||
|
const shownId = superTile.dataset.superShown;
|
||||||
|
expect(String(state.activeBucket)).toBe(String(shownId));
|
||||||
|
expect(document.getElementById('bucket-super-popover')).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking the caret twice toggles the popover closed', async () => {
|
||||||
const parent = await makeBucket('__sb_toggle_parent__');
|
const parent = await makeBucket('__sb_toggle_parent__');
|
||||||
const child = await makeBucket('__sb_toggle_child__');
|
const child = await makeBucket('__sb_toggle_child__');
|
||||||
await patchBucket(child.id, { parent_id: parent.id });
|
await patchBucket(child.id, { parent_id: parent.id });
|
||||||
@@ -235,10 +254,11 @@ describe('Super-buckets: Bar rendering', () => {
|
|||||||
|
|
||||||
const superTile = document.querySelector(`#bucket-bar [data-bucket-super="${parent.id}"]`);
|
const superTile = document.querySelector(`#bucket-bar [data-bucket-super="${parent.id}"]`);
|
||||||
if (!superTile) return;
|
if (!superTile) return;
|
||||||
click(superTile);
|
const caret = superTile.querySelector('[data-super-caret]');
|
||||||
|
click(caret);
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
expect(document.getElementById('bucket-super-popover')).toExist();
|
expect(document.getElementById('bucket-super-popover')).toExist();
|
||||||
click(superTile);
|
click(caret);
|
||||||
await sleep(100);
|
await sleep(100);
|
||||||
expect(document.getElementById('bucket-super-popover')).toBe(null);
|
expect(document.getElementById('bucket-super-popover')).toBe(null);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user