Implement MonacoUSA Portal redesign foundations
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:
Matt 2025-08-30 18:25:21 +02:00
parent de75d2d764
commit c39936984b
29 changed files with 7278 additions and 1 deletions

View File

@ -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"
}
}
},
}

View File

@ -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.

68
.serena/project.yml Normal file
View File

@ -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"

480
Design/README.md Normal file
View File

@ -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*

View File

@ -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>

View File

@ -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>

View File

@ -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>

753
Design/design-system.md Normal file
View File

@ -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.*

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

276
components/ui/GlassCard.vue Normal file
View File

@ -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>

49
components/ui/Icon.vue Normal file
View File

@ -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>

View File

@ -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>

369
components/ui/StatsCard.vue Normal file
View File

@ -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>

View File

@ -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>

View File

@ -14,7 +14,7 @@ export default defineNuxtConfig({
console.log(`🌐 Server listening on http://${host}:${port}`) console.log(`🌐 Server listening on http://${host}:${port}`)
} }
}, },
modules: ["vuetify-nuxt-module"], modules: ["vuetify-nuxt-module", "@vueuse/motion/nuxt"],
css: [], css: [],
app: { app: {
head: { head: {
@ -88,6 +88,70 @@ export default defineNuxtConfig({
appName: "MonacoUSA Portal", appName: "MonacoUSA Portal",
domain: process.env.NUXT_PUBLIC_DOMAIN || "https://portal.monacousa.org", domain: process.env.NUXT_PUBLIC_DOMAIN || "https://portal.monacousa.org",
keycloakIssuer: process.env.NUXT_KEYCLOAK_ISSUER || "https://auth.monacousa.org/realms/monacousa", 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: { vuetify: {

120
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@vite-pwa/nuxt": "^0.10.8", "@vite-pwa/nuxt": "^0.10.8",
"@vueuse/motion": "^3.0.3",
"cookie": "^0.6.0", "cookie": "^0.6.0",
"formidable": "^3.5.4", "formidable": "^3.5.4",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
@ -5480,6 +5481,12 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT" "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": { "node_modules/@types/yauzl": {
"version": "2.10.3", "version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@ -5995,6 +6002,64 @@
"vuetify": "^3.0.0" "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": { "node_modules/@whatwg-node/disposablestack": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz", "resolved": "https://registry.npmjs.org/@whatwg-node/disposablestack/-/disposablestack-0.0.6.tgz",
@ -8853,6 +8918,21 @@
"url": "https://github.com/sponsors/rawify" "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": { "node_modules/fresh": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
@ -9378,6 +9458,12 @@
"he": "bin/he" "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": { "node_modules/hookable": {
"version": "5.5.3", "version": "5.5.3",
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
@ -12871,6 +12957,24 @@
"pathe": "^2.0.3" "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": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "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==", "integrity": "sha512-FL8EeKFFyNQv5cMnXI31CIMCsFarSVI2bF0U0ImeNE3g/F1IvJQyqzOXxPBRXiwQfyBTlbNe88jh1jFW0O/jiQ==",
"license": "ISC" "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": { "node_modules/stylehacks": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz",

View File

@ -19,6 +19,7 @@
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@vite-pwa/nuxt": "^0.10.8", "@vite-pwa/nuxt": "^0.10.8",
"@vueuse/motion": "^3.0.3",
"cookie": "^0.6.0", "cookie": "^0.6.0",
"formidable": "^3.5.4", "formidable": "^3.5.4",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",

613
pages/dashboard/mockup.vue Normal file
View File

@ -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>

710
pages/events/mockup.vue Normal file
View File

@ -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>