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..d9d0e4a 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). +## 1.3.1 + +### 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/README.md b/README.md index 9654b8c..13d2fde 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,20 @@ A professional, local-first **AI Prompt Management System** built for speed, organization, and portability. [![Deployment Status](https://github.com/NaviAndrei/PromptLibrary/actions/workflows/deploy.yml/badge.svg)](https://github.com/NaviAndrei/PromptLibrary/actions/workflows/deploy.yml) -[![Version](https://img.shields.io/badge/version-1.2.5-blue.svg)](package.json) +[![Version](https://img.shields.io/badge/version-1.3.1-blue.svg)](package.json) ## ✨ Features - **📂 Smart Workspaces**: Virtual folder organization for grouping related prompts. +- **🎛️ Unified Control Bar**: Search, view toggles, model filters, and date range live together in one compact header. - **⚡ Full-Text Search**: High-performance local search across title, body, and tags (powered by **FlexSearch**). +- **🧭 Sidebar Polish**: Helpful section notes, clearer dividers, and an inset scrollbar keep the left panel readable. +- **🏷️ Sidebar-Driven Tags**: The Add New Prompt form suggests tags from the sidebar cloud and supports comma-separated multi-tag entry. +- **🗂️ Prompt Templates**: Reusable templates can be edited in a modal and added as prompts with one click. - **🌗 Dark Mode**: Premium dark/light themes that persist across sessions. - **🧩 Variable Injection**: Use `{{variable}}` syntax with dynamic UI inputs. - **💄 Advanced Filtering**: Specific Model and Date-Range (inclusive) filters. +- **📝 Plain-Text Prompt View**: Expanded prompt content stays selectable for easier copy and reuse. - **🕐 Version History**: Automatic snapshots of your prompts with visual diffing. - **🏛️ IndexedDB**: Massive local capacity (hundreds of MBs) replacing the 5MB browser limit. - **📱 PWA v2 Native**: Full offline access with background asset caching and connectivity monitoring. @@ -22,25 +27,27 @@ A professional, local-first **AI Prompt Management System** built for speed, org - **Core**: React 19 + TypeScript 5.9 + Vite 8 - **Storage**: IndexedDB (Primary) + LocalStorage (Settings) - **Search**: FlexSearch (In-Memory Engine) -- **Styling**: Vanilla CSS (Global Variables) +- **Styling**: Vanilla CSS (Global Variables + responsive shells) - **Icons**: Lucide React -- **Formatting**: React Markdown + Syntax Highlighting (Prism) - **Security**: CodeQL Workflow Analysis ## 🚀 Getting Started 1. **Clone the repository**: + ```bash git clone https://github.com/NaviAndrei/PromptLibrary.git cd PromptLibrary ``` 2. **Install dependencies**: + ```bash npm install ``` 3. **Run in development mode**: + ```bash npm run dev ``` @@ -61,4 +68,5 @@ The project is automatically deployed to **GitHub Pages** via GitHub Actions on Distributed under the MIT License. See `LICENSE` for more information. --- + Built with ❤️ by **NaviAndrei** 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 && ( +
+ + +
+ )} + +
+ +