mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-01-09 22:38:10 -05:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29f4534001 | ||
|
|
ed9324d611 | ||
|
|
438b3c5211 | ||
|
|
efeeb7a796 | ||
|
|
6b1ff0ab21 | ||
|
|
acb925f5a9 | ||
|
|
761293ede7 | ||
|
|
e004e50037 | ||
|
|
44a6c03bc8 | ||
|
|
d794afe405 | ||
|
|
e4ac322227 | ||
|
|
1fc19da19f | ||
|
|
b213068680 | ||
|
|
bf3af207b9 | ||
|
|
e28ba224b5 | ||
|
|
5b7697c5ab | ||
|
|
0701b7d263 | ||
|
|
aac29025fb | ||
|
|
6928f9a312 | ||
|
|
ef2e985d3f | ||
|
|
1df945556d |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -347,3 +347,6 @@ ENV
|
||||
web/package-lock.json
|
||||
.gitignore_backup
|
||||
web/static/*.png
|
||||
|
||||
# Local VSCode project settings
|
||||
.vscode/
|
||||
|
||||
52
Alma.md
52
Alma.md
@@ -1,3 +1,5 @@
|
||||
# SPQA Policy and State for Alma Security
|
||||
|
||||
## Document Purpose
|
||||
|
||||
This document captures the SPQA policy and State for Alma Security, a security startup out of Redwood City, Ca.
|
||||
@@ -22,7 +24,7 @@ The mission of Alma Security is to ensure businesses can continuously authentica
|
||||
|
||||
## Company Goals (G1 means goal 1, G2 is goal 2, etc. Treat each item (goal/kpi/etc) as half as important as the one before it.)
|
||||
|
||||
NOTE: Some goals are things like project rollouts which serve the higher goals. In that case they shouldn't always be considered so much lower priority because one is serving the other.
|
||||
NOTE: Some goals are things like project rollout which serve the higher goals. In that case they shouldn't always be considered so much lower priority because one is serving the other.
|
||||
|
||||
## Company Goals
|
||||
|
||||
@@ -37,7 +39,7 @@ NOTE: Some goals are things like project rollouts which serve the higher goals.
|
||||
|
||||
## Company KPIs
|
||||
|
||||
- K1: Current marketshare percentage
|
||||
- K1: Current market share percentage
|
||||
- K2: Number of active customers
|
||||
- K3: Current churn percentage
|
||||
- K4: Launched_in_Europe (yes/no)
|
||||
@@ -45,22 +47,22 @@ NOTE: Some goals are things like project rollouts which serve the higher goals.
|
||||
|
||||
-----------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
## Security Team Mission
|
||||
## Security Team Mission
|
||||
|
||||
- SM1: Protect Alma Security's customers and intellectual property from security and privacy incidents.
|
||||
|
||||
## Security Team Goals
|
||||
|
||||
- SG1: Secure all customer data -- especially biometric -- from security and privacy incidents.
|
||||
- SG1: Secure all customer data -- especially biometric -- from security and privacy incidents.
|
||||
- SG2: Protect Alma Security's intellectual property from being captured by unauthorized parties.
|
||||
- SG3: Reach a time to detect malicious behavior of less than 4 minutes by January 2025
|
||||
- SG4: Ensure the public trusts our product, because it's an authentication product we can't survive if people don't trust us.
|
||||
- SG5: Reach a time to remediate critical vulnerabilties on crown jewel systems of less than 16 hours by August 2025
|
||||
- SG6: Reach a time to remediate critical vulnerabilties on all systems of less than 3 days by August 2025
|
||||
- SG5: Reach a time to remediate critical vulnerabilities on crown jewel systems of less than 16 hours by August 2025
|
||||
- SG6: Reach a time to remediate critical vulnerabilities on all systems of less than 3 days by August 2025
|
||||
- SG5: Reach a time to remediate critical vulnerabilities on crown jewel systems of less than 16 hours by August 2025
|
||||
- SG6: Reach a time to remediate critical vulnerabilities on all systems of less than 3 days by August 2025
|
||||
- SG7: Complete audit of Apple Passkey integration by February 2025
|
||||
- SG8: Complete remediation of Apple Passkey vulns by February 2025
|
||||
- SG8: Complete remediation of Apple Passkey vulnerabilities by February 2025
|
||||
|
||||
## Security Team KPIs (How we measure the team)
|
||||
|
||||
@@ -72,7 +74,7 @@ NOTE: Some goals are things like project rollouts which serve the higher goals.
|
||||
|
||||
## Risk Register (The things we're most worried about)
|
||||
|
||||
- R1: Our infrastructure security team is understaffed by 50% after 5 key people left
|
||||
- R1: Our infrastructure security team is understaffed by 50% after 5 key people left
|
||||
- R2: We are not currently monitoring our external perimeter for attack surface related vulnerabilities like open ports, listening applications, unknown hosts, unknown subdomains pointing to these things, etc. We only do scans once every couple of months and we don't really have anyone to look at the results
|
||||
- R3: It takes us multiple days to investigate potential malicious behavior on our systems.
|
||||
- R4: We lack a full list of our assets, including externally facing hosts, S3 buckets, etc., which make up our attack surface
|
||||
@@ -92,7 +94,7 @@ So our risk register looks like this:
|
||||
2. Our perimeter is not being monitored for attack surface related vulnerabilities
|
||||
3. It takes us too long to detect and start investigating malicious behavior on our systems
|
||||
4. We do not have a full list of our assets, which makes it difficult to know what we need to protect
|
||||
5. We have a low public trust score due to the events of 2022
|
||||
5. We have a low public trust score due to the events of 2022
|
||||
|
||||
### Strategies
|
||||
|
||||
@@ -136,14 +138,14 @@ $INSERT GRAPHS OF KPI PROGRESS OVER TIME HERE$
|
||||
- alma.amazon-domain.com is our primary S3 bucket that contains everything, but it's not public readable or listable
|
||||
- We have a root account for our AWS account that doesn't yet have 2FA on it, but we're working on fixing that within a few weeks (but it's been open for a few months)
|
||||
- We also use Postgres for all our databases.
|
||||
- Developers have root access to the all kubernetes nodes via SSH on port 45,001 using a shared developer key issued during laptop provisioning.
|
||||
- Developers have root access to the all kubernetes nodes via SSH on port 45,001 using a shared developer key issued during laptop provisioning.
|
||||
- We're a kubernetes shop and do everything through AWS
|
||||
- We're logging most stuff to Cloudtrail and we kind of use guarduty, but we don't have a 24/7 team to monitor alerts and logs. We should add that to our list of challenges next time we update our overarll policy
|
||||
- We're logging most stuff to Cloudtrail and we kind of use Amazon Guarduty, but we don't have a 24/7 team to monitor alerts and logs. We should add that to our list of challenges next time we update our overall policy
|
||||
- We also have a Windows infrastructure because some key personnel came from Microsoft. The DC is hosted in our head office which is in Redwood City, and anyone who works in that office (most of the 300 employees) uses that to log in when they start work. The domain is ALMA.
|
||||
- There's a domain-joined fileserver running Windows 2012 that most people use to upload new ideas and plans for new products. It uses Windows authentication from the domain.
|
||||
- We use a palo alto firewall with 2fa using windows authenticator tied to SSO.
|
||||
- There's a domain-joined fileserver running Windows 2012 that most people use to upload new ideas and plans for new products. It uses Windows authentication from the domain.
|
||||
- We use a Palo Alto Networks firewall with 2fa using windows authenticator tied to SSO.
|
||||
- The name of the AI system doing all this context creation using SPQA is Alma, which is also the name of the company.
|
||||
- We use Workday for HR stuff. Slack for realtime communications. Outlook 365 as a service. Sentinel One on the workstations and laptops. Servers in AWS are mostly Amazon Linux 2 with a few Ubuntu boxes that are a few years old.
|
||||
- We use Workday for HR stuff. Slack for realtime communications. Outlook 365 as a service. Sentinel One on the workstations and laptops. Servers in AWS are mostly Amazon Linux 2 with a few Ubuntu boxes that are a few years old.
|
||||
- We also primarily use Postgres for all of our systems.
|
||||
|
||||
## Team
|
||||
@@ -160,19 +162,19 @@ PROJECT NAME | PROJECT DESCRIPTION | PROJECT PRIORITY | PROJECT MEMBERS | START
|
||||
|
||||
WAF Install | Install a WAF in front of our main web app | Critical | Nadia Khan | 2024-01-01 - Ongoing | In Progress | $112K one-time, $9K/month
|
||||
|
||||
Multi-Factor Authentication (MFA) Rollout | Implement MFA across all internal and external systems | Critical | Chris Magaan | 2024-01-15 | 2024-05-01 | Planned | $80K one-time, $5K/month
|
||||
Multi-Factor Authentication (MFA) Rollout | Implement MFA across all internal and external systems | Critical | Chris Magann | 2024-01-15 | 2024-05-01 | Planned | $80K one-time, $5K/month
|
||||
|
||||
Procure and Implement ASM | Implement continuous monitoring for attack surface vulnerabilities | High | Tigan Wang | 2024-02-15 | 2024-06-15 | Not Started | $75K one-time, $6K/month
|
||||
Procure and Implement ASM | Implement continuous monitoring for attack surface vulnerabilities | High | Tigan Wang | 2024-02-15 | 2024-06-15 | Not Started | $75K one-time, $6K/month
|
||||
|
||||
Data Encryption Upgrade | Upgrade encryption protocols for all sensitive data | Medium | Nadia Khan | 2024-04-01 | 2024-08-01 | Planned | $95K one-time
|
||||
Data Encryption Upgrade | Upgrade encryption protocols for all sensitive data | Medium | Nadia Khan | 2024-04-01 | 2024-08-01 | Planned | $95K one-time
|
||||
|
||||
Incident Response Enhancement | Develop and implement a 24/7 incident response team | High | Nadia Khan | 2024-03-01 | 2024-07-01 | In Progress | $150K one-time, $10K/month
|
||||
Incident Response Enhancement | Develop and implement a 24/7 incident response team | High | Nadia Khan | 2024-03-01 | 2024-07-01 | In Progress | $150K one-time, $10K/month
|
||||
|
||||
Cloud Security Optimization | Optimize AWS cloud security configurations and practices | Medium | Tigan Wang | 2024-02-01 | 2024-06-01 | In Progress | $100K one-time, $8K/month
|
||||
Cloud Security Optimization | Optimize AWS cloud security configurations and practices | Medium | Tigan Wang | 2024-02-01 | 2024-06-01 | In Progress | $100K one-time, $8K/month
|
||||
|
||||
S3 Bucket Security | Review and secure all S3 buckets to prevent data breaches | High | Chris Magaan | 2024-01-10 | 2024-04-10 | In Progress | $70K one-time, $5K/month
|
||||
S3 Bucket Security | Review and secure all S3 buckets to prevent data breaches | High | Chris Magann | 2024-01-10 | 2024-04-10 | In Progress | $70K one-time, $5K/month
|
||||
|
||||
SQL Injection Mitigation | Implement measures to eliminate SQL injection vulnerabilities | High | Tigan Wang | 2024-01-20 | 2024-05-20 | Not Started | $60K one-time
|
||||
SQL Injection Mitigation | Implement measures to eliminate SQL injection vulnerabilities | High | Tigan Wang | 2024-01-20 | 2024-05-20 | Not Started | $60K one-time
|
||||
|
||||
## SECURITY POSTURE (To be referenced for compliance questions and security questionnaires)
|
||||
|
||||
@@ -286,7 +288,9 @@ First draft of the incident response plan created, but not tested.
|
||||
June 2019
|
||||
|
||||
Enforced MFA for Google Workspace admin accounts; standard user
|
||||
|
||||
## CURRENT STATE (KPIs, Metrics, Project Activity Updates, etc.)
|
||||
|
||||
- October 2022: Current time to detect malicious behavior is 81 hours
|
||||
- October 2022: Current time to start investigating malicious behavior is 82 hours
|
||||
- October 2022: Current time to remediate critical vulnerabilities on crown jewel systems is 21 days
|
||||
@@ -308,7 +312,7 @@ Enforced MFA for Google Workspace admin accounts; standard user
|
||||
- January 2024: Current time to start investigating malicious behavior is 14 hours
|
||||
- January 2024: Current time to remediate critical vulnerabilities on crown jewel systems is 8 days
|
||||
- January 2024: Current time to remediate critical vulnerabilities on all systems is 12 days
|
||||
- March 2024: We're now remediating crits on crown jewels in less than 6 days
|
||||
- April 2024: We're now remediating all criticals within 11 days
|
||||
- July 2024: Criticals are now being fixed in 9 days
|
||||
- March 2024: We're now remediating critical vulnerabilities on crown jewels in less than 6 days
|
||||
- April 2024: We're now remediating all critical vulnerabilities within 11 days
|
||||
- July 2024: critical vulnerabilities are now being fixed in 9 days
|
||||
- On August 5 we got remediation of critical vulnerabilities down to 7 days
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,8 +58,18 @@ Step 5: Create Aliases for Patterns
|
||||
Add the following to your .zshrc or .bashrc file to create shorter commands:
|
||||
|
||||
```bash
|
||||
# Define the base directory for Obsidian notes,
|
||||
obsidian_base="/path/to/obsidian"
|
||||
|
||||
# The following three lines of code are path examples, replace with your actual path.
|
||||
|
||||
# Add fabric to PATH
|
||||
export PATH="/Users/USERNAME/Documents/fabric:$PATH"
|
||||
|
||||
# Define the base directory for Obsidian notes
|
||||
obsidian_base="/Users/USERNAME/Documents/fabric/web/myfiles/Fabric_obsidian"
|
||||
|
||||
# Define the patterns directory
|
||||
patterns_dir="/Users/USERNAME/Documents/fabric/patterns"
|
||||
|
||||
|
||||
# Loop through all files in the ~/.config/fabric/patterns directory
|
||||
|
||||
@@ -118,6 +128,8 @@ npm run dev
|
||||
If you get an ** ERROR **.
|
||||
It would be much appreciated that you copy /paste your error in your favorite LLM before opening a ticket, 90% of the time your llm will point you to the solution.
|
||||
|
||||
Also if you modify patterns, descriptions or tags in Pattern_Descriptions/pattern_descriptions.json, make sure to copy the file over in web/static/data/pattern_descriptions.json
|
||||
|
||||
_____ ______ ______
|
||||
|
||||
OPTIONAL: Create Start/Stop Scripts
|
||||
|
||||
30
cli/flags.go
30
cli/flags.go
@@ -9,9 +9,6 @@ import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/danielmiessler/fabric/common"
|
||||
"github.com/jessevdk/go-flags"
|
||||
@@ -155,13 +152,15 @@ func Init() (ret *Flags, err error) {
|
||||
}
|
||||
|
||||
// Handle stdin and messages
|
||||
// Handle stdin and messages
|
||||
info, _ := os.Stdin.Stat()
|
||||
pipedToStdin := (info.Mode() & os.ModeCharDevice) == 0
|
||||
|
||||
// Append positional arguments to the message (custom message)
|
||||
if len(args) > 0 {
|
||||
ret.Message = AppendMessage(ret.Message, args[len(args)-1])
|
||||
}
|
||||
|
||||
pipedToStdin := !term.IsTerminal(int(os.Stdin.Fd()))
|
||||
if pipedToStdin {
|
||||
var pipedMessage string
|
||||
if pipedMessage, err = readStdin(); err != nil {
|
||||
@@ -234,24 +233,17 @@ func loadYAMLConfig(configPath string) (*Flags, error) {
|
||||
func readStdin() (ret string, err error) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
var sb strings.Builder
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
line, readErr := reader.ReadString('\n')
|
||||
if readErr != nil {
|
||||
if errors.Is(readErr, io.EOF) {
|
||||
sb.WriteString(strings.TrimSpace(line)) // Ensure last line is added
|
||||
}
|
||||
close(done)
|
||||
return
|
||||
for {
|
||||
if line, readErr := reader.ReadString('\n'); readErr != nil {
|
||||
if errors.Is(readErr, io.EOF) {
|
||||
sb.WriteString(line)
|
||||
break
|
||||
}
|
||||
err = fmt.Errorf("error reading piped message from stdin: %w", readErr)
|
||||
return
|
||||
} else {
|
||||
sb.WriteString(line)
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
ret = sb.String()
|
||||
return
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/danielmiessler/fabric/plugins/ai/dryrun"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/gemini"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/groq"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/litellm"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/lmstudio"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/mistral"
|
||||
"github.com/danielmiessler/fabric/plugins/ai/ollama"
|
||||
@@ -53,11 +54,22 @@ func NewPluginRegistry(db *fsdb.Db) (ret *PluginRegistry, err error) {
|
||||
|
||||
ret.Defaults = tools.NeeDefaults(ret.GetModels)
|
||||
|
||||
ret.VendorsAll.AddVendors(openai.NewClient(), ollama.NewClient(), azure.NewClient(), groq.NewClient(),
|
||||
ret.VendorsAll.AddVendors(
|
||||
openai.NewClient(),
|
||||
ollama.NewClient(),
|
||||
azure.NewClient(),
|
||||
groq.NewClient(),
|
||||
gemini.NewClient(),
|
||||
//gemini_openai.NewClient(),
|
||||
anthropic.NewClient(), siliconcloud.NewClient(),
|
||||
openrouter.NewClient(), lmstudio.NewClient(), mistral.NewClient(), deepseek.NewClient(), exolab.NewClient())
|
||||
anthropic.NewClient(),
|
||||
siliconcloud.NewClient(),
|
||||
openrouter.NewClient(),
|
||||
lmstudio.NewClient(),
|
||||
mistral.NewClient(),
|
||||
deepseek.NewClient(),
|
||||
exolab.NewClient(),
|
||||
litellm.NewClient(),
|
||||
)
|
||||
_ = ret.Configure()
|
||||
|
||||
return
|
||||
|
||||
@@ -1 +1 @@
|
||||
"1.4.151"
|
||||
"1.4.156"
|
||||
|
||||
@@ -207,4 +207,4 @@ Brief one-line summary from AI analysis of what each pattern does.
|
||||
203. **write_nuclei_template_rule**: Generates Nuclei YAML templates for detecting vulnerabilities using HTTP requests, matchers, extractors, and dynamic data extraction.
|
||||
204. **write_pull-request**: Drafts detailed pull request descriptions, explaining changes, providing reasoning, and identifying potential bugs from the git diff command output.
|
||||
205. **write_semgrep_rule**: Creates accurate and working Semgrep rules based on input, following syntax guidelines and specific language considerations.
|
||||
206. **youtubbe_summary**: Create concise, timestamped Youtube video summaries that highlight key points.
|
||||
206. **youtube_summary**: Create concise, timestamped Youtube video summaries that highlight key points.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
15
plugins/ai/litellm/litellm.go
Normal file
15
plugins/ai/litellm/litellm.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package litellm
|
||||
|
||||
import (
|
||||
"github.com/danielmiessler/fabric/plugins/ai/openai"
|
||||
)
|
||||
|
||||
func NewClient() (ret *Client) {
|
||||
ret = &Client{}
|
||||
ret.Client = openai.NewClientCompatible("LiteLLM", "http://localhost:4000", nil)
|
||||
return
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
*openai.Client
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
package main
|
||||
|
||||
var version = "v1.4.151"
|
||||
var version = "v1.4.156"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
@@ -92,8 +157,9 @@
|
||||
<!-- Chat Area -->
|
||||
<div class="flex-1 flex flex-col min-h-0">
|
||||
<!-- Chat History -->
|
||||
<div class="flex-1 min-h-0 bg-background/5 rounded-lg overflow-hidden">
|
||||
<div class="flex-1 min-h-0 bg-background/5 rounded-lg overflow-y-scroll scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent hover:scrollbar-thumb-white/20">
|
||||
<ChatMessages />
|
||||
<div class="h-32"></div> <!-- Spacer div to ensure scrolling works properly -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -102,8 +168,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 {
|
||||
|
||||
@@ -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] 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>
|
||||
@@ -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