From b59ad76827e8d9119190f401915d0540f9b7b459 Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 3 Apr 2026 11:18:36 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=9A=80=20release:=20v1.3.0=20-=20Unif?= =?UTF-8?q?ied=20Control=20Bar,=202026=20Models,=20&=20Independent=20Grid?= =?UTF-8?q?=20Expansion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 14 ++ package-lock.json | 141 +++++++++++++- package.json | 6 +- src/App.tsx | 263 +++++++------------------- src/components/PromptForm.tsx | 21 ++- src/components/SearchBar.tsx | 7 +- src/components/StorageUsage.tsx | 31 ++-- src/constants.ts | 33 ++++ src/index.css | 314 +++++++++++++++++++++++++++++--- 9 files changed, 587 insertions(+), 243 deletions(-) create mode 100644 src/constants.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 324a4c2..53decf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.0] - 2026-04-03 +### Added +- **2026 LLM Models**: Support for 25+ next-gen models including GPT-5.4, Claude 4.6, Gemini 3.1, and Llama 4. +- **Unified Control Bar**: Compact, horizontal layout combining search, model filters, and date range. +- **View Mode Toggle**: Clear, premium buttons for switching between Grid and List views. +- **Global Constants**: Unified `constants.ts` for managing LLM models across the app. +- **Independent Card Height**: Improved grid layout where expanding one card doesn't stretch others in the same row. +- **Hover Micro-interactions**: Added subtle "lift-up" translate and shadow effect on cards. + +### Fixed +- **Variable Detection**: Enhanced regex support for hints and placeholders `{{name:hint}}`. +- **Search Bar Contrast**: Fixed dark mode visibility and icon alignment issues. +- **Date Filter UX**: Improved contrast and focus states for consistent theme-aware filtering. + ## [1.2.8] - 2026-04-03 ### Changed ๐ŸŽจ diff --git a/package-lock.json b/package-lock.json index 94bc565..a7c235b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,20 @@ { "name": "prompt-library", - "version": "1.2.5", + "version": "1.2.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "prompt-library", - "version": "1.2.5", + "version": "1.2.8", "license": "MIT", "dependencies": { + "@types/react-datepicker": "^6.2.0", "@types/react-window": "^1.8.8", "flexsearch": "^0.7.43", "lucide-react": "^1.7.0", "react": "^19.2.4", + "react-datepicker": "^9.1.0", "react-dom": "^19.2.4", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.1", @@ -1701,6 +1703,59 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -2374,6 +2429,17 @@ "csstype": "^3.2.2" } }, + "node_modules/@types/react-datepicker": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-6.2.0.tgz", + "integrity": "sha512-+JtO4Fm97WLkJTH8j8/v3Ldh7JCNRwjMYjRaKh4KHH0M3jJoXtwiD3JBCsdlg3tsFIw9eQSqyAPeVDN2H2oM9Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.2", + "@types/react": "*", + "date-fns": "^3.3.1" + } + }, "node_modules/@types/react-dom": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", @@ -3169,6 +3235,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3331,6 +3406,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7127,6 +7212,52 @@ "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-9.1.0.tgz", + "integrity": "sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.15", + "clsx": "^2.1.1", + "date-fns": "^4.1.0" + }, + "peerDependencies": { + "date-fns-tz": "^3.0.0", + "react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "date-fns-tz": { + "optional": true + } + } + }, + "node_modules/react-datepicker/node_modules/@floating-ui/react": { + "version": "0.27.19", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.19.tgz", + "integrity": "sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.8", + "@floating-ui/utils": "^0.2.11", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/react-datepicker/node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/react-dom": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", @@ -8054,6 +8185,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", diff --git a/package.json b/package.json index 4591c39..bce9787 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "prompt-library", "private": false, "description": "A modern, sleek web application for managing AI prompts. Built with React, TypeScript, and Vite.", - "version": "1.2.8", + "version": "1.3.0", "author": "Ivan Andrei (NaviAndrei)", "license": "MIT", "type": "module", @@ -16,16 +16,18 @@ "deploy": "gh-pages -d dist" }, "dependencies": { + "@types/react-datepicker": "^6.2.0", "@types/react-window": "^1.8.8", + "flexsearch": "^0.7.43", "lucide-react": "^1.7.0", "react": "^19.2.4", + "react-datepicker": "^9.1.0", "react-dom": "^19.2.4", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.1", "react-window": "^1.8.10", "remark-gfm": "^4.0.1", "sonner": "^2.0.7", - "flexsearch": "^0.7.43", "vite-plugin-pwa": "^1.2.0" }, "devDependencies": { diff --git a/src/App.tsx b/src/App.tsx index ed468df..a2d2f1f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -14,48 +14,39 @@ import { Toaster, toast } from 'sonner'; import { LayoutGrid, List, Moon, Sun } from 'lucide-react'; import { usePromptFilters } from './hooks/usePromptFilters'; import { useIndexedDB } from './hooks/useIndexedDB'; +import { LLM_MODELS } from './constants'; function App() { - // Large Datasets use IndexedDB for unlimited capacity const [prompts, setPrompts] = useIndexedDB('prompts', []); const [workspaces, setWorkspaces] = useIndexedDB('workspaces', []); const [promptHistory, setPromptHistory] = useIndexedDB('history', [], 'prompt-history'); - // Lightweight UI settings continue to use localStorage for instant initialization const [currentWorkspaceId, setCurrentWorkspaceId] = useLocalStorage('current-workspace', null); const [searchQuery, setSearchQuery] = useState(''); const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light'); const [viewMode, setViewMode] = useLocalStorage<'list' | 'grid'>('view-mode', 'grid'); - // UI State for editing and filtering (ephemeral) const [editingPrompt, setEditingPrompt] = useState(null); const [selectedTag, setSelectedTag] = useState(null); - // Advanced search filters const [selectedModel, setSelectedModel] = useState(null); const [dateRange, setDateRange] = useState<{ start: Date | null, end: Date | null }>({ start: null, end: null }); - // Apply global theme to the HTML tag useEffect(() => { document.documentElement.setAttribute('data-theme', theme); }, [theme]); - // Compute all unique tags for the sidebar const allTags = useMemo(() => { const counts: Record = {}; - prompts.forEach(p => p.tags.forEach(t => { - counts[t] = (counts[t] || 0) + 1; - })); + prompts.forEach(p => p.tags.forEach(t => { counts[t] = (counts[t] || 0) + 1; })); return Object.entries(counts).sort((a, b) => b[1] - a[1]); }, [prompts]); - // Compute all unique AI models for filtering const allModels = useMemo(() => { const models = new Set(prompts.map(p => p.model)); return Array.from(models).sort(); }, [prompts]); - // Encapsulated data filtering logic using custom hook const filteredPrompts = usePromptFilters(prompts, { searchQuery, selectedTag, @@ -64,47 +55,24 @@ function App() { dateRange }); - // Handler for Create or Update save const handleSavePrompt = (promptData: Omit) => { const now = new Date().toISOString(); - if (editingPrompt) { - // Capture a snapshot of the OLD version - use its own updatedAt timestamp - // This ensures stable sort order and avoids duplicate keys in history - const snapshot: PromptVersion = { - promptId: editingPrompt.id, - body: editingPrompt.body, - savedAt: editingPrompt.updatedAt ?? now, - }; + const snapshot: PromptVersion = { promptId: editingPrompt.id, body: editingPrompt.body, savedAt: editingPrompt.updatedAt ?? now }; setPromptHistory(prev => [snapshot, ...prev]); - - // Update existing prompt - const updatedPrompts = prompts.map(p => - p.id === editingPrompt.id - ? { ...p, ...promptData, updatedAt: now } - : p - ); + const updatedPrompts = prompts.map(p => p.id === editingPrompt.id ? { ...p, ...promptData, updatedAt: now } : p); setPrompts(updatedPrompts); setEditingPrompt(null); toast.success('Prompt updated successfully!'); } else { - // Create new prompt (priority: form selection > global filter) - const newPrompt: Prompt = { - ...promptData, - id: crypto.randomUUID(), - createdAt: now, - updatedAt: now, - workspaceId: promptData.workspaceId ?? currentWorkspaceId ?? undefined, - }; + const newPrompt: Prompt = { ...promptData, id: crypto.randomUUID(), createdAt: now, updatedAt: now, workspaceId: promptData.workspaceId ?? currentWorkspaceId ?? undefined }; setPrompts([newPrompt, ...prompts]); toast.success('Prompt created successfully!'); } }; - // Handler to process JSON import const handleImportPrompts = (imported: Prompt[]) => { setPrompts(imported); - window.localStorage.setItem('prompts', JSON.stringify(imported)); toast.success(`Successfully imported ${imported.length} prompts!`); }; @@ -113,21 +81,15 @@ function App() { window.scrollTo({ top: 0, behavior: 'smooth' }); }; - const handleClearForm = () => { - setEditingPrompt(null); - }; + const handleClearForm = () => { setEditingPrompt(null); }; const handleDeletePrompt = (id: string) => { setPrompts(prompts.filter(p => p.id !== id)); - // Clear delete prompt version history setPromptHistory(prev => prev.filter(v => v.promptId !== id)); - if (editingPrompt?.id === id) { - setEditingPrompt(null); - } + if (editingPrompt?.id === id) setEditingPrompt(null); toast.info('Prompt deleted.'); }; - // Workspace handlers const handleAddWorkspace = (ws: Workspace) => { setWorkspaces(prev => [...prev, ws]); toast.success(`Workspace "${ws.name}" created!`); @@ -135,34 +97,20 @@ function App() { const handleDeleteWorkspace = (id: string) => { setWorkspaces(prev => prev.filter(ws => ws.id !== id)); - // Detach deleted workspace from all associated prompts for consistency - setPrompts(prev => prev.map(p => - p.workspaceId === id ? { ...p, workspaceId: undefined } : p - )); - // Reset to "All" if current workspace is deleted + setPrompts(prev => prev.map(p => p.workspaceId === id ? { ...p, workspaceId: undefined } : p)); if (currentWorkspaceId === id) setCurrentWorkspaceId(null); toast.info('Workspace deleted.'); }; - // Move a prompt from one workspace to another (Drag-and-Drop) const handleMovePromptToWorkspace = (promptId: string, workspaceId: string | null) => { const targetPrompt = prompts.find(p => p.id === promptId); if (!targetPrompt) return; - - // Do nothing if the prompt is already in the target workspace - if (targetPrompt.workspaceId === (workspaceId || undefined)) return; - - const updatedPrompts = prompts.map(p => - p.id === promptId ? { ...p, workspaceId: workspaceId || undefined, updatedAt: new Date().toISOString() } : p - ); - + const updatedPrompts = prompts.map(p => p.id === promptId ? { ...p, workspaceId: workspaceId || undefined, updatedAt: new Date().toISOString() } : p); setPrompts(updatedPrompts); - const wsName = workspaceId ? workspaces.find(w => w.id === workspaceId)?.name : 'All Prompts'; toast.success(`Prompt "${targetPrompt.title}" moved to ${wsName}`); }; - // Get versions for a specific prompt const getVersionsForPrompt = (promptId: string): PromptVersion[] => { return promptHistory.filter(v => v.promptId === promptId); }; @@ -170,18 +118,13 @@ function App() { return (
- -
-
+
+

