Redesign link field dropdown with modern single-column layout

Replace overwhelming multi-column layout with clean, scannable single-column design:
- Add search functionality to filter pages
- Convert to single-column layout for better readability
- Use subtle section headers instead of bright color blocks
- Improve item styling with checkboxes, better spacing, and hover states
- Add proper empty states and no-results messaging
- Increase max height and make more compact

This transforms the dropdown from a janky color explosion into a modern, usable selector.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andrew Brown 2025-09-30 00:56:12 -07:00
parent b90a9a30e9
commit 1aaf563db6
2 changed files with 80 additions and 34 deletions

View File

@ -5,9 +5,23 @@ function alpineMultiSelectController() {
selected: [],
show: false,
sourceFieldId: '',
searchQuery: '',
open() { this.show = true },
close() { this.show = false },
isOpen() { return this.show === true },
filterOptions() {
// Update filteredOptions for each optgroup based on search query
this.optgroups.forEach(optgroup => {
if (!this.searchQuery || this.searchQuery.trim() === '') {
optgroup.filteredOptions = optgroup.options;
} else {
const query = this.searchQuery.toLowerCase();
optgroup.filteredOptions = optgroup.options.filter(option =>
option.text.toLowerCase().includes(query)
);
}
});
},
select(index, event) {
if (!this.options[index].selected) {
this.options[index].selected = true;
@ -79,13 +93,16 @@ function alpineMultiSelectController() {
// Finally, add it as a valid optgroup with options
if (optionsForThisOptGroup.length > 0) {
this.optgroups.push({
const optgroupData = {
label: optgroups[i].label,
icon: window.ContentTypeData[optgroups[i].label].icon,
color: window.ContentTypeData[optgroups[i].label].color,
iconColor: window.ContentTypeData[optgroups[i].label].text_color || 'text-gray-600',
plural: window.ContentTypeData[optgroups[i].label].plural,
options: optionsForThisOptGroup
});
options: optionsForThisOptGroup,
filteredOptions: optionsForThisOptGroup // Initialize with all options
};
this.optgroups.push(optgroupData);
// And also track all the options in a flat array so we can reference them with a single index
for (let j = 0; j < optionsForThisOptGroup.length; j++) {

View File

@ -81,30 +81,52 @@
<%# Opened state %>
<div class="w-full">
<div x-show.transition.origin.top="isOpen()" class="absolute shadow-lg top-100 bg-white z-40 w-full left-0 rounded-lg" x-on:click.away="close">
<div class="flex flex-row w-full overflow-y-auto max-h-64">
<template x-for="(optgroup,groupIndex) in optgroups" :key="groupIndex" class="overflow-auto">
<div class="w-full">
<%# Optgroup styling %>
<div class="w-full py-1 rounded-t-lg text-white inline-flex items-center" x-bind:class="optgroup.color">
<span x-text="optgroup.icon" class="material-icons mx-1"></span>
<span x-text="optgroup.plural" class="text-xs font-medium"></span>
<div x-show.transition.origin.top="isOpen()" class="absolute shadow-xl top-100 bg-white z-40 w-full left-0 rounded-lg border border-gray-200 overflow-hidden" x-on:click.away="close">
<%# Search box %>
<div class="p-3 border-b border-gray-200 bg-gray-50">
<input type="text"
x-model="searchQuery"
@input="filterOptions()"
placeholder="Search pages..."
class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none"
@click.stop>
</div>
<div class="overflow-y-auto max-h-80">
<template x-for="(optgroup,groupIndex) in optgroups" :key="groupIndex">
<div x-show="optgroup.filteredOptions && optgroup.filteredOptions.length > 0">
<%# Subtle section header %>
<div class="sticky top-0 bg-gray-100 px-3 py-1.5 border-b border-gray-200 z-10">
<div class="flex items-center text-xs font-semibold text-gray-600 uppercase tracking-wider">
<span x-text="optgroup.icon" class="material-icons text-sm mr-1.5" x-bind:class="optgroup.iconColor"></span>
<span x-text="optgroup.plural"></span>
</div>
</div>
<div class="mb-2">
<template x-for="(option, optionIndex) in optgroup.options" :key="optionIndex" class="overflow-auto">
<%# Option styling %>
<div class="cursor-pointer w-full border-gray-100 rounded-t hover:bg-blue-50" @click="select(option.index,$event)">
<div class="flex w-full px-2 pl-2 border-transparent border-l-2 relative">
<div class="w-full items-center flex justify-between">
<div class="w-4">
<svg x-show="option.selected" class="svg-icon" viewBox="0 0 20 20">
<path fill="none" d="M7.197,16.963H7.195c-0.204,0-0.399-0.083-0.544-0.227l-6.039-6.082c-0.3-0.302-0.297-0.788,0.003-1.087 C0.919,9.266,1.404,9.269,1.702,9.57l5.495,5.536L18.221,4.083c0.301-0.301,0.787-0.301,1.087,0c0.301,0.3,0.301,0.787,0,1.087 L7.741,16.738C7.596,16.882,7.401,16.963,7.197,16.963z"></path>
</svg>
</div>
<div class="mx-2 py-2 leading-6 flex-1" x-model="option" x-text="option.text"></div>
<div>
<img :src="`${option.imageUrl}`" class="h-8 w-8 rounded-full">
</div>
<%# Options list %>
<div class="py-1">
<template x-for="(option, optionIndex) in optgroup.filteredOptions" :key="optionIndex">
<div class="cursor-pointer hover:bg-blue-50 transition-colors"
@click="select(option.index,$event)"
:class="option.selected ? 'bg-blue-50' : ''">
<div class="flex items-center px-3 py-2.5 gap-3">
<%# Checkbox/checkmark %>
<div class="flex-shrink-0 w-5 h-5 rounded border-2 flex items-center justify-center transition-colors"
:class="option.selected ? 'border-blue-500 bg-blue-500' : 'border-gray-300'">
<svg x-show="option.selected" class="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<%# Page name %>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-900 truncate" x-text="option.text"></div>
</div>
<%# Thumbnail %>
<div class="flex-shrink-0">
<img :src="option.imageUrl" class="h-8 w-8 rounded-full object-cover border border-gray-200" :alt="option.text">
</div>
</div>
</div>
@ -113,14 +135,21 @@
</div>
</template>
<%# Placeholder the link dropdown when there are no linkable pages to show %>
<div x-show="options.length == 0">
<p class="p-4">
<i class="material-icons float-left mr-2 text-notebook-blue">info</i>
This field only accepts links to
<%= linkable_types.to_sentence %> notebook pages &mdash; and you haven't created
any of those yet!
</p>
<%# No results message %>
<div x-show="searchQuery && !optgroups.some(g => g.filteredOptions && g.filteredOptions.length > 0)" class="px-4 py-8 text-center">
<i class="material-icons text-gray-300 text-4xl mb-2">search_off</i>
<p class="text-sm text-gray-500">No pages found matching "<span x-text="searchQuery"></span>"</p>
</div>
<%# Empty state %>
<div x-show="options.length == 0 && !searchQuery" class="px-4 py-8">
<div class="text-center">
<i class="material-icons text-gray-300 text-4xl mb-2">link_off</i>
<p class="text-sm text-gray-600">
This field only accepts links to <%= linkable_types.to_sentence %> pages
</p>
<p class="text-xs text-gray-500 mt-1">You haven't created any of those yet!</p>
</div>
</div>
</div>
</div>