mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-11 17:16:57 -05:00
POC: TanStack Query POC implementation (#567)
* POC: TanStack Query implementation with conditional devtools - Replace manual useState polling with TanStack Query for projects/tasks - Add comprehensive query key factories for cache management - Implement optimistic updates with automatic rollback - Create progress polling hooks with smart completion detection - Add VITE_SHOW_DEVTOOLS environment variable for conditional devtools - Remove legacy hooks: useDatabaseMutation, usePolling, useProjectMutation - Update components to use mutation hooks directly (reduce prop drilling) - Enhanced QueryClient with optimized polling and caching settings 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: Remove unused DataTab component and PRP templates from DocsTab - Delete unused DataTab.tsx (956 lines) - no imports found in codebase - Remove PRP template system from DocsTab.tsx (424 lines removed) - Simplify document templates to basic markdown and meeting notes - Reduce DocsTab from 1,494 to 1,070 lines * feat: Add vertical slice architecture foundation for projects feature - Create features/projects/ directory structure - Add barrel exports and documentation for components, hooks, services, types, utils - Prepare for migrating 8,300+ lines of project-related code - Enable future feature flagging and modular architecture * remove: Delete entire PRP directory (4,611 lines) - Remove PRPViewer component and all related files - Delete 29 PRP-related files including sections, renderers, utilities - Clean up unused complex document rendering system - Simplifies codebase by removing over-engineered flip card viewer Files removed: - PRPViewer.tsx/css - Main component - sections/ - 13 specialized section components - components/ - 5 rendering components - utils/ - 6 utility files - renderers/ - Section rendering logic - types/ - PRP type definitions Part of frontend vertical slice refactoring effort. * refactor: Replace DraggableTaskCard with simplified vertical slice components - Remove complex DraggableTaskCard.tsx (268 lines) - Create TaskCard.tsx (87 lines) with glassmorphism styling preserved - Create TaskCardActions.tsx (83 lines) for separated action buttons - Move to features/projects/components/tasks/ vertical slice architecture Changes: - Remove flip animation complexity (100+ lines removed) - Preserve beautiful glassmorphism effects and hover states - Maintain drag-and-drop, selection, priority indicators - Fix card height issues and column stacking - Add visible task descriptions (no tooltip needed) - Update TaskBoardView and TaskTableView imports - Add lint:files npm script for targeted linting Result: 68% code reduction (268→87 lines) while preserving visual design All linting errors resolved, zero warnings on new components. * refactor: Remove PRP templates and PRPViewer from DocsTab - Remove PRP template system from DOCUMENT_TEMPLATES (424 lines) - Remove PRPViewer import and usage in beautiful view mode - Simplify document templates to basic markdown and meeting notes - Replace PRPViewer with temporary unavailable message - Reduce DocsTab from 1,494 to 1,070 lines Templates removed: - Complex PRP templates with structured sections - Over-engineered document generation logic - Unused template complexity Keeps essential functionality: - Basic markdown document template - Meeting notes template - Document creation and management - Template modal and selection Part of frontend cleanup removing unused PRP functionality. * refactor: Migrate to vertical slice architecture with Radix primitives - Migrated TasksTab, BoardView, TableView to features/projects/tasks - Created new UI primitives layer with Radix components - Replaced custom components with Radix primitives - Added MDXEditor to replace Milkdown - Removed Milkdown dependencies - Fixed all TypeScript errors in features directory - Established vertical slice pattern for features * refactor: Complete migration to vertical slice architecture - Migrated DocsTab to features/projects/documents - Replaced Milkdown with MDXEditor for markdown editing - Removed all crawling logic from DocsTab (documents only) - Migrated VersionHistoryModal to use Radix primitives - Removed old components/project-tasks directory - Fixed all TypeScript errors in features directory - Removed Milkdown dependencies from package.json * refactor: Align document system with backend JSONB storage reality - Create proper document hooks using project updates (not individual endpoints) - Refactor DocsTab to use TanStack Query for all data fetching - Remove non-existent document API endpoints from projectService - Implement optimistic updates for document operations - Fix document deletion to work with JSONB array structure Documents are stored as JSONB array in project.docs field, not as separate database records. This refactor aligns the frontend with this backend reality. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * fix: Simplify DocumentEditor and improve Documents sidebar styling - Use MDXEditor with out-of-the-box settings (no hacky overrides) - Update Documents sidebar with Tron-like glassmorphism theme - Fix document content extraction for JSONB structure - Improve empty state and search input styling - Add proper icons and hover effects to match app theme 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Complete migration to vertical slice architecture with TanStack Query + Radix This completes the project refactoring with no backwards compatibility, making the migration fully complete as requested. ## Core Architecture Changes - Migrated all types from centralized src/types/ to feature-based architecture - Completed vertical slice organization with projects/tasks/documents hierarchy - Full TanStack Query integration across all data operations - Radix UI primitives integrated throughout feature components ## Type Safety & Error Handling (Alpha Principles) - Eliminated all unsafe 'any' types with proper TypeScript unions - Added comprehensive error boundaries with detailed error context - Implemented detailed error logging with variable context following alpha principles - Added optimistic updates with proper rollback patterns across all mutations ## Smart Data Management - Created smart polling system that respects page visibility/focus state - Optimized query invalidation strategy to prevent cascade invalidations - Added proper JSONB type unions for database fields (ProjectPRD, ProjectDocs, etc.) - Fixed task ordering with integer precision to avoid float precision issues ## Files Changed - Moved src/types/project.ts → src/features/projects/types/ - Updated all 60+ files with new import paths and type references - Added FeatureErrorBoundary.tsx for granular error handling - Created useSmartPolling.ts hook for intelligent polling behavior - Added comprehensive task ordering utilities with proper limits - Removed deprecated utility files (debounce.ts, taskOrdering.ts) ## Breaking Changes (No Backwards Compatibility) - Removed centralized types directory completely - Changed TaskPriority from "urgent" to "critical" - All components now use feature-scoped types and hooks - Full migration to TanStack Query patterns with no legacy fallbacks Fixes all critical issues from code review and completes the refactoring milestone. * Fix remaining centralized type imports in project components Updated all project feature components to use the new vertical slice type imports from '../types' instead of '../../../types/project'. This completes the final step of the migration with no backwards compatibility remaining: - ProjectsView.tsx - ProjectList.tsx - NewProjectModal.tsx - ProjectCard.tsx - useProjectQueries.ts All project-related code now uses feature-scoped types exclusively. * refactor: Complete vertical slice service architecture migration Breaks down monolithic projectService (558 lines) into focused, feature-scoped services following true vertical slice architecture with no backwards compatibility. ## Service Architecture Changes - projectService.ts → src/features/projects/services/projectService.ts (Project CRUD) - → src/features/projects/tasks/services/taskService.ts (Task management) - → src/features/projects/documents/services/documentService.ts (Document versioning) - → src/features/projects/shared/api.ts (Common utilities & error handling) ## Benefits Achieved - True vertical slice: Each feature owns its complete service stack - Better separation: Task operations isolated from project operations - Easier testing: Individual services can be mocked independently - Team scalability: Features can be developed independently - Code splitting: Better tree-shaking and bundle optimization - Clearer dependencies: Services import only what they need ## Files Changed - Created 4 new focused service files with proper separation of concerns - Updated 5+ hook files to use feature-scoped service imports - Removed monolithic src/services/projectService.ts (17KB) - Updated VersionHistoryModal to use documentService instead of commented TODOs - All service index files properly export their focused services ## Validation - Build passes successfully confirming all imports are correct - All existing functionality preserved with no breaking changes - Error handling patterns maintained across all new services - No remaining references to old monolithic service This completes the final step of vertical slice architecture migration. * feat: Add Biome linter for /features directory - Replace ESLint with Biome for 35x faster linting - Configure Biome for AI-friendly JSON output - Fix all auto-fixable issues (formatting, imports) - Add targeted suppressions for legitimate ARIA roles - Set practical formatting rules (120 char line width) - Add npm scripts for various Biome operations - Document Biome usage for AI assistants * chore: Configure IDE settings for Biome/ESLint separation - Add .zed/settings.json for Zed IDE configuration - Configure ESLint to ignore /src/features (handled by Biome) - Add .zed to .gitignore - Enable Biome LSP for features, ESLint for legacy code - Configure Ruff for Python files * fix: Resolve critical TypeScript errors in features directory - Fix property access errors with proper type narrowing - Move TaskCounts to tasks types (vertical slice architecture) - Add formatZodErrors helper for validation error handling - Fix query return types with explicit typing - Remove unused _githubRepoId variable - Resolve ambiguous exports between modules - Reduced TypeScript errors from 40 to 28 * fix: resolve final TypeScript error in features directory - Update UseTaskEditorReturn interface to properly type projectFeatures - Change from unknown[] to explicit shape with id, label, type, and color properties - All TypeScript errors in /src/features now resolved * docs: improve CLAUDE.md with comprehensive development commands and architecture details - Add detailed frontend and backend development commands - Document vertical slice architecture with folder structure - Include TanStack Query patterns and code examples - Add backend service layer and error handling patterns - Document smart polling hooks and HTTP polling architecture - Include specific commands for TypeScript checking and linting - Add MCP tools documentation and debugging steps * fix: Correct Radix UI Select disabled prop usage and drag-drop bounds - Move disabled prop from Select root to SelectTrigger for proper functionality - Remove redundant manual disabled styling (opacity-50, cursor-not-allowed) - Add aria-disabled for enhanced accessibility compliance - Fix TasksTab bounds check to allow dropping at end of columns - Components: TaskPriority, TaskAssignee, TasksTab 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * fix: Improve API reliability and task management - Fix DELETE operations by handling 204 No Content responses in both callAPI and apiRequest - Replace custom calculateReorderPosition with battle-tested getReorderTaskOrder utility - Fix DeleteConfirmModal default open prop to prevent unexpected modal visibility - Add SSR guards to useSmartPolling hook to prevent crashes in non-browser environments 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * feat: Add review task count support with clean UI design - Add review field to TaskCounts interface for type consistency - Update backend to return separate review counts instead of mapping to doing - Enhance ProjectCard to display review tasks in clean 3-column layout - Combine doing+review counts in project cards for optimal visual design - Maintain granular data for detailed views (Kanban board still shows separate review column) Resolves CodeRabbit suggestion about missing review status while preserving clean UI 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Enhance FeatureErrorBoundary with TanStack Query integration - Add onReset callback prop for external reset handlers - Fix getDerivedStateFromError TypeScript return type - Gate console logging to development/test environments only - Add accessibility attributes (role=alert, aria-live, aria-hidden) - Integrate QueryErrorResetBoundary in ProjectsViewWithBoundary 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: improve code formatting and consistency - Fix line breaks and formatting in TasksTab.tsx task reordering - Clean up import formatting in ProjectsView.tsx - Standardize quote usage in useSmartPolling.ts 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Migrate toast notifications to Radix UI primitives in features directory - Add @radix-ui/react-toast dependency - Create toast.tsx primitive with glassmorphism styling - Implement useToast hook matching legacy API - Add ToastProvider component wrapping Radix primitives - Update all 13 feature files to use new toast system - Maintain dual toast systems (legacy for non-features, new for features) - Fix biome linting issues with auto-formatting This migration establishes Radix UI as the foundation for the features vertical slice architecture while maintaining backward compatibility. Co-Authored-By: Claude <noreply@anthropic.com> * chore: Remove accidentally committed PRP file PRP files are for local development planning only and should not be in version control * refactor: simplify documents feature to read-only viewer - Remove MDXEditor and all editing capabilities due to persistent state issues - Add DocumentViewer component for reliable read-only display - Add migration warning banner clarifying project documents will be lost - Remove all mutation hooks, services, and components - Clean up unused types and dead code - Fix linting issues (SVG accessibility, array keys) - Simplify to display existing JSONB documents from project.docs field This temporary read-only state allows users to view existing documents while the feature undergoes migration to a more robust storage solution. * fix: eliminate duplicate toast notifications and React key warnings - Remove duplicate toast calls from component callbacks (TasksTab, useTaskActions, etc) - Keep toast notifications only in mutation definitions for single source of truth - Add success toast for task status changes in useTaskQueries - Improve toast ID generation with timestamp + random string to prevent duplicates - Remove unused useToast imports from components This fixes the 'Encountered two children with the same key' warning by ensuring only one toast is created per action instead of multiple simultaneous toasts. * feat: add optimistic updates for task and project creation - Implement optimistic updates for useCreateTask mutation - Tasks now appear instantly with temporary ID - Replaced with real task from server on success - Rollback on error with proper error handling - Implement optimistic updates for useCreateProject mutation - Projects appear immediately in the list - Temporary ID replaced with real one on success - Proper rollback on failure - Both mutations follow existing patterns from update/delete operations - Provides instant visual feedback improving perceived performance - Eliminates 2-3 second delay before items appear in UI * style: apply Biome formatting and remove unused dependencies - Format code with Biome standards - Remove unused showToast from useCallback dependencies in TasksTab - Minor formatting adjustments for better readability * fix: remove unused showToast import from TasksTab - Remove unused useToast hook import and usage - Fixes Biome noUnusedVariables error * fix: sort projects by creation date instead of alphabetically - Change project list sorting to: pinned first, then newest first - Ensures new projects appear on the left (after pinned) as expected - Maintains chronological order instead of alphabetical - Better UX for seeing recently created projects * optimize: adjust polling intervals for better performance - Projects: 20s polling (was 10s), 15s stale time (was 3s) - Tasks: 5s polling (was 8s) for faster MCP updates, 10s stale time (was 2s) - Background: 60s for all (was 24-30s) when tab not focused - Hidden tabs: Polling disabled (unchanged) Benefits: - Tasks update faster (5s) to reflect MCP server changes quickly - Projects poll less frequently (20s) as they change less often - Longer stale times reduce unnecessary refetches during navigation - Background polling reduced to save resources when not actively using app * feat: Add ETag support to reduce bandwidth by 70-90% - Created ETag-aware API client (apiWithEtag.ts) with caching - Integrated with TanStack Query for seamless cache management - Updated all services to use ETag-aware API calls - Added cache invalidation after mutations - Handles 304 Not Modified responses efficiently - Includes colored console logging for debugging - Works with 5-second task polling and 20-second project polling * fix: TanStack Query improvements from CodeRabbit review - Fixed concurrent project creation bug by tracking specific temp IDs - Unified task counts query keys to fix cache invalidation - Added TypeScript generics to getQueryData calls for type safety - Added return type to useTaskCounts hook - Prevented double refetch with refetchOnWindowFocus: false - Improved cache cleanup with exact: false on removeQueries 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * feat: improve ProjectList animations, sorting, and accessibility - Added initial/animate props to fix Framer Motion animations - Made sort deterministic with invalid date guards and ID tie-breaker - Added ARIA roles for better screen reader support: - role=status for loading state - role=alert for error state - role=list for project container - role=listitem for ProjectCard - Improved robustness against malformed date strings 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * fix: use consistent ORDER_INCREMENT value for task ordering - Fixed bug where TasksTab used 100 while utils used 1000 for increments - Exported ORDER_INCREMENT constant from task-ordering utils - Updated TasksTab to import and use the shared constant - Ensures consistent task ordering behavior across the application 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * fix: improve type safety and correctness in task mutations - Changed error handling to throw Error objects instead of strings - Added TypeScript generics to delete mutation for better type safety - Fixed incorrect Task shape by removing non-existent fields (deleted_at, subtasks) - Track specific tempId for optimistic updates to avoid replacing wrong tasks 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> * Delete report.md * fix: address CodeRabbit review feedback for TanStack Query implementation - Enable refetchOnWindowFocus for immediate data refresh when returning to tab - Add proper TypeScript generics to useUpdateTask mutation for server response merge - Normalize HTTP methods to uppercase in ETag cache to prevent cache key mismatches - Add ETAG_DEBUG flag to control console logging (only in dev mode) - Fix 304 cache miss handling with proper error and ETag cleanup - Update outdated comments and add explicit type annotations - Rename getETagCacheStats property from 'endpoints' to 'keys' for accuracy --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
136
archon-ui-main/src/features/ui/primitives/alert-dialog.tsx
Normal file
136
archon-ui-main/src/features/ui/primitives/alert-dialog.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
import React from "react";
|
||||
import { cn, glassmorphism } from "./styles";
|
||||
|
||||
// Root
|
||||
export const AlertDialog = AlertDialogPrimitive.Root;
|
||||
|
||||
// Trigger
|
||||
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
||||
|
||||
// Portal
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
||||
|
||||
// Overlay with backdrop blur
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50",
|
||||
"bg-black/50 backdrop-blur-sm",
|
||||
"data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
||||
|
||||
// Content with Tron glassmorphism
|
||||
export const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> & {
|
||||
variant?: "default" | "destructive";
|
||||
}
|
||||
>(({ className, variant = "default", ...props }, ref) => {
|
||||
const variantStyles = {
|
||||
default: cn(
|
||||
"before:bg-gradient-to-r before:from-cyan-500 before:to-fuchsia-500",
|
||||
"before:shadow-[0_0_10px_2px_rgba(34,211,238,0.4)]",
|
||||
"dark:before:shadow-[0_0_20px_5px_rgba(34,211,238,0.7)]",
|
||||
),
|
||||
destructive: cn(
|
||||
"before:bg-red-500",
|
||||
"before:shadow-[0_0_10px_2px_rgba(239,68,68,0.4)]",
|
||||
"dark:before:shadow-[0_0_20px_5px_rgba(239,68,68,0.7)]",
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
|
||||
"p-6 rounded-md backdrop-blur-md",
|
||||
"w-full max-w-lg",
|
||||
glassmorphism.background.card,
|
||||
glassmorphism.border.default,
|
||||
glassmorphism.shadow.elevated,
|
||||
// Top gradient bar
|
||||
"before:content-[''] before:absolute before:top-0 before:left-0 before:right-0",
|
||||
"before:h-[2px] before:rounded-t-[4px]",
|
||||
variantStyles[variant],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
});
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
||||
|
||||
// Header
|
||||
export const AlertDialogHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
||||
),
|
||||
);
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader";
|
||||
|
||||
// Footer
|
||||
export const AlertDialogFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter";
|
||||
|
||||
// Title
|
||||
export const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", "text-gray-900 dark:text-gray-100", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
||||
|
||||
// Description
|
||||
export const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-gray-600 dark:text-gray-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
|
||||
|
||||
// Action (main CTA button)
|
||||
export const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => <AlertDialogPrimitive.Action ref={ref} className={className} {...props} />);
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
||||
|
||||
// Cancel button
|
||||
export const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => <AlertDialogPrimitive.Cancel ref={ref} className={className} {...props} />);
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
||||
132
archon-ui-main/src/features/ui/primitives/button.tsx
Normal file
132
archon-ui-main/src/features/ui/primitives/button.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import React from "react";
|
||||
import { cn } from "./styles";
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "default" | "destructive" | "outline" | "ghost" | "link" | "cyan";
|
||||
size?: "default" | "sm" | "lg" | "icon" | "xs";
|
||||
loading?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "default", size = "default", loading = false, disabled, children, ...props }, ref) => {
|
||||
const baseStyles = cn(
|
||||
"inline-flex items-center justify-center rounded-md font-medium",
|
||||
"transition-all duration-300",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-500",
|
||||
"disabled:pointer-events-none disabled:opacity-50",
|
||||
loading && "cursor-wait",
|
||||
);
|
||||
|
||||
const variants = {
|
||||
default: cn(
|
||||
"backdrop-blur-md",
|
||||
"bg-gradient-to-b from-cyan-500/90 to-cyan-600/90",
|
||||
"dark:from-cyan-400/80 dark:to-cyan-500/80",
|
||||
"text-white dark:text-gray-900",
|
||||
"border border-cyan-400/30 dark:border-cyan-300/30",
|
||||
"hover:from-cyan-400 hover:to-cyan-500",
|
||||
"dark:hover:from-cyan-300 dark:hover:to-cyan-400",
|
||||
"hover:shadow-[0_0_20px_rgba(34,211,238,0.5)]",
|
||||
"dark:hover:shadow-[0_0_25px_rgba(34,211,238,0.7)]",
|
||||
),
|
||||
destructive: cn(
|
||||
"backdrop-blur-md",
|
||||
"bg-gradient-to-b from-red-500/90 to-red-600/90",
|
||||
"dark:from-red-400/80 dark:to-red-500/80",
|
||||
"text-white",
|
||||
"border border-red-400/30 dark:border-red-300/30",
|
||||
"hover:from-red-400 hover:to-red-500",
|
||||
"dark:hover:from-red-300 dark:hover:to-red-400",
|
||||
"hover:shadow-[0_0_20px_rgba(239,68,68,0.5)]",
|
||||
"dark:hover:shadow-[0_0_25px_rgba(239,68,68,0.7)]",
|
||||
),
|
||||
outline: cn(
|
||||
"backdrop-blur-md",
|
||||
"bg-gradient-to-b from-white/50 to-white/30",
|
||||
"dark:from-gray-900/50 dark:to-black/50",
|
||||
"text-gray-900 dark:text-cyan-100",
|
||||
"border border-gray-300/50 dark:border-cyan-500/50",
|
||||
"hover:from-white/70 hover:to-white/50",
|
||||
"dark:hover:from-gray-900/70 dark:hover:to-black/70",
|
||||
"hover:border-cyan-500/50 dark:hover:border-cyan-400/50",
|
||||
"hover:shadow-[0_0_15px_rgba(34,211,238,0.3)]",
|
||||
"dark:hover:shadow-[0_0_20px_rgba(34,211,238,0.5)]",
|
||||
),
|
||||
ghost: cn(
|
||||
"text-gray-700 dark:text-cyan-100",
|
||||
"hover:bg-gray-100/50 dark:hover:bg-cyan-500/10",
|
||||
"hover:backdrop-blur-md",
|
||||
),
|
||||
link: cn(
|
||||
"text-cyan-600 dark:text-cyan-400",
|
||||
"underline-offset-4 hover:underline",
|
||||
"hover:text-cyan-500 dark:hover:text-cyan-300",
|
||||
),
|
||||
cyan: cn(
|
||||
"backdrop-blur-md",
|
||||
"bg-gradient-to-b from-cyan-100/80 to-white/60",
|
||||
"dark:from-cyan-500/20 dark:to-cyan-500/10",
|
||||
"text-cyan-700 dark:text-cyan-100",
|
||||
"border border-cyan-300/50 dark:border-cyan-500/50",
|
||||
"hover:from-cyan-200/90 hover:to-cyan-100/70",
|
||||
"dark:hover:from-cyan-400/30 dark:hover:to-cyan-500/20",
|
||||
"hover:shadow-[0_0_20px_rgba(34,211,238,0.5)]",
|
||||
"dark:hover:shadow-[0_0_25px_rgba(34,211,238,0.7)]",
|
||||
),
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
xs: "h-7 px-2 text-xs",
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn(baseStyles, variants[variant], sizes[size], className)}
|
||||
ref={ref}
|
||||
disabled={disabled || loading}
|
||||
{...props}
|
||||
>
|
||||
{loading && (
|
||||
<svg
|
||||
className="mr-2 h-4 w-4 animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="Loading"
|
||||
role="img"
|
||||
>
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
|
||||
export interface IconButtonProps extends Omit<ButtonProps, "size" | "children"> {
|
||||
icon: React.ReactNode;
|
||||
"aria-label": string;
|
||||
}
|
||||
|
||||
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(({ icon, className, ...props }, ref) => {
|
||||
return (
|
||||
<Button ref={ref} size="icon" className={cn("relative", className)} {...props}>
|
||||
{icon}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
IconButton.displayName = "IconButton";
|
||||
228
archon-ui-main/src/features/ui/primitives/combobox.tsx
Normal file
228
archon-ui-main/src/features/ui/primitives/combobox.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* ComboBox Primitive
|
||||
*
|
||||
* A searchable dropdown component built with Radix UI Popover and Command
|
||||
* Provides autocomplete functionality with keyboard navigation
|
||||
*/
|
||||
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Button } from "./button";
|
||||
import { cn } from "./styles";
|
||||
|
||||
export interface ComboBoxOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface ComboBoxProps {
|
||||
options: ComboBoxOption[];
|
||||
value?: string;
|
||||
onValueChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
searchPlaceholder?: string;
|
||||
emptyMessage?: string;
|
||||
className?: string;
|
||||
isLoading?: boolean;
|
||||
allowCustomValue?: boolean;
|
||||
}
|
||||
|
||||
export const ComboBox = React.forwardRef<HTMLButtonElement, ComboBoxProps>(
|
||||
(
|
||||
{
|
||||
options,
|
||||
value,
|
||||
onValueChange,
|
||||
placeholder = "Select option...",
|
||||
searchPlaceholder = "Search...",
|
||||
emptyMessage = "No results found.",
|
||||
className,
|
||||
isLoading = false,
|
||||
allowCustomValue = false,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [search, setSearch] = React.useState("");
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
// Filter options based on search
|
||||
const filteredOptions = React.useMemo(() => {
|
||||
if (!search) return options;
|
||||
|
||||
const searchLower = search.toLowerCase();
|
||||
return options.filter(
|
||||
(option) =>
|
||||
option.label.toLowerCase().includes(searchLower) ||
|
||||
option.value.toLowerCase().includes(searchLower) ||
|
||||
option.description?.toLowerCase().includes(searchLower),
|
||||
);
|
||||
}, [options, search]);
|
||||
|
||||
// Find current option label
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
const displayValue = selectedOption?.label || value || "";
|
||||
|
||||
// Handle selection
|
||||
const handleSelect = (optionValue: string) => {
|
||||
onValueChange(optionValue);
|
||||
setOpen(false);
|
||||
setSearch("");
|
||||
};
|
||||
|
||||
// Handle custom value input
|
||||
const handleCustomValue = () => {
|
||||
if (allowCustomValue && search && !filteredOptions.some((opt) => opt.label === search)) {
|
||||
onValueChange(search);
|
||||
setOpen(false);
|
||||
setSearch("");
|
||||
}
|
||||
};
|
||||
|
||||
// Focus input when opening
|
||||
React.useEffect(() => {
|
||||
if (open && inputRef.current) {
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className={cn(
|
||||
"w-full justify-between font-normal",
|
||||
!displayValue && "text-gray-500 dark:text-gray-400",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="truncate">
|
||||
{isLoading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
Loading...
|
||||
</span>
|
||||
) : (
|
||||
displayValue || placeholder
|
||||
)}
|
||||
</span>
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
className={cn(
|
||||
"w-full min-w-[var(--radix-popover-trigger-width)] max-h-[300px] p-1",
|
||||
"bg-gradient-to-b from-white/95 to-white/90",
|
||||
"dark:from-gray-900/95 dark:to-black/95",
|
||||
"backdrop-blur-xl",
|
||||
"border border-gray-200 dark:border-gray-700",
|
||||
"rounded-lg shadow-xl",
|
||||
"shadow-cyan-500/10 dark:shadow-cyan-400/10",
|
||||
"z-50",
|
||||
)}
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
>
|
||||
{/* Search Input */}
|
||||
<div className="p-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && allowCustomValue && search) {
|
||||
e.preventDefault();
|
||||
handleCustomValue();
|
||||
}
|
||||
}}
|
||||
placeholder={searchPlaceholder}
|
||||
className={cn(
|
||||
"w-full px-3 py-1.5 text-sm",
|
||||
"bg-white/50 dark:bg-black/50",
|
||||
"border border-gray-200 dark:border-gray-700",
|
||||
"rounded-md",
|
||||
"text-gray-900 dark:text-white",
|
||||
"placeholder-gray-500 dark:placeholder-gray-400",
|
||||
"focus:outline-none focus:border-cyan-400",
|
||||
"focus:shadow-[0_0_10px_rgba(34,211,238,0.2)]",
|
||||
"transition-all duration-200",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Options List */}
|
||||
<div className="overflow-y-auto max-h-[200px] p-1">
|
||||
{isLoading ? (
|
||||
<div className="py-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<Loader2 className="h-4 w-4 animate-spin mx-auto mb-2" />
|
||||
Loading options...
|
||||
</div>
|
||||
) : filteredOptions.length === 0 ? (
|
||||
<div className="py-6 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{emptyMessage}
|
||||
{allowCustomValue && search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCustomValue}
|
||||
className={cn(
|
||||
"mt-2 block w-full",
|
||||
"px-3 py-1.5 text-left text-sm",
|
||||
"bg-cyan-50/50 dark:bg-cyan-900/20",
|
||||
"text-cyan-600 dark:text-cyan-400",
|
||||
"rounded-md",
|
||||
"hover:bg-cyan-100/50 dark:hover:bg-cyan-800/30",
|
||||
"transition-colors duration-200",
|
||||
)}
|
||||
>
|
||||
Create "{search}"
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
filteredOptions.map((option) => (
|
||||
<button
|
||||
type="button"
|
||||
key={option.value}
|
||||
onClick={() => handleSelect(option.value)}
|
||||
className={cn(
|
||||
"relative flex w-full items-center px-3 py-2",
|
||||
"text-sm rounded-md",
|
||||
"hover:bg-gray-100/80 dark:hover:bg-white/10",
|
||||
"text-gray-900 dark:text-white",
|
||||
"transition-colors duration-200",
|
||||
"focus:outline-none focus:bg-gray-100/80 dark:focus:bg-white/10",
|
||||
value === option.value && "bg-cyan-50/50 dark:bg-cyan-900/20",
|
||||
)}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
value === option.value ? "opacity-100 text-cyan-600 dark:text-cyan-400" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="font-medium">{option.label}</div>
|
||||
{option.description && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{option.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ComboBox.displayName = "ComboBox";
|
||||
133
archon-ui-main/src/features/ui/primitives/dialog.tsx
Normal file
133
archon-ui-main/src/features/ui/primitives/dialog.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import React from "react";
|
||||
import { cn } from "./styles";
|
||||
|
||||
// Dialog Root and Trigger
|
||||
export const Dialog = DialogPrimitive.Root;
|
||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
||||
export const DialogPortal = DialogPrimitive.Portal;
|
||||
export const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
// Dialog Overlay with glassmorphism
|
||||
export const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50",
|
||||
"backdrop-blur-sm bg-black/50 dark:bg-black/70",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
// Dialog Content with Tron-style glassmorphism
|
||||
export const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}
|
||||
>(({ className, children, showCloseButton = true, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
|
||||
"p-6 rounded-md backdrop-blur-md",
|
||||
"w-full max-w-2xl",
|
||||
// Matching original glassmorphism
|
||||
"bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30",
|
||||
"border border-gray-200 dark:border-zinc-800/50",
|
||||
"shadow-[0_10px_30px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)]",
|
||||
// Top gradient bar (matching original)
|
||||
"before:content-[''] before:absolute before:top-0 before:left-0 before:right-0",
|
||||
"before:h-[2px] before:rounded-t-[4px]",
|
||||
"before:bg-gradient-to-r before:from-cyan-500 before:to-fuchsia-500",
|
||||
"before:shadow-[0_0_10px_2px_rgba(34,211,238,0.4)]",
|
||||
"dark:before:shadow-[0_0_20px_5px_rgba(34,211,238,0.7)]",
|
||||
// Top gradient glow (matching original)
|
||||
"after:content-[''] after:absolute after:top-0 after:left-0 after:right-0",
|
||||
"after:h-16 after:bg-gradient-to-b",
|
||||
"after:from-cyan-100 after:to-white dark:after:from-cyan-500/20 dark:after:to-fuchsia-500/5",
|
||||
"after:rounded-t-md after:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative z-10">{children}</div>
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
className={cn(
|
||||
"absolute right-4 top-4 z-20",
|
||||
"text-gray-500 dark:text-gray-400",
|
||||
"hover:text-gray-700 dark:hover:text-white",
|
||||
"transition-colors",
|
||||
)}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
// Dialog Header
|
||||
export const DialogHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex flex-col space-y-1.5 text-center sm:text-left", "mb-4", className)} {...props} />
|
||||
),
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
// Dialog Footer
|
||||
export const DialogFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", "mt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
// Dialog Title
|
||||
export const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-xl font-bold",
|
||||
"bg-gradient-to-r from-cyan-400 to-fuchsia-500",
|
||||
"text-transparent bg-clip-text",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
// Dialog Description
|
||||
export const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-gray-600 dark:text-gray-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
161
archon-ui-main/src/features/ui/primitives/dropdown-menu.tsx
Normal file
161
archon-ui-main/src/features/ui/primitives/dropdown-menu.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, Circle } from "lucide-react";
|
||||
import React from "react";
|
||||
import { cn, glassmorphism } from "./styles";
|
||||
|
||||
// Core components
|
||||
export const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
export const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
// Dropdown Menu Content with Tron glassmorphism
|
||||
export const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-[10000] min-w-[8rem] overflow-hidden rounded-lg p-1",
|
||||
// Glassmorphism
|
||||
glassmorphism.background.strong,
|
||||
glassmorphism.border.default,
|
||||
glassmorphism.shadow.lg,
|
||||
// Animation
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2",
|
||||
"data-[side=left]:slide-in-from-right-2",
|
||||
"data-[side=right]:slide-in-from-left-2",
|
||||
"data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
// Menu Item with hover effects
|
||||
export const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
icon?: React.ReactNode;
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, icon, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm",
|
||||
"transition-all duration-150 outline-none",
|
||||
glassmorphism.interactive.hover,
|
||||
"focus:bg-cyan-500/10 dark:focus:bg-cyan-400/10",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{icon && <span className="flex-shrink-0">{icon}</span>}
|
||||
{props.children}
|
||||
</DropdownMenuPrimitive.Item>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
// Checkbox Item
|
||||
export const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm",
|
||||
"transition-all duration-150 outline-none",
|
||||
glassmorphism.interactive.hover,
|
||||
"focus:bg-cyan-500/10 dark:focus:bg-cyan-400/10",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
// Radio Item
|
||||
export const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-pointer select-none items-center rounded-md py-1.5 pl-8 pr-2 text-sm",
|
||||
"transition-all duration-150 outline-none",
|
||||
glassmorphism.interactive.hover,
|
||||
"focus:bg-cyan-500/10 dark:focus:bg-cyan-400/10",
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
// Label
|
||||
export const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("px-2 py-1.5 text-xs font-semibold text-gray-600 dark:text-gray-400", inset && "pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
// Separator
|
||||
export const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-gray-200 dark:bg-gray-700", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
// Shortcut
|
||||
export const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span className={cn("ml-auto text-xs tracking-widest text-gray-500 dark:text-gray-400", className)} {...props} />
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
26
archon-ui-main/src/features/ui/primitives/index.ts
Normal file
26
archon-ui-main/src/features/ui/primitives/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Radix UI Primitives with Glassmorphism Styling
|
||||
*
|
||||
* This is our design system foundation for the /features directory.
|
||||
* All new components in features should use these primitives.
|
||||
*
|
||||
* Migration strategy:
|
||||
* - Old components in /components use legacy custom UI
|
||||
* - New components in /features use these Radix primitives
|
||||
* - Gradually migrate as we refactor
|
||||
*/
|
||||
|
||||
export * from "./alert-dialog";
|
||||
|
||||
// Export all primitives
|
||||
export * from "./button";
|
||||
export * from "./combobox";
|
||||
export * from "./dialog";
|
||||
export * from "./dropdown-menu";
|
||||
export * from "./input";
|
||||
export * from "./select";
|
||||
// Export style utilities
|
||||
export * from "./styles";
|
||||
export * from "./tabs";
|
||||
export * from "./toast";
|
||||
export * from "./tooltip";
|
||||
110
archon-ui-main/src/features/ui/primitives/input.tsx
Normal file
110
archon-ui-main/src/features/ui/primitives/input.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
import { cn } from "./styles";
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
export const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, error, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"w-full rounded-md py-2 px-3",
|
||||
"bg-white/50 dark:bg-black/70",
|
||||
"border border-gray-300 dark:border-gray-700",
|
||||
"text-gray-900 dark:text-white",
|
||||
"placeholder:text-gray-500 dark:placeholder:text-gray-400",
|
||||
"focus:outline-none focus:border-cyan-400",
|
||||
"focus:shadow-[0_0_10px_rgba(34,211,238,0.2)]",
|
||||
"transition-all duration-300",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
error && "border-red-500 dark:border-red-400 focus:border-red-500 focus:shadow-[0_0_10px_rgba(239,68,68,0.2)]",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Input.displayName = "Input";
|
||||
|
||||
export interface TextAreaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
error?: boolean;
|
||||
}
|
||||
|
||||
export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(({ className, error, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"w-full rounded-md py-2 px-3",
|
||||
"bg-white/50 dark:bg-black/70",
|
||||
"border border-gray-300 dark:border-gray-700",
|
||||
"text-gray-900 dark:text-white",
|
||||
"placeholder:text-gray-500 dark:placeholder:text-gray-400",
|
||||
"focus:outline-none focus:border-cyan-400",
|
||||
"focus:shadow-[0_0_10px_rgba(34,211,238,0.2)]",
|
||||
"transition-all duration-300",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"resize-y min-h-[80px]",
|
||||
error && "border-red-500 dark:border-red-400 focus:border-red-500 focus:shadow-[0_0_10px_rgba(239,68,68,0.2)]",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
TextArea.displayName = "TextArea";
|
||||
|
||||
// Label component for form fields
|
||||
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
||||
({ className, children, required, ...props }, ref) => {
|
||||
return (
|
||||
// biome-ignore lint/a11y/noLabelWithoutControl: htmlFor is passed through props spread
|
||||
<label
|
||||
ref={ref}
|
||||
className={cn("block text-sm font-medium", "text-gray-700 dark:text-gray-300", "mb-1", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{required && <span className="text-red-500 ml-1">*</span>}
|
||||
</label>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Label.displayName = "Label";
|
||||
|
||||
// FormField wrapper for consistent spacing
|
||||
export interface FormFieldProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const FormField: React.FC<FormFieldProps> = ({ children, className }) => {
|
||||
return <div className={cn("space-y-1", className)}>{children}</div>;
|
||||
};
|
||||
|
||||
// FormGrid for two-column layouts
|
||||
export interface FormGridProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
columns?: 1 | 2 | 3;
|
||||
}
|
||||
|
||||
export const FormGrid: React.FC<FormGridProps> = ({ children, className, columns = 2 }) => {
|
||||
const gridCols = {
|
||||
1: "grid-cols-1",
|
||||
2: "grid-cols-2",
|
||||
3: "grid-cols-3",
|
||||
};
|
||||
|
||||
return <div className={cn("grid gap-4", gridCols[columns], className)}>{children}</div>;
|
||||
};
|
||||
136
archon-ui-main/src/features/ui/primitives/select.tsx
Normal file
136
archon-ui-main/src/features/ui/primitives/select.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import React from "react";
|
||||
import { cn } from "./styles";
|
||||
|
||||
// Select Root - just re-export
|
||||
export const Select = SelectPrimitive.Root;
|
||||
export const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
// Select Trigger with glassmorphism styling
|
||||
export const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> & {
|
||||
showChevron?: boolean;
|
||||
}
|
||||
>(({ className = "", children, showChevron = true, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-2 px-3 py-2 rounded-lg",
|
||||
"backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60",
|
||||
"dark:from-white/10 dark:to-black/30",
|
||||
"border border-gray-200 dark:border-gray-700",
|
||||
"transition-all duration-200",
|
||||
"hover:border-cyan-400/50 hover:shadow-[0_0_10px_rgba(34,211,238,0.2)]",
|
||||
"focus:outline-none focus:border-cyan-500 focus:shadow-[0_0_15px_rgba(34,211,238,0.3)]",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"data-[placeholder]:text-gray-500 dark:data-[placeholder]:text-gray-400",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showChevron && (
|
||||
<SelectPrimitive.Icon className="ml-auto">
|
||||
<ChevronDown className="w-3 h-3 opacity-60" />
|
||||
</SelectPrimitive.Icon>
|
||||
)}
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
// Select Content with glassmorphism and Portal for z-index solution
|
||||
export const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className = "", children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-[10000] min-w-[8rem] overflow-hidden rounded-lg",
|
||||
// Matching our card glassmorphism
|
||||
"backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60",
|
||||
"dark:from-white/10 dark:to-black/30",
|
||||
"border border-gray-200 dark:border-zinc-800/50",
|
||||
// Tron shadow with subtle cyan glow
|
||||
"shadow-[0_10px_30px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)]",
|
||||
"shadow-cyan-500/5 dark:shadow-cyan-500/10",
|
||||
// Text colors matching rest of app
|
||||
"text-gray-900 dark:text-gray-100",
|
||||
// Animation
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2",
|
||||
"data-[side=left]:slide-in-from-right-2",
|
||||
"data-[side=right]:slide-in-from-left-2",
|
||||
"data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
sideOffset={5}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport className="p-1">{children}</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
// Select Item with hover effects
|
||||
export const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & {
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
>(({ className = "", children, icon, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex items-center text-sm outline-none",
|
||||
"transition-all duration-150 cursor-pointer rounded-md",
|
||||
"pl-8 pr-3 py-2", // Added left padding for checkmark space
|
||||
// Text colors
|
||||
"text-gray-700 dark:text-gray-200",
|
||||
// Hover state with subtle cyan tint
|
||||
"hover:bg-cyan-500/20 dark:hover:bg-cyan-400/20",
|
||||
"hover:text-gray-900 dark:hover:text-white",
|
||||
// Focus state
|
||||
"focus:bg-cyan-500/20 dark:focus:bg-cyan-400/20",
|
||||
"focus:text-gray-900 dark:focus:text-white",
|
||||
// Disabled state
|
||||
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
// Selected/checked state with stronger cyan
|
||||
"data-[state=checked]:bg-cyan-500/30 dark:data-[state=checked]:bg-cyan-400/30",
|
||||
"data-[state=checked]:text-cyan-700 dark:data-[state=checked]:text-cyan-300",
|
||||
"data-[state=checked]:font-medium",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator className="absolute left-2 flex h-4 w-4 items-center justify-center">
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
<SelectPrimitive.ItemText className="flex items-center gap-2">
|
||||
{icon && <span className="flex-shrink-0">{icon}</span>}
|
||||
{children}
|
||||
</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
// Export group and label for completeness
|
||||
export const SelectGroup = SelectPrimitive.Group;
|
||||
export const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className = "", ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={`px-2 py-1.5 text-xs font-semibold text-gray-600 dark:text-gray-400 ${className}`}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
141
archon-ui-main/src/features/ui/primitives/styles.ts
Normal file
141
archon-ui-main/src/features/ui/primitives/styles.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Shared style utilities for Radix primitives
|
||||
* Tron-inspired glassmorphism design system
|
||||
*
|
||||
* Theme Support:
|
||||
* - All styles use Tailwind's dark: prefix for automatic theme switching
|
||||
* - Theme is managed by ThemeContext (light/dark)
|
||||
* - For runtime theme values, use useThemeAware hook
|
||||
*/
|
||||
|
||||
// Base glassmorphism classes with Tron aesthetic
|
||||
export const glassmorphism = {
|
||||
// Background variations - matching existing Card.tsx patterns
|
||||
background: {
|
||||
subtle: "backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30",
|
||||
strong: "backdrop-blur-md bg-gradient-to-b from-white/95 to-white/90 dark:from-gray-800/95 dark:to-gray-900/95",
|
||||
card: "backdrop-blur-md bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30",
|
||||
// Tron-style colored backgrounds
|
||||
cyan: "backdrop-blur-md bg-gradient-to-b from-cyan-100/80 dark:from-cyan-500/20 to-white/60 dark:to-cyan-500/5",
|
||||
blue: "backdrop-blur-md bg-gradient-to-b from-blue-100/80 dark:from-blue-500/20 to-white/60 dark:to-blue-500/5",
|
||||
purple:
|
||||
"backdrop-blur-md bg-gradient-to-b from-purple-100/80 dark:from-purple-500/20 to-white/60 dark:to-purple-500/5",
|
||||
},
|
||||
|
||||
// Border styles with neon glow
|
||||
border: {
|
||||
default: "border border-gray-200 dark:border-gray-700",
|
||||
cyan: "border-cyan-300 dark:border-cyan-500/30",
|
||||
blue: "border-blue-300 dark:border-blue-500/30",
|
||||
purple: "border-purple-300 dark:border-purple-500/30",
|
||||
focus: "focus:border-cyan-500 focus:shadow-[0_0_20px_5px_rgba(34,211,238,0.5)]",
|
||||
hover: "hover:border-cyan-400/70 hover:shadow-[0_0_15px_rgba(34,211,238,0.4)]",
|
||||
},
|
||||
|
||||
// Interactive states
|
||||
interactive: {
|
||||
base: "transition-all duration-200",
|
||||
hover: "hover:bg-cyan-500/10 dark:hover:bg-cyan-400/10",
|
||||
active: "active:bg-cyan-500/20 dark:active:bg-cyan-400/20",
|
||||
selected:
|
||||
"data-[state=checked]:bg-cyan-500/20 dark:data-[state=checked]:bg-cyan-400/20 data-[state=checked]:text-cyan-700 dark:data-[state=checked]:text-cyan-300",
|
||||
disabled: "disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
},
|
||||
|
||||
// Animation presets
|
||||
animation: {
|
||||
fadeIn:
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
slideIn: "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
||||
slideFromTop: "data-[side=bottom]:slide-in-from-top-2",
|
||||
slideFromBottom: "data-[side=top]:slide-in-from-bottom-2",
|
||||
slideFromLeft: "data-[side=right]:slide-in-from-left-2",
|
||||
slideFromRight: "data-[side=left]:slide-in-from-right-2",
|
||||
},
|
||||
|
||||
// Shadow effects with Tron-style neon glow
|
||||
shadow: {
|
||||
sm: "shadow-sm dark:shadow-md",
|
||||
md: "shadow-md dark:shadow-lg",
|
||||
lg: "shadow-lg dark:shadow-2xl",
|
||||
elevated: "shadow-[0_10px_30px_-15px_rgba(0,0,0,0.1)] dark:shadow-[0_10px_30px_-15px_rgba(0,0,0,0.7)]",
|
||||
// Neon glow effects matching Card.tsx
|
||||
glow: {
|
||||
cyan: "shadow-[0_0_10px_2px_rgba(34,211,238,0.4)] dark:shadow-[0_0_20px_5px_rgba(34,211,238,0.7)]",
|
||||
blue: "shadow-[0_0_10px_2px_rgba(59,130,246,0.4)] dark:shadow-[0_0_20px_5px_rgba(59,130,246,0.7)]",
|
||||
purple: "shadow-[0_0_10px_2px_rgba(168,85,247,0.4)] dark:shadow-[0_0_20px_5px_rgba(168,85,247,0.7)]",
|
||||
green: "shadow-[0_0_10px_2px_rgba(16,185,129,0.4)] dark:shadow-[0_0_20px_5px_rgba(16,185,129,0.7)]",
|
||||
pink: "shadow-[0_0_10px_2px_rgba(236,72,153,0.4)] dark:shadow-[0_0_20px_5px_rgba(236,72,153,0.7)]",
|
||||
orange: "shadow-[0_0_10px_2px_rgba(251,146,60,0.4)] dark:shadow-[0_0_20px_5px_rgba(251,146,60,0.7)]",
|
||||
},
|
||||
},
|
||||
|
||||
// Priority colors (matching our task system)
|
||||
priority: {
|
||||
critical: {
|
||||
background: "bg-red-100/80 dark:bg-red-500/20",
|
||||
text: "text-red-600 dark:text-red-400",
|
||||
hover: "hover:bg-red-200 dark:hover:bg-red-500/30",
|
||||
glow: "hover:shadow-[0_0_10px_rgba(239,68,68,0.3)]",
|
||||
},
|
||||
high: {
|
||||
background: "bg-orange-100/80 dark:bg-orange-500/20",
|
||||
text: "text-orange-600 dark:text-orange-400",
|
||||
hover: "hover:bg-orange-200 dark:hover:bg-orange-500/30",
|
||||
glow: "hover:shadow-[0_0_10px_rgba(249,115,22,0.3)]",
|
||||
},
|
||||
medium: {
|
||||
background: "bg-blue-100/80 dark:bg-blue-500/20",
|
||||
text: "text-blue-600 dark:text-blue-400",
|
||||
hover: "hover:bg-blue-200 dark:hover:bg-blue-500/30",
|
||||
glow: "hover:shadow-[0_0_10px_rgba(59,130,246,0.3)]",
|
||||
},
|
||||
low: {
|
||||
background: "bg-gray-100/80 dark:bg-gray-500/20",
|
||||
text: "text-gray-600 dark:text-gray-400",
|
||||
hover: "hover:bg-gray-200 dark:hover:bg-gray-500/30",
|
||||
glow: "hover:shadow-[0_0_10px_rgba(107,114,128,0.3)]",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Compound styles for common patterns
|
||||
export const compoundStyles = {
|
||||
// Standard interactive element (buttons, menu items, etc.)
|
||||
interactiveElement: `
|
||||
${glassmorphism.interactive.base}
|
||||
${glassmorphism.interactive.hover}
|
||||
${glassmorphism.interactive.disabled}
|
||||
`,
|
||||
|
||||
// Floating panels (dropdowns, popovers, tooltips)
|
||||
floatingPanel: `
|
||||
${glassmorphism.background.strong}
|
||||
${glassmorphism.border.default}
|
||||
${glassmorphism.shadow.lg}
|
||||
${glassmorphism.animation.fadeIn}
|
||||
${glassmorphism.animation.slideIn}
|
||||
`,
|
||||
|
||||
// Form controls (inputs, selects, etc.)
|
||||
formControl: `
|
||||
${glassmorphism.background.subtle}
|
||||
${glassmorphism.border.default}
|
||||
${glassmorphism.border.hover}
|
||||
${glassmorphism.border.focus}
|
||||
${glassmorphism.interactive.base}
|
||||
${glassmorphism.interactive.disabled}
|
||||
`,
|
||||
|
||||
// Cards and containers
|
||||
card: `
|
||||
${glassmorphism.background.card}
|
||||
${glassmorphism.border.default}
|
||||
${glassmorphism.shadow.md}
|
||||
`,
|
||||
};
|
||||
|
||||
// Utility function to combine classes
|
||||
export function cn(...classes: (string | undefined | false)[]): string {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
103
archon-ui-main/src/features/ui/primitives/tabs.tsx
Normal file
103
archon-ui-main/src/features/ui/primitives/tabs.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import React from "react";
|
||||
import { cn } from "./styles";
|
||||
|
||||
// Root
|
||||
export const Tabs = TabsPrimitive.Root;
|
||||
|
||||
// List
|
||||
export const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List ref={ref} className={cn("relative", className)} role="tablist" {...props}>
|
||||
{/* Subtle neon glow effect */}
|
||||
<div className="absolute inset-0 rounded-lg opacity-30 blur-[1px] bg-gradient-to-r from-blue-500/10 via-purple-500/10 to-pink-500/10 pointer-events-none" />
|
||||
{props.children}
|
||||
</TabsPrimitive.List>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
// Trigger
|
||||
export const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & {
|
||||
color?: "blue" | "purple" | "pink" | "orange" | "cyan" | "green";
|
||||
}
|
||||
>(({ className, color = "blue", ...props }, ref) => {
|
||||
const colorMap = {
|
||||
blue: {
|
||||
text: "data-[state=active]:text-blue-600 dark:data-[state=active]:text-blue-400",
|
||||
glow: "data-[state=active]:bg-blue-500 data-[state=active]:shadow-[0_0_10px_2px_rgba(59,130,246,0.4)] dark:data-[state=active]:shadow-[0_0_20px_5px_rgba(59,130,246,0.7)]",
|
||||
hover: "hover:text-blue-500 dark:hover:text-blue-400/70",
|
||||
},
|
||||
purple: {
|
||||
text: "data-[state=active]:text-purple-600 dark:data-[state=active]:text-purple-400",
|
||||
glow: "data-[state=active]:bg-purple-500 data-[state=active]:shadow-[0_0_10px_2px_rgba(168,85,247,0.4)] dark:data-[state=active]:shadow-[0_0_20px_5px_rgba(168,85,247,0.7)]",
|
||||
hover: "hover:text-purple-500 dark:hover:text-purple-400/70",
|
||||
},
|
||||
pink: {
|
||||
text: "data-[state=active]:text-pink-600 dark:data-[state=active]:text-pink-400",
|
||||
glow: "data-[state=active]:bg-pink-500 data-[state=active]:shadow-[0_0_10px_2px_rgba(236,72,153,0.4)] dark:data-[state=active]:shadow-[0_0_20px_5px_rgba(236,72,153,0.7)]",
|
||||
hover: "hover:text-pink-500 dark:hover:text-pink-400/70",
|
||||
},
|
||||
orange: {
|
||||
text: "data-[state=active]:text-orange-600 dark:data-[state=active]:text-orange-400",
|
||||
glow: "data-[state=active]:bg-orange-500 data-[state=active]:shadow-[0_0_10px_2px_rgba(249,115,22,0.4)] dark:data-[state=active]:shadow-[0_0_20px_5px_rgba(249,115,22,0.7)]",
|
||||
hover: "hover:text-orange-500 dark:hover:text-orange-400/70",
|
||||
},
|
||||
cyan: {
|
||||
text: "data-[state=active]:text-cyan-600 dark:data-[state=active]:text-cyan-400",
|
||||
glow: "data-[state=active]:bg-cyan-500 data-[state=active]:shadow-[0_0_10px_2px_rgba(34,211,238,0.4)] dark:data-[state=active]:shadow-[0_0_20px_5px_rgba(34,211,238,0.7)]",
|
||||
hover: "hover:text-cyan-500 dark:hover:text-cyan-400/70",
|
||||
},
|
||||
green: {
|
||||
text: "data-[state=active]:text-emerald-600 dark:data-[state=active]:text-emerald-400",
|
||||
glow: "data-[state=active]:bg-emerald-500 data-[state=active]:shadow-[0_0_10px_2px_rgba(16,185,129,0.4)] dark:data-[state=active]:shadow-[0_0_20px_5px_rgba(16,185,129,0.7)]",
|
||||
hover: "hover:text-emerald-500 dark:hover:text-emerald-400/70",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative px-24 py-10 font-mono transition-all duration-300 z-10",
|
||||
"text-gray-600 dark:text-gray-400",
|
||||
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
|
||||
"disabled:pointer-events-none disabled:opacity-50",
|
||||
colorMap[color].text,
|
||||
colorMap[color].hover,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
{/* Active state neon indicator - only show when active */}
|
||||
<span
|
||||
className={cn(
|
||||
"absolute bottom-0 left-0 right-0 w-full h-[2px]",
|
||||
"data-[state=active]:block hidden",
|
||||
colorMap[color].glow,
|
||||
)}
|
||||
/>
|
||||
</TabsPrimitive.Trigger>
|
||||
);
|
||||
});
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
// Content
|
||||
export const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
161
archon-ui-main/src/features/ui/primitives/toast.tsx
Normal file
161
archon-ui-main/src/features/ui/primitives/toast.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import * as ToastPrimitive from "@radix-ui/react-toast";
|
||||
import { AlertCircle, CheckCircle, Info, X, XCircle } from "lucide-react";
|
||||
import React from "react";
|
||||
import { cn, glassmorphism } from "./styles";
|
||||
|
||||
// Toast Provider - wraps the app
|
||||
export const ToastProvider = ToastPrimitive.Provider;
|
||||
|
||||
// Toast Viewport - where toasts appear
|
||||
export const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitive.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-4 right-4 z-[100]",
|
||||
"flex flex-col gap-2",
|
||||
"w-full max-w-[420px]",
|
||||
"pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitive.Viewport.displayName;
|
||||
|
||||
// Toast Root
|
||||
export const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitive.Root> & {
|
||||
variant?: "default" | "success" | "error" | "warning";
|
||||
}
|
||||
>(({ className, variant = "default", ...props }, ref) => {
|
||||
const variantStyles = {
|
||||
default: cn(glassmorphism.background.card, glassmorphism.border.default, glassmorphism.shadow.elevated),
|
||||
success: cn(
|
||||
"backdrop-blur-md bg-gradient-to-b from-green-100/80 dark:from-green-500/20 to-white/60 dark:to-green-500/5",
|
||||
"border-green-300 dark:border-green-500/30",
|
||||
"shadow-[0_0_10px_2px_rgba(16,185,129,0.4)] dark:shadow-[0_0_20px_5px_rgba(16,185,129,0.7)]",
|
||||
),
|
||||
error: cn(
|
||||
"backdrop-blur-md bg-gradient-to-b from-red-100/80 dark:from-red-500/20 to-white/60 dark:to-red-500/5",
|
||||
"border-red-300 dark:border-red-500/30",
|
||||
"shadow-[0_0_10px_2px_rgba(239,68,68,0.4)] dark:shadow-[0_0_20px_5px_rgba(239,68,68,0.7)]",
|
||||
),
|
||||
warning: cn(
|
||||
"backdrop-blur-md bg-gradient-to-b from-orange-100/80 dark:from-orange-500/20 to-white/60 dark:to-orange-500/5",
|
||||
"border-orange-300 dark:border-orange-500/30",
|
||||
"shadow-[0_0_10px_2px_rgba(251,146,60,0.4)] dark:shadow-[0_0_20px_5px_rgba(251,146,60,0.7)]",
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative group p-4 rounded-md border",
|
||||
"pointer-events-auto",
|
||||
"transition-all duration-200",
|
||||
glassmorphism.animation.fadeIn,
|
||||
"data-[swipe=cancel]:transform-none",
|
||||
"data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]",
|
||||
"data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]",
|
||||
"data-[state=open]:slide-in-from-right",
|
||||
"data-[state=closed]:fade-out-80",
|
||||
variantStyles[variant],
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitive.Root.displayName;
|
||||
|
||||
// Toast Action
|
||||
export const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center",
|
||||
"rounded-md px-3 py-1 text-sm font-medium",
|
||||
"bg-cyan-500/20 dark:bg-cyan-400/20",
|
||||
"border border-cyan-300 dark:border-cyan-500/30",
|
||||
"text-cyan-700 dark:text-cyan-300",
|
||||
"hover:bg-cyan-500/30 dark:hover:bg-cyan-400/30",
|
||||
glassmorphism.interactive.base,
|
||||
glassmorphism.interactive.disabled,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitive.Action.displayName;
|
||||
|
||||
// Toast Close
|
||||
export const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitive.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitive.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitive.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute top-2 right-2",
|
||||
"text-gray-500 dark:text-gray-400",
|
||||
"hover:text-gray-700 dark:hover:text-white",
|
||||
"transition-colors",
|
||||
"opacity-0 group-hover:opacity-100",
|
||||
"focus:opacity-100 focus:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</ToastPrimitive.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitive.Close.displayName;
|
||||
|
||||
// Toast Title
|
||||
export const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold text-gray-900 dark:text-white", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitive.Title.displayName;
|
||||
|
||||
// Toast Description
|
||||
export const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("mt-1 text-sm text-gray-600 dark:text-gray-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitive.Description.displayName;
|
||||
|
||||
// Helper function to get icon for toast type
|
||||
export function getToastIcon(type: "success" | "error" | "info" | "warning") {
|
||||
switch (type) {
|
||||
case "success":
|
||||
return <CheckCircle className="h-5 w-5 text-green-600 dark:text-green-400" />;
|
||||
case "error":
|
||||
return <XCircle className="h-5 w-5 text-red-600 dark:text-red-400" />;
|
||||
case "info":
|
||||
return <Info className="h-5 w-5 text-blue-600 dark:text-blue-400" />;
|
||||
case "warning":
|
||||
return <AlertCircle className="h-5 w-5 text-orange-600 dark:text-orange-400" />;
|
||||
}
|
||||
}
|
||||
77
archon-ui-main/src/features/ui/primitives/tooltip.tsx
Normal file
77
archon-ui-main/src/features/ui/primitives/tooltip.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import React from "react";
|
||||
import { cn } from "./styles";
|
||||
|
||||
// Provider
|
||||
export const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
// Root
|
||||
export const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
// Trigger
|
||||
export const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
// Content with Tron glassmorphism
|
||||
export const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md px-3 py-1.5 text-xs",
|
||||
// Tron-style glassmorphism with neon glow - dark in both themes
|
||||
"backdrop-blur-md",
|
||||
"bg-gradient-to-b from-gray-900/95 to-black/95",
|
||||
"dark:from-gray-900/95 dark:to-black/95",
|
||||
// Neon border with cyan glow
|
||||
"border border-cyan-500/50 dark:border-cyan-400/50",
|
||||
"shadow-[0_0_15px_rgba(34,211,238,0.5)] dark:shadow-[0_0_15px_rgba(34,211,238,0.7)]",
|
||||
// Text colors - cyan in both modes for Tron effect
|
||||
"text-cyan-100 dark:text-cyan-100",
|
||||
// Subtle inner glow effect
|
||||
"before:absolute before:inset-0 before:rounded-md",
|
||||
"before:bg-gradient-to-b before:from-cyan-500/10 before:to-transparent",
|
||||
"before:pointer-events-none",
|
||||
// Animation with more dramatic entrance
|
||||
"animate-in fade-in-0 zoom-in-95",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2",
|
||||
"data-[side=left]:slide-in-from-right-2",
|
||||
"data-[side=right]:slide-in-from-left-2",
|
||||
"data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
// Simple tooltip wrapper for common use case
|
||||
export interface SimpleTooltipProps {
|
||||
children: React.ReactNode;
|
||||
content: string;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
align?: "start" | "center" | "end";
|
||||
delayDuration?: number;
|
||||
}
|
||||
|
||||
export const SimpleTooltip: React.FC<SimpleTooltipProps> = ({
|
||||
children,
|
||||
content,
|
||||
side = "top",
|
||||
align = "center",
|
||||
delayDuration = 200,
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip delayDuration={delayDuration}>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent side={side} align={align}>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user