Prompt Library

-

Manage, filter, and synchronize your AI prompts directly from your browser.

+

Manage, filter, and synchronize your AI prompts.

- @@ -190,146 +133,76 @@ function App() {
-
-
- + {/* ๐Ÿ› ๏ธ Modern Unified Control Bar */} +
+
+
+ +
+
+ + +
-
- - + +
+
+ + +
+ +
+ + setDateRange(prev => ({ ...prev, start: e.target.value ? new Date(e.target.value) : null }))}/> +
+ +
+ + setDateRange(prev => ({ ...prev, end: e.target.value ? new Date(e.target.value) : null }))}/> +
+ + {(selectedModel || dateRange.start || dateRange.end) && ( + + )}
- {/* Advanced Filter Bar */} -
- - -
- - setDateRange(prev => ({ ...prev, start: e.target.value ? new Date(e.target.value) : null }))} - /> - - setDateRange(prev => ({ ...prev, end: e.target.value ? new Date(e.target.value) : null }))} - /> -
- - {(selectedModel || dateRange.start || dateRange.end) && ( - - )} -
- - t[0])} - onClear={handleClearForm} - workspaces={workspaces} - currentWorkspaceId={currentWorkspaceId} - /> - + t[0])} onClear={handleClearForm} workspaces={workspaces} currentWorkspaceId={currentWorkspaceId}/> +
-
- Total: {prompts.length} | Found: {filteredPrompts.length} - {(() => { - // Optimized lookup: find the workspace once per render - const currentWs = currentWorkspaceId ? workspaces.find(w => w.id === currentWorkspaceId) : null; - if (!currentWs) return null; - return ( - - {currentWs.icon} {currentWs.name} - - ); - })()} -
+
Total: {prompts.length} | Found: {filteredPrompts.length}
- +
diff --git a/src/components/PromptForm.tsx b/src/components/PromptForm.tsx index 6cb0baa..12cb4db 100644 --- a/src/components/PromptForm.tsx +++ b/src/components/PromptForm.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import type { Prompt, Workspace } from '../types'; +import { LLM_MODELS } from '../constants'; interface PromptFormProps { onSave: (prompt: Omit) => void; @@ -16,7 +17,7 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear, works const [title, setTitle] = useState(editingPrompt?.title || ''); const [body, setBody] = useState(editingPrompt?.body || ''); const [tagsInput, setTagsInput] = useState(editingPrompt ? editingPrompt.tags.join(', ') : ''); - const [model, setModel] = useState(editingPrompt?.model || 'GPT-4o'); + const [model, setModel] = useState(editingPrompt?.model || LLM_MODELS[0]); // Selected workspace in the form (defaults to current or prompt's workspace) const [selectedWorkspaceId, setSelectedWorkspaceId] = useState( editingPrompt?.workspaceId ?? currentWorkspaceId ?? '' @@ -26,7 +27,7 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear, works setTitle(''); setBody(''); setTagsInput(''); - setModel('GPT-4o'); + setModel(LLM_MODELS[0]); setSelectedWorkspaceId(currentWorkspaceId ?? ''); }; @@ -50,7 +51,7 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear, works title: title.trim(), body: body.trim(), tags: tagsArray, - model: model.trim() || 'Generic', + model: model || LLM_MODELS[0], workspaceId: selectedWorkspaceId || undefined, }); @@ -108,12 +109,18 @@ export function PromptForm({ onSave, editingPrompt, existingTags, onClear, works
- setModel(e.target.value)} - placeholder="e.g., Claude 3.5 Sonnet" - /> + className="model-select" + > + {LLM_MODELS.map(m => ( + + ))} + {!LLM_MODELS.includes(model) && model && ( + + )} +
diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 4973c44..d707fa7 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -1,21 +1,22 @@ - +import { Search } from 'lucide-react'; interface SearchBarProps { searchQuery: string; onSearchChange: (query: string) => void; } -// Search bar component -// Receives the query and update function as props to be a controlled component +// Search bar component with icon and premium UX export function SearchBar({ searchQuery, onSearchChange }: SearchBarProps) { return (
+ onSearchChange(e.target.value)} + aria-label="Search prompts" />
); diff --git a/src/components/StorageUsage.tsx b/src/components/StorageUsage.tsx index 6e7c53c..c6d6f86 100644 --- a/src/components/StorageUsage.tsx +++ b/src/components/StorageUsage.tsx @@ -70,20 +70,24 @@ export function StorageUsage({ promptsCount }: StorageUsageProps) { Storage Usage - {/* Online/Offline Badge */}
-
+
{isOnline ? 'ONLINE' : 'OFFLINE'}
@@ -99,12 +103,15 @@ export function StorageUsage({ promptsCount }: StorageUsageProps) { overflow: 'hidden', border: '1px solid var(--border-color)' }}> -
80 ? 'var(--danger-color)' : percentage > 50 ? '#f59e0b' : 'var(--primary-color)', - transition: 'width 0.3s ease' - }} /> +
80 ? 'var(--danger-color)' : percentage > 50 ? '#f59e0b' : 'var(--primary-color)', + transition: 'width 0.8s cubic-bezier(0.4, 0, 0.2, 1)' + }} + />
diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..fbf81f4 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,33 @@ +/** + * State-of-the-art 2026 LLM models list (Global Constants) + */ +export const LLM_MODELS = [ + /* --- Flagship Models (2026) --- */ + 'GPT-5.4 Thinking', + 'GPT-5.4 mini', + 'Claude 4.6 Opus', + 'Claude 4.6 Sonnet', + 'Gemini 3.1 Pro', + 'Gemini 3.1 Flash', + 'Llama 4 Maverick', + 'Llama 4 Scout', + + /* --- High Consistency Models --- */ + 'GPT-4o', + 'GPT-4o mini', + 'GPT-o1', + 'Claude 3.5 Sonnet', + 'Gemini 1.5 Pro', + + /* --- Specialized & Open Source --- */ + 'DeepSeek-V4 (New)', + 'DeepSeek-V3.2', + 'Qwen 3.6 Plus', + 'Qwen 3.6 Turbo', + 'Mistral Large 2', + 'Mixtral 8x22B', + 'Grok-3', + 'Perplexity Sonar (2026)', + 'Llama 3.1 405B', + 'Flux.1 (Image Gen Context)' +]; diff --git a/src/index.css b/src/index.css index 569b7dd..4222b9b 100644 --- a/src/index.css +++ b/src/index.css @@ -108,31 +108,222 @@ body { /* Search Bar */ .search-container { - margin-bottom: 0; + position: relative; + width: 100%; } -@media (max-width: 600px) { - .search-row { - flex-direction: column; - align-items: stretch !important; - } +.search-icon { + position: absolute; + left: 1.25rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + pointer-events: none; + transition: all 0.3s ease; + z-index: 2; +} + +/* ๐Ÿ› ๏ธ Modern Unified Control Bar - RECONSTRUCTED */ +.control-bar { + display: flex; + flex-direction: column; + gap: 1.25rem; + padding: 1.25rem; + margin-bottom: 2rem; + background: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: 16px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); +} + +.control-row { + display: flex; + align-items: center; + gap: 1rem; + width: 100%; +} + +.main-row { + gap: 1.5rem; +} + +.search-container-wrapper { + flex: 1; +} + +.view-toggle-group { + display: flex; + background: var(--bg-color); + padding: 4px; + border-radius: 10px; + border: 1px solid var(--border-color); +} + +.view-btn { + background: transparent; + border: none; + border-radius: 7px; + padding: 8px 14px; + color: var(--text-muted); + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.view-btn.active { + background: var(--surface-color); + color: var(--primary-color); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1); + border: 1px solid rgba(59, 130, 246, 0.2); +} + +/* ๐Ÿงช Filter Row - Balanced & Horizontal */ +.filter-row { + border-top: 1px solid var(--border-color); + padding-top: 1rem; + justify-content: flex-start; + flex-wrap: nowrap; /* Keep it on one line for desktop */ +} + +.filter-item { + display: flex; + align-items: center; + gap: 0.6rem; +} + +.minimal-select, .minimal-date { + background: var(--bg-color); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 8px 12px; + color: var(--text-dark); + font-size: 0.85rem; + outline: none; + transition: all 0.2s; +} + +.minimal-select:hover, .minimal-date:hover { + border-color: var(--primary-color); +} + +.label-text { + font-weight: 700; + font-size: 0.7rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1px; +} + +/* ๐Ÿƒ Prompt Cards - Unified Height & Alignment */ +.prompt-list.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1.5rem; + align-items: start; /* CRITICAL: Allows cards to have independent heights! */ +} + +.prompt-card { + display: flex; + flex-direction: column; + height: auto; /* Changed from 100% to auto */ + padding: 1.5rem; + border-radius: 16px; + background: var(--surface-color); + border: 1px solid var(--border-color); + transition: transform 0.2s, box-shadow 0.2s; +} + +.prompt-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1); +} + +.prompt-header { + margin-bottom: 1rem; +} + +.prompt-header h4 { + font-size: 1.1rem; + line-height: 1.4; + margin-bottom: 0.5rem; + height: 3rem; /* Fixed height for title to align cards */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.prompt-actions { + display: flex; + gap: 0.4rem; + margin-bottom: 1rem; +} + +.token-badge { + background: rgba(239, 68, 68, 0.1); + color: #ef4444; + font-weight: 700; + border-radius: 6px; + padding: 2px 8px; + font-size: 0.75rem; } +.model-badge { + background: rgba(59, 130, 246, 0.1); + color: var(--primary-color); + border-radius: 99px; + padding: 4px 12px; + font-size: 0.7rem; + font-weight: 600; + margin-bottom: 0.75rem; + display: inline-block; +} + +.prompt-body-preview pre { + background: var(--bg-color); + padding: 1rem; + border-radius: 8px; + font-family: 'Inter', sans-serif; + font-size: 0.9rem; + line-height: 1.6; +} + +/* Search bar and Inputs Consistency */ .search-input { width: 100%; - padding: 1rem 1.25rem; + padding: 0.85rem 1.25rem 0.85rem 3.5rem !important; /* Increased padding to 3.5rem and forced it to ensure visibility */ font-size: 1rem; border: 1px solid var(--border-color); border-radius: var(--radius-md); outline: none; - transition: border-color 0.2s, box-shadow 0.2s; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); background-color: var(--surface-color); box-shadow: var(--shadow-sm); + color: var(--text-dark); +} + +.search-input::placeholder { + color: var(--text-muted); + opacity: 0.6; +} + +[data-theme="dark"] .search-input { + color: #f8fafc; /* High contrast bright text for dark mode */ +} + +[data-theme="dark"] .search-input::placeholder { + color: #94a3b8; /* Brighter placeholder for dark mode */ + opacity: 0.8; } .search-input:focus { border-color: var(--primary-color); - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); + box-shadow: 0 8px 30px rgba(59, 130, 246, 0.15); + transform: translateY(-2px); +} + +.search-input:focus + .search-icon { + color: var(--primary-color); + transform: translateY(-50%) scale(1.1); } /* HTML Form */ @@ -172,7 +363,7 @@ label { font-size: 0.95rem; } -input[type="text"], textarea { +input[type="text"], input[type="date"], textarea { width: 100%; padding: 0.75rem; border: 1px solid var(--border-color); @@ -180,11 +371,66 @@ input[type="text"], textarea { font-family: inherit; font-size: 1rem; outline: none; - transition: border-color 0.2s; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + background-color: var(--surface-color); + color: var(--text-dark); +} + +input[type="date"] { + cursor: pointer; + position: relative; +} + +/* Force dark mode for the native calendar picker on supported browsers */ +[data-theme="dark"] input[type="date"] { + color-scheme: dark; +} + +input[type="date"]::-webkit-calendar-picker-indicator { + cursor: pointer; + border-radius: 4px; + padding: 4px; + transition: transform 0.2s ease; + opacity: 0.6; } -input[type="text"]:focus, textarea:focus { +input[type="date"]:hover::-webkit-calendar-picker-indicator { + transform: scale(1.1); + opacity: 1; + background: rgba(59, 130, 246, 0.1); +} + +input[type="text"]:focus, input[type="date"]:focus, textarea:focus { border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); + transform: translateY(-1px); +} + +/* ๐Ÿ“… Enhanced Filter Inputs */ +.filter-date-input { + background-color: var(--surface-color); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + padding: 0.5rem 0.75rem; + font-size: 0.85rem; + color: var(--text-dark); + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + min-width: 140px; +} + +.filter-date-input:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); + transform: translateY(-1px); +} + +.filter-label { + font-size: 0.8rem; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; } textarea { @@ -301,6 +547,7 @@ button { gap: 1.5rem; max-height: calc(100vh - 4rem); overflow-y: auto; + transition: all 0.3s ease; } /* Custom minimal scrollbar for sidebar */ @@ -318,6 +565,36 @@ button { background-color: var(--text-muted); } +.tags-list li, .workspace-item { + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +.tags-list li:hover, .workspace-item:hover { + transform: translateX(6px); + background: rgba(59, 130, 246, 0.08); +} + +@keyframes storagePulse { + 0% { opacity: 0.8; } + 50% { opacity: 1; filter: brightness(1.1); } + 100% { opacity: 0.8; } +} + +.storage-progress-fill-animated { + animation: storagePulse 3s infinite ease-in-out; +} + +@keyframes onlinePulse { + 0% { transform: scale(1); opacity: 0.7; } + 50% { transform: scale(1.4); opacity: 1; } + 100% { transform: scale(1); opacity: 0.7; } +} + +.status-dot-pulse { + animation: onlinePulse 2s infinite ease-in-out; +} + .sidebar-header { margin-bottom: 1rem; } @@ -767,21 +1044,14 @@ button { } /* Workspace selector in the form */ -.workspace-select { +.workspace-select, .model-select { width: 100%; padding: 0.75rem; + background-color: var(--surface-color); border: 1px solid var(--border-color); border-radius: var(--radius-md); - font-family: inherit; - font-size: 1rem; - background: var(--surface-color); color: var(--text-dark); - outline: none; - cursor: pointer; - transition: border-color 0.2s; -} - -.workspace-select:focus { + font-size: 1rem; border-color: var(--primary-color); } From e51c0f2ec796f90c6a87b952e7121d8d7147bcb2 Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 3 Apr 2026 11:28:38 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8E=A8=20style:=20refined=20CSS=20for?= =?UTF-8?q?matting=20for=20better=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.css | 153 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 118 insertions(+), 35 deletions(-) diff --git a/src/index.css b/src/index.css index 4222b9b..1a83a66 100644 --- a/src/index.css +++ b/src/index.css @@ -1,6 +1,7 @@ /* Modern and Minimal CSS for Prompt Library */ -:root, [data-theme="light"] { +:root, +[data-theme="light"] { /* Light-Premium Color Palette */ --bg-color: #f8fafc; --surface-color: #ffffff; @@ -35,18 +36,27 @@ } body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + Helvetica, + Arial, + sans-serif; line-height: 1.5; color: var(--text-dark); background-color: var(--bg-color); - transition: background-color 0.3s ease, color 0.3s ease; + transition: + background-color 0.3s ease, + color 0.3s ease; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } - /* Base Reset */ * { box-sizing: border-box; @@ -190,7 +200,8 @@ body { gap: 0.6rem; } -.minimal-select, .minimal-date { +.minimal-select, +.minimal-date { background: var(--bg-color); border: 1px solid var(--border-color); border-radius: 8px; @@ -201,7 +212,8 @@ body { transition: all 0.2s; } -.minimal-select:hover, .minimal-date:hover { +.minimal-select:hover, +.minimal-date:hover { border-color: var(--primary-color); } @@ -229,7 +241,9 @@ body { border-radius: 16px; background: var(--surface-color); border: 1px solid var(--border-color); - transition: transform 0.2s, box-shadow 0.2s; + transition: + transform 0.2s, + box-shadow 0.2s; } .prompt-card:hover { @@ -282,11 +296,38 @@ body { background: var(--bg-color); padding: 1rem; border-radius: 8px; - font-family: 'Inter', sans-serif; + font-family: "Inter", sans-serif; font-size: 0.9rem; line-height: 1.6; } +/* ๐Ÿ–‹๏ธ Markdown Content Standard Padding & Font */ +.markdown-body { + font-size: 0.9rem !important; + line-height: 1.6 !important; + color: var(--text-dark); + background: var(--bg-color); + padding: 1rem; + border-radius: 8px; + font-family: "Inter", sans-serif !important; +} + +.markdown-body p, +.markdown-body ul, +.markdown-body ol { + margin-bottom: 1rem; + font-weight: 400 !important; /* Force normal weight to avoid "bold" text issue */ +} + +.markdown-body h1, +.markdown-body h2, +.markdown-body h3 { + margin-top: 1.5rem; + margin-bottom: 1rem; + font-weight: 600; + color: var(--primary-color); +} + /* Search bar and Inputs Consistency */ .search-input { width: 100%; @@ -363,7 +404,9 @@ label { font-size: 0.95rem; } -input[type="text"], input[type="date"], textarea { +input[type="text"], +input[type="date"], +textarea { width: 100%; padding: 0.75rem; border: 1px solid var(--border-color); @@ -400,7 +443,9 @@ input[type="date"]:hover::-webkit-calendar-picker-indicator { background: rgba(59, 130, 246, 0.1); } -input[type="text"]:focus, input[type="date"]:focus, textarea:focus { +input[type="text"]:focus, +input[type="date"]:focus, +textarea:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); transform: translateY(-1px); @@ -565,20 +610,29 @@ button { background-color: var(--text-muted); } -.tags-list li, .workspace-item { +.tags-list li, +.workspace-item { transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); position: relative; } -.tags-list li:hover, .workspace-item:hover { +.tags-list li:hover, +.workspace-item:hover { transform: translateX(6px); background: rgba(59, 130, 246, 0.08); } @keyframes storagePulse { - 0% { opacity: 0.8; } - 50% { opacity: 1; filter: brightness(1.1); } - 100% { opacity: 0.8; } + 0% { + opacity: 0.8; + } + 50% { + opacity: 1; + filter: brightness(1.1); + } + 100% { + opacity: 0.8; + } } .storage-progress-fill-animated { @@ -586,9 +640,18 @@ button { } @keyframes onlinePulse { - 0% { transform: scale(1); opacity: 0.7; } - 50% { transform: scale(1.4); opacity: 1; } - 100% { transform: scale(1); opacity: 0.7; } + 0% { + transform: scale(1); + opacity: 0.7; + } + 50% { + transform: scale(1.4); + opacity: 1; + } + 100% { + transform: scale(1); + opacity: 0.7; + } } .status-dot-pulse { @@ -641,7 +704,9 @@ button { color: white; } -.tag-name { font-weight: 500; } +.tag-name { + font-weight: 500; +} .tag-count { font-size: 0.75rem; background: var(--border-color); @@ -656,8 +721,13 @@ button { } @media (max-width: 768px) { - .app-layout { flex-direction: column; } - .sidebar { width: 100%; position: static; } + .app-layout { + flex-direction: column; + } + .sidebar { + width: 100%; + position: static; + } } /* Prompt List and Elements */ @@ -756,7 +826,7 @@ button { } .prompt-body-preview pre.collapsed { - font-family: 'Consolas', monospace; + font-family: "Consolas", monospace; font-size: 0.9rem; color: var(--text-dark); white-space: pre-wrap; @@ -797,8 +867,11 @@ button { /* Modals */ .modal-overlay { position: fixed; - top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0,0,0,0.5); + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; @@ -864,7 +937,8 @@ button { .markdown-body p:last-child { margin-bottom: 0; } -.markdown-body ul, .markdown-body ol { +.markdown-body ul, +.markdown-body ol { margin-bottom: 1rem; padding-left: 1.5rem; } @@ -878,7 +952,7 @@ button { background: var(--border-color); padding: 0.2rem 0.4rem; border-radius: 4px; - font-family: 'Consolas', monospace; + font-family: "Consolas", monospace; font-size: 0.85em; } .markdown-body > div { @@ -1044,7 +1118,8 @@ button { } /* Workspace selector in the form */ -.workspace-select, .model-select { +.workspace-select, +.model-select { width: 100%; padding: 0.75rem; background-color: var(--surface-color); @@ -1061,7 +1136,11 @@ button { .variable-injector { margin-top: 1rem; padding: 1rem; - background: linear-gradient(135deg, rgba(59, 130, 246, 0.08), rgba(139, 92, 246, 0.08)); + background: linear-gradient( + 135deg, + rgba(59, 130, 246, 0.08), + rgba(139, 92, 246, 0.08) + ); border: 1px solid rgba(59, 130, 246, 0.25); border-radius: var(--radius-md); } @@ -1135,7 +1214,6 @@ button { opacity: 1; } - .variable-inputs-grid { display: flex; flex-direction: column; @@ -1199,7 +1277,7 @@ button { .variable-input-number { max-width: 120px; text-align: center; - font-family: 'Consolas', monospace; + font-family: "Consolas", monospace; font-weight: 600; } @@ -1283,7 +1361,7 @@ button { /* Line-by-line visual diff block */ .diff-view { - font-family: 'Consolas', 'Fira Code', monospace; + font-family: "Consolas", "Fira Code", monospace; font-size: 0.82rem; line-height: 1.6; overflow-x: auto; @@ -1320,11 +1398,16 @@ button { min-width: 12px; } -.diff-line.diff-added .diff-marker { color: #10b981; } -.diff-line.diff-removed .diff-marker { color: #ef4444; } -.diff-line.diff-equal .diff-marker { color: var(--border-color); } +.diff-line.diff-added .diff-marker { + color: #10b981; +} +.diff-line.diff-removed .diff-marker { + color: #ef4444; +} +.diff-line.diff-equal .diff-marker { + color: var(--border-color); +} .diff-text { flex: 1; } - From 1db15776a92913bf54c23c313501a4f795160666 Mon Sep 17 00:00:00 2001 From: NaviAndrei Date: Fri, 3 Apr 2026 16:03:11 +0300 Subject: [PATCH 3/5] feat: finalize sidebar polish and tag picker --- .github/skills/ux-design/SKILL.md | 186 ++++++++ .github/skills/web-ui-design/SKILL.md | 204 ++++++++ CHANGELOG.md | 33 ++ src/App.tsx | 508 +++++++++++++------- src/components/CleanupAssistant.tsx | 261 ++++++---- src/components/PromptForm.tsx | 418 ++++++++++------ src/components/PromptList.tsx | 661 ++++++++++++++------------ src/components/Sidebar.tsx | 78 +-- src/components/StorageUsage.tsx | 270 ++++++----- src/components/TemplateManager.tsx | 451 +++++++++++++----- src/components/WorkspaceManager.tsx | 332 +++++++------ src/index.css | 455 ++++++++++++++---- 12 files changed, 2626 insertions(+), 1231 deletions(-) create mode 100644 .github/skills/ux-design/SKILL.md create mode 100644 .github/skills/web-ui-design/SKILL.md diff --git a/.github/skills/ux-design/SKILL.md b/.github/skills/ux-design/SKILL.md new file mode 100644 index 0000000..9317aae --- /dev/null +++ b/.github/skills/ux-design/SKILL.md @@ -0,0 +1,186 @@ +--- +name: ux-design +description: 'UX design principles, heuristics, and content guidance. Use for UX reviews, microcopy, usability, information architecture, task flows, error prevention, feedback, and outcome-oriented design.' +--- + +# UX Design โ€” v2 (2025โ€“2026 Edition) + +Use this skill when designing or reviewing user experiences, task flows, information architecture, usability, microcopy, feedback states, emotional tone, agentic flows, and user-facing guidance. + +> This skill is optimized to produce human-feeling, non-generic UX โ€” designed against the tide of AI-sameness. +> The 2026 UX priority: Design Deeper to Differentiate. + +## Core Philosophy + +Generic AI-generated UX produces flows that are technically correct but emotionally inert โ€” correct patterns assembled without intent, taste, or empathy. +Great UX in 2026 is defined by three forces: + +1. Human-centered outcomes: every decision traces back to a real user goal, not a product metric. +2. Emotional accessibility: the interface responds to the user's state, not just their input. +3. Intentional craft: at least one UX decision per flow should reflect authorship โ€” a deliberate choice that a template would never make. + +## Core Principles + +- Design around user goals, not feature checklists. +- Speak the user's language โ€” not internal product jargon, not AI-generated filler copy. +- Reduce cognitive load: make the next step obvious without requiring the user to remember previous steps. +- Make system status visible with timely, honest, human-sounding feedback. +- Prevent errors through constraints, safe defaults, and clear confirmations. +- Preserve user control: every flow needs a clear cancel, undo, or exit. +- Favor recognition over recall โ€” show options, labels, and context in view. +- Keep interactions simple, predictable, and consistent โ€” but not sterile. +- Design for emotional accessibility: reduce stress triggers, celebrate small wins, respond empathetically to errors. +- Apply inclusive design as innovation, not compliance โ€” cognitive diversity, multimodal input, and situational limitations are in scope. + +## 2025โ€“2026 UX Shifts (Anti-Generic Layer) + +These are the forces separating craft UX from AI-generated sameness. + +### Emotional Accessibility + +Emotionally accessible design reduces anxiety, friction, and user overwhelm as first-class UX goals โ€” not afterthoughts. + +- Map hesitation moments, stress triggers, and confidence indicators in the user journey. +- Rewrite error states to be supportive, not clinical: "We couldn't save your work โ€” tap Retry and it'll be there." over "Error 500." +- Add quiet modes or reduced-motion settings as a user-controlled preference, not just an OS override. +- Celebrate micro-wins in onboarding and long forms: progress affirmations, gentle completion cues. +- Nielsen Norman Group data: emotionally accessible design improves user satisfaction by 20% and loyalty by up to 15%. + +### Agentic UX + +AI agents acting on behalf of users are a standard pattern in 2026 โ€” 60% of designers expect them to have major impact this year. +Design for human-agent collaboration, not just human-UI interaction: + +- Make agent actions visible and reversible: "Copilot scheduled 3 reports โ€” undo?" +- Show agent confidence level when it matters: low confidence should surface a confirmation step. +- Design clear handoff moments: when the agent defers to the human, the transition must feel intentional and trust-building. +- Never let an agent take a destructive or irreversible action silently. + +### Predictive & Context-Aware Flows + +- Interfaces should surface the next likely action before the user explicitly requests it. +- Adapt content density and navigation based on usage context (task type, time of day, prior behavior). +- Use progressive disclosure as a default strategy for complex flows โ€” show the minimum viable step, reveal more on demand. + +### Ethical & Transparent UX + +- EU regulation and user trust now require consent-first, bias-aware, and transparent data practices. +- Every data collection point must be clearly communicated โ€” no dark patterns, no ambiguous opt-ins. +- AI-generated content in the UI must be disclosed. Users have the right to know. +- Bias-aware: test flows for different user groups; don't design only for the median user. + +### Micro-Interactions as UX Language + +- Every meaningful state change deserves a micro-interaction: save confirmation, toggle feedback, list item addition. +- Micro-interactions must communicate status, not just decorate. +- Use physics-based easing for human feel โ€” spring on appear, ease-out on dismiss. +- 50% of designers are already adding micro-interactions to current work; it is now a baseline expectation. + +## UX Checklist + +Before recommending or changing any UX pattern, verify: + +- The user can understand the page or step purpose within 3 seconds. +- The primary action is visually and contextually dominant. +- Labels, buttons, and helper text are task-oriented and specific. +- The interface shows what is happening right now (loading, saving, processing). +- Empty, loading, success, warning, and error states are all designed โ€” not assumed. +- The flow supports recovery from mistakes without restarting. +- The user never needs to remember hidden context from a previous step. +- The design works on both desktop and mobile, with touch targets โ‰ฅ 44ร—44px. +- The most important content and action appears first. +- The experience avoids unnecessary steps โ€” every screen earns its place. +- Emotional tone is appropriate: does this state feel stressful, punitive, or cold? +- At least one decision in this flow was a deliberate human authorship choice โ€” not a template default. + +## Pattern Guidance + +### Flows and Tasks + +- Break complex tasks into clear, meaningful steps โ€” each step should feel like progress. +- Keep each step reversible wherever technically feasible. +- Surface progress indicators for tasks that take time or have multiple stages. +- Never force a restart because of a single error โ€” preserve user input across validation failures. +- Name steps with user-language outcomes: "Choose your plan" not "Step 2 of 5." +- For agentic flows: design visible checkpoints where the user can review, approve, or cancel agent actions. + +### Microcopy + +- Use the 3 C's: Clarity, Concision, Character. +- Plain language over product jargon. No AI-sounding filler: "Streamline your workflow" means nothing. +- Button labels are task verbs: "Save Draft", "Delete Account", "Run Report" โ€” never "Submit" or "OK." +- Error messages name the problem and give the fix: "Email already in use โ€” sign in or use a different address." +- Empty states guide the next action: "No reports yet โ€” create your first one โ†’" +- Success states confirm with specificity: "Report saved to your dashboard" not "Done!" +- Emotionally high-stakes moments (deletion, payment, account changes) warrant warmer, slower-paced copy. + +### Feedback and States + +- Confirm important changes within 300ms of user action. +- Distinguish success (green/checkmark), warning (amber/triangle), and error (red/!) states visually and semantically. +- Loading states: use skeleton screens for content-heavy areas; spinners only for brief, bounded waits. +- Design empty states as opportunities: include a clear CTA, a short human explanation, and optionally a visual. +- Undoable destructive actions should show an inline undo toast for 5โ€“8 seconds before finalizing. + +### Forms and Inputs + +- Ask only for what is necessary at this step โ€” defer optional fields to later in the flow. +- Group related fields with visible section labels. +- Always-visible labels; never use placeholder-as-label. +- Validate on blur, not on submit โ€” surface errors as they occur, not all at once. +- Smart defaults reduce input burden: pre-fill known data, suggest based on context. +- Every form needs a Cancel path that does not silently discard unsaved work without warning. +- For long or multi-step forms: show a summary before the final submission action. + +### Navigation and Information Architecture + +- Prioritize the most common path โ€” secondary paths should be findable, not equal in prominence. +- Use familiar category labels โ€” creative navigation names add friction. +- Apply progressive disclosure in dense sections: collapse secondary options behind a clear "More" affordance. +- Never bury an essential action (account settings, export, delete) more than 2 levels deep. +- On mobile: bottom navigation bar for app-like products with 3โ€“5 top-level destinations. Hamburger menus are a last resort. + +### Inclusive and Accessible UX + +- Design for cognitive diversity: support users with ADHD, dyslexia, or anxiety through clean layout, short chunked content, and clear signposting. +- Multimodal input is a baseline expectation: flows should work with keyboard, touch, voice, and assistive technology. +- Provide user-controlled accessibility settings where feasible: font size, contrast level, reduced motion, text-to-speech. +- WCAG AA is the floor, not the target. Aim for AA+ on text contrast and focus visibility. +- Test flows with screen readers and keyboard-only navigation before shipping. + +## Common Anti-Patterns + +- Vague labels: "Submit", "Continue", "OK" โ€” use the action the user is actually performing. +- Too many competing actions at the same visual level. +- Hidden or unclear selection state โ€” the user cannot tell what is selected or active. +- Long forms without grouping, progress, or context โ€” users lose orientation. +- Errors that describe the problem but give no path to resolution. +- Flows that assume the user remembers decisions made 3 screens ago. +- Emotionally flat error states that feel punitive rather than helpful. +- Agentic actions that execute silently without a visible confirmation or undo path. +- Progressive disclosure used to hide important actions rather than manage complexity. +- Onboarding flows that front-load every feature before the user has context to understand them. +- Consent dialogs designed to confuse rather than inform (dark patterns). +- Interfaces that only work for the median user โ€” no accommodation for different abilities, contexts, or mental models. + +## Output Expectations + +When using this skill, every output should include: + +- A recommended UX direction with specific reasoning. +- Microcopy improvements โ€” before and after examples where applicable. +- Emotional tone assessment: is this state calm, stressful, or cold? How to correct it. +- Feedback state coverage: loading, empty, success, warning, error. +- Accessibility and recovery path considerations. +- Agentic UX notes if the flow involves AI acting on the user's behalf. +- Risks and tradeoffs for each proposed approach. +- One explicitly called-out human authorship decision โ€” what makes this not a template. + +## Default Response Style + +- State the recommended option first, then justify it. +- Explain the human or emotional quality that separates it from a generic pattern. +- Call out UX risks, friction points, and likely user misunderstandings explicitly. +- Provide before/after microcopy rewrites when copy is in scope. +- Keep guidance implementation-friendly: reference specific patterns, component names, or interaction models by name. +- When in doubt: reduce the steps, sharpen the copy, raise the emotional quality. diff --git a/.github/skills/web-ui-design/SKILL.md b/.github/skills/web-ui-design/SKILL.md new file mode 100644 index 0000000..00b3d39 --- /dev/null +++ b/.github/skills/web-ui-design/SKILL.md @@ -0,0 +1,204 @@ +--- +name: web-ui-design +description: 'Best In Class Web UI Design Principles and Patterns. Use for web UI design, interface reviews, responsive layouts, forms, cards, modals, navigation, accessibility, and interaction patterns.' +--- + +# Web UI Design โ€” v2 (2025โ€“2026 Edition) + +Use this skill when designing, reviewing, or refining web interfaces, UI components, dashboards, forms, modals, navigation, cards, and responsive layouts. + +> Optimized to produce non-generic, character-rich, human-feeling interfaces. +> Counters the sterile symmetry of default AI-generated design. + +--- + +## Core Philosophy + +The 2026 design era is defined by three words: **Human, Balanced, and Alive.** + +Generic AI design produces polished but characterless output โ€” symmetrical grids, identical card radii, safe color palettes, predictable hierarchy. + +Great design in 2026 uses **controlled tension**: visual asymmetry with intent, kinetic elements with restraint, typography with personality, and tactile textures with purpose. + +Every decision should serve the user's task _and_ signal that a human made a considered choice. + +--- + +## Core Principles + +- Prioritize clarity, concision, and character in UI copy. +- Optimize for the user's task, not decorative complexity โ€” but never mistake sterility for minimalism. +- Use familiar web patterns as a floor, not a ceiling. +- Make current state obvious through immediate, visible feedback. +- Reduce memory load: show options, labels, and next actions in context. +- Prevent errors with constraints, validation, safe defaults, and clear cancel/undo paths. +- Layouts should feel calm and focused, not emptied out. +- Respect accessibility and responsive behavior on all viewports. +- Design for reduced visual noise โ€” control brightness and contrast to prevent eye strain, especially in dashboard contexts. + +--- + +## 2025โ€“2026 Design Language (Anti-Generic Toolkit) + +These are specific, actionable techniques to break the "template look." + +### Typography as Structure + +- Use **variable fonts** with tight tracking on large headings (`letter-spacing: -0.03em` to `-0.05em`) and generous line-height on body text. +- Mix one expressive display typeface with one utilitarian sans-serif โ€” never two neutral system fonts. +- **Kinetic typography**: subtle entrance animations on key headings (slide-in, opacity fade, letter-space collapse). Reserve for hero sections and milestone moments only. +- Scale type aggressively for hierarchy โ€” don't be afraid of 80โ€“120px headings on desktop. +- Type scale minimum: display, heading, body, label, caption. Each step must be visually distinct. + +### Layout and Spatial Design + +- **Bento Grid layouts** for dashboards, landing sections, and feature showcases: modular asymmetric tiles of different sizes that coexist in a coherent visual system. +- Break the grid intentionally: offset one element, overlap an image onto a section border, or rotate a label 1โ€“2ยฐ. This signals human authorship. +- Use **spatial hierarchy** โ€” vary depth through shadows and layering, not just size. Cards at different perceived Z-levels communicate importance. +- Avoid equal-width column grids everywhere. Use `grid-template-columns` with intentional imbalance (`2fr 1fr`, `3fr 1fr 1fr`). +- In Bento layouts: vary tile sizes to create visual rhythm, never uniform repetition. + +### Color and Surface + +- **Evolved Glassmorphism**: frosted-glass surfaces with `backdrop-filter: blur()` โ€” pair with dynamic blurs that respond to scroll or dark mode context. +- Dark mode is **standard, not optional** in 2026. Design both modes from the start; never port one to the other as an afterthought. +- Use two-tone or gradient surfaces on feature cards โ€” subtle linear or radial gradients (low contrast, high elegance), never flat fills. +- Add **micro-texture** on large background areas: noise grain at 3โ€“8% opacity via CSS `filter` or SVG `feTurbulence` โ€” avoids the "printed on screen" flatness of generic AI output. +- Limit accent color use: one true accent, one muted variant, neutrals for everything else. + +### Motion and Interactivity + +- Interfaces should feel **alive** โ€” use enter/exit animations on modals, list items, and route transitions. +- **Physics-based easing** over linear or ease-in-out: `cubic-bezier(0.34, 1.56, 0.64, 1)` for springy appears; slow-out for dismissals. +- Scroll-linked animations (parallax depth, sticky reveals) โ€” subtle, purposeful. Not decoration: use to guide attention. +- Every interactive element must have a hover _and_ active state that feels visually distinct from the rest state. +- Animations that play on every scroll event are noise. Reserve motion for meaningful state transitions. + +### Organic Shapes and Imperfection + +- Replace sharp rectangle backgrounds with blob SVG dividers, soft rounded sections, or asymmetric hero cutouts. +- Introduce **one hand-drawn or illustrated element** per major page (icon, divider, annotation arrow) to signal human craft. +- Avoid identical border radii across all components: utility chips `4px`, cards `12โ€“16px`, hero modules `24px+` or `0` (intentional sharp edge as a design statement). +- Pure black dark mode โ€” `background: black` with white text, no depth โ€” is not dark mode. It is laziness. Layer surfaces. + +--- + +## Design Checklist + +Before proposing or delivering any UI, verify: + +- [ ] The primary action is visually dominant and unambiguous. +- [ ] The page has one dominant focal point and a clear visual hierarchy. +- [ ] Labels are specific, action-oriented, and need no additional explanation. +- [ ] Buttons use task-aligned verbs โ€” not "Submit", use "Save Template", "Run Analysis", "Add Member". +- [ ] Status, loading, success, and error states are all designed โ€” not assumed. +- [ ] Spacing and sizing follow an 8px base grid throughout. +- [ ] The layout works at 375px, 768px, and 1440px. +- [ ] Color contrast passes WCAG AA: 4.5:1 for body text, 3:1 for large text and UI components. +- [ ] All interactive elements are keyboard-reachable and focus-visible. +- [ ] Modals render above all content and have a discoverable close path. +- [ ] Does this look like a template? If yes, apply one anti-generic technique before shipping. +- [ ] Is there at least one typographic, color, or layout decision that required a conscious human choice? + +--- + +## Pattern Guidance + +### Forms + +- Group related fields together with clear section labels. +- Always-visible labels โ€” no placeholder-as-label. +- Short helper text only when it prevents a common error. +- **Inline validation on blur**, not on submit. +- Single-column layouts on mobile; max 2 columns on desktop for complex forms. +- Provide clear Reset / Cancel actions; never assume the user wants to lose progress. +- Use **smart defaults** to reduce input burden: pre-fill known values, suggest based on context. + +### Buttons and Actions + +- One primary CTA per view. Secondary actions are visually quiet (ghost or text-style). +- Button labels mirror the user's goal: "Export as PDF", "Create Workspace", "Delete Forever". +- Destructive actions require a distinct visual treatment (error/warning color) plus a confirmation step. +- Avoid icon-only controls unless the icon is universally understood (โœ•, โ˜ฐ, โ†‘). +- On mobile, minimum touch target: 44ร—44px. + +### Cards and Lists + +- Content must be scannable โ€” lead with the most important attribute. +- Metadata is secondary: smaller, muted, never competing with the title. +- Preserve whitespace; card padding minimum 16px, prefer 20โ€“24px. +- Expansion and collapse affordance must be obvious (chevron icon + hover state). +- In Bento layouts: vary tile sizes to create visual rhythm, not uniform repetition. + +### Modals and Overlays + +- Use modals only for tasks requiring focused attention: confirmation, form completion, detail view. +- Overlay dims background to `rgba(0,0,0,0.5)` minimum โ€” blocks background interaction explicitly. +- Close button: top-right, always visible, `aria-label="Close"`. +- Constrain modal height to `90vh`; allow inner scroll if content overflows. +- Render via portal or top-level to avoid `z-index` stack conflicts. +- Do not use a modal for information that can live inline or in a drawer. + +### Responsive Layouts + +- Design mobile-first; desktop is an enhancement. +- Avoid any fixed pixel widths on containers โ€” use `max-width` with `width: 100%`. +- Bento grids collapse to single-column on mobile via `grid-template-columns: 1fr`. +- Touch targets, tap areas, and font sizes (minimum 16px body) must be rechecked at 375px. +- Navigation: hamburger menu is acceptable on mobile only when there are more than 5 items; prefer a bottom tab bar for app-like interfaces. + +--- + +## UX Writing Guidance + +- Use the **3 C's**: Clarity, Concision, Character. +- Microcopy must be short and task-focused: "Add as Prompt" beats "Click here to add". +- Avoid jargon unless the audience expects it โ€” a DevOps dashboard can say "pipeline"; a consumer app cannot. +- Error messages: state the problem and the fix. "Password must be 8+ characters โ€” try again." not "Invalid input." +- Empty states are not an afterthought: write copy that guides the next action ("No templates yet โ€” create your first one โ†’"). +- Avoid AI-sounding copy: "Streamline your workflow with powerful synergies" signals template. Write like a human who knows the user's actual job. + +--- + +## Common Anti-Patterns + +**Visual and Layout** + +- Too many competing primary actions on one screen. +- Decorative gradients and shadows that serve no hierarchy purpose. +- Identical card grids โ€” same size, same radius, same padding, uniform columns: the hallmark of generic AI output. +- Equal-weight typography โ€” if everything is 16px medium, nothing is important. +- Dark mode that is just `background: black` with white text โ€” no depth, no surface layering. +- Animations that play on every scroll event rather than on meaningful state transitions. + +**Interaction and State** + +- Hidden or unclear selection state. +- Overusing popovers, modals, and tooltips as a substitute for clear inline design. +- Forms requiring excessive scrolling before the user can complete the task. +- Inconsistent spacing, border-radius, or icon weight within one view. + +--- + +## Output Expectations + +Every UI proposal must include: + +- **Recommended layout approach** with grid structure specified (`grid-template-columns`, breakpoints, container widths). +- **Typography scale**: at minimum display, heading, body, label, and caption sizes. +- **Spacing and rhythm** annotated against the 8px base grid. +- **Color decisions**: surface, text, accent, destructive, muted โ€” both light and dark mode. +- **Motion notes**: what animates, how (easing curve), and why it earns its place. +- **One anti-generic decision explicitly called out**: what makes this not a template โ€” the conscious human choice. +- **Accessibility and responsive notes**: contrast ratios, keyboard behavior, mobile viewport verification. +- **Risks and tradeoffs** for each proposed pattern. + +--- + +## Default Response Style + +- State the recommended option first, then justify it. +- Explain the **human design choice** that separates it from AI-generated output. +- Call out layout, accessibility, and responsiveness risks explicitly. +- Keep recommendations implementation-friendly: name CSS properties, grid strategies, and component patterns by name. +- When in doubt: simplify the layout, amplify the typography. diff --git a/CHANGELOG.md b/CHANGELOG.md index 53decf9..074fd9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **Sidebar guidance**: Short explanatory text for Workspaces, Tags, Templates, Cleanup, and Storage sections to improve scanability. +- **Tag suggestions dropdown**: The Tags field in Add New Prompt now uses the Sidebar Tags Cloud as its source and supports inserting multiple comma-separated tags. +- **Dropdown affordance**: Added a conditional chevron indicator for the tag picker when sidebar tags are available. + +### Changed + +- **Sidebar layout**: Refined section spacing, separators, surfaces, and theme tokens for a cleaner navigation-first sidebar. +- **Control bar styling**: Tightened spacing, card radius, and theme treatment for a more consistent top control surface. +- **Prompt form UX**: Replaced the native datalist with a custom, accessible tag suggestion menu while preserving comma-separated entry. + +### Fixed + +- **Sidebar scanability**: Improved hierarchy and divider visibility between sidebar sections in both light and dark themes. +- **Scrollbar containment**: Kept the sidebar scrollbar inset inside the shell so it reads as part of the sidebar instead of bleeding outside the edge. +- **Form consistency**: Cleaned up the tags field behavior so suggestions only appear when sidebar tags exist. + ## [1.3.0] - 2026-04-03 + ### Added + - **2026 LLM Models**: Support for 25+ next-gen models including GPT-5.4, Claude 4.6, Gemini 3.1, and Llama 4. - **Unified Control Bar**: Compact, horizontal layout combining search, model filters, and date range. - **View Mode Toggle**: Clear, premium buttons for switching between Grid and List views. @@ -15,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Hover Micro-interactions**: Added subtle "lift-up" translate and shadow effect on cards. ### Fixed + - **Variable Detection**: Enhanced regex support for hints and placeholders `{{name:hint}}`. - **Search Bar Contrast**: Fixed dark mode visibility and icon alignment issues. - **Date Filter UX**: Improved contrast and focus states for consistent theme-aware filtering. @@ -22,16 +45,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.2.8] - 2026-04-03 ### Changed ๐ŸŽจ + - **Sidebar UX**: Adฤƒugat scrollable container independent ศ™i scrollbar minimalist pentru coloana stรขngฤƒ (Workspaces, Tags, Templates), prevenind scroll-ul รฎntregii pagini pe liste lungi. ## [1.2.7] - 2026-04-03 ### Fixed ๐Ÿ› ๏ธ + - Adฤƒugat suport pentru extragerea textului `hint` din variabile ศ™i utilizarea acestuia ca `placeholder` dinamic รฎn interfaศ›a graficฤƒ (`VariableInjector`). ## [1.2.6] - 2026-04-03 ### Fixed ๐Ÿ› ๏ธ + - Imbunฤƒtฤƒศ›it regex-ul din `VariableInjector` ศ™i `TemplateManager` pentru a detecta impecabil variabile cu sintaxฤƒ complexฤƒ (ex: `{{VAR=hint cu caractere speciale}}`). - Rezolvat conflictele de compilare (Vite 8 Rolldown) ศ™i fixat tipabilitฤƒศ›ile TypeScript din zona de cฤƒutare hibridฤƒ FlexSearch. - Fixat dependenศ›ele peer conflictuale la integrarea modulului `vite-plugin-pwa` offline. @@ -40,6 +66,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.2.5] - 2026-03-28 ### Added ๐Ÿš€ + - **IndexedDB Persistence**: Unlimited local storage capacity, bypassing the 5MB browser limit. - **Full-Text Local Search**: High-performance semantic searching via FlexSearch (title, body, tags). - **PWA v2 + Offline Sync**: Background asset caching and offline functionality with connectivity badging. @@ -48,12 +75,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **CI/CD Build Audit**: Integrated GitHub Actions with CodeQL security and build size auditing. ### Changed ๐ŸŽจ + - **O(1) Reactive Refactoring**: Massive architectural cleanup using specialized custom hooks and modular components. - **Sidebar UX**: Optimized layout with flexbox vertical docking for tools and system monitors. ## [1.2.0] - 2026-03-27 ### Added ๐Ÿš€ + - **Smart Workspaces**: Virtual folder organization for grouping related prompts. - **Dynamic Variable Injector**: Regex-based detection of `{{variable}}` with live UI input fields. - **Prompt Version History**: Automatic snapshots of prompt bodies on every edit. @@ -61,6 +90,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **English Translation**: Complete transition of the UI and documentation to English. ### Changed ๐ŸŽจ + - Improved Grid layout for high-resolution displays (3+ columns). - Styled Version History modal and Workspace accent colors. - Sidebar structure updated to include Workspace Manager. @@ -68,17 +98,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.1.1] - 2026-03-26 ### Added โœจ + - **Dark Mode**: High-contrast theme with persistent storage. - **Smart Tags Autocomplete**: Multi-tag detection in the input form. - **Markdown Highlighting**: Improved syntax rendering for code blocks. ### Fixed ๐Ÿ› ๏ธ + - Resolved critical state sync issue where JSON imports didn't trigger immediate UI refreshes. - Cleaned up Git merge conflicts across the main app shell. ## [1.0.0] - 2026-03-25 ### Added ๐ŸŽ‰ + - Initial release of the Prompt Library PWA. - Basic CRUD operations, Tagging, and Search. - GitHub Gist Cloud Sync and Local JSON Backup/Restore. diff --git a/src/App.tsx b/src/App.tsx index a2d2f1f..7e219aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { useState, useMemo, useEffect } from 'react'; import type { Prompt, Workspace, PromptVersion } from './types'; +import type { PromptTemplate } from './types'; import { useLocalStorage } from './hooks/useLocalStorage'; import { SearchBar } from './components/SearchBar'; import { PromptForm } from './components/PromptForm'; @@ -11,202 +12,349 @@ import { TemplateManager } from './components/TemplateManager'; import { StorageUsage } from './components/StorageUsage'; import { CleanupAssistant } from './components/CleanupAssistant'; import { Toaster, toast } from 'sonner'; -import { LayoutGrid, List, Moon, Sun } from 'lucide-react'; +import { Moon, Sun } from 'lucide-react'; import { usePromptFilters } from './hooks/usePromptFilters'; import { useIndexedDB } from './hooks/useIndexedDB'; import { LLM_MODELS } from './constants'; function App() { - const [prompts, setPrompts] = useIndexedDB('prompts', []); - const [workspaces, setWorkspaces] = useIndexedDB('workspaces', []); - const [promptHistory, setPromptHistory] = useIndexedDB('history', [], 'prompt-history'); - - const [currentWorkspaceId, setCurrentWorkspaceId] = useLocalStorage('current-workspace', null); - const [searchQuery, setSearchQuery] = useState(''); - const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light'); - const [viewMode, setViewMode] = useLocalStorage<'list' | 'grid'>('view-mode', 'grid'); - - const [editingPrompt, setEditingPrompt] = useState(null); - const [selectedTag, setSelectedTag] = useState(null); - - const [selectedModel, setSelectedModel] = useState(null); - const [dateRange, setDateRange] = useState<{ start: Date | null, end: Date | null }>({ start: null, end: null }); - - useEffect(() => { - document.documentElement.setAttribute('data-theme', theme); - }, [theme]); - - const allTags = useMemo(() => { - const counts: Record = {}; - prompts.forEach(p => p.tags.forEach(t => { counts[t] = (counts[t] || 0) + 1; })); - return Object.entries(counts).sort((a, b) => b[1] - a[1]); - }, [prompts]); - - const allModels = useMemo(() => { - const models = new Set(prompts.map(p => p.model)); - return Array.from(models).sort(); - }, [prompts]); - - const filteredPrompts = usePromptFilters(prompts, { - searchQuery, - selectedTag, - currentWorkspaceId, - selectedModel, - dateRange - }); - - const handleSavePrompt = (promptData: Omit) => { - const now = new Date().toISOString(); - if (editingPrompt) { - const snapshot: PromptVersion = { promptId: editingPrompt.id, body: editingPrompt.body, savedAt: editingPrompt.updatedAt ?? now }; - setPromptHistory(prev => [snapshot, ...prev]); - const updatedPrompts = prompts.map(p => p.id === editingPrompt.id ? { ...p, ...promptData, updatedAt: now } : p); - setPrompts(updatedPrompts); - setEditingPrompt(null); - toast.success('Prompt updated successfully!'); - } else { - const newPrompt: Prompt = { ...promptData, id: crypto.randomUUID(), createdAt: now, updatedAt: now, workspaceId: promptData.workspaceId ?? currentWorkspaceId ?? undefined }; - setPrompts([newPrompt, ...prompts]); - toast.success('Prompt created successfully!'); - } - }; + const [prompts, setPrompts] = useIndexedDB('prompts', []); + const [workspaces, setWorkspaces] = useIndexedDB('workspaces', []); + const [promptHistory, setPromptHistory] = useIndexedDB( + 'history', + [], + 'prompt-history', + ); - const handleImportPrompts = (imported: Prompt[]) => { - setPrompts(imported); - toast.success(`Successfully imported ${imported.length} prompts!`); - }; + const [currentWorkspaceId, setCurrentWorkspaceId] = useLocalStorage< + string | null + >('current-workspace', null); + const [searchQuery, setSearchQuery] = useState(''); + const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light'); + const [viewMode, setViewMode] = useLocalStorage<'list' | 'grid'>( + 'view-mode', + 'grid', + ); - const handleEditPrompt = (prompt: Prompt) => { - setEditingPrompt(prompt); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }; + const [editingPrompt, setEditingPrompt] = useState(null); + const [selectedTag, setSelectedTag] = useState(null); - const handleClearForm = () => { setEditingPrompt(null); }; + const [selectedModel, setSelectedModel] = useState(null); + const [dateRange, setDateRange] = useState<{ + start: Date | null; + end: Date | null; + }>({ start: null, end: null }); - const handleDeletePrompt = (id: string) => { - setPrompts(prompts.filter(p => p.id !== id)); - setPromptHistory(prev => prev.filter(v => v.promptId !== id)); - if (editingPrompt?.id === id) setEditingPrompt(null); - toast.info('Prompt deleted.'); - }; + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme); + }, [theme]); - const handleAddWorkspace = (ws: Workspace) => { - setWorkspaces(prev => [...prev, ws]); - toast.success(`Workspace "${ws.name}" created!`); - }; + const allTags = useMemo(() => { + const counts: Record = {}; + prompts.forEach((p) => + p.tags.forEach((t) => { + counts[t] = (counts[t] || 0) + 1; + }), + ); + return Object.entries(counts).sort((a, b) => b[1] - a[1]); + }, [prompts]); - const handleDeleteWorkspace = (id: string) => { - setWorkspaces(prev => prev.filter(ws => ws.id !== id)); - setPrompts(prev => prev.map(p => p.workspaceId === id ? { ...p, workspaceId: undefined } : p)); - if (currentWorkspaceId === id) setCurrentWorkspaceId(null); - toast.info('Workspace deleted.'); - }; + const allModels = useMemo(() => { + const models = new Set(prompts.map((p) => p.model)); + return Array.from(models).sort(); + }, [prompts]); - const handleMovePromptToWorkspace = (promptId: string, workspaceId: string | null) => { - const targetPrompt = prompts.find(p => p.id === promptId); - if (!targetPrompt) return; - const updatedPrompts = prompts.map(p => p.id === promptId ? { ...p, workspaceId: workspaceId || undefined, updatedAt: new Date().toISOString() } : p); - setPrompts(updatedPrompts); - const wsName = workspaceId ? workspaces.find(w => w.id === workspaceId)?.name : 'All Prompts'; - toast.success(`Prompt "${targetPrompt.title}" moved to ${wsName}`); - }; + const filteredPrompts = usePromptFilters(prompts, { + searchQuery, + selectedTag, + currentWorkspaceId, + selectedModel, + dateRange, + }); + + const handleSavePrompt = ( + promptData: Omit, + ) => { + const now = new Date().toISOString(); + if (editingPrompt) { + const snapshot: PromptVersion = { + promptId: editingPrompt.id, + body: editingPrompt.body, + savedAt: editingPrompt.updatedAt ?? now, + }; + setPromptHistory((prev) => [snapshot, ...prev]); + const updatedPrompts = prompts.map((p) => + p.id === editingPrompt.id ? { ...p, ...promptData, updatedAt: now } : p, + ); + setPrompts(updatedPrompts); + setEditingPrompt(null); + toast.success('Prompt updated successfully!'); + } else { + const newPrompt: Prompt = { + ...promptData, + id: crypto.randomUUID(), + createdAt: now, + updatedAt: now, + workspaceId: promptData.workspaceId ?? currentWorkspaceId ?? undefined, + }; + setPrompts([newPrompt, ...prompts]); + toast.success('Prompt created successfully!'); + } + }; + + const handleImportPrompts = (imported: Prompt[]) => { + setPrompts(imported); + toast.success(`Successfully imported ${imported.length} prompts!`); + }; + + const handleEditPrompt = (prompt: Prompt) => { + setEditingPrompt(prompt); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + const handleClearForm = () => { + setEditingPrompt(null); + }; + + const handleDeletePrompt = (id: string) => { + setPrompts(prompts.filter((p) => p.id !== id)); + setPromptHistory((prev) => prev.filter((v) => v.promptId !== id)); + if (editingPrompt?.id === id) setEditingPrompt(null); + toast.info('Prompt deleted.'); + }; + + const handleAddWorkspace = (ws: Workspace) => { + setWorkspaces((prev) => [...prev, ws]); + toast.success(`Workspace "${ws.name}" created!`); + }; + + const handleDeleteWorkspace = (id: string) => { + setWorkspaces((prev) => prev.filter((ws) => ws.id !== id)); + setPrompts((prev) => + prev.map((p) => + p.workspaceId === id ? { ...p, workspaceId: undefined } : p, + ), + ); + if (currentWorkspaceId === id) setCurrentWorkspaceId(null); + toast.info('Workspace deleted.'); + }; + + const handleMovePromptToWorkspace = ( + promptId: string, + workspaceId: string | null, + ) => { + const targetPrompt = prompts.find((p) => p.id === promptId); + if (!targetPrompt) return; + const updatedPrompts = prompts.map((p) => + p.id === promptId + ? { + ...p, + workspaceId: workspaceId || undefined, + updatedAt: new Date().toISOString(), + } + : p, + ); + setPrompts(updatedPrompts); + const wsName = workspaceId + ? workspaces.find((w) => w.id === workspaceId)?.name + : 'All Prompts'; + toast.success(`Prompt "${targetPrompt.title}" moved to ${wsName}`); + }; - const getVersionsForPrompt = (promptId: string): PromptVersion[] => { - return promptHistory.filter(v => v.promptId === promptId); + const handleAddPromptFromTemplate = (template: PromptTemplate) => { + const now = new Date().toISOString(); + const newPrompt: Prompt = { + id: crypto.randomUUID(), + title: template.name, + body: template.templateBody, + tags: [template.category], + model: LLM_MODELS[0], + createdAt: now, + updatedAt: now, + workspaceId: currentWorkspaceId ?? undefined, }; - return ( -
- -
-
-

Prompt Library

-

Manage, filter, and synchronize your AI prompts.

-
-
- - -
-
- -
- - -
- {/* ๐Ÿ› ๏ธ Modern Unified Control Bar */} -
-
-
- -
-
- - -
-
- -
-
- - -
- -
- - setDateRange(prev => ({ ...prev, start: e.target.value ? new Date(e.target.value) : null }))}/> -
- -
- - setDateRange(prev => ({ ...prev, end: e.target.value ? new Date(e.target.value) : null }))}/> -
- - {(selectedModel || dateRange.start || dateRange.end) && ( - - )} -
-
- - t[0])} onClear={handleClearForm} workspaces={workspaces} currentWorkspaceId={currentWorkspaceId}/> - -
-
Total: {prompts.length} | Found: {filteredPrompts.length}
-
- - -
-
+ setPrompts([newPrompt, ...prompts]); + toast.success(`Prompt created from template "${template.name}"!`); + }; + + const getVersionsForPrompt = (promptId: string): PromptVersion[] => { + return promptHistory.filter((v) => v.promptId === promptId); + }; + + return ( +
+ +
+
+

Prompt Library

+

Manage, filter, and synchronize your AI prompts.

- ); +
+ + +
+
+ +
+ + +
+ {/* ๐Ÿ› ๏ธ Modern Unified Control Bar */} +
+
+
+ +
+
+ +
+
+ + +
+ +
+ + + setDateRange((prev) => ({ + ...prev, + start: e.target.value ? new Date(e.target.value) : null, + })) + } + /> +
+ +
+ + + setDateRange((prev) => ({ + ...prev, + end: e.target.value ? new Date(e.target.value) : null, + })) + } + /> +
+ + {(selectedModel || dateRange.start || dateRange.end) && ( + + )} +
+
+ + t[0])} + onClear={handleClearForm} + workspaces={workspaces} + currentWorkspaceId={currentWorkspaceId} + /> + +
+
+ Total: {prompts.length} | Found: {filteredPrompts.length} +
+
+ + +
+
+
+ ); } export default App; diff --git a/src/components/CleanupAssistant.tsx b/src/components/CleanupAssistant.tsx index cd24844..90a47c4 100644 --- a/src/components/CleanupAssistant.tsx +++ b/src/components/CleanupAssistant.tsx @@ -3,17 +3,17 @@ import { Sparkles, Trash2, AlertTriangle, FileText, Copy } from 'lucide-react'; import type { Prompt } from '../types'; interface CleanupAssistantProps { - prompts: Prompt[]; - onDelete: (id: string) => void; + prompts: Prompt[]; + onDelete: (id: string) => void; } interface Suggestion { - id: string; - promptId: string; - type: 'large' | 'duplicate' | 'old'; - title: string; - reason: string; - impact: string; + id: string; + promptId: string; + type: 'large' | 'duplicate' | 'old'; + title: string; + reason: string; + impact: string; } /** @@ -24,107 +24,164 @@ interface Suggestion { * - Stale prompts (Not updated for 3+ months) */ export function CleanupAssistant({ prompts, onDelete }: CleanupAssistantProps) { - // Stable reference for "current" time to satisfy purity rules - const [now] = useState(() => Date.now()); + // Stable reference for "current" time to satisfy purity rules + const [now] = useState(() => Date.now()); - const suggestions = useMemo(() => { - const list: Suggestion[] = []; - const seenBodies = new Map(); // body -> first prompt title - - const THREE_MONTHS = 90 * 24 * 60 * 60 * 1000; + const suggestions = useMemo(() => { + const list: Suggestion[] = []; + const seenBodies = new Map(); // body -> first prompt title - prompts.forEach(p => { - // 1. Detect Duplicates by Body - if (seenBodies.has(p.body)) { - list.push({ - id: `dup-${p.id}`, - promptId: p.id, - type: 'duplicate', - title: p.title, - reason: `Duplicate content of "${seenBodies.get(p.body)}"`, - impact: 'Redundant storage' - }); - } else { - seenBodies.set(p.body, p.title); - } + const THREE_MONTHS = 90 * 24 * 60 * 60 * 1000; - // 2. Detect Large Prompts (> 10KB) - if (p.body.length > 10000) { - list.push({ - id: `large-${p.id}`, - promptId: p.id, - type: 'large', - title: p.title, - reason: `Large prompt (${(p.body.length / 1024).toFixed(1)} KB)`, - impact: 'High quota impact' - }); - } + prompts.forEach((p) => { + // 1. Detect Duplicates by Body + if (seenBodies.has(p.body)) { + list.push({ + id: `dup-${p.id}`, + promptId: p.id, + type: 'duplicate', + title: p.title, + reason: `Duplicate content of "${seenBodies.get(p.body)}"`, + impact: 'Redundant storage', + }); + } else { + seenBodies.set(p.body, p.title); + } + + // 2. Detect Large Prompts (> 10KB) + if (p.body.length > 10000) { + list.push({ + id: `large-${p.id}`, + promptId: p.id, + type: 'large', + title: p.title, + reason: `Large prompt (${(p.body.length / 1024).toFixed(1)} KB)`, + impact: 'High quota impact', + }); + } - // 3. Detect Stale Prompts (Unused for 90 days) - const lastUpdate = new Date(p.updatedAt).getTime(); - if (now - lastUpdate > THREE_MONTHS) { - list.push({ - id: `stale-${p.id}`, - promptId: p.id, - type: 'old', - title: p.title, - reason: `Stale (Last updated ${Math.floor((now - lastUpdate) / (24 * 60 * 60 * 1000))} days ago)`, - impact: 'Likely outdated' - }); - } + // 3. Detect Stale Prompts (Unused for 90 days) + const lastUpdate = new Date(p.updatedAt).getTime(); + if (now - lastUpdate > THREE_MONTHS) { + list.push({ + id: `stale-${p.id}`, + promptId: p.id, + type: 'old', + title: p.title, + reason: `Stale (Last updated ${Math.floor((now - lastUpdate) / (24 * 60 * 60 * 1000))} days ago)`, + impact: 'Likely outdated', }); + } + }); + + return list; + }, [prompts, now]); + + if (suggestions.length === 0) return null; + + return ( +
+

+ Cleanup Suggestions ({suggestions.length}) +

- return list; - }, [prompts, now]); +

