experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: groups detail view header with inline controls

+272 -28
+100 -13
features/groups/home.css
··· 331 331 background: var(--base01); 332 332 } 333 333 334 - /* Group detail header — shown when viewing a group's contents */ 334 + /* Group detail header — shown at the very top when viewing a group's contents */ 335 335 .group-detail-header { 336 - grid-column: 1 / -1; 337 336 display: flex; 338 337 align-items: center; 339 338 gap: 10px; 340 - padding: 4px 0 10px 0; 341 - margin-bottom: 4px; 339 + padding: 14px 16px 10px 16px; 342 340 border-bottom: 1px solid var(--base02); 343 341 } 344 342 345 - .group-header-dot { 346 - width: 12px; 347 - height: 12px; 348 - border-radius: 50%; 349 - flex-shrink: 0; 350 - } 351 - 352 343 .group-header-name { 353 - font-size: 16px; 354 - font-weight: 600; 344 + font-size: 18px; 345 + font-weight: 700; 355 346 color: var(--base05); 356 347 cursor: default; 357 348 white-space: nowrap; 358 349 overflow: hidden; 359 350 text-overflow: ellipsis; 360 351 min-width: 0; 352 + } 353 + 354 + .group-header-spacer { 361 355 flex: 1; 356 + } 357 + 358 + /* View mode buttons in group detail header */ 359 + .group-header-view-modes { 360 + display: flex; 361 + align-items: center; 362 + gap: 2px; 363 + flex-shrink: 0; 364 + } 365 + 366 + .group-header-view-modes .ghvm-btn { 367 + display: inline-flex; 368 + align-items: center; 369 + justify-content: center; 370 + width: 28px; 371 + height: 28px; 372 + padding: 0; 373 + border: 1px solid transparent; 374 + border-radius: 5px; 375 + background: transparent; 376 + color: var(--base04); 377 + cursor: pointer; 378 + transition: all 0.15s; 379 + } 380 + 381 + .group-header-view-modes .ghvm-btn:hover { 382 + background: var(--base02); 383 + color: var(--base05); 384 + } 385 + 386 + .group-header-view-modes .ghvm-btn.active { 387 + background: var(--base0D); 388 + color: var(--base00); 389 + border-color: var(--base0D); 390 + } 391 + 392 + /* Inline sort controls in search row (detail view) */ 393 + .inline-sort-controls { 394 + display: flex; 395 + align-items: center; 396 + gap: 4px; 397 + flex-shrink: 0; 398 + } 399 + 400 + .inline-sort-controls .inline-sort-select { 401 + height: 28px; 402 + padding: 0 22px 0 8px; 403 + font: inherit; 404 + font-size: 12px; 405 + color: var(--base05); 406 + background: var(--base01); 407 + border: 1px solid var(--base02); 408 + border-radius: 5px; 409 + cursor: pointer; 410 + appearance: none; 411 + outline: none; 412 + transition: border-color 0.15s; 413 + } 414 + 415 + .inline-sort-controls .inline-sort-select:focus { 416 + border-color: var(--base0D); 417 + } 418 + 419 + .inline-sort-controls .inline-sort-wrapper { 420 + position: relative; 421 + display: flex; 422 + align-items: center; 423 + } 424 + 425 + .inline-sort-controls .inline-sort-arrow { 426 + position: absolute; 427 + right: 6px; 428 + pointer-events: none; 429 + color: var(--base04); 430 + } 431 + 432 + .inline-sort-controls .inline-sort-dir-btn { 433 + display: inline-flex; 434 + align-items: center; 435 + justify-content: center; 436 + width: 28px; 437 + height: 28px; 438 + padding: 0; 439 + border: 1px solid var(--base02); 440 + border-radius: 5px; 441 + background: var(--base01); 442 + color: var(--base05); 443 + cursor: pointer; 444 + transition: all 0.15s; 445 + } 446 + 447 + .inline-sort-controls .inline-sort-dir-btn:hover { 448 + background: var(--base02); 362 449 } 363 450 364 451 .group-header-name[title] {
+1
features/groups/home.html
··· 33 33 </script> 34 34 </head> 35 35 <body> 36 + <div class="group-detail-header" style="display: none;"></div> 36 37 <div class="search-container"> 37 38 <div class="search-row"> 38 39 <peek-input
+171 -15
features/groups/home.js
··· 606 606 }; 607 607 608 608 /** 609 + * Create inline sort controls (dropdown + direction toggle) and append to a container. 610 + * Removes any existing inline sort controls first. 611 + */ 612 + const addInlineSortControls = (container) => { 613 + if (!container) return; 614 + 615 + // Remove existing inline sort controls 616 + const existing = container.querySelector('.inline-sort-controls'); 617 + if (existing) existing.remove(); 618 + 619 + const controls = document.createElement('div'); 620 + controls.className = 'inline-sort-controls'; 621 + 622 + // Sort dropdown wrapper 623 + const sortWrapper = document.createElement('div'); 624 + sortWrapper.className = 'inline-sort-wrapper'; 625 + 626 + const select = document.createElement('select'); 627 + select.className = 'inline-sort-select'; 628 + SORT_OPTIONS_ADDRESSES.forEach(opt => { 629 + const option = document.createElement('option'); 630 + option.value = opt.value; 631 + option.textContent = opt.label; 632 + if (opt.value === viewPrefs.sortBy) option.selected = true; 633 + select.appendChild(option); 634 + }); 635 + select.addEventListener('change', () => { 636 + viewPrefs.sortBy = select.value; 637 + saveViewPrefs(); 638 + renderCurrentView(); 639 + }); 640 + sortWrapper.appendChild(select); 641 + 642 + // Dropdown arrow 643 + const arrow = document.createElement('svg'); 644 + arrow.className = 'inline-sort-arrow'; 645 + arrow.setAttribute('width', '10'); 646 + arrow.setAttribute('height', '10'); 647 + arrow.setAttribute('viewBox', '0 0 10 10'); 648 + arrow.setAttribute('fill', 'none'); 649 + arrow.innerHTML = '<path d="M2.5 3.75L5 6.25L7.5 3.75" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'; 650 + sortWrapper.appendChild(arrow); 651 + 652 + controls.appendChild(sortWrapper); 653 + 654 + // Direction toggle button 655 + const dirBtn = document.createElement('button'); 656 + dirBtn.className = 'inline-sort-dir-btn'; 657 + dirBtn.title = viewPrefs.sortDirection === 'asc' ? 'Ascending' : 'Descending'; 658 + const updateDirIcon = () => { 659 + dirBtn.title = viewPrefs.sortDirection === 'asc' ? 'Ascending' : 'Descending'; 660 + dirBtn.innerHTML = viewPrefs.sortDirection === 'asc' 661 + ? '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 11V3M7 3L4 6M7 3L10 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>' 662 + : '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 3V11M7 11L4 8M7 11L10 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'; 663 + }; 664 + updateDirIcon(); 665 + dirBtn.addEventListener('click', () => { 666 + viewPrefs.sortDirection = viewPrefs.sortDirection === 'asc' ? 'desc' : 'asc'; 667 + saveViewPrefs(); 668 + updateDirIcon(); 669 + renderCurrentView(); 670 + }); 671 + controls.appendChild(dirBtn); 672 + 673 + container.appendChild(controls); 674 + }; 675 + 676 + /** 609 677 * Show the groups (tags) view 610 678 */ 611 679 const showGroups = async () => { ··· 635 703 searchInput.value = ''; 636 704 searchInput.placeholder = 'Search groups...'; 637 705 706 + // Show create group button again 707 + const createBtn = document.querySelector('.create-group-btn'); 708 + if (createBtn) createBtn.style.display = ''; 709 + 710 + // Show peek-grid-toolbar again 711 + const toolbar = document.querySelector('peek-grid-toolbar.grid-toolbar'); 712 + if (toolbar) toolbar.style.display = ''; 713 + 714 + // Remove inline sort controls from search row 715 + const inlineSort = document.querySelector('.inline-sort-controls'); 716 + if (inlineSort) inlineSort.remove(); 717 + 638 718 renderGroups(); 639 719 }; 640 720 ··· 648 728 const container = document.querySelector('.cards'); 649 729 container.innerHTML = ''; 650 730 updateToolbarSortOptions(); 731 + 732 + // Hide the group detail header when showing the groups list 733 + renderGroupHeader(); 651 734 652 735 // Separate promoted groups from regular tags 653 736 const promotedGroups = state.tags.filter(tag => isGroupTag(tag)); ··· 768 851 searchInput.value = ''; 769 852 searchInput.placeholder = `Search in ${tag.name}...`; 770 853 854 + // Hide create group button and form in detail view 855 + const createBtn = document.querySelector('.create-group-btn'); 856 + const createForm = document.querySelector('.create-group-form'); 857 + if (createBtn) createBtn.style.display = 'none'; 858 + if (createForm) createForm.style.display = 'none'; 859 + 860 + // Hide peek-grid-toolbar in detail view 861 + const toolbar = document.querySelector('peek-grid-toolbar.grid-toolbar'); 862 + if (toolbar) toolbar.style.display = 'none'; 863 + 864 + // Add inline sort controls to search row 865 + const searchRow = document.querySelector('.search-row'); 866 + addInlineSortControls(searchRow); 867 + 771 868 renderAddresses(); 772 869 }; 773 870 ··· 776 873 * Double-click the name to rename. 777 874 */ 778 875 const renderGroupHeader = () => { 779 - const container = document.querySelector('.cards'); 780 - const tag = state.currentTag; 781 - if (!tag) return; 876 + const header = document.querySelector('body > .group-detail-header'); 877 + if (!header) return; 782 878 783 - // Remove existing header if present 784 - const existing = container.querySelector('.group-detail-header'); 785 - if (existing) existing.remove(); 879 + const tag = state.currentTag; 880 + if (!tag) { 881 + // Hide header when not viewing a specific group 882 + header.style.display = 'none'; 883 + header.innerHTML = ''; 884 + return; 885 + } 786 886 787 - const header = document.createElement('div'); 788 - header.className = 'group-detail-header'; 789 - 790 - const colorDot = document.createElement('div'); 791 - colorDot.className = 'group-header-dot'; 792 - colorDot.style.backgroundColor = tag.color || '#999'; 793 - header.appendChild(colorDot); 887 + // Show and populate header 888 + header.style.display = ''; 889 + header.innerHTML = ''; 794 890 795 891 const nameEl = document.createElement('span'); 796 892 nameEl.className = 'group-header-name'; ··· 816 912 header.appendChild(renameBtn); 817 913 } 818 914 819 - container.insertBefore(header, container.firstChild); 915 + // Spacer to push view mode buttons to the right 916 + const spacer = document.createElement('div'); 917 + spacer.className = 'group-header-spacer'; 918 + header.appendChild(spacer); 919 + 920 + // View mode buttons 921 + const viewModes = document.createElement('div'); 922 + viewModes.className = 'group-header-view-modes'; 923 + 924 + const modes = [ 925 + { mode: 'columns', title: 'Grid view', svg: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="1" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="8" y="1" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="1" y="8" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="8" y="8" width="5" height="5" rx="1" stroke="currentColor" stroke-width="1.5"/></svg>' }, 926 + { mode: 'list', title: 'List view', svg: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><line x1="1" y1="3" x2="13" y2="3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="1" y1="7" x2="13" y2="7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="1" y1="11" x2="13" y2="11" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>' }, 927 + { mode: 'masonry', title: 'Masonry view', svg: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="1" width="5" height="7" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="8" y="1" width="5" height="4" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="1" y="10" width="5" height="3" rx="1" stroke="currentColor" stroke-width="1.5"/><rect x="8" y="7" width="5" height="6" rx="1" stroke="currentColor" stroke-width="1.5"/></svg>' }, 928 + { mode: 'freeform', title: 'Freeform view', svg: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="1" width="4" height="3" rx="0.5" stroke="currentColor" stroke-width="1.5"/><rect x="7" y="2" width="6" height="4" rx="0.5" stroke="currentColor" stroke-width="1.5"/><rect x="2" y="6" width="5" height="4" rx="0.5" stroke="currentColor" stroke-width="1.5"/><rect x="9" y="8" width="4" height="5" rx="0.5" stroke="currentColor" stroke-width="1.5"/></svg>' } 929 + ]; 930 + 931 + modes.forEach(({ mode, title, svg }) => { 932 + const btn = document.createElement('button'); 933 + btn.className = 'ghvm-btn' + (viewPrefs.viewMode === mode ? ' active' : ''); 934 + btn.title = title; 935 + btn.innerHTML = svg; 936 + btn.addEventListener('click', () => { 937 + viewPrefs.viewMode = mode; 938 + saveViewPrefs(); 939 + // Update peek-grid view-mode attribute 940 + const grid = document.querySelector('peek-grid.cards'); 941 + if (grid) grid.setAttribute('view-mode', mode); 942 + // Re-render header to update active state and freeform edit toggle 943 + renderGroupHeader(); 944 + renderCurrentView(); 945 + }); 946 + viewModes.appendChild(btn); 947 + }); 948 + 949 + // Freeform edit toggle (only visible when freeform mode is active) 950 + if (viewPrefs.viewMode === 'freeform') { 951 + const editBtn = document.createElement('button'); 952 + const grid = document.querySelector('peek-grid.cards'); 953 + const isEditing = grid && grid.getAttribute('freeform-editing') === 'true'; 954 + editBtn.className = 'ghvm-btn' + (isEditing ? ' active' : ''); 955 + editBtn.title = isEditing ? 'Exit edit mode' : 'Edit layout'; 956 + editBtn.style.marginLeft = '4px'; 957 + editBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M10.5 1.5L12.5 3.5L4.5 11.5L1.5 12.5L2.5 9.5L10.5 1.5Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>'; 958 + editBtn.addEventListener('click', () => { 959 + const g = document.querySelector('peek-grid.cards'); 960 + const toolbar = document.querySelector('.grid-toolbar'); 961 + const newVal = !(g && g.getAttribute('freeform-editing') === 'true'); 962 + if (g) g.setAttribute('freeform-editing', String(newVal)); 963 + if (toolbar) { 964 + toolbar.freeformEditing = newVal; 965 + toolbar.dispatchEvent(new CustomEvent('freeform-editing-change', { detail: { editing: newVal }, bubbles: true })); 966 + } 967 + renderGroupHeader(); 968 + }); 969 + viewModes.appendChild(editBtn); 970 + } 971 + 972 + header.appendChild(viewModes); 820 973 }; 821 974 822 975 /** ··· 900 1053 const message = state.searchQuery 901 1054 ? 'No pages match your search.' 902 1055 : 'No pages in this group yet.'; 903 - container.innerHTML = `<div class="empty-state">${message}</div>`; 1056 + const emptyEl = document.createElement('div'); 1057 + emptyEl.className = 'empty-state'; 1058 + emptyEl.textContent = message; 1059 + container.appendChild(emptyEl); 904 1060 return; 905 1061 } 906 1062