Compare commits

...

8 Commits

Author SHA1 Message Date
github-actions[bot]
44a6c03bc8 Update version to v1.4.155 and commit 2025-03-09 09:01:41 +00:00
Eugen Eisler
d794afe405 Merge pull request #1350 from jmd1010/pattern-search-implementation
Implement Pattern Tile search functionality
2025-03-09 10:00:28 +01:00
github-actions[bot]
e4ac322227 Update version to v1.4.154 and commit 2025-03-09 08:56:50 +00:00
Eugen Eisler
1fc19da19f Merge pull request #1349 from ksylvan/03-08-extra-version-declaration-removed
Fix: v1.4.153 does not compile because of extra version declaration
2025-03-09 09:55:37 +01:00
jmd1010
b213068680 Implement column resize functionnality 2025-03-08 17:34:49 -05:00
jmd1010
bf3af207b9 Implement Pattern Tile search functionality 2025-03-08 12:56:55 -05:00
Kayvan Sylvan
e28ba224b5 fix: update Azure client API version access path in tests 2025-03-08 09:52:20 -08:00
Kayvan Sylvan
5b7697c5ab chore: remove unnecessary version variable from main.go 2025-03-08 09:29:20 -08:00
14 changed files with 1343 additions and 879 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,6 @@ import (
"github.com/danielmiessler/fabric/cli"
)
var version string
func main() {
err := cli.Cli(version)
if err != nil && !flags.WroteHelp(err) {

View File

@@ -1 +1 @@
"1.4.153"
"1.4.155"

View File

@@ -48,8 +48,8 @@ func TestClientConfigure(t *testing.T) {
t.Errorf("Expected ApiClient to be initialized, got nil")
}
if client.ApiClient.Config.APIVersion != "2021-01-01" {
t.Errorf("Expected API version to be '2021-01-01', got %s", client.ApiClient.Config.APIVersion)
if client.ApiVersion.Value != "2021-01-01" {
t.Errorf("Expected API version to be '2021-01-01', got %s", client.ApiVersion.Value)
}
}

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.153"
var version = "v1.4.155"

View File

@@ -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

View File

@@ -14,18 +14,74 @@
import { featureFlags } from "$lib/config/features";
import { getDrawerStore } from '@skeletonlabs/skeleton';
import { systemPrompt, selectedPatternName } from "$lib/store/pattern-store";
import { onMount } from "svelte";
const drawerStore = getDrawerStore();
function openDrawer() {
drawerStore.open({});
}
// Column width state (percentage values)
let leftColumnWidth = 50;
let rightColumnWidth = 50;
let isDragging = false;
// Handle resize functionality
function startResize(e: MouseEvent | KeyboardEvent) {
isDragging = true;
e.preventDefault();
// Add event listeners for drag and release
window.addEventListener('mousemove', handleResize);
window.addEventListener('mouseup', stopResize);
}
// Handle keyboard events for accessibility
function handleKeyDown(e: KeyboardEvent) {
// Only respond to Enter or Space key
if (e.key === 'Enter' || e.key === ' ') {
startResize(e);
}
}
function handleResize(e: MouseEvent) {
if (!isDragging) return;
// Get container dimensions
const container = document.querySelector('.chat-container');
if (!container) return;
const containerRect = container.getBoundingClientRect();
const containerWidth = containerRect.width;
// Calculate percentage based on mouse position
const percentage = ((e.clientX - containerRect.left) / containerWidth) * 100;
// Apply constraints (left: 25-50%, right: 50-75%)
leftColumnWidth = Math.min(Math.max(percentage, 25), 50);
rightColumnWidth = 100 - leftColumnWidth;
}
function stopResize() {
isDragging = false;
window.removeEventListener('mousemove', handleResize);
window.removeEventListener('mouseup', stopResize);
}
// Clean up event listeners when component is destroyed
onMount(() => {
return () => {
window.removeEventListener('mousemove', handleResize);
window.removeEventListener('mouseup', stopResize);
};
});
$: showObsidian = $featureFlags.enableObsidianIntegration;
</script>
<div class="flex gap-0 p-2 w-full h-screen">
<div class="chat-container flex gap-0 p-2 w-full h-screen">
<!-- Left Column -->
<aside class="w-[50%] flex flex-col gap-2 pr-2">
<aside class="flex flex-col gap-2 pr-2" style="width: {leftColumnWidth}%">
<!-- Dropdowns Group -->
<div class="bg-background/5 p-2 rounded-lg">
<div class="rounded-lg bg-background/10">
@@ -56,8 +112,17 @@
</div>
</aside>
<!-- Resize Handle -->
<button
class="resize-handle"
on:mousedown={startResize}
on:keydown={handleKeyDown}
type="button"
aria-label="Resize chat panels"
></button>
<!-- Right Column -->
<div class="flex flex-col w-[50%] gap-2">
<div class="flex flex-col gap-2" style="width: {rightColumnWidth}%">
<!-- Header with Obsidian Settings -->
<div class="flex items-center justify-between px-2 py-1">
<div class="flex items-center gap-2">
@@ -102,8 +167,41 @@
<NoteDrawer />
<style>
.loading-message {
animation: flash 1.5s ease-in-out infinite;
.resize-handle {
width: 6px;
margin: 0 -3px;
height: 100%;
cursor: col-resize;
position: relative;
z-index: 10;
transition: background-color 0.2s;
}
.resize-handle::after {
content: "";
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
height: 100%;
width: 2px;
background-color: rgba(255, 255, 255, 0.1);
transition: background-color 0.2s, width 0.2s;
}
.resize-handle:hover::after,
.resize-handle:focus::after {
background-color: rgba(255, 255, 255, 0.3);
width: 4px;
}
.resize-handle:focus {
outline: none;
}
.resize-handle:focus-visible::after {
background-color: rgba(255, 255, 255, 0.5);
width: 4px;
}
@keyframes flash {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 />

View 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] text-emerald-900"
/>
</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>

View File

@@ -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