+ Review large, duplicate, or stale prompts. +

- if (suggestions.length === 0) return null; +
    + {suggestions.slice(0, 3).map((s) => ( +
  • +
    +
    + {s.title} +
    +
    + {s.type === 'large' && ( + + )} + {s.type === 'duplicate' && ( + + )} + {s.type === 'old' && ( + + )} + {s.reason} +
    +
    + +
  • + ))} +
- return ( -
-

- Cleanup Suggestions ({suggestions.length}) -

- -
    - {suggestions.slice(0, 3).map(s => ( -
  • -
    -
    {s.title}
    -
    - {s.type === 'large' && } - {s.type === 'duplicate' && } - {s.type === 'old' && } - {s.reason} -
    -
    - -
  • - ))} -
- - {suggestions.length > 3 && ( -
- + {suggestions.length - 3} more suggestions -
- )} + {suggestions.length > 3 && ( +
+ + {suggestions.length - 3} more suggestions
- ); + )} +
+ ); } diff --git a/src/components/PromptForm.tsx b/src/components/PromptForm.tsx index 12cb4db..5432f78 100644 --- a/src/components/PromptForm.tsx +++ b/src/components/PromptForm.tsx @@ -1,166 +1,280 @@ import { useState } from 'react'; import type { Prompt, Workspace } from '../types'; +import { ChevronDown } from 'lucide-react'; import { LLM_MODELS } from '../constants'; interface PromptFormProps { - onSave: (prompt: Omit) => void; - editingPrompt: Prompt | null; - existingTags: string[]; - onClear: () => void; - workspaces: Workspace[]; - currentWorkspaceId: string | null; + onSave: (prompt: Omit) => void; + editingPrompt: Prompt | null; + existingTags: string[]; + onClear: () => void; + workspaces: Workspace[]; + currentWorkspaceId: string | null; } // Form for creating or editing a prompt -export function PromptForm({ onSave, editingPrompt, existingTags, onClear, workspaces, currentWorkspaceId }: PromptFormProps) { - // Initial state set based on editingPrompt or defaults - const [title, setTitle] = useState(editingPrompt?.title || ''); - const [body, setBody] = useState(editingPrompt?.body || ''); - const [tagsInput, setTagsInput] = useState(editingPrompt ? editingPrompt.tags.join(', ') : ''); - const [model, setModel] = useState(editingPrompt?.model || LLM_MODELS[0]); - // Selected workspace in the form (defaults to current or prompt's workspace) - const [selectedWorkspaceId, setSelectedWorkspaceId] = useState( - editingPrompt?.workspaceId ?? currentWorkspaceId ?? '' - ); - - const resetForm = () => { - setTitle(''); - setBody(''); - setTagsInput(''); - setModel(LLM_MODELS[0]); - setSelectedWorkspaceId(currentWorkspaceId ?? ''); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - // Basic validation - if (!title.trim() || !body.trim()) { - alert('Title and prompt body are required.'); - return; - } - - // Parse tags from comma-separated string to array - const tagsArray = tagsInput - .split(',') - .map(t => t.trim()) - .filter(t => t.length > 0); - - // Call callback from App.tsx with clean data - onSave({ - title: title.trim(), - body: body.trim(), - tags: tagsArray, - model: model || LLM_MODELS[0], - workspaceId: selectedWorkspaceId || undefined, - }); - - // Clear form only on creation (not on edit) - if (!editingPrompt) { - resetForm(); - } - }; - - return ( -
-

