Implement MonacoUSA Portal redesign foundations
Build And Push Image / docker (push) Failing after 1m11s
Details
Build And Push Image / docker (push) Failing after 1m11s
Details
- Added VueUse Motion for animations with custom presets - Created base UI component library with glass morphism effects: * GlassCard - Flexible card component with 4 variants * MonacoButton - Multi-variant button system * FloatingInput - Modern input with floating labels * StatsCard - Dashboard statistics display * AnimatedNumber - Smooth number animations * Icon system - Modular icon components - Created comprehensive page mockups: * Dashboard mockup with stats, activity feed, and widgets * Events page with filtering, search, and calendar - Established Monaco brand design system (red #dc2626) - Configured spring animations and glass effects 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
de75d2d764
commit
c39936984b
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"serena": {
|
||||
"type": "stdio",
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"--from",
|
||||
"git+https://github.com/oraios/serena",
|
||||
"serena",
|
||||
"start-mcp-server",
|
||||
"--context",
|
||||
"ide-assistant",
|
||||
"--project",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"zen": {
|
||||
"type": "stdio",
|
||||
"command": "pwsh",
|
||||
"args": [
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"$p=(Get-Command uvx -ErrorAction SilentlyContinue).Source; if(-not $p){$c=@(\"$HOME\\.local\\bin\\uvx.exe\",\"C:\\\\Program Files\\\\uv\\\\bin\\\\uvx.exe\"); foreach($i in $c){ if(Test-Path $i){$p=$i; break}}}; if($p){ & $p --from git+https://github.com/BeehiveInnovations/zen-mcp-server.git zen-mcp-server } else { Write-Error 'uvx not found'; exit 1 }"
|
||||
],
|
||||
"env": {
|
||||
"GEMINI_API_KEY": "your_gemini_key",
|
||||
"OPENAI_API_KEY": "your_openai_key"
|
||||
}
|
||||
},
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "cmd",
|
||||
"args": [
|
||||
"/c",
|
||||
"npx",
|
||||
"-y",
|
||||
"@playwright/mcp@latest"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"context7": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@upstash/context7-mcp@latest"
|
||||
],
|
||||
"env": {}
|
||||
},
|
||||
"@21st-dev/magic": {
|
||||
"type": "stdio",
|
||||
"command": "cmd",
|
||||
"args": [
|
||||
"/c",
|
||||
"npx",
|
||||
"-y",
|
||||
"@21st-dev/magic@latest"
|
||||
],
|
||||
"env": {
|
||||
"API_KEY": "adb246737aabae0b2f124fc85dc03737a0f65d9660b786732c31578649da10e5"
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mkdir:*)",
|
||||
"Read(C:\\Users\\mpcia/**)",
|
||||
"Read(C:\\Users\\mpcia/**)",
|
||||
"mcp__serena__activate_project",
|
||||
"mcp__serena__list_dir",
|
||||
"mcp__playwright__browser_navigate",
|
||||
"mcp__playwright__browser_take_screenshot",
|
||||
"mcp__playwright__browser_snapshot",
|
||||
"mcp__playwright__browser_type",
|
||||
"mcp__playwright__browser_click",
|
||||
"mcp__playwright__browser_press_key",
|
||||
"mcp__playwright__browser_wait_for",
|
||||
"mcp__serena__find_symbol",
|
||||
"mcp__serena__search_for_pattern",
|
||||
"mcp___21st-dev_magic__21st_magic_component_inspiration",
|
||||
"mcp__context7__resolve-library-id",
|
||||
"mcp__context7__get-library-docs",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(git add:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 237 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
Binary file not shown.
|
|
@ -0,0 +1,68 @@
|
|||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||
# * For C, use cpp
|
||||
# * For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||
language: typescript
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "monacousa-portal"
|
||||
|
|
@ -0,0 +1,480 @@
|
|||
# MonacoUSA Portal Design System Implementation Guide
|
||||
|
||||
## 🎨 Overview
|
||||
|
||||
This comprehensive guide outlines the complete visual redesign of the MonacoUSA Portal, transitioning from a standard Vuetify implementation to a premium, custom design system featuring Monaco's signature red and white color scheme with modern glass morphism effects.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
1. [Design Philosophy](#design-philosophy)
|
||||
2. [Technical Stack](#technical-stack)
|
||||
3. [Migration Strategy](#migration-strategy)
|
||||
4. [Component Architecture](#component-architecture)
|
||||
5. [Implementation Roadmap](#implementation-roadmap)
|
||||
6. [Performance Guidelines](#performance-guidelines)
|
||||
7. [Accessibility Standards](#accessibility-standards)
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
### Core Principles
|
||||
- **Premium Feel**: Every interaction should feel smooth and sophisticated
|
||||
- **Brand Identity**: Monaco's red (#dc2626) as the primary accent color
|
||||
- **Modern Aesthetics**: Glass morphism, subtle animations, and floating elements
|
||||
- **User Experience**: Intuitive navigation with clear visual hierarchy
|
||||
- **Performance**: Animations that enhance, not hinder, user experience
|
||||
|
||||
### Visual Language
|
||||
- **Glass Morphism**: Semi-transparent surfaces with backdrop blur
|
||||
- **Gradient Accents**: Dynamic gradients from Monaco red to deeper shades
|
||||
- **Floating Elements**: Subtle shadows and depth for interactive components
|
||||
- **Micro-animations**: Smooth transitions on hover, click, and state changes
|
||||
- **Consistent Spacing**: 8px grid system for alignment and padding
|
||||
|
||||
## Technical Stack
|
||||
|
||||
### Current Setup
|
||||
- **Framework**: Nuxt 3.8.2
|
||||
- **UI Library**: Vuetify 3.4.7 (to be replaced)
|
||||
- **Icons**: Material Design Icons (transitioning to Lucide)
|
||||
- **Authentication**: Keycloak integration
|
||||
- **Styling**: SCSS with scoped components
|
||||
|
||||
### New Additions
|
||||
- **Animation Libraries**:
|
||||
- VueUse Motion (preferred for Vue integration)
|
||||
- GSAP (for complex animations)
|
||||
- Anime.js (lightweight alternative)
|
||||
- **Icons**: Lucide Icons (modern, customizable)
|
||||
- **Utilities**: Tailwind CSS (for rapid prototyping)
|
||||
- **State Management**: Pinia (already implemented)
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
### Phase 1: Foundation (Week 1-2)
|
||||
1. Set up new style architecture
|
||||
2. Create base component library
|
||||
3. Implement color system and typography
|
||||
4. Set up animation utilities
|
||||
|
||||
### Phase 2: Core Components (Week 3-4)
|
||||
1. Replace navigation components
|
||||
2. Implement custom dropdowns and selects
|
||||
3. Create button variants
|
||||
4. Build card components
|
||||
|
||||
### Phase 3: Page Templates (Week 5-6)
|
||||
1. Dashboard layouts
|
||||
2. Data tables with glass effects
|
||||
3. Form components
|
||||
4. Modal and dialog systems
|
||||
|
||||
### Phase 4: Polish & Optimization (Week 7-8)
|
||||
1. Performance tuning
|
||||
2. Accessibility audit
|
||||
3. Cross-browser testing
|
||||
4. Documentation completion
|
||||
|
||||
## Component Architecture
|
||||
|
||||
### File Structure
|
||||
```
|
||||
components/
|
||||
├── ui/ # Base UI components
|
||||
│ ├── MonacoButton.vue
|
||||
│ ├── GlassCard.vue
|
||||
│ ├── AnimatedDropdown.vue
|
||||
│ └── FloatingInput.vue
|
||||
├── layout/ # Layout components
|
||||
│ ├── DashboardSidebar.vue
|
||||
│ ├── AppHeader.vue
|
||||
│ └── PageContainer.vue
|
||||
├── features/ # Feature-specific components
|
||||
│ ├── MemberCard.vue
|
||||
│ ├── EventCalendar.vue
|
||||
│ └── DuesTracker.vue
|
||||
└── shared/ # Shared utilities
|
||||
├── LoadingSpinner.vue
|
||||
├── ErrorBoundary.vue
|
||||
└── TransitionWrapper.vue
|
||||
```
|
||||
|
||||
### Component Guidelines
|
||||
|
||||
#### Naming Convention
|
||||
- PascalCase for component files
|
||||
- Prefix with "Monaco" for custom branded components
|
||||
- Use descriptive names (e.g., `GlassDropdownMenu` not `Dropdown`)
|
||||
|
||||
#### Props & Events
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
variant?: 'primary' | 'glass' | 'gradient'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
animated?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
animated: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [event: MouseEvent]
|
||||
hover: [state: boolean]
|
||||
}>()
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Composition API Pattern
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useMonacoTheme } from '~/composables/useMonacoTheme'
|
||||
import { useGlassEffect } from '~/composables/useGlassEffect'
|
||||
|
||||
const { primaryColor, gradients } = useMonacoTheme()
|
||||
const { glassStyle, blurAmount } = useGlassEffect()
|
||||
</script>
|
||||
```
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Week 1: Setup & Foundation
|
||||
- [ ] Configure animation libraries
|
||||
- [ ] Set up SCSS architecture
|
||||
- [ ] Create color system utilities
|
||||
- [ ] Implement base glass morphism styles
|
||||
- [ ] Set up Lucide icons integration
|
||||
|
||||
### Week 2: Core Components
|
||||
- [ ] Custom dropdown components
|
||||
- [ ] Button system with variants
|
||||
- [ ] Card components with glass effects
|
||||
- [ ] Form inputs with floating labels
|
||||
- [ ] Navigation components
|
||||
|
||||
### Week 3: Dashboard Implementation
|
||||
- [ ] Member dashboard layout
|
||||
- [ ] Board dashboard enhancements
|
||||
- [ ] Admin panel structure
|
||||
- [ ] Widget components
|
||||
- [ ] Chart integrations
|
||||
|
||||
### Week 4: Advanced Features
|
||||
- [ ] Event calendar with animations
|
||||
- [ ] Member management interface
|
||||
- [ ] Dues payment flow
|
||||
- [ ] Profile components
|
||||
- [ ] Settings panels
|
||||
|
||||
### Week 5: Responsive Design
|
||||
- [ ] Mobile navigation
|
||||
- [ ] Tablet optimizations
|
||||
- [ ] Touch interactions
|
||||
- [ ] Gesture support
|
||||
- [ ] PWA enhancements
|
||||
|
||||
### Week 6: Testing & Optimization
|
||||
- [ ] Performance profiling
|
||||
- [ ] Bundle optimization
|
||||
- [ ] Lazy loading implementation
|
||||
- [ ] Animation performance tuning
|
||||
- [ ] Memory leak detection
|
||||
|
||||
## Performance Guidelines
|
||||
|
||||
### Animation Performance
|
||||
```scss
|
||||
// Use transform and opacity for animations
|
||||
.animated-element {
|
||||
will-change: transform, opacity;
|
||||
transform: translateZ(0); // Enable hardware acceleration
|
||||
}
|
||||
|
||||
// Avoid animating expensive properties
|
||||
// BAD: width, height, padding, margin
|
||||
// GOOD: transform, opacity, filter
|
||||
```
|
||||
|
||||
### Component Optimization
|
||||
```vue
|
||||
<!-- Use v-show for frequently toggled elements -->
|
||||
<div v-show="isVisible" class="glass-panel">
|
||||
<!-- Content -->
|
||||
</div>
|
||||
|
||||
<!-- Use v-if for conditionally rendered heavy components -->
|
||||
<HeavyComponent v-if="shouldRender" />
|
||||
|
||||
<!-- Implement lazy loading for routes -->
|
||||
<script setup>
|
||||
const MemberDashboard = defineAsyncComponent(() =>
|
||||
import('~/components/dashboards/MemberDashboard.vue')
|
||||
)
|
||||
</script>
|
||||
```
|
||||
|
||||
### Bundle Size Management
|
||||
- Tree-shake unused Vuetify components
|
||||
- Lazy load animation libraries
|
||||
- Use dynamic imports for heavy features
|
||||
- Implement code splitting by route
|
||||
- Optimize images with next-gen formats
|
||||
|
||||
## Accessibility Standards
|
||||
|
||||
### WCAG 2.1 Compliance
|
||||
- **Color Contrast**: Ensure 4.5:1 ratio for normal text
|
||||
- **Focus Indicators**: Clear visual focus states
|
||||
- **Keyboard Navigation**: Full keyboard support
|
||||
- **Screen Readers**: Proper ARIA labels
|
||||
- **Motion Sensitivity**: Respect prefers-reduced-motion
|
||||
|
||||
### Implementation Examples
|
||||
```vue
|
||||
<template>
|
||||
<button
|
||||
:aria-label="ariaLabel"
|
||||
:aria-pressed="isPressed"
|
||||
@click="handleClick"
|
||||
@keydown.enter="handleClick"
|
||||
@keydown.space.prevent="handleClick"
|
||||
class="monaco-button"
|
||||
:class="{ 'reduced-motion': prefersReducedMotion }"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.monaco-button {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.monaco-button.reduced-motion {
|
||||
transition: none;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Style Guidelines
|
||||
|
||||
### Color System
|
||||
```scss
|
||||
// Primary Colors
|
||||
$monaco-red: #dc2626;
|
||||
$monaco-red-dark: #b91c1c;
|
||||
$monaco-red-light: #ef4444;
|
||||
$monaco-white: #ffffff;
|
||||
|
||||
// Gradients
|
||||
$monaco-gradient: linear-gradient(135deg, $monaco-red 0%, $monaco-red-dark 100%);
|
||||
$monaco-gradient-reverse: linear-gradient(135deg, $monaco-red-light 0%, $monaco-red 100%);
|
||||
|
||||
// Glass Effects
|
||||
$glass-bg: rgba(255, 255, 255, 0.7);
|
||||
$glass-border: rgba(255, 255, 255, 0.3);
|
||||
$glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
$glass-blur: 20px;
|
||||
```
|
||||
|
||||
### Typography Scale
|
||||
```scss
|
||||
// Font Family
|
||||
$font-primary: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
$font-mono: 'Fira Code', 'Monaco', monospace;
|
||||
|
||||
// Font Sizes
|
||||
$text-xs: 0.75rem; // 12px
|
||||
$text-sm: 0.875rem; // 14px
|
||||
$text-base: 1rem; // 16px
|
||||
$text-lg: 1.125rem; // 18px
|
||||
$text-xl: 1.25rem; // 20px
|
||||
$text-2xl: 1.5rem; // 24px
|
||||
$text-3xl: 1.875rem; // 30px
|
||||
$text-4xl: 2.25rem; // 36px
|
||||
```
|
||||
|
||||
### Spacing System
|
||||
```scss
|
||||
// Based on 8px grid
|
||||
$space-1: 0.25rem; // 4px
|
||||
$space-2: 0.5rem; // 8px
|
||||
$space-3: 0.75rem; // 12px
|
||||
$space-4: 1rem; // 16px
|
||||
$space-5: 1.25rem; // 20px
|
||||
$space-6: 1.5rem; // 24px
|
||||
$space-8: 2rem; // 32px
|
||||
$space-10: 2.5rem; // 40px
|
||||
$space-12: 3rem; // 48px
|
||||
$space-16: 4rem; // 64px
|
||||
```
|
||||
|
||||
## Component Examples
|
||||
|
||||
### Glass Card Component
|
||||
```vue
|
||||
<template>
|
||||
<div class="glass-card" :class="[sizeClass, variantClass]">
|
||||
<div v-if="hasHeader" class="glass-card-header">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div class="glass-card-body">
|
||||
<slot />
|
||||
</div>
|
||||
<div v-if="hasFooter" class="glass-card-footer">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
variant?: 'light' | 'dark' | 'colored'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'md',
|
||||
variant: 'light'
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
const hasHeader = computed(() => !!slots.header)
|
||||
const hasFooter = computed(() => !!slots.footer)
|
||||
|
||||
const sizeClass = computed(() => `glass-card--${props.size}`)
|
||||
const variantClass = computed(() => `glass-card--${props.variant}`)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&--dark {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&--colored {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.1) 0%,
|
||||
rgba(185, 28, 28, 0.1) 100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Testing
|
||||
```typescript
|
||||
// components/__tests__/MonacoButton.spec.ts
|
||||
import { mount } from '@vue/test-utils'
|
||||
import MonacoButton from '~/components/ui/MonacoButton.vue'
|
||||
|
||||
describe('MonacoButton', () => {
|
||||
it('renders with correct variant class', () => {
|
||||
const wrapper = mount(MonacoButton, {
|
||||
props: { variant: 'gradient' }
|
||||
})
|
||||
expect(wrapper.classes()).toContain('monaco-button--gradient')
|
||||
})
|
||||
|
||||
it('emits click event', async () => {
|
||||
const wrapper = mount(MonacoButton)
|
||||
await wrapper.trigger('click')
|
||||
expect(wrapper.emitted('click')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### E2E Testing
|
||||
```typescript
|
||||
// e2e/dashboard.spec.ts
|
||||
import { test, expect } from '@playwright/test'
|
||||
|
||||
test('dashboard loads with glass morphism effects', async ({ page }) => {
|
||||
await page.goto('/dashboard')
|
||||
|
||||
const glassCard = page.locator('.glass-card').first()
|
||||
await expect(glassCard).toBeVisible()
|
||||
|
||||
const styles = await glassCard.evaluate(el =>
|
||||
window.getComputedStyle(el)
|
||||
)
|
||||
expect(styles.backdropFilter).toContain('blur')
|
||||
})
|
||||
```
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
### Pre-deployment
|
||||
- [ ] Run full test suite
|
||||
- [ ] Check bundle size (<500KB initial)
|
||||
- [ ] Validate accessibility scores
|
||||
- [ ] Test on all target browsers
|
||||
- [ ] Optimize images and assets
|
||||
- [ ] Review security headers
|
||||
|
||||
### Performance Metrics
|
||||
- First Contentful Paint: <1.5s
|
||||
- Time to Interactive: <3s
|
||||
- Cumulative Layout Shift: <0.1
|
||||
- First Input Delay: <100ms
|
||||
- Lighthouse Score: >90
|
||||
|
||||
### Browser Support
|
||||
- Chrome 90+
|
||||
- Firefox 88+
|
||||
- Safari 14+
|
||||
- Edge 90+
|
||||
- Mobile Safari 14+
|
||||
- Chrome Mobile 90+
|
||||
|
||||
## Resources
|
||||
|
||||
### Documentation
|
||||
- [Vue 3 Composition API](https://vuejs.org/guide/extras/composition-api-faq.html)
|
||||
- [Nuxt 3 Documentation](https://nuxt.com/docs)
|
||||
- [VueUse Motion](https://motion.vueuse.org/)
|
||||
- [GSAP Documentation](https://greensock.com/docs/)
|
||||
- [Lucide Icons](https://lucide.dev/)
|
||||
|
||||
### Design Inspiration
|
||||
- [Glass Morphism Examples](https://glassmorphism.com/)
|
||||
- [Monaco Brand Guidelines](internal-link)
|
||||
- [Material Design 3](https://m3.material.io/)
|
||||
|
||||
### Tools
|
||||
- [Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||
- [Bundle Analyzer](https://github.com/nuxt/devtools)
|
||||
- [Lighthouse CI](https://github.com/GoogleChrome/lighthouse-ci)
|
||||
|
||||
## Support
|
||||
|
||||
For questions or assistance with implementation:
|
||||
- Technical Lead: [Contact Info]
|
||||
- Design Team: [Contact Info]
|
||||
- Documentation: This guide and `/Design` folder
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: December 2024*
|
||||
*Version: 1.0.0*
|
||||
|
|
@ -0,0 +1,510 @@
|
|||
<template>
|
||||
<div class="animated-select" ref="selectRef">
|
||||
<button
|
||||
@click="toggleSelect"
|
||||
class="animated-select__trigger"
|
||||
:class="{ 'animated-select__trigger--open': isOpen }"
|
||||
>
|
||||
<div class="animated-select__display">
|
||||
<Transition name="slide-fade" mode="out-in">
|
||||
<span
|
||||
v-if="!selectedOption"
|
||||
key="placeholder"
|
||||
class="animated-select__placeholder"
|
||||
>
|
||||
{{ placeholder }}
|
||||
</span>
|
||||
<div
|
||||
v-else
|
||||
key="selected"
|
||||
class="animated-select__selected"
|
||||
>
|
||||
<Icon
|
||||
v-if="selectedOption.icon"
|
||||
:name="selectedOption.icon"
|
||||
class="animated-select__icon"
|
||||
/>
|
||||
<span>{{ selectedOption.label }}</span>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div class="animated-select__arrow">
|
||||
<svg
|
||||
class="animated-select__arrow-icon"
|
||||
:class="{ 'animated-select__arrow-icon--rotate': isOpen }"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="select-dropdown">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="animated-select__dropdown"
|
||||
:style="dropdownStyle"
|
||||
@click.stop
|
||||
>
|
||||
<div class="animated-select__options">
|
||||
<TransitionGroup name="option-list">
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:key="option.value"
|
||||
class="animated-select__option"
|
||||
:class="{
|
||||
'animated-select__option--selected': modelValue === option.value,
|
||||
'animated-select__option--highlighted': highlightedIndex === index,
|
||||
'animated-select__option--disabled': option.disabled
|
||||
}"
|
||||
:style="{ '--delay': `${index * 30}ms` }"
|
||||
@click="!option.disabled && selectOption(option)"
|
||||
@mouseenter="highlightedIndex = index"
|
||||
@mouseleave="highlightedIndex = -1"
|
||||
>
|
||||
<div class="animated-select__option-content">
|
||||
<Icon
|
||||
v-if="option.icon"
|
||||
:name="option.icon"
|
||||
class="animated-select__option-icon"
|
||||
/>
|
||||
<span class="animated-select__option-label">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span
|
||||
v-if="option.description"
|
||||
class="animated-select__option-description"
|
||||
>
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Transition name="check">
|
||||
<Icon
|
||||
v-if="modelValue === option.value"
|
||||
name="check"
|
||||
class="animated-select__option-check"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import Icon from '~/components/ui/Icon.vue'
|
||||
|
||||
interface SelectOption {
|
||||
label: string
|
||||
value: string | number
|
||||
icon?: string
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | number | null
|
||||
options: SelectOption[]
|
||||
placeholder?: string
|
||||
searchable?: boolean
|
||||
multiple?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: 'Select an option',
|
||||
searchable: false,
|
||||
multiple: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number | null]
|
||||
'change': [value: string | number | null]
|
||||
'open': []
|
||||
'close': []
|
||||
}>()
|
||||
|
||||
const selectRef = ref<HTMLElement>()
|
||||
const isOpen = ref(false)
|
||||
const highlightedIndex = ref(-1)
|
||||
const dropdownStyle = ref({})
|
||||
|
||||
const selectedOption = computed(() => {
|
||||
return props.options.find(opt => opt.value === props.modelValue)
|
||||
})
|
||||
|
||||
const toggleSelect = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
if (isOpen.value) {
|
||||
emit('open')
|
||||
nextTick(() => updateDropdownPosition())
|
||||
} else {
|
||||
emit('close')
|
||||
highlightedIndex.value = -1
|
||||
}
|
||||
}
|
||||
|
||||
const selectOption = (option: SelectOption) => {
|
||||
emit('update:modelValue', option.value)
|
||||
emit('change', option.value)
|
||||
isOpen.value = false
|
||||
highlightedIndex.value = -1
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const updateDropdownPosition = () => {
|
||||
if (!selectRef.value) return
|
||||
|
||||
const rect = selectRef.value.getBoundingClientRect()
|
||||
const spaceBelow = window.innerHeight - rect.bottom
|
||||
const spaceAbove = rect.top
|
||||
const dropdownHeight = 300 // Approximate max height
|
||||
|
||||
let top = rect.bottom + 8
|
||||
if (spaceBelow < dropdownHeight && spaceAbove > spaceBelow) {
|
||||
top = rect.top - dropdownHeight - 8
|
||||
}
|
||||
|
||||
dropdownStyle.value = {
|
||||
position: 'fixed',
|
||||
top: `${top}px`,
|
||||
left: `${rect.left}px`,
|
||||
width: `${rect.width}px`,
|
||||
zIndex: 9999
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (selectRef.value && !selectRef.value.contains(event.target as Node)) {
|
||||
isOpen.value = false
|
||||
highlightedIndex.value = -1
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpen.value) {
|
||||
isOpen.value = false
|
||||
highlightedIndex.value = -1
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyNavigation = (event: KeyboardEvent) => {
|
||||
if (!isOpen.value) return
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
highlightedIndex.value = Math.min(
|
||||
highlightedIndex.value + 1,
|
||||
props.options.length - 1
|
||||
)
|
||||
break
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0)
|
||||
break
|
||||
case 'Enter':
|
||||
event.preventDefault()
|
||||
if (highlightedIndex.value >= 0) {
|
||||
const option = props.options[highlightedIndex.value]
|
||||
if (!option.disabled) {
|
||||
selectOption(option)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
document.addEventListener('keydown', handleKeyNavigation)
|
||||
window.addEventListener('resize', updateDropdownPosition)
|
||||
window.addEventListener('scroll', updateDropdownPosition)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
document.removeEventListener('keydown', handleKeyNavigation)
|
||||
window.removeEventListener('resize', updateDropdownPosition)
|
||||
window.removeEventListener('scroll', updateDropdownPosition)
|
||||
})
|
||||
|
||||
watch(isOpen, (newVal) => {
|
||||
if (newVal) {
|
||||
nextTick(() => updateDropdownPosition())
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.animated-select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&__trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
outline: none;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(220, 38, 38, 0.2);
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #dc2626;
|
||||
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
|
||||
&--open {
|
||||
border-color: #dc2626;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&__display {
|
||||
flex: 1;
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
color: #71717a;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
&__selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #27272a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
&__arrow-icon {
|
||||
color: #dc2626;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&--rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__dropdown {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(30px);
|
||||
-webkit-backdrop-filter: blur(30px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 20px 40px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 1px rgba(220, 38, 38, 0.05);
|
||||
overflow: hidden;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(220, 38, 38, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__options {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
&__option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
margin: 0.125rem 0;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
animation-delay: var(--delay);
|
||||
|
||||
&:hover:not(&--disabled) {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.05) 0%,
|
||||
rgba(220, 38, 38, 0.1) 100%);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
&--highlighted {
|
||||
background: rgba(220, 38, 38, 0.05);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.1) 0%,
|
||||
rgba(220, 38, 38, 0.15) 100%);
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__option-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__option-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&__option-label {
|
||||
font-size: 0.9375rem;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&__option-description {
|
||||
font-size: 0.75rem;
|
||||
color: #71717a;
|
||||
margin-left: auto;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
&__option-check {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.slide-fade-enter-active,
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.slide-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.slide-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
.select-dropdown-enter-active,
|
||||
.select-dropdown-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.select-dropdown-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
.select-dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(-10px);
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
.option-list-enter-active,
|
||||
.option-list-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.option-list-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
|
||||
.option-list-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
|
||||
.check-enter-active,
|
||||
.check-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.check-enter-from,
|
||||
.check-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,713 @@
|
|||
<template>
|
||||
<div class="glass-dropdown" ref="dropdownRef">
|
||||
<div
|
||||
class="glass-dropdown__trigger"
|
||||
@click="toggleDropdown"
|
||||
:class="{ 'glass-dropdown__trigger--active': isOpen }"
|
||||
>
|
||||
<div class="glass-dropdown__trigger-content">
|
||||
<slot name="trigger">
|
||||
<div class="glass-dropdown__trigger-default">
|
||||
<Icon
|
||||
v-if="icon"
|
||||
:name="icon"
|
||||
class="glass-dropdown__trigger-icon"
|
||||
/>
|
||||
<span class="glass-dropdown__trigger-text">{{ label }}</span>
|
||||
<div class="glass-dropdown__trigger-arrow">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
class="glass-dropdown__arrow-svg"
|
||||
:class="{ 'glass-dropdown__arrow-svg--rotate': isOpen }"
|
||||
>
|
||||
<path
|
||||
d="M4 6L8 10L12 6"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="glass-dropdown__trigger-glow"></div>
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="glass-fade">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="glass-dropdown__backdrop"
|
||||
@click="closeDropdown"
|
||||
>
|
||||
<div
|
||||
class="glass-dropdown__menu"
|
||||
:style="menuStyle"
|
||||
@click.stop
|
||||
>
|
||||
<div class="glass-dropdown__menu-inner">
|
||||
<div
|
||||
v-if="$slots.header"
|
||||
class="glass-dropdown__header"
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
|
||||
<div class="glass-dropdown__items">
|
||||
<template v-if="groups.length > 0">
|
||||
<div
|
||||
v-for="(group, groupIndex) in groups"
|
||||
:key="groupIndex"
|
||||
class="glass-dropdown__group"
|
||||
>
|
||||
<div
|
||||
v-if="group.label"
|
||||
class="glass-dropdown__group-label"
|
||||
>
|
||||
{{ group.label }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(item, itemIndex) in group.items"
|
||||
:key="`${groupIndex}-${itemIndex}`"
|
||||
class="glass-dropdown__item"
|
||||
:class="{
|
||||
'glass-dropdown__item--active': isItemActive(item),
|
||||
'glass-dropdown__item--disabled': item.disabled,
|
||||
'glass-dropdown__item--danger': item.variant === 'danger'
|
||||
}"
|
||||
@click="!item.disabled && handleItemClick(item)"
|
||||
@mouseenter="hoveredItem = item"
|
||||
@mouseleave="hoveredItem = null"
|
||||
>
|
||||
<div class="glass-dropdown__item-content">
|
||||
<Icon
|
||||
v-if="item.icon"
|
||||
:name="item.icon"
|
||||
class="glass-dropdown__item-icon"
|
||||
/>
|
||||
|
||||
<div class="glass-dropdown__item-text">
|
||||
<div class="glass-dropdown__item-label">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.description"
|
||||
class="glass-dropdown__item-description"
|
||||
>
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.badge"
|
||||
class="glass-dropdown__item-badge"
|
||||
>
|
||||
{{ item.badge }}
|
||||
</div>
|
||||
|
||||
<Icon
|
||||
v-if="item.submenu"
|
||||
name="chevron-right"
|
||||
class="glass-dropdown__item-chevron"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Transition name="submenu-slide">
|
||||
<div
|
||||
v-if="item.submenu && hoveredItem === item"
|
||||
class="glass-dropdown__submenu"
|
||||
>
|
||||
<div
|
||||
v-for="(subItem, subIndex) in item.submenu"
|
||||
:key="subIndex"
|
||||
class="glass-dropdown__submenu-item"
|
||||
:class="{
|
||||
'glass-dropdown__submenu-item--disabled': subItem.disabled
|
||||
}"
|
||||
@click.stop="!subItem.disabled && handleSubmenuClick(subItem)"
|
||||
>
|
||||
<Icon
|
||||
v-if="subItem.icon"
|
||||
:name="subItem.icon"
|
||||
class="glass-dropdown__submenu-icon"
|
||||
/>
|
||||
<span>{{ subItem.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="groupIndex < groups.length - 1"
|
||||
class="glass-dropdown__divider"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="glass-dropdown__empty">
|
||||
<slot name="empty">
|
||||
<p>No items available</p>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="$slots.footer"
|
||||
class="glass-dropdown__footer"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-dropdown__menu-glow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import Icon from '~/components/ui/Icon.vue'
|
||||
|
||||
interface DropdownItem {
|
||||
label: string
|
||||
value?: string | number
|
||||
icon?: string
|
||||
description?: string
|
||||
badge?: string | number
|
||||
variant?: 'default' | 'danger'
|
||||
disabled?: boolean
|
||||
action?: () => void
|
||||
submenu?: DropdownItem[]
|
||||
}
|
||||
|
||||
interface DropdownGroup {
|
||||
label?: string
|
||||
items: DropdownItem[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
icon?: string
|
||||
groups?: DropdownGroup[]
|
||||
items?: DropdownItem[]
|
||||
modelValue?: string | number | null
|
||||
closeOnSelect?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
label: 'Menu',
|
||||
closeOnSelect: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number | null]
|
||||
'select': [item: DropdownItem]
|
||||
'open': []
|
||||
'close': []
|
||||
}>()
|
||||
|
||||
const dropdownRef = ref<HTMLElement>()
|
||||
const isOpen = ref(false)
|
||||
const hoveredItem = ref<DropdownItem | null>(null)
|
||||
const menuStyle = ref({})
|
||||
|
||||
const groups = computed(() => {
|
||||
if (props.groups && props.groups.length > 0) {
|
||||
return props.groups
|
||||
}
|
||||
if (props.items && props.items.length > 0) {
|
||||
return [{ items: props.items }]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const toggleDropdown = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
if (isOpen.value) {
|
||||
emit('open')
|
||||
nextTick(() => updateMenuPosition())
|
||||
} else {
|
||||
emit('close')
|
||||
hoveredItem.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const closeDropdown = () => {
|
||||
isOpen.value = false
|
||||
hoveredItem.value = null
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const handleItemClick = (item: DropdownItem) => {
|
||||
if (item.action) {
|
||||
item.action()
|
||||
}
|
||||
|
||||
if (item.value !== undefined) {
|
||||
emit('update:modelValue', item.value)
|
||||
}
|
||||
|
||||
emit('select', item)
|
||||
|
||||
if (props.closeOnSelect && !item.submenu) {
|
||||
closeDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmenuClick = (item: DropdownItem) => {
|
||||
if (item.action) {
|
||||
item.action()
|
||||
}
|
||||
|
||||
emit('select', item)
|
||||
|
||||
if (props.closeOnSelect) {
|
||||
closeDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
const isItemActive = (item: DropdownItem) => {
|
||||
return item.value !== undefined && item.value === props.modelValue
|
||||
}
|
||||
|
||||
const updateMenuPosition = () => {
|
||||
if (!dropdownRef.value) return
|
||||
|
||||
const rect = dropdownRef.value.getBoundingClientRect()
|
||||
const windowWidth = window.innerWidth
|
||||
const windowHeight = window.innerHeight
|
||||
|
||||
let top = rect.bottom + 8
|
||||
let left = rect.left
|
||||
|
||||
// Adjust if menu would go off screen
|
||||
const menuWidth = 320 // Approximate menu width
|
||||
const menuHeight = 400 // Approximate max menu height
|
||||
|
||||
if (left + menuWidth > windowWidth) {
|
||||
left = windowWidth - menuWidth - 16
|
||||
}
|
||||
|
||||
if (top + menuHeight > windowHeight && rect.top > menuHeight) {
|
||||
top = rect.top - menuHeight - 8
|
||||
}
|
||||
|
||||
menuStyle.value = {
|
||||
position: 'fixed',
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
minWidth: `${rect.width}px`,
|
||||
zIndex: 10001
|
||||
}
|
||||
}
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpen.value) {
|
||||
closeDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
window.addEventListener('resize', updateMenuPosition)
|
||||
window.addEventListener('scroll', updateMenuPosition)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
window.removeEventListener('resize', updateMenuPosition)
|
||||
window.removeEventListener('scroll', updateMenuPosition)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.glass-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&__trigger {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.625rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
backdrop-filter: blur(30px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow:
|
||||
0 4px 20px rgba(0, 0, 0, 0.04),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.8);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-color: rgba(220, 38, 38, 0.3);
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 8px 30px rgba(220, 38, 38, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
|
||||
.glass-dropdown__trigger-glow {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(220, 38, 38, 0.4);
|
||||
box-shadow:
|
||||
0 0 0 4px rgba(220, 38, 38, 0.1),
|
||||
0 8px 30px rgba(220, 38, 38, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 1);
|
||||
}
|
||||
}
|
||||
|
||||
&__trigger-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__trigger-default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&__trigger-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&__trigger-text {
|
||||
font-weight: 500;
|
||||
color: #27272a;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
&__trigger-arrow {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
&__arrow-svg {
|
||||
color: #dc2626;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&--rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__trigger-glow {
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.2) 0%,
|
||||
rgba(220, 38, 38, 0) 100%);
|
||||
border-radius: 14px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 10000;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
&__menu {
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(40px) saturate(200%);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(200%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 20px;
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.15),
|
||||
0 0 0 1px rgba(220, 38, 38, 0.05),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
overflow: hidden;
|
||||
min-width: 280px;
|
||||
max-width: 400px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg,
|
||||
rgba(220, 38, 38, 0.3) 0%,
|
||||
rgba(220, 38, 38, 0.1) 100%);
|
||||
border-radius: 10px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.9);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(180deg,
|
||||
rgba(220, 38, 38, 0.4) 0%,
|
||||
rgba(220, 38, 38, 0.2) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__menu-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
&__menu-glow {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
rgba(220, 38, 38, 0.1) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid rgba(220, 38, 38, 0.1);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid rgba(220, 38, 38, 0.1);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
&__group {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
&__group-label {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #dc2626;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(220, 38, 38, 0.2) 50%,
|
||||
transparent 100%);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
&__item {
|
||||
position: relative;
|
||||
padding: 0.625rem 0.75rem;
|
||||
margin: 0.125rem 0;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover:not(&--disabled) {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.08) 0%,
|
||||
rgba(220, 38, 38, 0.04) 100%);
|
||||
transform: translateX(4px);
|
||||
|
||||
.glass-dropdown__item-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.15) 0%,
|
||||
rgba(220, 38, 38, 0.08) 100%);
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 60%;
|
||||
background: #dc2626;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
color: #ef4444;
|
||||
|
||||
&:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
&__item-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: #dc2626;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&__item-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__item-label {
|
||||
font-size: 0.9375rem;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&__item-description {
|
||||
font-size: 0.75rem;
|
||||
color: #71717a;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
&__item-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
&__item-chevron {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: #71717a;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__submenu {
|
||||
position: absolute;
|
||||
left: calc(100% + 0.5rem);
|
||||
top: 0;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(30px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
padding: 0.25rem;
|
||||
min-width: 200px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&__submenu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.875rem;
|
||||
|
||||
&:hover:not(&--disabled) {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__submenu-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #71717a;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
.glass-fade-enter-active,
|
||||
.glass-fade-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.glass-fade-enter-from {
|
||||
opacity: 0;
|
||||
|
||||
.glass-dropdown__menu {
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.glass-fade-leave-to {
|
||||
opacity: 0;
|
||||
|
||||
.glass-dropdown__menu {
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.submenu-slide-enter-active,
|
||||
.submenu-slide-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.submenu-slide-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
.submenu-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
<template>
|
||||
<div class="monaco-dropdown" ref="dropdownRef">
|
||||
<button
|
||||
@click="toggleDropdown"
|
||||
class="monaco-dropdown__trigger"
|
||||
:class="[
|
||||
`monaco-dropdown__trigger--${variant}`,
|
||||
`monaco-dropdown__trigger--${size}`,
|
||||
{ 'monaco-dropdown__trigger--open': isOpen }
|
||||
]"
|
||||
:aria-expanded="isOpen"
|
||||
:aria-haspopup="true"
|
||||
>
|
||||
<slot name="trigger">
|
||||
<span class="monaco-dropdown__trigger-text">{{ label }}</span>
|
||||
</slot>
|
||||
<Icon
|
||||
:name="isOpen ? 'chevron-up' : 'chevron-down'"
|
||||
class="monaco-dropdown__trigger-icon"
|
||||
:class="{ 'monaco-dropdown__trigger-icon--rotate': isOpen }"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<Transition name="dropdown">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="monaco-dropdown__content"
|
||||
:class="[
|
||||
`monaco-dropdown__content--${variant}`,
|
||||
`monaco-dropdown__content--${position}`
|
||||
]"
|
||||
>
|
||||
<div class="monaco-dropdown__content-inner">
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:key="option.value || index"
|
||||
class="monaco-dropdown__item"
|
||||
:class="{
|
||||
'monaco-dropdown__item--active': activeIndex === index,
|
||||
'monaco-dropdown__item--selected': modelValue === option.value,
|
||||
'monaco-dropdown__item--disabled': option.disabled
|
||||
}"
|
||||
@click="!option.disabled && selectOption(option)"
|
||||
@mouseenter="activeIndex = index"
|
||||
@mouseleave="activeIndex = -1"
|
||||
>
|
||||
<Icon
|
||||
v-if="option.icon"
|
||||
:name="option.icon"
|
||||
class="monaco-dropdown__item-icon"
|
||||
/>
|
||||
<span class="monaco-dropdown__item-label">{{ option.label }}</span>
|
||||
<span v-if="option.shortcut" class="monaco-dropdown__item-shortcut">
|
||||
{{ option.shortcut }}
|
||||
</span>
|
||||
<Icon
|
||||
v-if="modelValue === option.value"
|
||||
name="check"
|
||||
class="monaco-dropdown__item-check"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import Icon from '~/components/ui/Icon.vue'
|
||||
|
||||
interface DropdownOption {
|
||||
label: string
|
||||
value: string | number
|
||||
icon?: string
|
||||
shortcut?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue?: string | number | null
|
||||
options: DropdownOption[]
|
||||
label?: string
|
||||
variant?: 'glass' | 'solid' | 'gradient' | 'outline'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
position?: 'bottom' | 'top' | 'left' | 'right'
|
||||
closeOnSelect?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
label: 'Select option',
|
||||
variant: 'glass',
|
||||
size: 'md',
|
||||
position: 'bottom',
|
||||
closeOnSelect: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | number]
|
||||
'change': [value: string | number]
|
||||
'open': []
|
||||
'close': []
|
||||
}>()
|
||||
|
||||
const dropdownRef = ref<HTMLElement>()
|
||||
const isOpen = ref(false)
|
||||
const activeIndex = ref(-1)
|
||||
|
||||
const toggleDropdown = () => {
|
||||
isOpen.value = !isOpen.value
|
||||
if (isOpen.value) {
|
||||
emit('open')
|
||||
} else {
|
||||
emit('close')
|
||||
activeIndex.value = -1
|
||||
}
|
||||
}
|
||||
|
||||
const selectOption = (option: DropdownOption) => {
|
||||
emit('update:modelValue', option.value)
|
||||
emit('change', option.value)
|
||||
|
||||
if (props.closeOnSelect) {
|
||||
isOpen.value = false
|
||||
activeIndex.value = -1
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
||||
isOpen.value = false
|
||||
activeIndex.value = -1
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpen.value) {
|
||||
isOpen.value = false
|
||||
activeIndex.value = -1
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.monaco-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
&__trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
font-weight: 500;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
outline: none;
|
||||
border: none;
|
||||
|
||||
&--glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #dc2626;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
&--solid {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(220, 38, 38, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: #b91c1c;
|
||||
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 16px rgba(220, 38, 38, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&--outline {
|
||||
background: transparent;
|
||||
color: #dc2626;
|
||||
border: 2px solid #dc2626;
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border-color: #b91c1c;
|
||||
}
|
||||
}
|
||||
|
||||
&--sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
&--md {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&--lg {
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
&--open {
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
&__trigger-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&--rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
min-width: 200px;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.25rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
|
||||
&--glass {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
&--solid {
|
||||
background: white;
|
||||
border: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.95) 0%,
|
||||
rgba(255, 255, 255, 0.85) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
|
||||
&--outline {
|
||||
background: white;
|
||||
border: 2px solid #dc2626;
|
||||
}
|
||||
|
||||
&--bottom {
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&--top {
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&--left {
|
||||
right: 100%;
|
||||
top: 0;
|
||||
margin-right: 0.5rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&--right {
|
||||
left: 100%;
|
||||
top: 0;
|
||||
margin-left: 0.5rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: #27272a;
|
||||
|
||||
&:hover:not(&--disabled) {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&--active:not(&--disabled) {
|
||||
background: rgba(220, 38, 38, 0.05);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__item-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__item-label {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
&__item-shortcut {
|
||||
font-size: 0.75rem;
|
||||
color: #71717a;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__item-check {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: #dc2626;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Transition animations
|
||||
.dropdown-enter-active,
|
||||
.dropdown-leave-active {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.dropdown-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
.dropdown-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
filter: blur(4px);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,753 @@
|
|||
# MonacoUSA Portal Design System
|
||||
|
||||
## 🎨 Visual Identity
|
||||
|
||||
### Brand Colors
|
||||
|
||||
#### Primary Palette
|
||||
```scss
|
||||
// Monaco Red Spectrum
|
||||
$monaco-red-50: #fef2f2;
|
||||
$monaco-red-100: #fee2e2;
|
||||
$monaco-red-200: #fecaca;
|
||||
$monaco-red-300: #fca5a5;
|
||||
$monaco-red-400: #f87171;
|
||||
$monaco-red-500: #ef4444;
|
||||
$monaco-red-600: #dc2626; // Primary Brand Color
|
||||
$monaco-red-700: #b91c1c;
|
||||
$monaco-red-800: #991b1b;
|
||||
$monaco-red-900: #7f1d1d;
|
||||
|
||||
// Neutral Palette
|
||||
$gray-50: #fafafa;
|
||||
$gray-100: #f4f4f5;
|
||||
$gray-200: #e4e4e7;
|
||||
$gray-300: #d4d4d8;
|
||||
$gray-400: #a1a1aa;
|
||||
$gray-500: #71717a;
|
||||
$gray-600: #52525b;
|
||||
$gray-700: #3f3f46;
|
||||
$gray-800: #27272a;
|
||||
$gray-900: #18181b;
|
||||
```
|
||||
|
||||
#### Gradient Definitions
|
||||
```scss
|
||||
// Primary Gradients
|
||||
$gradient-monaco: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
$gradient-monaco-light: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
$gradient-monaco-dark: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
|
||||
|
||||
// Accent Gradients
|
||||
$gradient-sunset: linear-gradient(135deg, #dc2626 0%, #f59e0b 100%);
|
||||
$gradient-wine: linear-gradient(135deg, #991b1b 0%, #4c1d95 100%);
|
||||
$gradient-royal: linear-gradient(135deg, #dc2626 0%, #1e40af 100%);
|
||||
|
||||
// Glass Gradients
|
||||
$gradient-glass-light: linear-gradient(135deg, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0.4) 100%);
|
||||
$gradient-glass-dark: linear-gradient(135deg, rgba(0,0,0,0.8) 0%, rgba(0,0,0,0.4) 100%);
|
||||
```
|
||||
|
||||
### Typography
|
||||
|
||||
#### Font Stack
|
||||
```scss
|
||||
// Primary Font Family
|
||||
$font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
|
||||
// Monospace Font Family
|
||||
$font-mono: 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
|
||||
|
||||
// Display Font (for headers)
|
||||
$font-display: 'Poppins', $font-sans;
|
||||
```
|
||||
|
||||
#### Type Scale
|
||||
```scss
|
||||
// Font Sizes
|
||||
$text-xs: 0.75rem; // 12px
|
||||
$text-sm: 0.875rem; // 14px
|
||||
$text-base: 1rem; // 16px
|
||||
$text-lg: 1.125rem; // 18px
|
||||
$text-xl: 1.25rem; // 20px
|
||||
$text-2xl: 1.5rem; // 24px
|
||||
$text-3xl: 1.875rem; // 30px
|
||||
$text-4xl: 2.25rem; // 36px
|
||||
$text-5xl: 3rem; // 48px
|
||||
$text-6xl: 3.75rem; // 60px
|
||||
$text-7xl: 4.5rem; // 72px
|
||||
|
||||
// Line Heights
|
||||
$leading-none: 1;
|
||||
$leading-tight: 1.25;
|
||||
$leading-snug: 1.375;
|
||||
$leading-normal: 1.5;
|
||||
$leading-relaxed: 1.625;
|
||||
$leading-loose: 2;
|
||||
|
||||
// Font Weights
|
||||
$font-thin: 100;
|
||||
$font-light: 300;
|
||||
$font-normal: 400;
|
||||
$font-medium: 500;
|
||||
$font-semibold: 600;
|
||||
$font-bold: 700;
|
||||
$font-extrabold: 800;
|
||||
$font-black: 900;
|
||||
```
|
||||
|
||||
### Spacing System
|
||||
|
||||
```scss
|
||||
// Based on 4px grid
|
||||
$space-0: 0; // 0px
|
||||
$space-px: 1px; // 1px
|
||||
$space-0_5: 0.125rem; // 2px
|
||||
$space-1: 0.25rem; // 4px
|
||||
$space-1_5: 0.375rem; // 6px
|
||||
$space-2: 0.5rem; // 8px
|
||||
$space-2_5: 0.625rem; // 10px
|
||||
$space-3: 0.75rem; // 12px
|
||||
$space-3_5: 0.875rem; // 14px
|
||||
$space-4: 1rem; // 16px
|
||||
$space-5: 1.25rem; // 20px
|
||||
$space-6: 1.5rem; // 24px
|
||||
$space-7: 1.75rem; // 28px
|
||||
$space-8: 2rem; // 32px
|
||||
$space-9: 2.25rem; // 36px
|
||||
$space-10: 2.5rem; // 40px
|
||||
$space-12: 3rem; // 48px
|
||||
$space-14: 3.5rem; // 56px
|
||||
$space-16: 4rem; // 64px
|
||||
$space-20: 5rem; // 80px
|
||||
$space-24: 6rem; // 96px
|
||||
$space-28: 7rem; // 112px
|
||||
$space-32: 8rem; // 128px
|
||||
```
|
||||
|
||||
### Border Radius
|
||||
|
||||
```scss
|
||||
$radius-none: 0;
|
||||
$radius-sm: 0.125rem; // 2px
|
||||
$radius-base: 0.25rem; // 4px
|
||||
$radius-md: 0.375rem; // 6px
|
||||
$radius-lg: 0.5rem; // 8px
|
||||
$radius-xl: 0.75rem; // 12px
|
||||
$radius-2xl: 1rem; // 16px
|
||||
$radius-3xl: 1.5rem; // 24px
|
||||
$radius-full: 9999px; // Pill shape
|
||||
```
|
||||
|
||||
### Shadows
|
||||
|
||||
```scss
|
||||
// Elevation Shadows
|
||||
$shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
$shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
||||
$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||
$shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
|
||||
// Glass Shadows
|
||||
$shadow-glass: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
$shadow-glass-hover: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
|
||||
// Monaco Brand Shadows
|
||||
$shadow-monaco: 0 10px 40px rgba(220, 38, 38, 0.15);
|
||||
$shadow-monaco-intense: 0 20px 60px rgba(220, 38, 38, 0.25);
|
||||
```
|
||||
|
||||
## 🎯 Component Patterns
|
||||
|
||||
### Glass Morphism
|
||||
|
||||
```scss
|
||||
@mixin glass-effect($bg-opacity: 0.7, $blur: 20px) {
|
||||
background: rgba(255, 255, 255, $bg-opacity);
|
||||
backdrop-filter: blur($blur);
|
||||
-webkit-backdrop-filter: blur($blur);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: $shadow-glass;
|
||||
}
|
||||
|
||||
@mixin glass-dark($bg-opacity: 0.7, $blur: 20px) {
|
||||
background: rgba(0, 0, 0, $bg-opacity);
|
||||
backdrop-filter: blur($blur);
|
||||
-webkit-backdrop-filter: blur($blur);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: $shadow-glass;
|
||||
}
|
||||
|
||||
@mixin glass-colored($color: $monaco-red-600, $opacity: 0.1, $blur: 20px) {
|
||||
background: rgba($color, $opacity);
|
||||
backdrop-filter: blur($blur);
|
||||
-webkit-backdrop-filter: blur($blur);
|
||||
border: 1px solid rgba($color, 0.2);
|
||||
box-shadow: 0 8px 32px rgba($color, 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
### Animation Patterns
|
||||
|
||||
```scss
|
||||
// Timing Functions
|
||||
$ease-in-out-smooth: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
$ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
$ease-in-out-elastic: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
|
||||
// Duration Scale
|
||||
$duration-instant: 0ms;
|
||||
$duration-fast: 150ms;
|
||||
$duration-base: 300ms;
|
||||
$duration-slow: 500ms;
|
||||
$duration-slower: 700ms;
|
||||
$duration-slowest: 1000ms;
|
||||
|
||||
// Animation Mixins
|
||||
@mixin hover-lift($distance: -2px, $duration: $duration-base) {
|
||||
transition: transform $duration $ease-in-out-smooth;
|
||||
|
||||
&:hover {
|
||||
transform: translateY($distance);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin hover-scale($scale: 1.05, $duration: $duration-base) {
|
||||
transition: transform $duration $ease-in-out-smooth;
|
||||
|
||||
&:hover {
|
||||
transform: scale($scale);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin pulse-animation($scale: 1.05, $duration: 2s) {
|
||||
animation: pulse $duration infinite;
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale($scale);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin float-animation($distance: 10px, $duration: 3s) {
|
||||
animation: float $duration ease-in-out infinite;
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-$distance);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Button Variants
|
||||
|
||||
```scss
|
||||
// Base Button
|
||||
@mixin button-base {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $space-2_5 $space-5;
|
||||
font-weight: $font-medium;
|
||||
font-size: $text-sm;
|
||||
line-height: $leading-tight;
|
||||
border-radius: $radius-xl;
|
||||
transition: all $duration-base $ease-in-out-smooth;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// Primary Button
|
||||
@mixin button-primary {
|
||||
@include button-base;
|
||||
background: $gradient-monaco;
|
||||
color: white;
|
||||
border: none;
|
||||
box-shadow: $shadow-md;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
box-shadow: $shadow-monaco;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Glass Button
|
||||
@mixin button-glass {
|
||||
@include button-base;
|
||||
@include glass-effect(0.8, 10px);
|
||||
color: $monaco-red-600;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: $shadow-glass-hover;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
// Ghost Button
|
||||
@mixin button-ghost {
|
||||
@include button-base;
|
||||
background: transparent;
|
||||
color: $monaco-red-600;
|
||||
border: 2px solid $monaco-red-600;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba($monaco-red-600, 0.1);
|
||||
border-color: $monaco-red-700;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Card Patterns
|
||||
|
||||
```scss
|
||||
// Base Card
|
||||
@mixin card-base {
|
||||
border-radius: $radius-2xl;
|
||||
padding: $space-6;
|
||||
transition: all $duration-base $ease-in-out-smooth;
|
||||
}
|
||||
|
||||
// Glass Card
|
||||
@mixin card-glass {
|
||||
@include card-base;
|
||||
@include glass-effect;
|
||||
|
||||
&:hover {
|
||||
@include hover-lift(-4px);
|
||||
box-shadow: $shadow-glass-hover;
|
||||
}
|
||||
}
|
||||
|
||||
// Gradient Card
|
||||
@mixin card-gradient {
|
||||
@include card-base;
|
||||
background: $gradient-monaco;
|
||||
color: white;
|
||||
box-shadow: $shadow-monaco;
|
||||
|
||||
&:hover {
|
||||
@include hover-lift(-4px);
|
||||
box-shadow: $shadow-monaco-intense;
|
||||
}
|
||||
}
|
||||
|
||||
// Floating Card
|
||||
@mixin card-floating {
|
||||
@include card-base;
|
||||
background: white;
|
||||
box-shadow: $shadow-lg;
|
||||
@include float-animation(5px, 4s);
|
||||
|
||||
&:hover {
|
||||
animation-play-state: paused;
|
||||
box-shadow: $shadow-xl;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Form Elements
|
||||
|
||||
```scss
|
||||
// Input Base
|
||||
@mixin input-base {
|
||||
width: 100%;
|
||||
padding: $space-3 $space-4;
|
||||
font-size: $text-base;
|
||||
border-radius: $radius-xl;
|
||||
transition: all $duration-base $ease-in-out-smooth;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
// Glass Input
|
||||
@mixin input-glass {
|
||||
@include input-base;
|
||||
@include glass-effect(0.6, 10px);
|
||||
border: 2px solid transparent;
|
||||
|
||||
&:focus {
|
||||
border-color: $monaco-red-600;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 0 3px rgba($monaco-red-600, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Floating Label
|
||||
@mixin floating-label {
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
position: absolute;
|
||||
left: $space-4;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: all $duration-base $ease-in-out-smooth;
|
||||
pointer-events: none;
|
||||
color: $gray-500;
|
||||
font-size: $text-base;
|
||||
}
|
||||
|
||||
input:focus + label,
|
||||
input:not(:placeholder-shown) + label {
|
||||
top: 0;
|
||||
transform: translateY(-50%) scale(0.8);
|
||||
background: white;
|
||||
padding: 0 $space-2;
|
||||
color: $monaco-red-600;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎭 Animation Library
|
||||
|
||||
### Entrance Animations
|
||||
|
||||
```scss
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scale-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: rotate(-10deg) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: rotate(0) scale(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Interaction Animations
|
||||
|
||||
```scss
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(-5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wiggle {
|
||||
0%, 100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(-3deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(3deg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Loading Animations
|
||||
|
||||
```scss
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 1;
|
||||
}
|
||||
80%, 100% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.shimmer-effect {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 Responsive Design
|
||||
|
||||
### Breakpoints
|
||||
|
||||
```scss
|
||||
// Mobile First Breakpoints
|
||||
$screen-sm: 640px; // Small devices
|
||||
$screen-md: 768px; // Medium devices
|
||||
$screen-lg: 1024px; // Large devices
|
||||
$screen-xl: 1280px; // Extra large devices
|
||||
$screen-2xl: 1536px; // 2X Extra large devices
|
||||
|
||||
// Mixins
|
||||
@mixin sm {
|
||||
@media (min-width: $screen-sm) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin md {
|
||||
@media (min-width: $screen-md) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin lg {
|
||||
@media (min-width: $screen-lg) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin xl {
|
||||
@media (min-width: $screen-xl) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin 2xl {
|
||||
@media (min-width: $screen-2xl) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Container Widths
|
||||
|
||||
```scss
|
||||
.container {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: $space-4;
|
||||
padding-right: $space-4;
|
||||
|
||||
@include sm {
|
||||
max-width: $screen-sm;
|
||||
}
|
||||
|
||||
@include md {
|
||||
max-width: $screen-md;
|
||||
}
|
||||
|
||||
@include lg {
|
||||
max-width: $screen-lg;
|
||||
}
|
||||
|
||||
@include xl {
|
||||
max-width: $screen-xl;
|
||||
}
|
||||
|
||||
@include 2xl {
|
||||
max-width: $screen-2xl;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎪 Motion Preferences
|
||||
|
||||
```scss
|
||||
// Respect user's motion preferences
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-primary: #{$gray-900};
|
||||
--text-primary: #{$gray-50};
|
||||
--glass-bg: rgba(0, 0, 0, 0.7);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Utility Classes
|
||||
|
||||
### Display Utilities
|
||||
|
||||
```scss
|
||||
.hidden { display: none; }
|
||||
.block { display: block; }
|
||||
.inline-block { display: inline-block; }
|
||||
.inline { display: inline; }
|
||||
.flex { display: flex; }
|
||||
.inline-flex { display: inline-flex; }
|
||||
.grid { display: grid; }
|
||||
|
||||
// Flexbox Utilities
|
||||
.items-start { align-items: flex-start; }
|
||||
.items-center { align-items: center; }
|
||||
.items-end { align-items: flex-end; }
|
||||
.justify-start { justify-content: flex-start; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-end { justify-content: flex-end; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.flex-row { flex-direction: row; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.flex-wrap { flex-wrap: wrap; }
|
||||
.flex-1 { flex: 1 1 0%; }
|
||||
.flex-auto { flex: 1 1 auto; }
|
||||
```
|
||||
|
||||
### Spacing Utilities
|
||||
|
||||
```scss
|
||||
// Margin
|
||||
@each $name, $size in (
|
||||
'0': $space-0,
|
||||
'1': $space-1,
|
||||
'2': $space-2,
|
||||
'3': $space-3,
|
||||
'4': $space-4,
|
||||
'5': $space-5,
|
||||
'6': $space-6,
|
||||
'8': $space-8,
|
||||
'10': $space-10,
|
||||
'12': $space-12,
|
||||
'16': $space-16
|
||||
) {
|
||||
.m-#{$name} { margin: $size; }
|
||||
.mt-#{$name} { margin-top: $size; }
|
||||
.mr-#{$name} { margin-right: $size; }
|
||||
.mb-#{$name} { margin-bottom: $size; }
|
||||
.ml-#{$name} { margin-left: $size; }
|
||||
.mx-#{$name} {
|
||||
margin-left: $size;
|
||||
margin-right: $size;
|
||||
}
|
||||
.my-#{$name} {
|
||||
margin-top: $size;
|
||||
margin-bottom: $size;
|
||||
}
|
||||
}
|
||||
|
||||
// Padding (same pattern as margin)
|
||||
```
|
||||
|
||||
### Text Utilities
|
||||
|
||||
```scss
|
||||
// Font Size
|
||||
.text-xs { font-size: $text-xs; }
|
||||
.text-sm { font-size: $text-sm; }
|
||||
.text-base { font-size: $text-base; }
|
||||
.text-lg { font-size: $text-lg; }
|
||||
.text-xl { font-size: $text-xl; }
|
||||
.text-2xl { font-size: $text-2xl; }
|
||||
.text-3xl { font-size: $text-3xl; }
|
||||
.text-4xl { font-size: $text-4xl; }
|
||||
|
||||
// Font Weight
|
||||
.font-thin { font-weight: $font-thin; }
|
||||
.font-light { font-weight: $font-light; }
|
||||
.font-normal { font-weight: $font-normal; }
|
||||
.font-medium { font-weight: $font-medium; }
|
||||
.font-semibold { font-weight: $font-semibold; }
|
||||
.font-bold { font-weight: $font-bold; }
|
||||
|
||||
// Text Alignment
|
||||
.text-left { text-align: left; }
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
.text-justify { text-align: justify; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*This design system provides the foundation for creating consistent, beautiful, and performant user interfaces across the MonacoUSA Portal.*
|
||||
|
|
@ -0,0 +1,651 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MonacoUSA Portal - Board Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffffff 25%, #fef2f2 50%, #ffffff 75%, #fff5f5 100%);
|
||||
}
|
||||
|
||||
/* Monaco Gradients */
|
||||
.monaco-gradient {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
}
|
||||
|
||||
.monaco-gradient-dark {
|
||||
background: linear-gradient(135deg, #991b1b 0%, #7f1d1d 100%);
|
||||
}
|
||||
|
||||
.monaco-gradient-vibrant {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 50%, #991b1b 100%);
|
||||
}
|
||||
|
||||
/* Glass Effects */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
box-shadow: 0 8px 32px 0 rgba(220, 38, 38, 0.08);
|
||||
}
|
||||
|
||||
.glass-red {
|
||||
background: linear-gradient(135deg, rgba(254, 202, 202, 0.3) 0%, rgba(252, 165, 165, 0.2) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(220, 38, 38, 0.15);
|
||||
box-shadow: 0 8px 32px 0 rgba(220, 38, 38, 0.12);
|
||||
}
|
||||
|
||||
.glass-dark {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Animated Elements */
|
||||
@keyframes slideInRight {
|
||||
from { transform: translateX(100px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideInUp {
|
||||
from { transform: translateY(30px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.slide-in-right {
|
||||
animation: slideInRight 0.8s ease-out;
|
||||
}
|
||||
|
||||
.slide-in-up {
|
||||
animation: slideInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Status Indicators */
|
||||
@keyframes pulse-green {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); }
|
||||
}
|
||||
|
||||
@keyframes pulse-red {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(220, 38, 38, 0); }
|
||||
}
|
||||
|
||||
@keyframes pulse-amber {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.7); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); }
|
||||
}
|
||||
|
||||
.pulse-green { animation: pulse-green 2s infinite; }
|
||||
.pulse-red { animation: pulse-red 2s infinite; }
|
||||
.pulse-amber { animation: pulse-amber 2s infinite; }
|
||||
|
||||
/* Card Effects */
|
||||
.card-3d {
|
||||
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.card-3d:hover {
|
||||
transform: translateY(-10px) rotateX(5deg) scale(1.02);
|
||||
box-shadow: 0 30px 60px rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
/* Gradient Text */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #7f1d1d 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Floating Elements */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||
50% { transform: translateY(-20px) rotate(2deg); }
|
||||
}
|
||||
|
||||
.floating {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Shimmer Effect */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -1000px 0; }
|
||||
100% { background-position: 1000px 0; }
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.4) 50%, transparent 100%);
|
||||
background-size: 1000px 100%;
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
|
||||
/* Data Grid Styles */
|
||||
.data-row {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.data-row:hover {
|
||||
background: linear-gradient(90deg, rgba(220, 38, 38, 0.05) 0%, rgba(220, 38, 38, 0.02) 100%);
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
/* Chart Container */
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen overflow-x-hidden">
|
||||
<!-- Animated Background -->
|
||||
<div class="fixed inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-40 -right-40 w-80 h-80 bg-red-200 rounded-full opacity-20 blur-3xl floating"></div>
|
||||
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-red-100 rounded-full opacity-20 blur-3xl floating" style="animation-delay: 2s;"></div>
|
||||
<div class="absolute top-1/2 left-1/3 w-60 h-60 bg-red-300 rounded-full opacity-10 blur-3xl floating" style="animation-delay: 4s;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Executive Header -->
|
||||
<header class="fixed top-0 left-0 right-0 z-50 glass border-b border-red-100">
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Brand -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="relative group">
|
||||
<div class="absolute inset-0 monaco-gradient rounded-xl blur opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
||||
<div class="relative w-12 h-12 bg-white rounded-xl overflow-hidden shadow-xl">
|
||||
<div class="h-1/2 bg-red-600"></div>
|
||||
<div class="h-1/2 bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold gradient-text">MonacoUSA Portal</h1>
|
||||
<p class="text-xs text-gray-600 font-medium">Board Executive Dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Controls -->
|
||||
<div class="hidden lg:flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2 px-4 py-2 glass-red rounded-full">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full pulse-green"></div>
|
||||
<span class="text-sm font-medium text-gray-700">System Status: Operational</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 px-4 py-2 glass rounded-full">
|
||||
<i data-lucide="users" class="w-4 h-4 text-gray-500"></i>
|
||||
<span class="text-sm font-medium text-gray-700">287 Active Members</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<button class="px-4 py-2 monaco-gradient text-white rounded-lg font-semibold hover:shadow-lg transition-all flex items-center space-x-2">
|
||||
<i data-lucide="user-plus" class="w-4 h-4"></i>
|
||||
<span>Add Member</span>
|
||||
</button>
|
||||
<button class="px-4 py-2 glass hover:bg-red-50 rounded-lg font-semibold transition-all flex items-center space-x-2">
|
||||
<i data-lucide="download" class="w-4 h-4"></i>
|
||||
<span>Export</span>
|
||||
</button>
|
||||
|
||||
<!-- Profile -->
|
||||
<div class="flex items-center space-x-3 pl-3 ml-3 border-l border-gray-200">
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-semibold text-gray-900">Board Executive</p>
|
||||
<p class="text-xs text-red-600">Administrative Access</p>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<img src="https://ui-avatars.com/api/?name=Board+Executive&background=dc2626&color=fff&bold=true"
|
||||
alt="Profile" class="w-11 h-11 rounded-full ring-2 ring-red-200">
|
||||
<span class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 border-2 border-white rounded-full"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Executive Sidebar -->
|
||||
<aside class="fixed left-0 top-20 bottom-0 w-72 glass-dark border-r border-red-100/20 overflow-y-auto">
|
||||
<!-- Quick Stats Panel -->
|
||||
<div class="p-6 border-b border-red-100/20">
|
||||
<div class="glass-red rounded-2xl p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-3">Today's Overview</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="text-center">
|
||||
<p class="text-2xl font-bold gradient-text">43</p>
|
||||
<p class="text-xs text-gray-600">Overdue</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-2xl font-bold text-green-600">$43K</p>
|
||||
<p class="text-xs text-gray-600">Revenue</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-2xl font-bold text-blue-600">89%</p>
|
||||
<p class="text-xs text-gray-600">Attendance</p>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<p class="text-2xl font-bold text-purple-600">15</p>
|
||||
<p class="text-xs text-gray-600">New</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="p-4">
|
||||
<div class="space-y-1">
|
||||
<a href="#" class="flex items-center space-x-3 px-4 py-3 rounded-xl monaco-gradient text-white font-medium shadow-lg">
|
||||
<i data-lucide="shield" class="w-5 h-5"></i>
|
||||
<span>Executive Overview</span>
|
||||
</a>
|
||||
<a href="#" class="flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-red-50 hover:text-red-600 font-medium transition-all">
|
||||
<i data-lucide="users" class="w-5 h-5"></i>
|
||||
<span>Member Management</span>
|
||||
<span class="ml-auto text-xs bg-red-100 text-red-600 px-2 py-1 rounded-full">43</span>
|
||||
</a>
|
||||
<a href="#" class="flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-red-50 hover:text-red-600 font-medium transition-all">
|
||||
<i data-lucide="credit-card" class="w-5 h-5"></i>
|
||||
<span>Dues & Revenue</span>
|
||||
</a>
|
||||
<a href="#" class="flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-red-50 hover:text-red-600 font-medium transition-all">
|
||||
<i data-lucide="calendar" class="w-5 h-5"></i>
|
||||
<span>Event Planning</span>
|
||||
</a>
|
||||
<a href="#" class="flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-red-50 hover:text-red-600 font-medium transition-all">
|
||||
<i data-lucide="bar-chart-3" class="w-5 h-5"></i>
|
||||
<span>Analytics</span>
|
||||
</a>
|
||||
<a href="#" class="flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-red-50 hover:text-red-600 font-medium transition-all">
|
||||
<i data-lucide="mail" class="w-5 h-5"></i>
|
||||
<span>Communications</span>
|
||||
<span class="ml-auto w-2 h-2 bg-red-500 rounded-full pulse-red"></span>
|
||||
</a>
|
||||
<a href="#" class="flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:bg-red-50 hover:text-red-600 font-medium transition-all">
|
||||
<i data-lucide="settings" class="w-5 h-5"></i>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Board Access Badge -->
|
||||
<div class="absolute bottom-4 left-4 right-4">
|
||||
<div class="monaco-gradient rounded-xl p-4 text-white text-center">
|
||||
<i data-lucide="shield-check" class="w-8 h-8 mx-auto mb-2"></i>
|
||||
<p class="text-xs font-semibold">BOARD EXECUTIVE</p>
|
||||
<p class="text-xs opacity-80">Full Administrative Access</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="ml-72 mt-20 p-8">
|
||||
<!-- Executive Header -->
|
||||
<div class="mb-8 slide-in-right">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold gradient-text mb-2">Board Executive Dashboard</h2>
|
||||
<p class="text-gray-600">Real-time organization management and insights</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-right">
|
||||
<p class="text-sm text-gray-500">Last Updated</p>
|
||||
<p class="text-lg font-semibold text-gray-900">2 min ago</p>
|
||||
</div>
|
||||
<button class="p-3 glass rounded-xl hover:bg-red-50 transition-colors">
|
||||
<i data-lucide="refresh-cw" class="w-5 h-5 text-gray-700"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Total Members KPI -->
|
||||
<div class="glass rounded-2xl p-6 card-3d slide-in-up">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-14 h-14 monaco-gradient rounded-xl flex items-center justify-center shadow-lg">
|
||||
<i data-lucide="users" class="w-7 h-7 text-white"></i>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1 text-green-600">
|
||||
<i data-lucide="trending-up" class="w-4 h-4"></i>
|
||||
<span class="text-sm font-bold">+12%</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mb-1">Total Members</p>
|
||||
<p class="text-3xl font-bold text-gray-900">287</p>
|
||||
<div class="mt-4 flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">Target: 300</span>
|
||||
<div class="w-24 bg-gray-200 rounded-full h-2">
|
||||
<div class="monaco-gradient h-2 rounded-full" style="width: 95.7%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Revenue KPI -->
|
||||
<div class="glass rounded-2xl p-6 card-3d slide-in-up" style="animation-delay: 0.1s">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-14 h-14 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<i data-lucide="dollar-sign" class="w-7 h-7 text-white"></i>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1 text-green-600">
|
||||
<i data-lucide="trending-up" class="w-4 h-4"></i>
|
||||
<span class="text-sm font-bold">+8%</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mb-1">YTD Revenue</p>
|
||||
<p class="text-3xl font-bold text-gray-900">$43,050</p>
|
||||
<div class="mt-4 flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">Goal: $50,000</span>
|
||||
<div class="w-24 bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-gradient-to-r from-green-500 to-emerald-600 h-2 rounded-full" style="width: 86%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overdue Dues KPI -->
|
||||
<div class="glass rounded-2xl p-6 card-3d slide-in-up" style="animation-delay: 0.2s">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-14 h-14 bg-gradient-to-br from-amber-500 to-orange-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<i data-lucide="alert-triangle" class="w-7 h-7 text-white"></i>
|
||||
</div>
|
||||
<span class="px-2 py-1 bg-amber-100 text-amber-700 text-xs font-bold rounded-full pulse-amber">ACTION</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mb-1">Overdue Dues</p>
|
||||
<p class="text-3xl font-bold text-gray-900">43</p>
|
||||
<div class="mt-4 flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">Amount: $6,450</span>
|
||||
<button class="text-amber-600 font-semibold hover:text-amber-700">View All →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Event Success KPI -->
|
||||
<div class="glass rounded-2xl p-6 card-3d slide-in-up" style="animation-delay: 0.3s">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-14 h-14 bg-gradient-to-br from-purple-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg">
|
||||
<i data-lucide="calendar-check" class="w-7 h-7 text-white"></i>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1 text-green-600">
|
||||
<i data-lucide="trending-up" class="w-4 h-4"></i>
|
||||
<span class="text-sm font-bold">+25%</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mb-1">Avg Attendance</p>
|
||||
<p class="text-3xl font-bold text-gray-900">89%</p>
|
||||
<div class="mt-4 flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">Last 3 events</span>
|
||||
<div class="flex -space-x-2">
|
||||
<div class="w-6 h-6 bg-green-500 rounded-full border-2 border-white"></div>
|
||||
<div class="w-6 h-6 bg-green-500 rounded-full border-2 border-white"></div>
|
||||
<div class="w-6 h-6 bg-amber-500 rounded-full border-2 border-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Priority Actions Banner -->
|
||||
<div class="mb-8 slide-in-right">
|
||||
<div class="monaco-gradient-vibrant rounded-2xl p-6 text-white relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 w-64 h-64 bg-white rounded-full blur-3xl opacity-10"></div>
|
||||
<div class="relative z-10 flex items-center justify-between">
|
||||
<div class="flex items-center space-x-6">
|
||||
<div class="w-16 h-16 bg-white/20 backdrop-blur rounded-xl flex items-center justify-center">
|
||||
<i data-lucide="alert-circle" class="w-8 h-8"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold mb-1">43 Members with Overdue Dues</h3>
|
||||
<p class="text-white/90">Total outstanding amount: $6,450 • Average days overdue: 67</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button class="px-5 py-2.5 bg-white/20 backdrop-blur rounded-lg font-semibold hover:bg-white/30 transition-all">
|
||||
View Details
|
||||
</button>
|
||||
<button class="px-5 py-2.5 bg-white text-red-600 rounded-lg font-semibold hover:shadow-xl transition-all flex items-center space-x-2">
|
||||
<i data-lucide="send" class="w-4 h-4"></i>
|
||||
<span>Send Reminders</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Member Management Panel -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Dues Management</h3>
|
||||
<div class="flex space-x-2">
|
||||
<button class="px-3 py-1.5 text-sm font-medium text-white monaco-gradient rounded-lg">Overdue (43)</button>
|
||||
<button class="px-3 py-1.5 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-lg">Due Soon (5)</button>
|
||||
<button class="px-3 py-1.5 text-sm font-medium text-gray-600 hover:bg-gray-100 rounded-lg">Paid (239)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Member Cards -->
|
||||
<div class="space-y-4">
|
||||
<!-- Member Card 1 -->
|
||||
<div class="glass-red rounded-xl p-4 data-row">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<img src="https://ui-avatars.com/api/?name=Annette+Anderson&background=dc2626&color=fff"
|
||||
alt="Member" class="w-12 h-12 rounded-full ring-2 ring-red-200">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Annette Anderson</h4>
|
||||
<div class="flex items-center space-x-3 text-sm text-gray-500">
|
||||
<span>ID: MUSA-16</span>
|
||||
<span>•</span>
|
||||
<span class="text-red-600 font-medium">85 days overdue</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-right">
|
||||
<p class="text-lg font-bold text-gray-900">$150</p>
|
||||
<p class="text-xs text-gray-500">Due Jun 6, 2024</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button class="p-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i data-lucide="check" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="p-2 glass hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<i data-lucide="mail" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="p-2 glass hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<i data-lucide="more-vertical" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Member Card 2 -->
|
||||
<div class="glass-red rounded-xl p-4 data-row">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<img src="https://ui-avatars.com/api/?name=Danilo+Copiz&background=dc2626&color=fff"
|
||||
alt="Member" class="w-12 h-12 rounded-full ring-2 ring-red-200">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Danilo Copiz</h4>
|
||||
<div class="flex items-center space-x-3 text-sm text-gray-500">
|
||||
<span>ID: MUSA-19</span>
|
||||
<span>•</span>
|
||||
<span class="text-red-600 font-medium">85 days overdue</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-right">
|
||||
<p class="text-lg font-bold text-gray-900">$150</p>
|
||||
<p class="text-xs text-gray-500">Due Jun 6, 2024</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button class="p-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i data-lucide="check" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="p-2 glass hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<i data-lucide="mail" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="p-2 glass hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<i data-lucide="more-vertical" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Member Card 3 -->
|
||||
<div class="glass-red rounded-xl p-4 data-row">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-4">
|
||||
<img src="https://ui-avatars.com/api/?name=Ian+Sosso&background=dc2626&color=fff"
|
||||
alt="Member" class="w-12 h-12 rounded-full ring-2 ring-red-200">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">Ian Sosso</h4>
|
||||
<div class="flex items-center space-x-3 text-sm text-gray-500">
|
||||
<span>ID: MUSA-78</span>
|
||||
<span>•</span>
|
||||
<span class="text-red-600 font-medium">81 days overdue</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="text-right">
|
||||
<p class="text-lg font-bold text-gray-900">$150</p>
|
||||
<p class="text-xs text-gray-500">Due Jun 10, 2024</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button class="p-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
|
||||
<i data-lucide="check" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="p-2 glass hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<i data-lucide="mail" class="w-4 h-4"></i>
|
||||
</button>
|
||||
<button class="p-2 glass hover:bg-gray-50 rounded-lg transition-colors">
|
||||
<i data-lucide="more-vertical" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 text-center">
|
||||
<button class="text-red-600 hover:text-red-700 font-semibold">
|
||||
View All 43 Overdue Members →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Analytics Panel -->
|
||||
<div class="space-y-6">
|
||||
<!-- Revenue Chart -->
|
||||
<div class="glass rounded-2xl p-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-4">Revenue Trend</h3>
|
||||
<div class="chart-container">
|
||||
<canvas id="revenueChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="glass rounded-2xl p-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-4">Quick Actions</h3>
|
||||
<div class="space-y-3">
|
||||
<button class="w-full flex items-center justify-between p-3 glass-red rounded-xl hover:shadow-md transition-all group">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i data-lucide="user-plus" class="w-5 h-5 text-red-600"></i>
|
||||
<span class="font-medium text-gray-700">Add New Member</span>
|
||||
</div>
|
||||
<i data-lucide="arrow-right" class="w-4 h-4 text-gray-400 group-hover:translate-x-1 transition-transform"></i>
|
||||
</button>
|
||||
<button class="w-full flex items-center justify-between p-3 glass-red rounded-xl hover:shadow-md transition-all group">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i data-lucide="calendar-plus" class="w-5 h-5 text-red-600"></i>
|
||||
<span class="font-medium text-gray-700">Create Event</span>
|
||||
</div>
|
||||
<i data-lucide="arrow-right" class="w-4 h-4 text-gray-400 group-hover:translate-x-1 transition-transform"></i>
|
||||
</button>
|
||||
<button class="w-full flex items-center justify-between p-3 glass-red rounded-xl hover:shadow-md transition-all group">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i data-lucide="file-text" class="w-5 h-5 text-red-600"></i>
|
||||
<span class="font-medium text-gray-700">Generate Report</span>
|
||||
</div>
|
||||
<i data-lucide="arrow-right" class="w-4 h-4 text-gray-400 group-hover:translate-x-1 transition-transform"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
// Revenue Chart
|
||||
const ctx = document.getElementById('revenueChart').getContext('2d');
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 250);
|
||||
gradient.addColorStop(0, 'rgba(220, 38, 38, 0.3)');
|
||||
gradient.addColorStop(1, 'rgba(220, 38, 38, 0)');
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
|
||||
datasets: [{
|
||||
label: 'Revenue',
|
||||
data: [32000, 34000, 35500, 38000, 41000, 43050],
|
||||
borderColor: '#dc2626',
|
||||
backgroundColor: gradient,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointBackgroundColor: '#fff',
|
||||
pointBorderColor: '#dc2626',
|
||||
pointBorderWidth: 2,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return '$' + (value / 1000) + 'k';
|
||||
}
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,601 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MonacoUSA Portal - Member Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(135deg, #fef3f3 0%, #ffffff 50%, #fef3f3 100%);
|
||||
}
|
||||
|
||||
/* Monaco Red Gradient Variations */
|
||||
.monaco-gradient {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
}
|
||||
|
||||
.monaco-gradient-soft {
|
||||
background: linear-gradient(135deg, #fecaca 0%, #fca5a5 100%);
|
||||
}
|
||||
|
||||
.monaco-gradient-vibrant {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 50%, #991b1b 100%);
|
||||
}
|
||||
|
||||
.monaco-gradient-subtle {
|
||||
background: linear-gradient(135deg, #fee2e2 0%, #fecaca 100%);
|
||||
}
|
||||
|
||||
/* Glass Morphism Effects */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px 0 rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
|
||||
.glass-red {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(220, 38, 38, 0.2);
|
||||
box-shadow: 0 8px 32px 0 rgba(220, 38, 38, 0.15);
|
||||
}
|
||||
|
||||
.glass-dark {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Animated Gradients */
|
||||
.animated-gradient {
|
||||
background: linear-gradient(270deg, #dc2626, #ef4444, #dc2626);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
/* Glow Effects */
|
||||
.glow-red {
|
||||
box-shadow: 0 0 40px rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
.glow-red-intense {
|
||||
box-shadow: 0 0 60px rgba(220, 38, 38, 0.5),
|
||||
0 0 100px rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
/* Card Hover Effects */
|
||||
.card-hover {
|
||||
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: 0 20px 60px rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
/* Floating Animation */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
.floating {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Pulse Animation */
|
||||
@keyframes pulse-red {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 20px rgba(220, 38, 38, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse-red 2s infinite;
|
||||
}
|
||||
|
||||
/* Background Pattern */
|
||||
.pattern-monaco {
|
||||
background-color: #ffffff;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 80%, rgba(220, 38, 38, 0.1) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(220, 38, 38, 0.08) 0%, transparent 50%),
|
||||
radial-gradient(circle at 40% 40%, rgba(220, 38, 38, 0.05) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
/* Sidebar Styles */
|
||||
.sidebar-item {
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: linear-gradient(90deg, rgba(220, 38, 38, 0.1) 0%, transparent 100%);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-item:hover::before {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-item.active {
|
||||
background: linear-gradient(90deg, rgba(220, 38, 38, 0.15) 0%, rgba(220, 38, 38, 0.05) 100%);
|
||||
border-left: 4px solid #dc2626;
|
||||
}
|
||||
|
||||
/* Number Counter Animation */
|
||||
@keyframes countUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.count-up {
|
||||
animation: countUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
/* Shimmer Effect */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -1000px 0; }
|
||||
100% { background-position: 1000px 0; }
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
|
||||
background-size: 1000px 100%;
|
||||
animation: shimmer 3s infinite;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="pattern-monaco min-h-screen">
|
||||
<!-- Animated Background Elements -->
|
||||
<div class="fixed inset-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute top-20 left-20 w-72 h-72 bg-red-200 rounded-full opacity-20 blur-3xl floating"></div>
|
||||
<div class="absolute bottom-20 right-20 w-96 h-96 bg-red-300 rounded-full opacity-15 blur-3xl floating" style="animation-delay: 3s;"></div>
|
||||
<div class="absolute top-1/2 left-1/2 w-64 h-64 bg-red-100 rounded-full opacity-20 blur-3xl floating" style="animation-delay: 1.5s;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Premium Header -->
|
||||
<header class="fixed top-0 left-0 right-0 z-50 glass border-b border-white/20">
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Left Section -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<!-- Monaco Flag Logo Animation -->
|
||||
<div class="relative group">
|
||||
<div class="absolute inset-0 monaco-gradient rounded-xl blur-lg opacity-50 group-hover:opacity-75 transition-opacity"></div>
|
||||
<div class="relative w-12 h-12 bg-white rounded-xl overflow-hidden shadow-xl">
|
||||
<div class="h-1/2 bg-red-600"></div>
|
||||
<div class="h-1/2 bg-white"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold bg-gradient-to-r from-red-600 to-red-800 bg-clip-text text-transparent">
|
||||
MonacoUSA Portal
|
||||
</h1>
|
||||
<p class="text-xs text-gray-600 font-medium">Excellence in Community</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Search -->
|
||||
<div class="hidden lg:flex items-center">
|
||||
<div class="relative">
|
||||
<input type="text" placeholder="Search members, events, documents..."
|
||||
class="w-96 px-5 py-2.5 pl-12 glass rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-red-500/30 transition-all">
|
||||
<i data-lucide="search" class="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"></i>
|
||||
<kbd class="absolute right-4 top-1/2 -translate-y-1/2 text-xs bg-gray-100 px-2 py-1 rounded">⌘K</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Section -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Quick Actions -->
|
||||
<button class="p-2.5 glass rounded-xl hover:bg-red-50 transition-colors group">
|
||||
<i data-lucide="plus" class="w-5 h-5 text-gray-700 group-hover:text-red-600 transition-colors"></i>
|
||||
</button>
|
||||
|
||||
<!-- Notifications with Badge -->
|
||||
<button class="relative p-2.5 glass rounded-xl hover:bg-red-50 transition-colors group">
|
||||
<i data-lucide="bell" class="w-5 h-5 text-gray-700 group-hover:text-red-600 transition-colors"></i>
|
||||
<span class="absolute top-1 right-1 w-2.5 h-2.5 bg-red-500 rounded-full pulse"></span>
|
||||
</button>
|
||||
|
||||
<!-- Premium User Profile -->
|
||||
<div class="flex items-center space-x-3 pl-3 ml-3 border-l border-gray-200">
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-semibold text-gray-900">Matthew Ciaccio</p>
|
||||
<p class="text-xs text-gray-500">Gold Member</p>
|
||||
</div>
|
||||
<div class="relative group cursor-pointer">
|
||||
<div class="absolute inset-0 monaco-gradient rounded-full blur opacity-0 group-hover:opacity-50 transition-opacity"></div>
|
||||
<img src="https://ui-avatars.com/api/?name=Matthew+Ciaccio&background=dc2626&color=fff&bold=true"
|
||||
alt="Profile" class="relative w-11 h-11 rounded-full ring-2 ring-red-100">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Modern Sidebar -->
|
||||
<aside class="fixed left-0 top-20 bottom-0 w-72 glass-dark border-r border-white/10 overflow-y-auto">
|
||||
<!-- User Status Card -->
|
||||
<div class="p-6">
|
||||
<div class="glass-red rounded-2xl p-4 relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 w-32 h-32 monaco-gradient rounded-full blur-2xl opacity-30"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center space-x-3 mb-3">
|
||||
<div class="w-12 h-12 rounded-xl monaco-gradient flex items-center justify-center">
|
||||
<i data-lucide="crown" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-gray-900">Gold Member</p>
|
||||
<p class="text-xs text-gray-600">Since 2019</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-gray-600">Status</span>
|
||||
<span class="font-semibold text-green-600">Active</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-xs">
|
||||
<span class="text-gray-600">Points</span>
|
||||
<span class="font-semibold text-gray-900">2,450</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="px-4 pb-6">
|
||||
<div class="space-y-1">
|
||||
<a href="#" class="sidebar-item active flex items-center space-x-3 px-4 py-3 rounded-xl text-red-600 font-medium">
|
||||
<i data-lucide="layout-dashboard" class="w-5 h-5"></i>
|
||||
<span>Dashboard</span>
|
||||
<span class="ml-auto text-xs bg-red-100 text-red-600 px-2 py-1 rounded-full">New</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:text-red-600 font-medium">
|
||||
<i data-lucide="calendar-days" class="w-5 h-5"></i>
|
||||
<span>Events</span>
|
||||
<span class="ml-auto text-xs text-gray-400">12</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:text-red-600 font-medium">
|
||||
<i data-lucide="users" class="w-5 h-5"></i>
|
||||
<span>Members</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:text-red-600 font-medium">
|
||||
<i data-lucide="wallet" class="w-5 h-5"></i>
|
||||
<span>Dues & Payments</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:text-red-600 font-medium">
|
||||
<i data-lucide="file-text" class="w-5 h-5"></i>
|
||||
<span>Documents</span>
|
||||
</a>
|
||||
<a href="#" class="sidebar-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:text-red-600 font-medium">
|
||||
<i data-lucide="message-circle" class="w-5 h-5"></i>
|
||||
<span>Messages</span>
|
||||
<span class="ml-auto w-2 h-2 bg-red-500 rounded-full pulse"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||
<p class="px-4 text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">Quick Actions</p>
|
||||
<div class="space-y-1">
|
||||
<button class="w-full sidebar-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:text-red-600 font-medium">
|
||||
<i data-lucide="download" class="w-5 h-5"></i>
|
||||
<span>Download ID Card</span>
|
||||
</button>
|
||||
<button class="w-full sidebar-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 hover:text-red-600 font-medium">
|
||||
<i data-lucide="headphones" class="w-5 h-5"></i>
|
||||
<span>Support</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-72 mt-20 p-8">
|
||||
<!-- Hero Welcome Section -->
|
||||
<div class="mb-8">
|
||||
<div class="glass rounded-3xl p-8 relative overflow-hidden">
|
||||
<div class="absolute top-0 right-0 w-96 h-96 monaco-gradient rounded-full blur-3xl opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<h2 class="text-4xl font-bold text-gray-900 mb-2">Welcome back, Matthew! 👋</h2>
|
||||
<p class="text-gray-600 text-lg">Your membership is active and you have 2 upcoming events this week.</p>
|
||||
|
||||
<!-- Quick Stats Bar -->
|
||||
<div class="flex items-center space-x-8 mt-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full pulse"></div>
|
||||
<span class="text-sm text-gray-600">Portal Status: <strong class="text-green-600">Online</strong></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="users" class="w-4 h-4 text-gray-400"></i>
|
||||
<span class="text-sm text-gray-600">Members Online: <strong>34</strong></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="calendar" class="w-4 h-4 text-gray-400"></i>
|
||||
<span class="text-sm text-gray-600">Next Event: <strong>Dec 15</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Premium Stats Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Membership Card -->
|
||||
<div class="glass rounded-2xl p-6 card-hover relative overflow-hidden group">
|
||||
<div class="absolute inset-0 monaco-gradient opacity-0 group-hover:opacity-10 transition-opacity"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-green-400 to-green-600 flex items-center justify-center shadow-lg">
|
||||
<i data-lucide="check-circle" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<span class="text-xs font-bold text-green-600 bg-green-100 px-3 py-1 rounded-full">ACTIVE</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mb-1">Membership Status</p>
|
||||
<p class="text-3xl font-bold text-gray-900 count-up">Active</p>
|
||||
<div class="mt-4 pt-4 border-t border-gray-100">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">Expires</span>
|
||||
<span class="font-semibold text-gray-700">Dec 31, 2024</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dues Card -->
|
||||
<div class="glass rounded-2xl p-6 card-hover relative overflow-hidden group">
|
||||
<div class="absolute inset-0 animated-gradient opacity-0 group-hover:opacity-10 transition-opacity"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-12 h-12 rounded-xl monaco-gradient flex items-center justify-center shadow-lg">
|
||||
<i data-lucide="credit-card" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<span class="text-xs font-bold text-amber-600 bg-amber-100 px-3 py-1 rounded-full shimmer">DUE SOON</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mb-1">Annual Dues</p>
|
||||
<p class="text-3xl font-bold text-gray-900 count-up">$150</p>
|
||||
<div class="mt-4 pt-4 border-t border-gray-100">
|
||||
<button class="w-full py-2 monaco-gradient text-white rounded-lg font-semibold hover:shadow-lg transition-all">
|
||||
Pay Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Events Card -->
|
||||
<div class="glass rounded-2xl p-6 card-hover relative overflow-hidden group">
|
||||
<div class="absolute inset-0 monaco-gradient opacity-0 group-hover:opacity-10 transition-opacity"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-400 to-indigo-600 flex items-center justify-center shadow-lg">
|
||||
<i data-lucide="calendar-check" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<span class="text-xs font-bold text-indigo-600 bg-indigo-100 px-3 py-1 rounded-full">THIS YEAR</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mb-1">Events Attended</p>
|
||||
<p class="text-3xl font-bold text-gray-900 count-up">12</p>
|
||||
<div class="mt-4 pt-4 border-t border-gray-100">
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-500">Upcoming</span>
|
||||
<span class="font-semibold text-gray-700">3 events</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Points Card -->
|
||||
<div class="glass rounded-2xl p-6 card-hover relative overflow-hidden group">
|
||||
<div class="absolute inset-0 monaco-gradient opacity-0 group-hover:opacity-10 transition-opacity"></div>
|
||||
<div class="relative">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-gradient-to-br from-purple-400 to-purple-600 flex items-center justify-center shadow-lg">
|
||||
<i data-lucide="trophy" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<span class="text-xs font-bold text-purple-600 bg-purple-100 px-3 py-1 rounded-full">TOP 10%</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mb-1">Member Points</p>
|
||||
<p class="text-3xl font-bold text-gray-900 count-up">2,450</p>
|
||||
<div class="mt-4 pt-4 border-t border-gray-100">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="monaco-gradient h-2 rounded-full" style="width: 78%"></div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-2">550 points to Platinum</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Featured Event Banner -->
|
||||
<div class="mb-8">
|
||||
<div class="monaco-gradient rounded-3xl p-8 relative overflow-hidden glow-red">
|
||||
<div class="absolute top-0 right-0 w-96 h-96 bg-white rounded-full blur-3xl opacity-10"></div>
|
||||
<div class="relative z-10 flex items-center justify-between">
|
||||
<div class="text-white">
|
||||
<div class="flex items-center space-x-3 mb-3">
|
||||
<div class="w-10 h-10 bg-white/20 backdrop-blur rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="sparkles" class="w-5 h-5"></i>
|
||||
</div>
|
||||
<span class="text-sm font-semibold bg-white/20 px-3 py-1 rounded-full">FEATURED EVENT</span>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold mb-2">Annual Monaco Gala 2024</h3>
|
||||
<p class="text-white/90 mb-4 max-w-xl">Join us for an elegant evening celebrating Monaco's heritage and culture. Black tie event with dinner, entertainment, and networking.</p>
|
||||
<div class="flex items-center space-x-6 text-sm">
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="calendar" class="w-4 h-4"></i>
|
||||
<span>December 15, 2024</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="clock" class="w-4 h-4"></i>
|
||||
<span>7:00 PM - 11:00 PM</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<i data-lucide="map-pin" class="w-4 h-4"></i>
|
||||
<span>Monaco Embassy, Washington DC</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-3">
|
||||
<button class="px-6 py-3 bg-white text-red-600 rounded-xl font-semibold hover:shadow-xl transition-all">
|
||||
RSVP Now
|
||||
</button>
|
||||
<button class="px-6 py-3 bg-white/20 backdrop-blur text-white rounded-xl font-semibold hover:bg-white/30 transition-all">
|
||||
View Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Recent Activity Feed -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="glass rounded-2xl p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-bold text-gray-900">Recent Activity</h3>
|
||||
<button class="text-sm text-red-600 hover:text-red-700 font-medium">View All</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Activity Item -->
|
||||
<div class="flex items-start space-x-4 p-4 hover:bg-gray-50 rounded-xl transition-colors">
|
||||
<div class="w-10 h-10 rounded-xl bg-green-100 flex items-center justify-center flex-shrink-0">
|
||||
<i data-lucide="user-plus" class="w-5 h-5 text-green-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">New member joined</p>
|
||||
<p class="text-sm text-gray-600 mt-1">Sarah Johnson joined as a Silver member</p>
|
||||
<p class="text-xs text-gray-400 mt-2">2 hours ago</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Item -->
|
||||
<div class="flex items-start space-x-4 p-4 hover:bg-gray-50 rounded-xl transition-colors">
|
||||
<div class="w-10 h-10 rounded-xl bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<i data-lucide="calendar" class="w-5 h-5 text-blue-600"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Event reminder</p>
|
||||
<p class="text-sm text-gray-600 mt-1">Wine Tasting Event is coming up in 3 days</p>
|
||||
<p class="text-xs text-gray-400 mt-2">5 hours ago</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Item -->
|
||||
<div class="flex items-start space-x-4 p-4 hover:bg-gray-50 rounded-xl transition-colors">
|
||||
<div class="w-10 h-10 rounded-xl monaco-gradient flex items-center justify-center flex-shrink-0">
|
||||
<i data-lucide="trophy" class="w-5 h-5 text-white"></i>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">Achievement unlocked!</p>
|
||||
<p class="text-sm text-gray-600 mt-1">You've attended 10+ events this year</p>
|
||||
<p class="text-xs text-gray-400 mt-2">1 day ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Member Spotlight -->
|
||||
<div>
|
||||
<div class="glass rounded-2xl p-6">
|
||||
<h3 class="text-xl font-bold text-gray-900 mb-6">Member Spotlight</h3>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="relative inline-block mb-4">
|
||||
<img src="https://ui-avatars.com/api/?name=John+Smith&background=dc2626&color=fff&size=128"
|
||||
alt="Member" class="w-24 h-24 rounded-full ring-4 ring-red-100">
|
||||
<div class="absolute bottom-0 right-0 w-7 h-7 bg-green-500 rounded-full border-3 border-white flex items-center justify-center">
|
||||
<i data-lucide="check" class="w-4 h-4 text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="text-lg font-semibold text-gray-900">John Smith</h4>
|
||||
<p class="text-sm text-gray-500 mb-1">Board Member</p>
|
||||
<div class="inline-flex items-center space-x-2 px-3 py-1 bg-gradient-to-r from-amber-400 to-orange-500 text-white rounded-full text-xs font-semibold mt-2">
|
||||
<i data-lucide="award" class="w-3 h-3"></i>
|
||||
<span>PLATINUM MEMBER</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-gray-100">
|
||||
<div class="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-gray-900">15</p>
|
||||
<p class="text-xs text-gray-500">Years</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-gray-900">89</p>
|
||||
<p class="text-xs text-gray-500">Events</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-2xl font-bold text-gray-900">5.2k</p>
|
||||
<p class="text-xs text-gray-500">Points</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="w-full mt-6 px-4 py-2 monaco-gradient text-white rounded-lg font-semibold hover:shadow-lg transition-all">
|
||||
View Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Floating Action Button -->
|
||||
<div class="fixed bottom-8 right-8">
|
||||
<button class="w-14 h-14 monaco-gradient rounded-full shadow-2xl hover:shadow-3xl transition-all glow-red-intense flex items-center justify-center group">
|
||||
<i data-lucide="plus" class="w-6 h-6 text-white group-hover:rotate-45 transition-transform"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
lucide.createIcons();
|
||||
|
||||
// Add number animation on scroll
|
||||
const observerOptions = {
|
||||
threshold: 0.5
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('count-up');
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
document.querySelectorAll('.count-up').forEach(el => {
|
||||
observer.observe(el);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<span>{{ displayValue }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
|
||||
interface Props {
|
||||
value: number
|
||||
duration?: number
|
||||
format?: (value: number) => string
|
||||
delay?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
duration: 1000,
|
||||
format: (value: number) => value.toLocaleString(),
|
||||
delay: 0
|
||||
})
|
||||
|
||||
const displayValue = ref(props.format(0))
|
||||
const startTimestamp = ref<number | null>(null)
|
||||
const startValue = ref(0)
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
if (!startTimestamp.value) {
|
||||
startTimestamp.value = timestamp
|
||||
}
|
||||
|
||||
const progress = Math.min((timestamp - startTimestamp.value) / props.duration, 1)
|
||||
|
||||
// Easing function for smooth animation
|
||||
const easeOutQuart = (t: number) => 1 - Math.pow(1 - t, 4)
|
||||
const easedProgress = easeOutQuart(progress)
|
||||
|
||||
const currentValue = startValue.value + (props.value - startValue.value) * easedProgress
|
||||
displayValue.value = props.format(currentValue)
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
const startAnimation = () => {
|
||||
startTimestamp.value = null
|
||||
|
||||
if (props.delay > 0) {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(animate)
|
||||
}, props.delay)
|
||||
} else {
|
||||
requestAnimationFrame(animate)
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.value, (newValue, oldValue) => {
|
||||
startValue.value = oldValue || 0
|
||||
startAnimation()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
startAnimation()
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,417 @@
|
|||
<template>
|
||||
<div
|
||||
class="floating-input"
|
||||
:class="[
|
||||
`floating-input--${variant}`,
|
||||
{
|
||||
'floating-input--focused': isFocused || modelValue,
|
||||
'floating-input--error': error,
|
||||
'floating-input--disabled': disabled
|
||||
}
|
||||
]"
|
||||
>
|
||||
<div class="floating-input__wrapper">
|
||||
<Icon
|
||||
v-if="leftIcon"
|
||||
:name="leftIcon"
|
||||
class="floating-input__icon floating-input__icon--left"
|
||||
/>
|
||||
|
||||
<input
|
||||
:id="inputId"
|
||||
v-model="modelValue"
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
:autocomplete="autocomplete"
|
||||
class="floating-input__field"
|
||||
:class="{
|
||||
'floating-input__field--with-left-icon': leftIcon,
|
||||
'floating-input__field--with-right-icon': rightIcon || clearable
|
||||
}"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
|
||||
<label
|
||||
:for="inputId"
|
||||
class="floating-input__label"
|
||||
:class="{
|
||||
'floating-input__label--floating': isFocused || modelValue,
|
||||
'floating-input__label--with-icon': leftIcon
|
||||
}"
|
||||
>
|
||||
{{ label }}
|
||||
<span v-if="required" class="floating-input__required">*</span>
|
||||
</label>
|
||||
|
||||
<button
|
||||
v-if="clearable && modelValue"
|
||||
type="button"
|
||||
class="floating-input__clear"
|
||||
@click="clearInput"
|
||||
>
|
||||
<Icon name="x" />
|
||||
</button>
|
||||
|
||||
<Icon
|
||||
v-if="rightIcon && !clearable"
|
||||
:name="rightIcon"
|
||||
class="floating-input__icon floating-input__icon--right"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Transition name="message">
|
||||
<div v-if="error || helperText" class="floating-input__message">
|
||||
<Icon
|
||||
v-if="error"
|
||||
name="alert-circle"
|
||||
class="floating-input__message-icon"
|
||||
/>
|
||||
<span>{{ error || helperText }}</span>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import Icon from '~/components/ui/Icon.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue?: string
|
||||
label: string
|
||||
type?: 'text' | 'email' | 'password' | 'tel' | 'url' | 'number'
|
||||
variant?: 'glass' | 'solid' | 'outline'
|
||||
leftIcon?: string
|
||||
rightIcon?: string
|
||||
error?: string
|
||||
helperText?: string
|
||||
required?: boolean
|
||||
disabled?: boolean
|
||||
readonly?: boolean
|
||||
clearable?: boolean
|
||||
autocomplete?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'text',
|
||||
variant: 'glass',
|
||||
required: false,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
clearable: false,
|
||||
autocomplete: 'off'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
'focus': []
|
||||
'blur': []
|
||||
'clear': []
|
||||
}>()
|
||||
|
||||
const isFocused = ref(false)
|
||||
const inputId = computed(() => `input-${Math.random().toString(36).substr(2, 9)}`)
|
||||
|
||||
const handleFocus = () => {
|
||||
isFocused.value = true
|
||||
emit('focus')
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
isFocused.value = false
|
||||
emit('blur')
|
||||
}
|
||||
|
||||
const clearInput = () => {
|
||||
emit('update:modelValue', '')
|
||||
emit('clear')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.floating-input {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// Base styles
|
||||
.floating-input--glass & {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover:not(.floating-input--disabled &) {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(220, 38, 38, 0.2);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.floating-input--solid & {
|
||||
background: white;
|
||||
border: 2px solid #e5e5e5;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover:not(.floating-input--disabled &) {
|
||||
border-color: #d4d4d4;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.floating-input--outline & {
|
||||
background: transparent;
|
||||
border: 2px solid #d4d4d4;
|
||||
|
||||
&:hover:not(.floating-input--disabled &) {
|
||||
border-color: #a3a3a3;
|
||||
}
|
||||
}
|
||||
|
||||
// Focus state
|
||||
.floating-input--focused & {
|
||||
border-color: #dc2626;
|
||||
box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.1);
|
||||
transform: translateY(-1px);
|
||||
|
||||
.floating-input--glass& {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
// Error state
|
||||
.floating-input--error & {
|
||||
border-color: #ef4444;
|
||||
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
// Disabled state
|
||||
.floating-input--disabled & {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
&__field {
|
||||
flex: 1;
|
||||
padding: 1.25rem 1rem 0.5rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
color: #27272a;
|
||||
transition: padding 0.2s ease;
|
||||
|
||||
&--with-left-icon {
|
||||
padding-left: 3rem;
|
||||
}
|
||||
|
||||
&--with-right-icon {
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
// Remove autofill background
|
||||
&:-webkit-autofill {
|
||||
-webkit-box-shadow: 0 0 0 1000px transparent inset;
|
||||
-webkit-text-fill-color: #27272a;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 1rem;
|
||||
color: #71717a;
|
||||
pointer-events: none;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: transparent;
|
||||
padding: 0 0.25rem;
|
||||
|
||||
&--with-icon {
|
||||
left: 3rem;
|
||||
}
|
||||
|
||||
&--floating {
|
||||
top: 0.75rem;
|
||||
transform: translateY(0);
|
||||
font-size: 0.75rem;
|
||||
color: #dc2626;
|
||||
font-weight: 500;
|
||||
|
||||
.floating-input--glass & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(255, 255, 255, 0.9) 0%,
|
||||
rgba(255, 255, 255, 0.9) 50%,
|
||||
transparent 50%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.floating-input--solid & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
white 0%,
|
||||
white 50%,
|
||||
transparent 50%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.floating-input--error &--floating {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
&__required {
|
||||
color: #ef4444;
|
||||
margin-left: 0.125rem;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: #dc2626;
|
||||
|
||||
&--left {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
&--right {
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__clear {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: #dc2626;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.2);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #71717a;
|
||||
|
||||
.floating-input--error & {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
&__message-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
.message-enter-active,
|
||||
.message-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.message-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.message-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.floating-input {
|
||||
&__field {
|
||||
color: white;
|
||||
|
||||
&:-webkit-autofill {
|
||||
-webkit-text-fill-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
.floating-input--glass & {
|
||||
background: rgba(30, 30, 30, 0.7);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.floating-input--solid & {
|
||||
background: #27272a;
|
||||
border-color: #3f3f46;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
color: #a3a3a3;
|
||||
|
||||
&--floating {
|
||||
.floating-input--glass & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(30, 30, 30, 0.9) 0%,
|
||||
rgba(30, 30, 30, 0.9) 50%,
|
||||
transparent 50%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
.floating-input--solid & {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
#27272a 0%,
|
||||
#27272a 50%,
|
||||
transparent 50%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,276 @@
|
|||
<template>
|
||||
<div
|
||||
v-motion
|
||||
:initial="animated ? animationConfig.initial : {}"
|
||||
:enter="animated ? animationConfig.enter : {}"
|
||||
:hovered="hoverable ? { scale: 1.02 } : {}"
|
||||
:delay="delay"
|
||||
class="glass-card"
|
||||
:class="[
|
||||
`glass-card--${variant}`,
|
||||
`glass-card--${size}`,
|
||||
{
|
||||
'glass-card--clickable': clickable,
|
||||
'glass-card--elevated': elevated
|
||||
}
|
||||
]"
|
||||
>
|
||||
<div v-if="hasHeader" class="glass-card__header">
|
||||
<slot name="header">
|
||||
<h3 v-if="title" class="glass-card__title">{{ title }}</h3>
|
||||
<p v-if="subtitle" class="glass-card__subtitle">{{ subtitle }}</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="glass-card__body">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div v-if="hasFooter" class="glass-card__footer">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
|
||||
<div v-if="gradient" class="glass-card__gradient"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots } from 'vue'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
subtitle?: string
|
||||
variant?: 'light' | 'dark' | 'colored' | 'gradient'
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
clickable?: boolean
|
||||
hoverable?: boolean
|
||||
elevated?: boolean
|
||||
gradient?: boolean
|
||||
animated?: boolean
|
||||
delay?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: 'light',
|
||||
size: 'md',
|
||||
clickable: false,
|
||||
hoverable: true,
|
||||
elevated: true,
|
||||
gradient: false,
|
||||
animated: true,
|
||||
delay: 0
|
||||
})
|
||||
|
||||
const slots = useSlots()
|
||||
const hasHeader = computed(() => !!slots.header || props.title || props.subtitle)
|
||||
const hasFooter = computed(() => !!slots.footer)
|
||||
|
||||
const animationConfig = {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
scale: 0.95
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 200,
|
||||
damping: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.glass-card {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
// Glass effect base
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.3) 0%,
|
||||
rgba(255, 255, 255, 0.1) 100%);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Variants
|
||||
&--light {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #27272a;
|
||||
}
|
||||
|
||||
&--dark {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&--colored {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.1) 0%,
|
||||
rgba(185, 28, 28, 0.05) 100%);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(220, 38, 38, 0.2);
|
||||
color: #27272a;
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(255, 255, 255, 0.8) 0%,
|
||||
rgba(255, 255, 255, 0.4) 100%);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
color: #27272a;
|
||||
}
|
||||
|
||||
// Sizes
|
||||
&--sm {
|
||||
.glass-card__body {
|
||||
padding: 1rem;
|
||||
}
|
||||
.glass-card__header {
|
||||
padding: 1rem 1rem 0.5rem;
|
||||
}
|
||||
.glass-card__footer {
|
||||
padding: 0.5rem 1rem 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
&--md {
|
||||
.glass-card__body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.glass-card__header {
|
||||
padding: 1.5rem 1.5rem 0.75rem;
|
||||
}
|
||||
.glass-card__footer {
|
||||
padding: 0.75rem 1.5rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&--lg {
|
||||
.glass-card__body {
|
||||
padding: 2rem;
|
||||
}
|
||||
.glass-card__header {
|
||||
padding: 2rem 2rem 1rem;
|
||||
}
|
||||
.glass-card__footer {
|
||||
padding: 1rem 2rem 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&--xl {
|
||||
.glass-card__body {
|
||||
padding: 2.5rem;
|
||||
}
|
||||
.glass-card__header {
|
||||
padding: 2.5rem 2.5rem 1.25rem;
|
||||
}
|
||||
.glass-card__footer {
|
||||
padding: 1.25rem 2.5rem 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// States
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
&--elevated {
|
||||
box-shadow:
|
||||
0 10px 40px rgba(0, 0, 0, 0.1),
|
||||
0 2px 10px rgba(0, 0, 0, 0.05),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
|
||||
&:hover {
|
||||
box-shadow:
|
||||
0 20px 60px rgba(0, 0, 0, 0.15),
|
||||
0 4px 15px rgba(0, 0, 0, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
// Header
|
||||
&__header {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 0.875rem;
|
||||
margin: 0.25rem 0 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
// Body
|
||||
&__body {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// Footer
|
||||
&__footer {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
// Gradient overlay
|
||||
&__gradient {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(220, 38, 38, 0.05) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.glass-card--light {
|
||||
background: rgba(30, 30, 30, 0.7);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<component
|
||||
:is="iconComponent"
|
||||
v-if="iconComponent"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
<span v-else class="icon-placeholder">
|
||||
{{ name }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, defineAsyncComponent } from 'vue'
|
||||
|
||||
interface Props {
|
||||
name: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// Map common icon names to Heroicons components
|
||||
// This is a simplified version - in production you'd import the actual icons
|
||||
const iconMap: Record<string, any> = {
|
||||
'chevron-down': defineAsyncComponent(() => import('./icons/ChevronDownIcon.vue')),
|
||||
'chevron-up': defineAsyncComponent(() => import('./icons/ChevronUpIcon.vue')),
|
||||
'x': defineAsyncComponent(() => import('./icons/XIcon.vue')),
|
||||
'check': defineAsyncComponent(() => import('./icons/CheckIcon.vue')),
|
||||
'alert-circle': defineAsyncComponent(() => import('./icons/AlertCircleIcon.vue')),
|
||||
'trending-up': defineAsyncComponent(() => import('./icons/TrendingUpIcon.vue')),
|
||||
'trending-down': defineAsyncComponent(() => import('./icons/TrendingDownIcon.vue')),
|
||||
'minus': defineAsyncComponent(() => import('./icons/MinusIcon.vue')),
|
||||
}
|
||||
|
||||
const iconComponent = computed(() => iconMap[props.name])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-placeholder {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-size: 0.75em;
|
||||
color: currentColor;
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,329 @@
|
|||
<template>
|
||||
<button
|
||||
v-motion
|
||||
:initial="animated ? { scale: 0.95, opacity: 0 } : {}"
|
||||
:enter="animated ? { scale: 1, opacity: 1 } : {}"
|
||||
:hovered="hoverable ? { scale: 1.05 } : {}"
|
||||
:tapped="{ scale: 0.95 }"
|
||||
:delay="delay"
|
||||
class="monaco-button"
|
||||
:class="[
|
||||
`monaco-button--${variant}`,
|
||||
`monaco-button--${size}`,
|
||||
{
|
||||
'monaco-button--block': block,
|
||||
'monaco-button--loading': loading,
|
||||
'monaco-button--icon-only': !$slots.default && icon
|
||||
}
|
||||
]"
|
||||
:disabled="disabled || loading"
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<span v-if="loading" class="monaco-button__spinner">
|
||||
<svg class="monaco-button__spinner-svg" viewBox="0 0 24 24">
|
||||
<circle
|
||||
class="monaco-button__spinner-circle"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<Icon
|
||||
v-if="icon && !loading"
|
||||
:name="icon"
|
||||
class="monaco-button__icon"
|
||||
:class="{ 'monaco-button__icon--left': $slots.default }"
|
||||
/>
|
||||
|
||||
<span v-if="$slots.default" class="monaco-button__content">
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
<Icon
|
||||
v-if="rightIcon && !loading"
|
||||
:name="rightIcon"
|
||||
class="monaco-button__icon monaco-button__icon--right"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Icon from '~/components/ui/Icon.vue'
|
||||
|
||||
interface Props {
|
||||
variant?: 'primary' | 'secondary' | 'glass' | 'gradient' | 'outline' | 'ghost'
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
||||
icon?: string
|
||||
rightIcon?: string
|
||||
block?: boolean
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
hoverable?: boolean
|
||||
animated?: boolean
|
||||
delay?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
block: false,
|
||||
disabled: false,
|
||||
loading: false,
|
||||
hoverable: true,
|
||||
animated: true,
|
||||
delay: 0
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
click: [event: MouseEvent]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.monaco-button {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
outline: none;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
|
||||
// Create shimmer effect for gradient variant
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.2),
|
||||
transparent
|
||||
);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
// Variants
|
||||
&--primary {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
box-shadow: 0 4px 14px rgba(220, 38, 38, 0.25);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #b91c1c;
|
||||
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.35);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
background: white;
|
||||
color: #dc2626;
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #fef2f2;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&--glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #dc2626;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(220, 38, 38, 0.2);
|
||||
box-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.15),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&--gradient {
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 14px rgba(220, 38, 38, 0.25);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
box-shadow: 0 6px 20px rgba(220, 38, 38, 0.35);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&--outline {
|
||||
background: transparent;
|
||||
color: #dc2626;
|
||||
border: 2px solid #dc2626;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border-color: #b91c1c;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&--ghost {
|
||||
background: transparent;
|
||||
color: #dc2626;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// Sizes
|
||||
&--xs {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&--sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&--md {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&--lg {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1.125rem;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
&--xl {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.25rem;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
// States
|
||||
&--block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--icon-only {
|
||||
aspect-ratio: 1;
|
||||
padding: 0;
|
||||
|
||||
&.monaco-button--xs { width: 1.75rem; }
|
||||
&.monaco-button--sm { width: 2rem; }
|
||||
&.monaco-button--md { width: 2.5rem; }
|
||||
&.monaco-button--lg { width: 3rem; }
|
||||
&.monaco-button--xl { width: 3.5rem; }
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--loading {
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// Icons
|
||||
&__icon {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--left {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
&--right {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Spinner
|
||||
&__spinner {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__spinner-svg {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
&__spinner-circle {
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 64;
|
||||
stroke-dashoffset: 64;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% { stroke-dashoffset: 64; }
|
||||
50% { stroke-dashoffset: 16; }
|
||||
100% { stroke-dashoffset: 64; }
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.monaco-button {
|
||||
&--secondary {
|
||||
background: #27272a;
|
||||
color: #dc2626;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #3f3f46;
|
||||
}
|
||||
}
|
||||
|
||||
&--glass {
|
||||
background: rgba(30, 30, 30, 0.7);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
<template>
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 20 }"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: delay * 100,
|
||||
type: 'spring',
|
||||
stiffness: 200,
|
||||
damping: 20
|
||||
}
|
||||
}"
|
||||
class="stats-card"
|
||||
:class="[
|
||||
`stats-card--${variant}`,
|
||||
{ 'stats-card--clickable': clickable }
|
||||
]"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<div class="stats-card__header">
|
||||
<div class="stats-card__icon-wrapper">
|
||||
<Icon
|
||||
:name="icon"
|
||||
class="stats-card__icon"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="trend" class="stats-card__trend" :class="`stats-card__trend--${trend.type}`">
|
||||
<Icon
|
||||
:name="trend.type === 'up' ? 'trending-up' : trend.type === 'down' ? 'trending-down' : 'minus'"
|
||||
class="stats-card__trend-icon"
|
||||
/>
|
||||
<span>{{ trend.value }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-card__content">
|
||||
<h3 class="stats-card__label">{{ label }}</h3>
|
||||
<div class="stats-card__value-wrapper">
|
||||
<span v-if="prefix" class="stats-card__prefix">{{ prefix }}</span>
|
||||
<AnimatedNumber
|
||||
:value="value"
|
||||
:duration="1500"
|
||||
:format="format"
|
||||
class="stats-card__value"
|
||||
/>
|
||||
<span v-if="suffix" class="stats-card__suffix">{{ suffix }}</span>
|
||||
</div>
|
||||
<p v-if="description" class="stats-card__description">{{ description }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="progress !== undefined" class="stats-card__progress">
|
||||
<div class="stats-card__progress-bar">
|
||||
<div
|
||||
class="stats-card__progress-fill"
|
||||
:style="{ width: `${Math.min(100, Math.max(0, progress))}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span class="stats-card__progress-label">{{ progress }}% Complete</span>
|
||||
</div>
|
||||
|
||||
<div v-if="sparkline" class="stats-card__sparkline">
|
||||
<svg
|
||||
viewBox="0 0 100 40"
|
||||
preserveAspectRatio="none"
|
||||
class="stats-card__sparkline-svg"
|
||||
>
|
||||
<polyline
|
||||
:points="sparklinePoints"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<polyline
|
||||
:points="`${sparklinePoints} 100,40 0,40`"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Icon from '~/components/ui/Icon.vue'
|
||||
import AnimatedNumber from '~/components/ui/AnimatedNumber.vue'
|
||||
|
||||
interface Trend {
|
||||
type: 'up' | 'down' | 'neutral'
|
||||
value: number
|
||||
}
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
value: number
|
||||
icon: string
|
||||
variant?: 'glass' | 'solid' | 'gradient' | 'outline'
|
||||
prefix?: string
|
||||
suffix?: string
|
||||
description?: string
|
||||
trend?: Trend
|
||||
progress?: number
|
||||
sparkline?: number[]
|
||||
clickable?: boolean
|
||||
format?: (value: number) => string
|
||||
delay?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: 'glass',
|
||||
clickable: false,
|
||||
delay: 0,
|
||||
format: (value: number) => value.toLocaleString()
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
|
||||
const sparklinePoints = computed(() => {
|
||||
if (!props.sparkline || props.sparkline.length === 0) return ''
|
||||
|
||||
const data = props.sparkline
|
||||
const max = Math.max(...data)
|
||||
const min = Math.min(...data)
|
||||
const range = max - min || 1
|
||||
|
||||
return data
|
||||
.map((value, index) => {
|
||||
const x = (index / (data.length - 1)) * 100
|
||||
const y = 40 - ((value - min) / range) * 35
|
||||
return `${x},${y}`
|
||||
})
|
||||
.join(' ')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stats-card {
|
||||
position: relative;
|
||||
padding: 1.5rem;
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
overflow: hidden;
|
||||
|
||||
// Glass variant
|
||||
&--glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.08),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.5);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// Solid variant
|
||||
&--solid {
|
||||
background: white;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// Gradient variant
|
||||
&--gradient {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.05) 0%,
|
||||
rgba(220, 38, 38, 0.02) 100%);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(220, 38, 38, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(220, 38, 38, 0.08);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.08) 0%,
|
||||
rgba(220, 38, 38, 0.03) 100%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
// Outline variant
|
||||
&--outline {
|
||||
background: transparent;
|
||||
border: 2px solid rgba(220, 38, 38, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.05);
|
||||
border-color: rgba(220, 38, 38, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(220, 38, 38, 0.1) 0%,
|
||||
rgba(220, 38, 38, 0.05) 100%);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&__trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
|
||||
&--up {
|
||||
color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
&--down {
|
||||
color: #ef4444;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
}
|
||||
|
||||
&--neutral {
|
||||
color: #6b7280;
|
||||
background: rgba(107, 114, 128, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&__trend-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
&__content {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
&__value-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #27272a;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__prefix,
|
||||
&__suffix {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__progress {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&__progress-bar {
|
||||
height: 6px;
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
&__progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #dc2626 0%, #b91c1c 100%);
|
||||
border-radius: 3px;
|
||||
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
&__progress-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__sparkline {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 40px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&__sparkline-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.stats-card {
|
||||
&--glass {
|
||||
background: rgba(30, 30, 30, 0.7);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&--solid {
|
||||
background: #27272a;
|
||||
}
|
||||
|
||||
&__value {
|
||||
color: white;
|
||||
}
|
||||
|
||||
&__label,
|
||||
&__description,
|
||||
&__progress-label {
|
||||
color: #a3a3a3;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</template>
|
||||
|
|
@ -14,7 +14,7 @@ export default defineNuxtConfig({
|
|||
console.log(`🌐 Server listening on http://${host}:${port}`)
|
||||
}
|
||||
},
|
||||
modules: ["vuetify-nuxt-module"],
|
||||
modules: ["vuetify-nuxt-module", "@vueuse/motion/nuxt"],
|
||||
css: [],
|
||||
app: {
|
||||
head: {
|
||||
|
|
@ -88,6 +88,70 @@ export default defineNuxtConfig({
|
|||
appName: "MonacoUSA Portal",
|
||||
domain: process.env.NUXT_PUBLIC_DOMAIN || "https://portal.monacousa.org",
|
||||
keycloakIssuer: process.env.NUXT_KEYCLOAK_ISSUER || "https://auth.monacousa.org/realms/monacousa",
|
||||
motion: {
|
||||
directives: {
|
||||
'pop-bottom': {
|
||||
initial: {
|
||||
scale: 0,
|
||||
opacity: 0,
|
||||
y: 100
|
||||
},
|
||||
visible: {
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 250,
|
||||
damping: 25
|
||||
}
|
||||
}
|
||||
},
|
||||
'fade-in': {
|
||||
initial: {
|
||||
opacity: 0
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
duration: 600
|
||||
}
|
||||
}
|
||||
},
|
||||
'slide-up': {
|
||||
initial: {
|
||||
y: 100,
|
||||
opacity: 0
|
||||
},
|
||||
enter: {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
transition: {
|
||||
type: 'spring',
|
||||
stiffness: 100,
|
||||
damping: 20
|
||||
}
|
||||
}
|
||||
},
|
||||
'glass-fade': {
|
||||
initial: {
|
||||
opacity: 0,
|
||||
scale: 0.95,
|
||||
filter: 'blur(10px)'
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
filter: 'blur(0px)',
|
||||
transition: {
|
||||
duration: 500,
|
||||
type: 'spring',
|
||||
stiffness: 200
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
vuetify: {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@vite-pwa/nuxt": "^0.10.8",
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"cookie": "^0.6.0",
|
||||
"formidable": "^3.5.4",
|
||||
"handlebars": "^4.7.8",
|
||||
|
|
@ -5480,6 +5481,12 @@
|
|||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yauzl": {
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||
|
|
@ -5995,6 +6002,64 @@
|
|||
"vuetify": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.8.0.tgz",
|
||||
"integrity": "sha512-rmBcgpEpxY0ZmyQQR94q1qkUcHREiLxQwNyWrtjMDipD0WTH/JBcAt0gdcn2PsH0SA76ec291cHFngmyaBhlxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "13.8.0",
|
||||
"@vueuse/shared": "13.8.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.8.0.tgz",
|
||||
"integrity": "sha512-BYMp3Gp1kBUPv7AfQnJYP96mkX7g7cKdTIgwv/Jgd+pfQhz678naoZOAcknRtPLP4cFblDDW7rF4e3KFa+PfIA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/motion": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/motion/-/motion-3.0.3.tgz",
|
||||
"integrity": "sha512-4B+ITsxCI9cojikvrpaJcLXyq0spj3sdlzXjzesWdMRd99hhtFI6OJ/1JsqwtF73YooLe0hUn/xDR6qCtmn5GQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"@vueuse/shared": "^13.0.0",
|
||||
"defu": "^6.1.4",
|
||||
"framesync": "^6.1.2",
|
||||
"popmotion": "^11.0.5",
|
||||
"style-value-types": "^5.1.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@nuxt/kit": "^3.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.8.0.tgz",
|
||||
"integrity": "sha512-x4nfM0ykW+RmNJ4/1IzZsuLuWWrNTxlTWUiehTGI54wnOxIgI9EDdu/O5S77ac6hvQ3hk2KpOVFHaM0M796Kbw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@whatwg-node/disposablestack": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz",
|
||||
|
|
@ -8853,6 +8918,21 @@
|
|||
"url": "https://github.com/sponsors/rawify"
|
||||
}
|
||||
},
|
||||
"node_modules/framesync": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/framesync/-/framesync-6.1.2.tgz",
|
||||
"integrity": "sha512-jBTqhX6KaQVDyus8muwZbBeGGP0XgujBRbQ7gM7BRdS3CadCZIHiawyzYLnafYcvZIh5j8WE7cxZKFn7dXhu9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/framesync/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
|
|
@ -9378,6 +9458,12 @@
|
|||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/hey-listen": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz",
|
||||
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hookable": {
|
||||
"version": "5.5.3",
|
||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
||||
|
|
@ -12871,6 +12957,24 @@
|
|||
"pathe": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/popmotion": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.5.tgz",
|
||||
"integrity": "sha512-la8gPM1WYeFznb/JqF4GiTkRRPZsfaj2+kCxqQgr2MJylMmIKUwBfWW8Wa5fml/8gmtlD5yI01MP1QCZPWmppA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"framesync": "6.1.2",
|
||||
"hey-listen": "^1.0.8",
|
||||
"style-value-types": "5.1.2",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/popmotion/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
|
@ -14876,6 +14980,22 @@
|
|||
"integrity": "sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/style-value-types": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.1.2.tgz",
|
||||
"integrity": "sha512-Vs9fNreYF9j6W2VvuDTP7kepALi7sk0xtk2Tu8Yxi9UoajJdEVpNpCov0HsLTqXvNGKX+Uv09pkozVITi1jf3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hey-listen": "^1.0.8",
|
||||
"tslib": "2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/style-value-types/node_modules/tslib": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
|
||||
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/stylehacks": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@vite-pwa/nuxt": "^0.10.8",
|
||||
"@vueuse/motion": "^3.0.3",
|
||||
"cookie": "^0.6.0",
|
||||
"formidable": "^3.5.4",
|
||||
"handlebars": "^4.7.8",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,613 @@
|
|||
<template>
|
||||
<div class="dashboard-mockup">
|
||||
<!-- Header -->
|
||||
<header class="dashboard-header">
|
||||
<div class="dashboard-header__content">
|
||||
<h1
|
||||
v-motion
|
||||
:initial="{ opacity: 0, x: -20 }"
|
||||
:enter="{ opacity: 1, x: 0 }"
|
||||
class="dashboard-header__title"
|
||||
>
|
||||
Welcome back, {{ userName }}
|
||||
</h1>
|
||||
<p
|
||||
v-motion
|
||||
:initial="{ opacity: 0, x: -20 }"
|
||||
:enter="{ opacity: 1, x: 0, transition: { delay: 100 } }"
|
||||
class="dashboard-header__subtitle"
|
||||
>
|
||||
Here's what's happening with MonacoUSA today
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-header__actions">
|
||||
<MonacoButton variant="glass" icon="bell">
|
||||
Notifications
|
||||
</MonacoButton>
|
||||
<MonacoButton variant="primary" icon="plus">
|
||||
New Event
|
||||
</MonacoButton>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<section class="dashboard-stats">
|
||||
<StatsCard
|
||||
v-for="(stat, index) in stats"
|
||||
:key="stat.label"
|
||||
:label="stat.label"
|
||||
:value="stat.value"
|
||||
:icon="stat.icon"
|
||||
:prefix="stat.prefix"
|
||||
:suffix="stat.suffix"
|
||||
:trend="stat.trend"
|
||||
:progress="stat.progress"
|
||||
:sparkline="stat.sparkline"
|
||||
:delay="index"
|
||||
variant="glass"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="dashboard-grid">
|
||||
<!-- Recent Activity -->
|
||||
<GlassCard
|
||||
title="Recent Activity"
|
||||
variant="glass"
|
||||
:delay="400"
|
||||
class="dashboard-activity"
|
||||
>
|
||||
<div class="activity-list">
|
||||
<div
|
||||
v-for="(activity, index) in recentActivities"
|
||||
:key="index"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, x: -20 }"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { delay: 500 + (index * 50) }
|
||||
}"
|
||||
class="activity-item"
|
||||
>
|
||||
<div class="activity-item__icon">
|
||||
<span :class="`activity-icon activity-icon--${activity.type}`">
|
||||
{{ activity.icon }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="activity-item__content">
|
||||
<p class="activity-item__text">{{ activity.text }}</p>
|
||||
<span class="activity-item__time">{{ activity.time }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<!-- Upcoming Events -->
|
||||
<GlassCard
|
||||
title="Upcoming Events"
|
||||
variant="glass"
|
||||
:delay="450"
|
||||
class="dashboard-events"
|
||||
>
|
||||
<div class="events-list">
|
||||
<div
|
||||
v-for="(event, index) in upcomingEvents"
|
||||
:key="index"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 20 }"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { delay: 550 + (index * 50) }
|
||||
}"
|
||||
class="event-card"
|
||||
>
|
||||
<div class="event-card__date">
|
||||
<span class="event-card__day">{{ event.day }}</span>
|
||||
<span class="event-card__month">{{ event.month }}</span>
|
||||
</div>
|
||||
<div class="event-card__content">
|
||||
<h4 class="event-card__title">{{ event.title }}</h4>
|
||||
<p class="event-card__location">{{ event.location }}</p>
|
||||
<div class="event-card__attendees">
|
||||
<span class="event-card__count">{{ event.attendees }} attending</span>
|
||||
<MonacoButton variant="ghost" size="sm">
|
||||
View Details
|
||||
</MonacoButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<!-- Member Status -->
|
||||
<GlassCard
|
||||
title="Member Status"
|
||||
variant="gradient"
|
||||
:delay="500"
|
||||
class="dashboard-member-status"
|
||||
>
|
||||
<div class="member-status">
|
||||
<div class="member-status__badge">
|
||||
<span class="badge badge--active">Active Member</span>
|
||||
</div>
|
||||
<div class="member-status__info">
|
||||
<div class="status-item">
|
||||
<span class="status-item__label">Dues Status</span>
|
||||
<span class="status-item__value status-item__value--success">Paid</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-item__label">Next Payment</span>
|
||||
<span class="status-item__value">January 2025</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-item__label">Member Since</span>
|
||||
<span class="status-item__value">March 2023</span>
|
||||
</div>
|
||||
</div>
|
||||
<MonacoButton variant="primary" block>
|
||||
Manage Membership
|
||||
</MonacoButton>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<GlassCard
|
||||
title="Quick Actions"
|
||||
variant="glass"
|
||||
:delay="550"
|
||||
class="dashboard-actions"
|
||||
>
|
||||
<div class="quick-actions">
|
||||
<button
|
||||
v-for="(action, index) in quickActions"
|
||||
:key="action.label"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, scale: 0.8 }"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { delay: 600 + (index * 50) }
|
||||
}"
|
||||
class="action-button"
|
||||
>
|
||||
<span class="action-button__icon">{{ action.icon }}</span>
|
||||
<span class="action-button__label">{{ action.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import GlassCard from '~/components/ui/GlassCard.vue'
|
||||
import MonacoButton from '~/components/ui/MonacoButton.vue'
|
||||
import StatsCard from '~/components/ui/StatsCard.vue'
|
||||
|
||||
const userName = ref('John')
|
||||
|
||||
const stats = ref([
|
||||
{
|
||||
label: 'Total Members',
|
||||
value: 1234,
|
||||
icon: 'users',
|
||||
trend: { type: 'up', value: 12 },
|
||||
sparkline: [30, 40, 35, 50, 49, 60, 70, 91, 95]
|
||||
},
|
||||
{
|
||||
label: 'Events This Month',
|
||||
value: 8,
|
||||
icon: 'calendar',
|
||||
suffix: ' events',
|
||||
trend: { type: 'up', value: 33 }
|
||||
},
|
||||
{
|
||||
label: 'Dues Collected',
|
||||
value: 45670,
|
||||
icon: 'dollar',
|
||||
prefix: '$',
|
||||
trend: { type: 'up', value: 5 },
|
||||
progress: 78
|
||||
},
|
||||
{
|
||||
label: 'Active Projects',
|
||||
value: 12,
|
||||
icon: 'briefcase',
|
||||
trend: { type: 'neutral', value: 0 }
|
||||
}
|
||||
])
|
||||
|
||||
const recentActivities = ref([
|
||||
{
|
||||
icon: '👤',
|
||||
type: 'member',
|
||||
text: 'New member John Doe joined',
|
||||
time: '2 hours ago'
|
||||
},
|
||||
{
|
||||
icon: '📅',
|
||||
type: 'event',
|
||||
text: 'Summer Gala event created',
|
||||
time: '5 hours ago'
|
||||
},
|
||||
{
|
||||
icon: '💳',
|
||||
type: 'payment',
|
||||
text: 'Sarah Smith paid dues',
|
||||
time: '1 day ago'
|
||||
},
|
||||
{
|
||||
icon: '📝',
|
||||
type: 'update',
|
||||
text: 'Board meeting minutes posted',
|
||||
time: '2 days ago'
|
||||
}
|
||||
])
|
||||
|
||||
const upcomingEvents = ref([
|
||||
{
|
||||
day: '15',
|
||||
month: 'DEC',
|
||||
title: 'Monaco Winter Gala',
|
||||
location: 'Grand Ballroom',
|
||||
attendees: 120
|
||||
},
|
||||
{
|
||||
day: '22',
|
||||
month: 'DEC',
|
||||
title: 'Board Meeting',
|
||||
location: 'Conference Room A',
|
||||
attendees: 15
|
||||
},
|
||||
{
|
||||
day: '31',
|
||||
month: 'DEC',
|
||||
title: 'New Year Celebration',
|
||||
location: 'Monaco Club',
|
||||
attendees: 200
|
||||
}
|
||||
])
|
||||
|
||||
const quickActions = ref([
|
||||
{ icon: '📝', label: 'Register for Event' },
|
||||
{ icon: '💳', label: 'Pay Dues' },
|
||||
{ icon: '📊', label: 'View Reports' },
|
||||
{ icon: '👥', label: 'Member Directory' },
|
||||
{ icon: '📧', label: 'Send Newsletter' },
|
||||
{ icon: '⚙️', label: 'Settings' }
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dashboard-mockup {
|
||||
padding: 2rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #27272a;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 1rem;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
gap: 1.5rem;
|
||||
|
||||
.dashboard-activity {
|
||||
grid-column: span 8;
|
||||
}
|
||||
|
||||
.dashboard-events {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
.dashboard-member-status {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
grid-column: span 8;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.05);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin: 0 0 0.25rem;
|
||||
color: #27272a;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
|
||||
&--member {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
&--event {
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
}
|
||||
|
||||
&--payment {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
&--update {
|
||||
background: rgba(251, 146, 60, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.events-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.event-card {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
&__date {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__day {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__month {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0 0 0.25rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #27272a;
|
||||
}
|
||||
|
||||
&__location {
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__attendees {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 0.75rem;
|
||||
color: #dc2626;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.member-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
|
||||
&__badge {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
|
||||
&--active {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
|
||||
&__label {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #27272a;
|
||||
|
||||
&--success {
|
||||
color: #10b981;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1.5rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-color: rgba(220, 38, 38, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #27272a;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 1024px) {
|
||||
.dashboard-grid {
|
||||
.dashboard-activity,
|
||||
.dashboard-events,
|
||||
.dashboard-member-status,
|
||||
.dashboard-actions {
|
||||
grid-column: span 12;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
|
||||
&__actions {
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,710 @@
|
|||
<template>
|
||||
<div class="events-mockup">
|
||||
<!-- Header -->
|
||||
<header class="events-header">
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: -20 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
class="events-header__content"
|
||||
>
|
||||
<h1 class="events-header__title">Events</h1>
|
||||
<p class="events-header__subtitle">Discover and join MonacoUSA events</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: -20 }"
|
||||
:enter="{ opacity: 1, y: 0, transition: { delay: 100 } }"
|
||||
class="events-header__actions"
|
||||
>
|
||||
<FloatingInput
|
||||
v-model="searchQuery"
|
||||
label="Search events..."
|
||||
leftIcon="search"
|
||||
variant="glass"
|
||||
clearable
|
||||
/>
|
||||
<MonacoButton variant="primary" icon="plus">
|
||||
Create Event
|
||||
</MonacoButton>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 20 }"
|
||||
:enter="{ opacity: 1, y: 0, transition: { delay: 200 } }"
|
||||
class="events-filters"
|
||||
>
|
||||
<div class="filter-chips">
|
||||
<button
|
||||
v-for="filter in filters"
|
||||
:key="filter.value"
|
||||
class="filter-chip"
|
||||
:class="{ 'filter-chip--active': selectedFilter === filter.value }"
|
||||
@click="selectedFilter = filter.value"
|
||||
>
|
||||
{{ filter.label }}
|
||||
<span v-if="filter.count" class="filter-chip__count">{{ filter.count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="view-toggles">
|
||||
<button
|
||||
class="view-toggle"
|
||||
:class="{ 'view-toggle--active': viewMode === 'grid' }"
|
||||
@click="viewMode = 'grid'"
|
||||
>
|
||||
<span>⊞</span> Grid
|
||||
</button>
|
||||
<button
|
||||
class="view-toggle"
|
||||
:class="{ 'view-toggle--active': viewMode === 'list' }"
|
||||
@click="viewMode = 'list'"
|
||||
>
|
||||
<span>☰</span> List
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Events Grid/List -->
|
||||
<div
|
||||
class="events-container"
|
||||
:class="`events-container--${viewMode}`"
|
||||
>
|
||||
<div
|
||||
v-for="(event, index) in events"
|
||||
:key="event.id"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 30 }"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 300 + (index * 50),
|
||||
type: 'spring',
|
||||
stiffness: 200,
|
||||
damping: 20
|
||||
}
|
||||
}"
|
||||
class="event-card-full"
|
||||
:class="{ 'event-card-full--featured': event.featured }"
|
||||
>
|
||||
<div class="event-card-full__image">
|
||||
<img :src="event.image" :alt="event.title" />
|
||||
<div v-if="event.featured" class="event-card-full__badge">Featured</div>
|
||||
<div class="event-card-full__date-overlay">
|
||||
<span class="date-day">{{ event.date.day }}</span>
|
||||
<span class="date-month">{{ event.date.month }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="event-card-full__content">
|
||||
<div class="event-card-full__header">
|
||||
<h3 class="event-card-full__title">{{ event.title }}</h3>
|
||||
<span class="event-card-full__category">{{ event.category }}</span>
|
||||
</div>
|
||||
|
||||
<p class="event-card-full__description">{{ event.description }}</p>
|
||||
|
||||
<div class="event-card-full__meta">
|
||||
<div class="meta-item">
|
||||
<span class="meta-icon">📍</span>
|
||||
<span class="meta-text">{{ event.location }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-icon">🕐</span>
|
||||
<span class="meta-text">{{ event.time }}</span>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-icon">👥</span>
|
||||
<span class="meta-text">{{ event.attendees }} attending</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="event-card-full__footer">
|
||||
<div class="event-card-full__price">
|
||||
<span v-if="event.price === 0" class="price-free">Free</span>
|
||||
<span v-else class="price-amount">${{ event.price }}</span>
|
||||
</div>
|
||||
|
||||
<div class="event-card-full__actions">
|
||||
<MonacoButton variant="ghost" size="sm" icon="heart">
|
||||
Save
|
||||
</MonacoButton>
|
||||
<MonacoButton variant="primary" size="sm">
|
||||
Register
|
||||
</MonacoButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load More -->
|
||||
<div
|
||||
v-motion
|
||||
:initial="{ opacity: 0 }"
|
||||
:enter="{ opacity: 1, transition: { delay: 800 } }"
|
||||
class="load-more"
|
||||
>
|
||||
<MonacoButton variant="glass" icon="refresh" block>
|
||||
Load More Events
|
||||
</MonacoButton>
|
||||
</div>
|
||||
|
||||
<!-- Floating Calendar Widget -->
|
||||
<GlassCard
|
||||
variant="glass"
|
||||
class="calendar-widget"
|
||||
:animated="true"
|
||||
:delay="900"
|
||||
>
|
||||
<h4 class="calendar-widget__title">Quick Calendar</h4>
|
||||
<div class="calendar-mini">
|
||||
<div class="calendar-mini__header">
|
||||
<button class="calendar-nav">‹</button>
|
||||
<span class="calendar-month">December 2024</span>
|
||||
<button class="calendar-nav">›</button>
|
||||
</div>
|
||||
<div class="calendar-mini__grid">
|
||||
<div
|
||||
v-for="day in 31"
|
||||
:key="day"
|
||||
class="calendar-day"
|
||||
:class="{
|
||||
'calendar-day--event': [5, 12, 15, 22, 31].includes(day),
|
||||
'calendar-day--today': day === 10
|
||||
}"
|
||||
>
|
||||
{{ day }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import GlassCard from '~/components/ui/GlassCard.vue'
|
||||
import MonacoButton from '~/components/ui/MonacoButton.vue'
|
||||
import FloatingInput from '~/components/ui/FloatingInput.vue'
|
||||
|
||||
const searchQuery = ref('')
|
||||
const selectedFilter = ref('all')
|
||||
const viewMode = ref('grid')
|
||||
|
||||
const filters = ref([
|
||||
{ label: 'All Events', value: 'all', count: 24 },
|
||||
{ label: 'Upcoming', value: 'upcoming', count: 12 },
|
||||
{ label: 'This Week', value: 'week', count: 5 },
|
||||
{ label: 'This Month', value: 'month', count: 8 },
|
||||
{ label: 'Free', value: 'free', count: 7 },
|
||||
{ label: 'Members Only', value: 'members', count: 10 }
|
||||
])
|
||||
|
||||
const events = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: 'Monaco Winter Gala 2024',
|
||||
category: 'Social',
|
||||
description: 'Join us for an elegant evening celebrating the Monaco-US friendship with fine dining, live entertainment, and networking.',
|
||||
image: '/api/placeholder/400/250',
|
||||
date: { day: '15', month: 'DEC' },
|
||||
time: '7:00 PM - 11:00 PM',
|
||||
location: 'Grand Ballroom, Downtown',
|
||||
attendees: 120,
|
||||
price: 150,
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Business Networking Lunch',
|
||||
category: 'Networking',
|
||||
description: 'Connect with fellow Monaco-US business professionals over lunch and expand your network.',
|
||||
image: '/api/placeholder/400/250',
|
||||
date: { day: '18', month: 'DEC' },
|
||||
time: '12:00 PM - 2:00 PM',
|
||||
location: 'Monaco Club',
|
||||
attendees: 45,
|
||||
price: 35,
|
||||
featured: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Cultural Exchange Workshop',
|
||||
category: 'Education',
|
||||
description: 'Learn about Monaco culture, history, and traditions in this interactive workshop.',
|
||||
image: '/api/placeholder/400/250',
|
||||
date: { day: '20', month: 'DEC' },
|
||||
time: '3:00 PM - 5:00 PM',
|
||||
location: 'Community Center',
|
||||
attendees: 30,
|
||||
price: 0,
|
||||
featured: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'New Year Celebration',
|
||||
category: 'Social',
|
||||
description: 'Ring in the new year with the MonacoUSA community! Champagne toast, live music, and dancing.',
|
||||
image: '/api/placeholder/400/250',
|
||||
date: { day: '31', month: 'DEC' },
|
||||
time: '9:00 PM - 2:00 AM',
|
||||
location: 'Monaco Club Rooftop',
|
||||
attendees: 200,
|
||||
price: 200,
|
||||
featured: true
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'Wine Tasting Evening',
|
||||
category: 'Social',
|
||||
description: 'Discover exceptional wines from Monaco and France guided by our sommelier.',
|
||||
image: '/api/placeholder/400/250',
|
||||
date: { day: '22', month: 'DEC' },
|
||||
time: '6:00 PM - 9:00 PM',
|
||||
location: 'Wine Gallery',
|
||||
attendees: 60,
|
||||
price: 75,
|
||||
featured: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: 'Board Meeting',
|
||||
category: 'Meeting',
|
||||
description: 'Monthly board meeting to discuss club activities and initiatives.',
|
||||
image: '/api/placeholder/400/250',
|
||||
date: { day: '28', month: 'DEC' },
|
||||
time: '5:00 PM - 7:00 PM',
|
||||
location: 'Conference Room A',
|
||||
attendees: 15,
|
||||
price: 0,
|
||||
featured: false
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.events-mockup {
|
||||
padding: 2rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
background: linear-gradient(135deg, #fef2f2 0%, #ffffff 100%);
|
||||
min-height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.events-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
color: #27272a;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.floating-input {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.events-filters {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.05);
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.filter-chips {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.05);
|
||||
border-color: rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
border-color: #dc2626;
|
||||
|
||||
.filter-chip__count {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
padding: 0 0.25rem;
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border-radius: 10px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.view-toggles {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border: 2px solid transparent;
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.05);
|
||||
border-color: rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: white;
|
||||
color: #dc2626;
|
||||
border-color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.events-container {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&--grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
}
|
||||
|
||||
&--list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.event-card-full {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&--featured {
|
||||
border: 2px solid #dc2626;
|
||||
box-shadow: 0 4px 16px rgba(220, 38, 38, 0.15);
|
||||
}
|
||||
|
||||
&__image {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
&__date-overlay {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.date-day {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #dc2626;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.date-month {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: #27272a;
|
||||
}
|
||||
|
||||
&__category {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
color: #dc2626;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
|
||||
&__price {
|
||||
.price-free {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10b981;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.price-amount {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
|
||||
.meta-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.load-more {
|
||||
max-width: 400px;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.calendar-widget {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
right: 2rem;
|
||||
width: 280px;
|
||||
z-index: 10;
|
||||
|
||||
&__title {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-mini {
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-nav {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #dc2626;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-month {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #27272a;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(220, 38, 38, 0.05);
|
||||
}
|
||||
|
||||
&--event {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
color: #dc2626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&--today {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 768px) {
|
||||
.events-header {
|
||||
&__actions {
|
||||
width: 100%;
|
||||
|
||||
.floating-input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.events-container--grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calendar-widget {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue