mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-10 06:48:04 -05:00
Implement Pattern Tile search functionality
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,9 @@ This Cummulative PR adds several Web UI and functionality improvements to make p
|
||||
## 🎥 Demo Video
|
||||
https://youtu.be/bhwtWXoMASA
|
||||
|
||||
updated to include latest enhancement: Pattern tiles search (last min.)
|
||||
https://youtu.be/fcVitd4Kb98
|
||||
|
||||
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
import { onMount } from 'svelte';
|
||||
import Modal from '$lib/components/ui/modal/Modal.svelte';
|
||||
import PatternList from '$lib/components/patterns/PatternList.svelte';
|
||||
import PatternTilesModal from '$lib/components/ui/modal/PatternTilesModal.svelte';
|
||||
import HelpModal from '$lib/components/ui/help/HelpModal.svelte';
|
||||
import { selectedPatternName } from '$lib/store/pattern-store';
|
||||
|
||||
let isMenuOpen = false;
|
||||
let showPatternModal = false;
|
||||
let showPatternTilesModal = false;
|
||||
let showHelpModal = false;
|
||||
|
||||
function goToGithub() {
|
||||
@@ -70,15 +72,33 @@
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button name="pattern-description"
|
||||
on:click={() => showPatternModal = true}
|
||||
class="inline-flex h-9 items-center justify-center rounded-full border bg-background px-3 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground gap-2"
|
||||
aria-label="Pattern Description"
|
||||
>
|
||||
<FileText class="h-4 w-4" />
|
||||
<span>Pattern Description</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Pattern Buttons Group -->
|
||||
<div class="flex items-center gap-3 mr-4">
|
||||
<!-- Pattern Tiles Button -->
|
||||
<button name="pattern-tiles"
|
||||
on:click={() => showPatternTilesModal = true}
|
||||
class="inline-flex h-10 items-center justify-center rounded-full border bg-background px-4 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground gap-2"
|
||||
aria-label="Pattern Tiles"
|
||||
>
|
||||
<FileText class="h-4 w-4" />
|
||||
<span>Pattern Tiles</span>
|
||||
</button>
|
||||
|
||||
<!-- Or text -->
|
||||
<span class="text-sm text-foreground/60 mx-1">or</span>
|
||||
|
||||
<!-- Pattern List Button -->
|
||||
<button name="pattern-list"
|
||||
on:click={() => showPatternModal = true}
|
||||
class="inline-flex h-10 items-center justify-center rounded-full border bg-background px-4 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground gap-2"
|
||||
aria-label="Pattern List"
|
||||
>
|
||||
<FileText class="h-4 w-4" />
|
||||
<span>Pattern List</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<button name="github"
|
||||
on:click={goToGithub}
|
||||
@@ -166,3 +186,16 @@
|
||||
on:close={() => showHelpModal = false}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
show={showPatternTilesModal}
|
||||
on:close={() => showPatternTilesModal = false}
|
||||
>
|
||||
<PatternTilesModal
|
||||
on:close={() => showPatternTilesModal = false}
|
||||
on:select={(e) => {
|
||||
selectedPatternName.set(e.detail);
|
||||
showPatternTilesModal = false;
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -1,52 +1,19 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount, createEventDispatcher } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import type { Pattern } from '$lib/interfaces/pattern-interface';
|
||||
import { favorites } from '$lib/store/favorites-store';
|
||||
import { patterns, patternAPI, systemPrompt, selectedPatternName } from '$lib/store/pattern-store';
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import TagFilterPanel from './TagFilterPanel.svelte';
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import TagFilterPanel from '$lib/components/patterns/TagFilterPanel.svelte';
|
||||
let tagFilterRef: TagFilterPanel;
|
||||
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
select: string;
|
||||
tagsChanged: string[]; // Add this line
|
||||
}>();
|
||||
|
||||
|
||||
let patternsContainer: HTMLDivElement;
|
||||
let sortBy: 'alphabetical' | 'favorites' = 'alphabetical';
|
||||
let searchText = ""; // For pattern filtering
|
||||
let selectedTags: string[] = [];
|
||||
|
||||
// First filter patterns by both text and tags
|
||||
// First filter patterns by both text and tags
|
||||
$: filteredPatterns = $patterns
|
||||
.filter((p: Pattern) =>
|
||||
p.Name.toLowerCase().includes(searchText.toLowerCase())
|
||||
)
|
||||
.filter((p: Pattern) =>
|
||||
selectedTags.length === 0 ||
|
||||
(p.tags && selectedTags.every(tag => p.tags.includes(tag)))
|
||||
);
|
||||
|
||||
// Then sort the filtered patterns
|
||||
$: sortedPatterns = sortBy === 'alphabetical'
|
||||
? [...filteredPatterns].sort((a: Pattern, b: Pattern) => a.Name.localeCompare(b.Name))
|
||||
: [
|
||||
...filteredPatterns.filter((p: Pattern) => $favorites.includes(p.Name)).sort((a: Pattern, b: Pattern) => a.Name.localeCompare(b.Name)),
|
||||
...filteredPatterns.filter((p: Pattern) => !$favorites.includes(p.Name)).sort((a: Pattern, b: Pattern) => a.Name.localeCompare(b.Name))
|
||||
];
|
||||
|
||||
|
||||
function handleTagFilter(event: CustomEvent<string[]>) {
|
||||
selectedTags = event.detail;
|
||||
}
|
||||
|
||||
|
||||
let selectedTags: string[] = [];
|
||||
import { cn } from "$lib/utils/utils";
|
||||
import type { Pattern } from '$lib/interfaces/pattern-interface';
|
||||
import { patterns, patternAPI, selectedPatternName } from '$lib/store/pattern-store';
|
||||
import { favorites } from '$lib/store/favorites-store';
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let searchQuery = '';
|
||||
let showOnlyFavorites = false;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await patternAPI.loadPatterns();
|
||||
@@ -54,156 +21,231 @@ function handleTagFilter(event: CustomEvent<string[]>) {
|
||||
console.error('Error loading patterns:', error);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleFavorite(name: string) {
|
||||
favorites.toggleFavorite(name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function toggleFavorite(patternName: string) {
|
||||
favorites.toggleFavorite(patternName);
|
||||
}
|
||||
|
||||
function selectPattern(patternName: string) {
|
||||
patternAPI.selectPattern(patternName);
|
||||
dispatch('select', patternName);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
function handleTagFilter(event: CustomEvent<string[]>) {
|
||||
selectedTags = event.detail;
|
||||
}
|
||||
|
||||
function toggleFavoritesFilter() {
|
||||
showOnlyFavorites = !showOnlyFavorites;
|
||||
}
|
||||
|
||||
// Apply filtering based on search query, favorites filter, and tag selection
|
||||
$: filteredPatterns = $patterns
|
||||
.filter(p => {
|
||||
// Apply favorites filter if enabled
|
||||
if (showOnlyFavorites && !$favorites.includes(p.Name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply tag filter if any tags are selected
|
||||
if (selectedTags.length > 0) {
|
||||
if (!p.tags || !selectedTags.every(tag => p.tags.includes(tag))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply search filter if query exists
|
||||
if (searchQuery.trim()) {
|
||||
return (
|
||||
p.Name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.Description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(p.tags && p.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())))
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="bg-primary-800 rounded-lg flex flex-col h-[85vh] w-[600px] shadow-lg relative">
|
||||
|
||||
<div class="flex flex-col border-b border-primary-700/30">
|
||||
<div class="flex justify-between items-center p-4">
|
||||
<b class="text-lg text-muted-foreground font-bold">Pattern Descriptions</b>
|
||||
<button
|
||||
on:click={() => dispatch('close')}
|
||||
class="text-muted-foreground hover:text-primary-300 transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<b class="text-lg text-muted-foreground font-bold">Pattern Descriptions</b>
|
||||
<button
|
||||
on:click={closeModal}
|
||||
class="text-muted-foreground hover:text-primary-300 transition-colors"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pb-4 flex items-center justify-between">
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={sortBy}
|
||||
value="alphabetical"
|
||||
class="radio"
|
||||
>
|
||||
Alphabetical
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={sortBy}
|
||||
value="favorites"
|
||||
class="radio"
|
||||
>
|
||||
Favorites First
|
||||
</label>
|
||||
</div>
|
||||
<div class="w-64 mr-4">
|
||||
<Input
|
||||
bind:value={searchText}
|
||||
placeholder="Search patterns..."
|
||||
class="text-emerald-900"
|
||||
/>
|
||||
<div class="flex-1 flex items-center">
|
||||
<div class="flex-1 mr-2">
|
||||
<Input
|
||||
bind:value={searchQuery}
|
||||
placeholder="Search patterns..."
|
||||
class="text-emerald-900"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Favorites button similar to PatternTilesModal -->
|
||||
<button
|
||||
on:click={toggleFavoritesFilter}
|
||||
class={cn(
|
||||
"px-3 py-1.5 rounded-md text-sm font-medium transition-all",
|
||||
showOnlyFavorites
|
||||
? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30"
|
||||
: "bg-primary-700/30 text-primary-300 border border-primary-600/20 hover:bg-primary-700/50"
|
||||
)}
|
||||
>
|
||||
<span class="mr-1">{showOnlyFavorites ? "★" : "☆"}</span>
|
||||
Favorites
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New tag display section -->
|
||||
<!-- Selected tags display -->
|
||||
<div class="px-4 pb-2">
|
||||
<div class="text-sm text-white/70 bg-primary-700/30 rounded-md p-2 flex justify-between items-center">
|
||||
<div>Tags: {selectedTags.length ? selectedTags.join(', ') : 'none'}</div>
|
||||
<button
|
||||
class="px-2 py-1 text-xs text-white/70 bg-primary-600/30 rounded hover:bg-primary-600/50 transition-colors"
|
||||
on:click={() => {
|
||||
selectedTags = [];
|
||||
dispatch('tagsChanged', selectedTags);
|
||||
}}
|
||||
>
|
||||
reset
|
||||
</button>
|
||||
<div class="flex flex-wrap gap-1 items-center">
|
||||
<span class="mr-1">Tags:</span>
|
||||
{#if selectedTags.length > 0}
|
||||
{#each selectedTags as tag}
|
||||
<div class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary-600/40 text-primary-200 border border-primary-500/30">
|
||||
{tag}
|
||||
<button
|
||||
class="ml-1 text-xs text-primary-300 hover:text-primary-100"
|
||||
on:click={() => {
|
||||
selectedTags = selectedTags.filter(t => t !== tag);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-primary-300/50">none</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="px-2 py-1 text-xs text-white/70 bg-primary-600/30 rounded hover:bg-primary-600/50 transition-colors"
|
||||
on:click={() => {
|
||||
selectedTags = [];
|
||||
if (tagFilterRef && typeof tagFilterRef.reset === 'function') {
|
||||
tagFilterRef.reset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="patterns-container p-4 flex-1 overflow-y-auto">
|
||||
{#if filteredPatterns.length === 0}
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<p class="text-primary-300">
|
||||
{showOnlyFavorites
|
||||
? "No favorite patterns found. Add some favorites first!"
|
||||
: "No patterns found matching your search."}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="patterns-list space-y-2">
|
||||
{#each filteredPatterns as pattern}
|
||||
<div class="pattern-item bg-primary/10 rounded-lg p-3">
|
||||
<div class="flex justify-between items-start gap-4 mb-2">
|
||||
<button
|
||||
class="text-xl font-bold text-primary-300 hover:text-primary-100 cursor-pointer transition-colors text-left w-full"
|
||||
on:click={() => selectPattern(pattern.Name)}
|
||||
>
|
||||
{pattern.Name}
|
||||
</button>
|
||||
<button
|
||||
class="text-muted-foreground hover:text-primary-300 transition-colors"
|
||||
on:click|stopPropagation={() => toggleFavorite(pattern.Name)}
|
||||
>
|
||||
{#if $favorites.includes(pattern.Name)}
|
||||
<span class="text-yellow-400">★</span>
|
||||
{:else}
|
||||
<span class="text-primary-400 hover:text-yellow-300">☆</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground break-words leading-relaxed">{pattern.Description}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<TagFilterPanel
|
||||
patterns={$patterns}
|
||||
on:tagsChanged={handleTagFilter}
|
||||
bind:this={tagFilterRef}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="patterns-container p-4 flex-1 overflow-y-auto"
|
||||
bind:this={patternsContainer}
|
||||
>
|
||||
<div class="patterns-list space-y-2">
|
||||
{#each sortedPatterns as pattern}
|
||||
<div class="pattern-item bg-primary/10 rounded-lg p-3">
|
||||
<div class="flex justify-between items-start gap-4 mb-2">
|
||||
<button
|
||||
class="text-xl font-bold text-primary-300 hover:text-primary-100 cursor-pointer transition-colors text-left w-full"
|
||||
on:click={() => {
|
||||
console.log('Selecting pattern:', pattern.Name);
|
||||
patternAPI.selectPattern(pattern.Name);
|
||||
searchText = "";
|
||||
tagFilterRef.reset();
|
||||
dispatch('select', pattern.Name);
|
||||
dispatch('close');
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.currentTarget.click();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pattern.Name}
|
||||
</button>
|
||||
<button
|
||||
class="text-muted-foreground hover:text-primary-300 transition-colors"
|
||||
on:click={() => toggleFavorite(pattern.Name)}
|
||||
>
|
||||
{#if $favorites.includes(pattern.Name)}
|
||||
★
|
||||
{:else}
|
||||
☆
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground break-words leading-relaxed">{pattern.Description}</p>
|
||||
</div>
|
||||
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
patterns={$patterns}
|
||||
on:tagsChanged={handleTagFilter}
|
||||
bind:this={tagFilterRef}
|
||||
hideToggleButton={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.patterns-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
-ms-overflow-style: thin;
|
||||
}
|
||||
/* Custom scrollbar styling */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.patterns-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(31, 41, 55, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pattern-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(156, 163, 175, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pattern-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(156, 163, 175, 0.5);
|
||||
}
|
||||
|
||||
/* h3.pattern-name {
|
||||
word-break: break-all;
|
||||
hyphens: auto;
|
||||
overflow-wrap: break-word;
|
||||
} */
|
||||
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.3) rgba(31, 41, 55, 0.2);
|
||||
}
|
||||
|
||||
.patterns-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
-ms-overflow-style: thin;
|
||||
}
|
||||
|
||||
.patterns-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pattern-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.pattern-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,17 +7,10 @@
|
||||
}>();
|
||||
|
||||
export let patterns: Pattern[];
|
||||
export let hideToggleButton = false; // New prop to hide the toggle button when used in modal
|
||||
let selectedTags: string[] = [];
|
||||
let isExpanded = false;
|
||||
|
||||
// Add console log to see what tags we're getting
|
||||
$: console.log('Available tags:', Array.from(new Set(patterns.flatMap(p => p.tags))));
|
||||
|
||||
// Add these debug logs
|
||||
$: console.log('Patterns received:', patterns);
|
||||
$: console.log('Tags extracted:', patterns.map(p => p.tags));
|
||||
$: console.log('Panel expanded:', isExpanded);
|
||||
|
||||
function toggleTag(tag: string) {
|
||||
selectedTags = selectedTags.includes(tag)
|
||||
? selectedTags.filter(t => t !== tag)
|
||||
@@ -36,15 +29,16 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="tag-panel {isExpanded ? 'expanded' : ''}" style="z-index: 50">
|
||||
<div class="tag-panel {isExpanded ? 'expanded' : ''} {hideToggleButton ? 'embedded' : ''}" style="z-index: 50">
|
||||
{#if !hideToggleButton}
|
||||
<div class="panel-header">
|
||||
<button class="close-btn" on:click={togglePanel}>
|
||||
{isExpanded ? 'Close Filter Tags ◀' : 'Open Filter Tags ▶'}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="panel-content">
|
||||
<div class="panel-content {hideToggleButton ? 'always-visible' : ''}">
|
||||
<div class="reset-container">
|
||||
<button
|
||||
class="reset-btn"
|
||||
@@ -56,7 +50,7 @@
|
||||
Reset All Tags
|
||||
</button>
|
||||
</div>
|
||||
{#each Array.from(new Set(patterns.flatMap(p => p.tags))).sort() as tag}
|
||||
{#each Array.from(new Set(patterns.flatMap(p => p.tags || []))).sort() as tag}
|
||||
<button
|
||||
class="tag-brick {selectedTags.includes(tag) ? 'selected' : ''}"
|
||||
on:click={() => toggleTag(tag)}
|
||||
@@ -67,6 +61,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
/* Default positioning for standalone mode */
|
||||
.tag-panel {
|
||||
position: fixed; /* Change to fixed positioning */
|
||||
left: calc(50% + 300px); /* Position starts after modal's right edge */
|
||||
@@ -76,19 +71,37 @@
|
||||
transition: left 0.3s ease;
|
||||
}
|
||||
|
||||
/* When embedded in another component, use relative positioning */
|
||||
.tag-panel.embedded {
|
||||
position: relative;
|
||||
left: auto;
|
||||
top: auto;
|
||||
transform: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tag-panel.expanded {
|
||||
left: calc(50% + 360px); /* Final position just to the right of modal */
|
||||
}
|
||||
|
||||
|
||||
.panel-content {
|
||||
.panel-content {
|
||||
display: none;
|
||||
padding: 12px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
/* Adjust max-height when embedded */
|
||||
.embedded .panel-content {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
/* When used in modal, always show content */
|
||||
.panel-content.always-visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tag-brick {
|
||||
@@ -102,7 +115,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.reset-container {
|
||||
width: 100%;
|
||||
padding-bottom: 8px;
|
||||
@@ -124,29 +136,9 @@
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.expanded .panel-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
/* .toggle-btn {
|
||||
position: absolute;
|
||||
left: -30px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 8px;
|
||||
background: var(--primary-800);
|
||||
border-radius: 4px 0 0 4px;
|
||||
cursor: pointer;
|
||||
.expanded .panel-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 0.9rem;
|
||||
box-shadow: -2px 0 5px rgba(0,0,0,0.2);
|
||||
} */
|
||||
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 8px;
|
||||
@@ -178,17 +170,11 @@
|
||||
margin-left: -50px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.close-btn:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tag-brick.selected {
|
||||
background: var(--primary-300);
|
||||
}
|
||||
.tag-brick.selected {
|
||||
background: var(--primary-300);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -9,15 +9,23 @@
|
||||
export let show = false;
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions a11y-no-static-element-interactions -->
|
||||
{#if show}
|
||||
<div
|
||||
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 mt-2"
|
||||
on:click={() => dispatch('close')}
|
||||
on:keydown={(e) => e.key === 'Escape' && dispatch('close')}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Modal dialog"
|
||||
tabindex="-1"
|
||||
transition:fade={{ duration: 200 }}
|
||||
>
|
||||
<div
|
||||
class="relative"
|
||||
on:click|stopPropagation
|
||||
role="document"
|
||||
aria-label="Modal content"
|
||||
transition:scale={{ duration: 200 }}
|
||||
>
|
||||
<slot />
|
||||
|
||||
333
web/src/lib/components/ui/modal/PatternTilesModal.svelte
Normal file
333
web/src/lib/components/ui/modal/PatternTilesModal.svelte
Normal file
@@ -0,0 +1,333 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import TagFilterPanel from '$lib/components/patterns/TagFilterPanel.svelte';
|
||||
let tagFilterRef: TagFilterPanel;
|
||||
let selectedTags: string[] = [];
|
||||
import { cn } from "$lib/utils/utils";
|
||||
import type { Pattern } from '$lib/interfaces/pattern-interface';
|
||||
import { patterns, patternAPI, selectedPatternName } from '$lib/store/pattern-store';
|
||||
import { favorites } from '$lib/store/favorites-store';
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let isTagPanelOpen = false;
|
||||
let searchQuery = '';
|
||||
let showOnlyFavorites = false;
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await patternAPI.loadPatterns();
|
||||
} catch (error) {
|
||||
console.error('Error loading patterns:', error);
|
||||
}
|
||||
});
|
||||
|
||||
function toggleFavorite(patternName: string) {
|
||||
favorites.toggleFavorite(patternName);
|
||||
}
|
||||
|
||||
function selectPattern(patternName: string) {
|
||||
patternAPI.selectPattern(patternName);
|
||||
dispatch('select', patternName);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
function toggleTagPanel() {
|
||||
isTagPanelOpen = !isTagPanelOpen;
|
||||
}
|
||||
|
||||
function handleTagFilter(event: CustomEvent<string[]>) {
|
||||
selectedTags = event.detail;
|
||||
}
|
||||
|
||||
function toggleFavoritesFilter() {
|
||||
showOnlyFavorites = !showOnlyFavorites;
|
||||
}
|
||||
|
||||
// Apply filtering based on search query, favorites filter, and tag selection
|
||||
$: filteredPatterns = $patterns
|
||||
.filter(p => {
|
||||
// Apply favorites filter if enabled
|
||||
if (showOnlyFavorites && !$favorites.includes(p.Name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply tag filter if any tags are selected
|
||||
if (selectedTags.length > 0) {
|
||||
if (!p.tags || !selectedTags.every(tag => p.tags.includes(tag))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply search filter if query exists
|
||||
if (searchQuery.trim()) {
|
||||
return (
|
||||
p.Name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.Description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
(p.tags && p.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())))
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Main container with flexible layout -->
|
||||
<div class="flex h-[85vh]">
|
||||
<!-- Modal container with responsive positioning -->
|
||||
<div class={cn(
|
||||
"flex flex-col bg-primary-800 rounded-lg shadow-xl transition-all duration-300",
|
||||
isTagPanelOpen
|
||||
? "w-[75vw]"
|
||||
: "w-full max-w-[95vw] mx-auto"
|
||||
)}>
|
||||
<!-- Header with grid layout -->
|
||||
<div class="grid grid-cols-[auto_auto_1fr_auto] items-center p-4 border-b border-primary-700/30 sticky top-0 bg-primary-800 z-10">
|
||||
<!-- Left column: Title -->
|
||||
<h2 class="text-xl font-semibold text-primary-200 mr-4">Pattern Library</h2>
|
||||
|
||||
<!-- Second column: Search -->
|
||||
<div class="mr-4">
|
||||
<Input
|
||||
bind:value={searchQuery}
|
||||
placeholder="Search patterns..."
|
||||
class="w-full min-w-[300px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Third column: Favorites button -->
|
||||
<div class="flex items-center">
|
||||
<button
|
||||
on:click={toggleFavoritesFilter}
|
||||
class={cn(
|
||||
"px-3 py-1.5 rounded-md text-sm font-medium transition-all",
|
||||
showOnlyFavorites
|
||||
? "bg-yellow-500/20 text-yellow-300 border border-yellow-500/30"
|
||||
: "bg-primary-700/30 text-primary-300 border border-primary-600/20 hover:bg-primary-700/50"
|
||||
)}
|
||||
>
|
||||
<span class="mr-1">{showOnlyFavorites ? "★" : "☆"}</span>
|
||||
Favorites
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Fourth column: Other controls -->
|
||||
<div class="flex items-center gap-3 justify-end">
|
||||
<!-- Single tag panel toggle button -->
|
||||
<button
|
||||
on:click={toggleTagPanel}
|
||||
class={cn(
|
||||
"px-3 py-1.5 rounded-md text-sm font-medium transition-all",
|
||||
isTagPanelOpen
|
||||
? "bg-blue-500/20 text-blue-300 border border-blue-500/30"
|
||||
: "bg-primary-700/30 text-primary-300 border border-primary-600/20 hover:bg-primary-700/50"
|
||||
)}
|
||||
>
|
||||
{isTagPanelOpen ? "Close Filter Tags ◀" : "Open Filter Tags ▶"}
|
||||
</button>
|
||||
|
||||
<!-- Close modal button -->
|
||||
<button
|
||||
on:click={closeModal}
|
||||
class="px-2 py-2 rounded-full bg-primary-700/40 text-primary-200 hover:bg-primary-700/60 hover:text-primary-100"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<span class="text-xl font-bold">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected tags display -->
|
||||
{#if selectedTags.length > 0}
|
||||
<div class="px-4 pb-2 pt-2 border-b border-primary-700/30">
|
||||
<div class="text-sm text-white/70 bg-primary-700/30 rounded-md p-2 flex justify-between items-center">
|
||||
<div class="flex flex-wrap gap-1 items-center">
|
||||
<span class="mr-1">Tags:</span>
|
||||
{#each selectedTags as tag}
|
||||
<div class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-primary-600/40 text-primary-200 border border-primary-500/30">
|
||||
{tag}
|
||||
<button
|
||||
class="ml-1 text-xs text-primary-300 hover:text-primary-100"
|
||||
on:click={() => {
|
||||
selectedTags = selectedTags.filter(t => t !== tag);
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
class="px-2 py-1 text-xs text-white/70 bg-primary-600/30 rounded hover:bg-primary-600/50 transition-colors"
|
||||
on:click={() => {
|
||||
selectedTags = [];
|
||||
if (tagFilterRef && typeof tagFilterRef.reset === 'function') {
|
||||
tagFilterRef.reset();
|
||||
}
|
||||
}}
|
||||
>
|
||||
reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Pattern tiles grid with scrolling -->
|
||||
<div class="flex-1 overflow-y-auto p-4 pattern-grid-container">
|
||||
{#if filteredPatterns.length === 0}
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<p class="text-primary-300">
|
||||
{showOnlyFavorites
|
||||
? "No favorite patterns found. Add some favorites first!"
|
||||
: "No patterns found matching your search."}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class={cn(
|
||||
"grid grid-cols-1 sm:grid-cols-2 gap-4",
|
||||
isTagPanelOpen
|
||||
? "md:grid-cols-2 lg:grid-cols-3"
|
||||
: "md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
|
||||
)}>
|
||||
{#each filteredPatterns as pattern}
|
||||
<button
|
||||
class="text-left border-2 border-primary-600/40 rounded-lg shadow-md hover:shadow-lg p-4 flex flex-col h-58 bg-primary-700/30 hover:bg-primary-700/50 transition-all transform hover:-translate-y-1 duration-200"
|
||||
on:click={() => selectPattern(pattern.Name)}
|
||||
>
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<h3 class="pattern-name font-bold text-base text-primary-200 leading-tight break-all overflow-hidden pr-2 w-[85%]">{pattern.Name}</h3>
|
||||
<button
|
||||
on:click|stopPropagation={() => toggleFavorite(pattern.Name)}
|
||||
class="focus:outline-none ml-1 mt-0.5"
|
||||
aria-label={$favorites.includes(pattern.Name) ? 'Remove from favorites' : 'Add to favorites'}
|
||||
>
|
||||
{#if $favorites.includes(pattern.Name)}
|
||||
<span class="text-yellow-400 text-xl">★</span>
|
||||
{:else}
|
||||
<span class="text-primary-400 text-xl hover:text-yellow-300">☆</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Pattern description with scrolling if needed -->
|
||||
<div class="flex-grow overflow-y-auto mb-1 pr-1 custom-scrollbar">
|
||||
<p class="text-sm text-primary-300/90 leading-relaxed">{pattern.Description}</p>
|
||||
</div>
|
||||
|
||||
<!-- Tags section -->
|
||||
{#if pattern.tags && pattern.tags.length > 0}
|
||||
<div class="flex flex-wrap gap-1 mt-2">
|
||||
{#each pattern.tags as tag}
|
||||
<span class="inline-flex items-center px-1 py-0.25 rounded-full text-[8px] font-medium bg-primary-600/40 text-primary-200 border border-primary-500/30">
|
||||
{tag}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tag filter panel - positioned on the right when open -->
|
||||
{#if isTagPanelOpen}
|
||||
<div class="tag-panel-container">
|
||||
<div class="tag-panel-header">
|
||||
<button class="tag-panel-close" on:click={toggleTagPanel}>
|
||||
<span class="text-lg">×</span>
|
||||
</button>
|
||||
<h3 class="text-sm font-medium text-primary-200">Filter Tags</h3>
|
||||
</div>
|
||||
<div class="tag-panel-content">
|
||||
<TagFilterPanel
|
||||
patterns={$patterns}
|
||||
on:tagsChanged={handleTagFilter}
|
||||
bind:this={tagFilterRef}
|
||||
hideToggleButton={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Custom scrollbar styling remains the same */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(31, 41, 55, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: rgba(156, 163, 175, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(156, 163, 175, 0.5);
|
||||
}
|
||||
|
||||
/* Add this to your <style> section */
|
||||
h3.pattern-name {
|
||||
word-break: break-all; /* Force breaks anywhere if needed */
|
||||
hyphens: auto; /* Enable hyphenation */
|
||||
overflow-wrap: break-word; /* Fallback */
|
||||
}
|
||||
|
||||
.custom-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.3) rgba(31, 41, 55, 0.2);
|
||||
}
|
||||
|
||||
.pattern-grid-container {
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(156, 163, 175, 0.3) rgba(31, 41, 55, 0.2);
|
||||
}
|
||||
|
||||
/* Tag panel styling */
|
||||
.tag-panel-container {
|
||||
width: 20vw;
|
||||
height: 100%;
|
||||
background-color: #1e293b; /* Use a solid color instead of var */
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||
z-index: 20;
|
||||
box-shadow: -2px 0 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.tag-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.tag-panel-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tag-panel-close:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.tag-panel-content {
|
||||
height: calc(100% - 49px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -14,6 +14,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions a11y-mouse-events-have-key-events -->
|
||||
<div class="tooltip-container">
|
||||
<div
|
||||
class="tooltip-trigger"
|
||||
@@ -21,6 +22,8 @@
|
||||
on:mouseleave={hideTooltip}
|
||||
on:focusin={showTooltip}
|
||||
on:focusout={hideTooltip}
|
||||
role="tooltip"
|
||||
aria-label="Tooltip trigger"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -33,9 +36,11 @@
|
||||
class:bottom="{position === 'bottom'}"
|
||||
class:left="{position === 'left'}"
|
||||
class:right="{position === 'right'}"
|
||||
role="tooltip"
|
||||
aria-label={text}
|
||||
>
|
||||
{text}
|
||||
<div class="tooltip-arrow" />
|
||||
<div class="tooltip-arrow" role="presentation" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user