{editingPrompt ? 'Edit Prompt' : 'Add New Prompt'}

- -
- - setTitle(e.target.value)} - placeholder="e.g., Python Code Refactor" - /> -
- -
-
- - setTagsInput(e.target.value)} - placeholder="e.g., refactor, python, clean-code" - list="tags-autocomplete" - autoComplete="off" - /> - - {(() => { - // Multi-tag autocomplete logic using native datalist - const parts = tagsInput.split(','); - const lastPart = parts[parts.length - 1].trim(); - const prefix = parts.length > 1 - ? parts.slice(0, -1).join(', ') + ', ' - : ''; - - return existingTags - .filter(tag => - tag.toLowerCase().includes(lastPart.toLowerCase()) && - !parts.map(p => p.trim().toLowerCase()).includes(tag.toLowerCase()) - ) - .slice(0, 10) - .map(tag => ( - -
-
- - setTitle(e.target.value)} + placeholder="e.g., Python Code Refactor" + /> +
+ +
+
+ +
+ { + setTagsInput(e.target.value); + setIsTagDropdownOpen(hasTagSuggestions); + setHighlightedTagIndex(0); + }} + onFocus={() => setIsTagDropdownOpen(hasTagSuggestions)} + onBlur={() => setIsTagDropdownOpen(false)} + onKeyDown={(e) => { + if (!hasTagSuggestions || !tagSuggestions.length) return; + + if (e.key === 'ArrowDown') { + e.preventDefault(); + setIsTagDropdownOpen(true); + setHighlightedTagIndex( + (index) => (index + 1) % tagSuggestions.length, + ); + } + + if (e.key === 'ArrowUp') { + e.preventDefault(); + setIsTagDropdownOpen(true); + setHighlightedTagIndex( + (index) => + (index - 1 + tagSuggestions.length) % + tagSuggestions.length, + ); + } + + if (e.key === 'Enter' && isTagDropdownOpen) { + const selectedTag = + tagSuggestions[highlightedTagIndex] ?? tagSuggestions[0]; + if (selectedTag) { + e.preventDefault(); + insertTag(selectedTag); + } + } + + if (e.key === 'Escape') { + setIsTagDropdownOpen(false); + } + }} + placeholder="e.g., refactor, python, clean-code" + autoComplete="off" + aria-autocomplete="list" + aria-expanded={ + hasTagSuggestions && + isTagDropdownOpen && + tagSuggestions.length > 0 + } + /> + + {hasTagSuggestions && ( + + )} + + {hasTagSuggestions && + isTagDropdownOpen && + tagSuggestions.length > 0 && ( +
+ {tagSuggestions.slice(0, 10).map((tag, index) => ( + + ))}
-
- - {/* Workspace Selector - visible if workspaces exist */} - {workspaces.length > 0 && ( -
- - + )} + + {hasTagSuggestions && + isTagDropdownOpen && + tagSuggestions.length === 0 && + activeTagQuery.length > 0 && ( +
+ No Sidebar Tags Cloud matches.
+ )} +
+
+
+ + +
+
+ + {/* Workspace Selector - visible if workspaces exist */} + {workspaces.length > 0 && ( +
+ + +
+ )} + +
+ +