Compare commits

...

11 Commits

Author SHA1 Message Date
d5ed3c1b52 changes 2026-04-07 17:42:53 -07:00
433228c90e merged 2025-08-13 09:45:38 -07:00
b7801a0caa offline docs 2025-08-13 09:33:19 -07:00
Bryce
2a451ce840 Merge branch 'animations' 2025-08-12 23:03:16 -07:00
Bryce
8e35d3a3c2 progress 2025-08-12 22:48:18 -07:00
Bryce
6c38af1507 feat: implement global error handling with Alpine.js and HTMX toast notifications
- Added Alpine.js store for managing error toast state
- Implemented server error toast notifications for 5xx errors
- Replaced manual JavaScript error handling with Alpine.js implementation
- Updated body tag to use HTMX response error event listener
- Improved error display with better styling and user experience

This change provides a consistent way to show server errors to users across the application using HTMX and Alpine.js, making error handling more maintainable and reusable.
2025-08-12 22:13:23 -07:00
Bryce
3385a8b7cb Improve toast notification styling and add X icon for closing 2025-08-12 20:23:01 -07:00
Bryce
93b5bc091c Add HTMX error handling with toast notifications for 5xx errors 2025-08-12 07:24:56 -07:00
Bryce
c159393809 Add 500 error simulation middleware and HTMX integration 2025-08-12 07:18:55 -07:00
de7e03f8e7 cleans up tests 2025-08-11 20:07:59 -07:00
e9c976619a background task 2025-08-11 19:26:40 -07:00
68 changed files with 6336 additions and 7 deletions

4
.env
View File

@@ -6,8 +6,8 @@ DATABASE_URL=postgresql://postgres:password@localhost:5432/email_organizer_dev
OPENAI_API_KEY=aaoeu
OPENAI_BASE_URL=http://workstation:5082/v1
OPENAI_MODEL=Qwen3-235B-A22B-Thinking-2507-GGUF
OPENAI_MODEL=Qwen3-235B-A22B-Instruct-2507-GGUF
AI_SERVICE_URL=http://workstation:5082/v1
AI_SERVICE_API_KEY=aoue
AI_MODEL=Qwen3-Coder-30B-A3B-Instruct-GGUF-roo
AI_MODEL=Qwen3-235B-A22B-Thinking-2507-GGUF

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
**/*.pyc
tmp/**/*
.serena/cache/**

52
.goosehints Normal file
View File

@@ -0,0 +1,52 @@
# Instructions
Here are special rules you must follow:
1. All forms should use regular form url encoding.
2. All routes should return html.
3. Use htmx for all dynamic content, when possible.
4. Use alpinejs for other dynamic content. For example, hiding or showing an element.
5. Prefer using daisyui over raw tailwind where possible.
6. Prefer using alpinejs over raw javascript. Raw javascript should almost never be needed.
7. Always print unhandled exceptions to the console.
8. Follow best practices for jinja template partials. That is, separate out components where appropriate.
9. Ask the user when there is something unclear.
10. I always run the server locally on port 5000, so you can use curl to test. No need to start it yourself.
11. Design docs go into docs/design/*.md. These docs are always kept up to date.
12. Before completing work, ensure that no design docs are left out of sync
13. Plans go into docs/plans/*.md. These may not be kept in sync, as they are just for brainstorming.
14. ****IMPORTANT**** Database migrations are automatically created via `flask db migrate -m 'message'`. **NEVER** create migrations by hand. You should never have to read the contents of migrations/
15. In the technical documentation, code should be used sparingly. Also, when needed, focus on the APIs, and only use snippets.
16. When you might need to reference htmx, you can fetch this url for all of the instructions: https://htmx.org/docs/
# Conventions
1. modals are rendered server-side, by targeting #modal-holder, and using an hx-trigger response attribute for open-modal.
2. modals are closed by adding the close-modal hx-trigger response attribute.
3. modals can be closed by triggering a close-modal event anywhere in the dom.
4. validation is done server-side. On modals, an error should cause the button to shake, and the invalid fields to be highlighted in red using normal daisyui paradigms. When relevant, there should be a notification banner inside the dialog-box to show the details of the error.
5. When validation is done outside of a modal, it should cause a notification banner with the details.
6. Testing is done with pytest.
7. Testing is done with beautifulsoup4
8. Only use comments where necessary. Prefer self-documenting code. For example:
```
is_adult = age >= 18
```
is preferred, and this is less preferred:
```
# check if the user is an adult
x = age >= 18
```
9. Buttons should be disabled, and a spinner should replace the icon content when loading. Only the button that was clicked should be disabled though. Typically this is done something like this:
```
<div data-loading-states>
<button class="..." hx-get="..." data-loading-disable>
<i class="fas fa-cog mr-2" data-loading-class="!hidden"></i>
<span data-loading-class="!hidden">Click Me!</span>
<span class="loading loading-spinner loading-xs hidden" data-loading-class-remove="hidden"></span> </button>
</div>
```
This will scope the disable to just the button, and disabled the button from being clicked. When in a modal, the whole modal should be disabled.
### context7 documentation library ids:
* HTMX: /bigskysoftware/htmx
* HTMX extensions: /bigskysoftware/htmx-extensions

18
.qwen/settings.json Normal file
View File

@@ -0,0 +1,18 @@
{
"mcpServers": {
"context7": {
"command": "npx",
"args": [
"-y",
"@upstash/context7-mcp"
],
"env": {
"DEFAULT_MINIMUM_TOKENS": ""
},
"alwaysAllow": [
"resolve-library-id",
"get-library-docs"
]
}
}
}

View File

@@ -1,5 +1,9 @@
{
"mcpServers": {
"serena": {
"command": "uvx",
"args": ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server", "--context", "ide-assistant", "--project", "/home/notid/dev/email-organizer"]
},
"context7": {
"command": "npx",
"args": [

View File

@@ -15,6 +15,7 @@ Here are special rules you must follow:
13. Plans go into docs/plans/*.md. These may not be kept in sync, as they are just for brainstorming.
14. ****IMPORTANT**** Database migrations are automatically created via `flask db migrate -m 'message'`. **NEVER** create migrations by hand. You should never have to read the contents of migrations/
15. In the technical documentation, code should be used sparingly. Also, when needed, focus on the APIs, and only use snippets.
16. If appropriate context has been given in the prompt for the task at hand, don't read the whole file.
# Conventions

44
.roomodes Normal file
View File

@@ -0,0 +1,44 @@
customModes:
- slug: user-story-creator
name: 📝 User Story Creator
roleDefinition: |
You are an agile requirements specialist focused on creating clear, valuable user stories. Your expertise includes:
- Crafting well-structured user stories following the standard format
- Breaking down complex requirements into manageable stories
- Identifying acceptance criteria and edge cases
- Ensuring stories deliver business value
- Maintaining consistent story quality and granularity
whenToUse: |
Use this mode when you need to create user stories, break down requirements into manageable pieces, or define acceptance criteria for features. Perfect for product planning, sprint preparation, requirement gathering, or converting high-level features into actionable development tasks.
description: Create structured agile user stories
groups:
- read
- edit
- command
source: project
customInstructions: |
Expected User Story Format:
Title: [Brief descriptive title]
As a [specific user role/persona],
I want to [clear action/goal],
So that [tangible benefit/value].
Acceptance Criteria:
1. [Criterion 1]
2. [Criterion 2]
3. [Criterion 3]
Story Types to Consider:
- Functional Stories (user interactions and features)
- Non-functional Stories (performance, security, usability)
- Epic Breakdown Stories (smaller, manageable pieces)
- Technical Stories (architecture, infrastructure)
Edge Cases and Considerations:
- Error scenarios
- Permission levels
- Data validation
- Performance requirements
- Security implications

View File

@@ -0,0 +1 @@
Fixed failing integration test in tests/integration/test_ai_rule_endpoints.py::TestAIRuleEndpoints::test_assess_rule_success. The issue was with the ai_rule_result.html template not properly rendering quality assessments from the assess-rule endpoint. Added logic to handle assessment results with quality score display.

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: python
# 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: "email-organizer"

View File

@@ -11,15 +11,50 @@ login_manager.login_view = 'auth.login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'warning'
from flask import Flask, request, make_response
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from config import config
from app.models import db, Base, User
from flask_migrate import Migrate
# Initialize Flask-Login
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.login_message = 'Please log in to access this page.'
login_manager.login_message_category = 'warning'
# Import scheduler (import here to avoid circular imports)
try:
from app.scheduler import Scheduler
except ImportError:
Scheduler = None
def create_app(config_name='default'):
app = Flask(__name__, static_folder='static', static_url_path='/static')
app.config.from_object(config[config_name])
# Add middleware to simulate 500 errors
@app.before_request
def check_for_simulate_500():
# Check if the X-Simulate-500 header is present
if request.headers.get('X-Simulate-500'):
response = make_response({'error': 'Simulated server error'}, 500)
response.headers['Content-Type'] = 'application/json'
return response
# Initialize extensions
db.init_app(app)
migrate = Migrate(app, db)
login_manager.init_app(app)
# Initialize and register scheduler if available
if Scheduler:
scheduler = Scheduler(app)
app.scheduler = scheduler
# Register blueprints
from app.routes import main
from app.auth import auth

View File

@@ -288,7 +288,7 @@ Return the rules in JSON format:
return " ".join(feedback)
@staticmethod
def generate_cache_key(folder_name: str, folder_type: str, rule_type: str, raw_text: str) -> str:
def generate_cache_key(folder_name: str, folder_type: str, rule_type: str, raw_text: str = '') -> str:
"""Generate a cache key for AI rule requests."""
key_string = f"{folder_name}:{folder_type}:{rule_type}:{raw_text}"
return hashlib.md5(key_string.encode()).hexdigest()

View File

@@ -32,3 +32,33 @@ def setup_dev():
print("Docker Compose not found. Please install Docker and Docker Compose.")
sys.exit(1)
@app.cli.command("start-scheduler")
@click.option('--interval', default=5, help='Interval in minutes between processing runs (default: 5)')
@with_appcontext
def start_scheduler(interval):
"""Start the background email processing scheduler."""
if not hasattr(app, 'scheduler'):
print("Scheduler not available. Make sure app/scheduler.py exists and is properly imported.")
sys.exit(1)
print(f"Starting email processing scheduler with {interval} minute interval...")
print("Press Ctrl+C to stop")
try:
# Start the scheduler
app.scheduler.start()
# Keep the main thread alive
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\nShutting down scheduler...")
app.scheduler.stop()
except Exception as e:
print(f"Error starting scheduler: {e}")
sys.exit(1)
# Import at the top of the file (add this import if not already present)
import time

369
app/email_processor.py Normal file
View File

@@ -0,0 +1,369 @@
from typing import List, Dict, Optional
from datetime import datetime, timedelta
import logging
import requests
from app.models import db, Folder, User, ProcessedEmail
from app.imap_service import IMAPService
from app.processed_emails_service import ProcessedEmailsService
from app.prompt_templates import build_destination_prompt
class EmailProcessor:
"""
Service class for processing emails in the background according to user-defined rules.
Handles automated organization of emails based on folder configurations.
"""
def __init__(self, user: User):
self.user = user
self.imap_service = IMAPService(user)
self.processed_emails_service = ProcessedEmailsService(user)
self.logger = logging.getLogger(__name__)
def process_user_emails(self) -> Dict[str, any]:
"""
Process emails for a user according to their folder rules.
Returns:
Dictionary with processing results including success count, error count, and timing
"""
result = {
'success_count': 0,
'error_count': 0,
'processed_folders': [],
'start_time': datetime.utcnow(),
'end_time': None,
'duration': None
}
try:
# Get all folders with organize_enabled = True, ordered by priority (highest first)
folders = Folder.query.filter_by(
user_id=self.user.id,
organize_enabled=True
).order_by(Folder.priority.desc()).all()
if not folders:
self.logger.info(f"No folders to process for user {self.user.email}")
result['end_time'] = datetime.utcnow()
result['duration'] = (result['end_time'] - result['start_time']).total_seconds()
return result
self.logger.info(f"Processing {len(folders)} folders for user {self.user.email}")
# Process each folder according to priority
for folder in folders:
try:
folder_result = self.process_folder_emails(folder)
result['success_count'] += folder_result['processed_count']
result['error_count'] += folder_result['error_count']
result['processed_folders'].append({
'folder_id': folder.id,
'folder_name': folder.name,
'processed_count': folder_result['processed_count'],
'error_count': folder_result['error_count']
})
self.logger.info(f"Processed {folder_result['processed_count']} emails for folder {folder.name}")
except Exception as e:
self.logger.error(f"Error processing folder {folder.name}: {str(e)}")
result['error_count'] += 1
continue
except Exception as e:
self.logger.error(f"Error in process_user_emails for user {self.user.email}: {str(e)}")
result['error_count'] += 1
finally:
result['end_time'] = datetime.utcnow()
result['duration'] = (result['end_time'] - result['start_time']).total_seconds()
return result
def process_folder_emails(self, folder: Folder) -> Dict[str, any]:
"""
Process emails for a specific folder according to its rules.
Args:
folder: The folder to process
Returns:
Dictionary with processing results for this folder
"""
result = {
'processed_count': 0,
'error_count': 0,
'folder_id': folder.id,
'folder_name': folder.name
}
try:
# Get pending emails for this folder
pending_email_uids = self.processed_emails_service.get_pending_emails(folder.name)
if not pending_email_uids:
self.logger.info(f"No pending emails to process for folder {folder.name}")
return result
self.logger.info(f"Processing {len(pending_email_uids)} pending emails for folder {folder.name}")
# Process emails in batches to manage system resources
batch_size = 10 # Configurable batch size
processed_batch_count = 0
for i in range(0, len(pending_email_uids), batch_size):
batch = pending_email_uids[i:i + batch_size]
batch_result = self._process_email_batch(folder, batch)
result['processed_count'] += batch_result['processed_count']
result['error_count'] += batch_result['error_count']
processed_batch_count += 1
self.logger.info(f"Processed batch {i//batch_size + 1} for folder {folder.name}: {batch_result['processed_count']} success, {batch_result['error_count']} errors")
# Update folder pending count after processing
self._update_folder_counts(folder)
except Exception as e:
self.logger.error(f"Error in process_folder_emails for folder {folder.name}: {str(e)}")
result['error_count'] += 1
return result
def _process_email_batch(self, folder: Folder, email_uids: List[str]) -> Dict[str, any]:
"""
Process a batch of emails for a folder.
Args:
folder: The folder whose rules to apply
email_uids: List of email UIDs to process
Returns:
Dictionary with processing results for this batch
"""
result = {
'processed_count': 0,
'error_count': 0
}
try:
# Connect to IMAP server
self.imap_service._connect()
# Login
username = self.user.imap_config.get('username', '')
password = self.user.imap_config.get('password', '')
self.imap_service.connection.login(username, password)
# Select the source folder
resp_code, content = self.imap_service.connection.select(folder.name)
if resp_code != 'OK':
raise Exception(f"Failed to select folder {folder.name}: {content}")
# Get all enabled folders for this user (for AI context)
all_folders = Folder.query.filter_by(user_id=self.user.id, organize_enabled=True).all()
rules = [
{
'name': f.name,
'rule_text': f.rule_text,
'priority': f.priority
}
for f in all_folders
]
# Get email headers for all emails in batch
emails = []
valid_uids = [] # Track which UIDs we successfully fetched headers for
for email_uid in email_uids:
try:
headers = self.imap_service.get_email_headers(folder.name, email_uid)
if headers:
emails.append({
'uid': email_uid,
'headers': headers
})
valid_uids.append(email_uid)
else:
self.logger.warning(f"Could not get headers for email {email_uid} in folder {folder.name}")
result['error_count'] += 1
except Exception as e:
self.logger.error(f"Error getting headers for email {email_uid}: {str(e)}")
result['error_count'] += 1
# Skip AI call if no valid emails
if not emails:
return result
# Get destinations from AI
destinations = self.get_email_destinations(emails, rules)
# Process each email based on AI decision
processed_uids = []
for email in emails:
try:
email_uid = email['uid']
destination_folder = destinations.get(email_uid)
if not destination_folder:
self.logger.warning(f"No destination determined for email {email_uid}")
result['error_count'] += 1
continue
# Skip if destination is same as current folder or INBOX (no move needed)
if destination_folder.lower() == 'inbox' or destination_folder == folder.name:
processed_uids.append(email_uid)
result['processed_count'] += 1
continue
# Move email to destination folder
if self._move_email(email_uid, folder.name, destination_folder):
processed_uids.append(email_uid)
result['processed_count'] += 1
else:
self.logger.error(f"Failed to move email {email_uid} from {folder.name} to {destination_folder}")
result['error_count'] += 1
except Exception as e:
self.logger.error(f"Error processing email {email_uid}: {str(e)}")
result['error_count'] += 1
continue
# Mark processed emails in the database
if processed_uids:
count = self.processed_emails_service.mark_emails_processed(folder.name, processed_uids)
if count != len(processed_uids):
self.logger.warning(f"Marked {count} emails as processed, but expected {len(processed_uids)}")
# Close folder and logout
self.imap_service.connection.close()
self.imap_service.connection.logout()
self.imap_service.connection = None
except Exception as e:
self.logger.error(f"Error in _process_email_batch: {str(e)}")
result['error_count'] += 1
# Clean up connection if needed
if self.imap_service.connection:
try:
self.imap_service.connection.close()
self.imap_service.connection.logout()
except:
pass
self.imap_service.connection = None
return result
def get_email_destinations(self, emails: List[Dict], rules: List[Dict]) -> Dict[str, str]:
"""
Get destination folders for a batch of emails using AI completion API.
Args:
emails: List of email dictionaries containing uid and headers
rules: List of folder rules with name, rule_text, and priority
Returns:
Dictionary mapping email UID to destination folder name
"""
destinations = {}
# Get API configuration from environment or config
api_url = self.user.get_setting('OPENAI_BASE_URL', 'http://localhost:1234/v1/completions')
api_key = self.user.get_setting('OPENAI_API_KEY', 'dummy-key')
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {api_key}'
}
for email in emails:
try:
# Build prompt using template
prompt = build_destination_prompt(email['headers'], rules)
payload = {
'prompt': prompt,
'temperature': 0.7,
'max_tokens': 50
}
response = requests.post(f'{api_url}', json=payload, headers=headers, timeout=30)
if response.status_code == 200:
# Extract folder name from response (should be just the folder name)
destination = response.text.strip()
destinations[email['uid']] = destination
else:
self.logger.error(f"AI API request failed: {response.status_code} - {response.text}")
# On error, don't assign a destination (will be handled by caller)
except Exception as e:
self.logger.error(f"Error calling AI API for email {email['uid']}: {str(e)}")
# Continue with other emails
continue
return destinations
def _move_email(self, email_uid: str, source_folder: str, destination_folder: str) -> bool:
"""
Move an email from source folder to destination folder.
Args:
email_uid: The UID of the email to move
source_folder: The current folder name
destination_folder: The destination folder name
Returns:
True if successful, False otherwise
"""
try:
# Copy email to destination folder
result = self.imap_service.connection.copy(email_uid, destination_folder)
if result[0] != 'OK':
self.logger.error(f"Copy failed for email {email_uid}: {result[1]}")
return False
# Store the message ID to ensure it was copied
# (In a real implementation, you might want to verify the copy)
# Delete from source folder
self.imap_service.connection.select(source_folder)
result = self.imap_service.connection.store(email_uid, '+FLAGS', '\Deleted')
if result[0] != 'OK':
self.logger.error(f"Mark as deleted failed for email {email_uid}: {result[1]}")
return False
# Expunge to permanently remove
result = self.imap_service.connection.expunge()
if result[0] != 'OK':
self.logger.error(f"Expunge failed for email {email_uid}: {result[1]}")
return False
return True
except Exception as e:
self.logger.error(f"Error moving email {email_uid} from {source_folder} to {destination_folder}: {str(e)}")
return False
def _update_folder_counts(self, folder: Folder) -> None:
"""
Update folder counts after processing.
Args:
folder: The folder to update
"""
try:
# Update pending count
pending_count = self.processed_emails_service.get_pending_count(folder.name)
folder.pending_count = pending_count
# Update total count (get from IMAP)
total_count = self.imap_service.get_folder_email_count(folder.name)
folder.total_count = total_count
db.session.commit()
except Exception as e:
self.logger.error(f"Error updating folder counts for {folder.name}: {str(e)}")
db.session.rollback()

46
app/prompt_templates.py Normal file
View File

@@ -0,0 +1,46 @@
from typing import List, Dict
def build_destination_prompt(email_headers: Dict[str, str], rules: List[Dict[str, any]]) -> str:
"""
Build a prompt for determining the best destination folder for an email.
Args:
email_headers: Dictionary containing email headers (subject, from, to, date)
rules: List of dictionaries containing folder rules with name, rule_text, and priority
Returns:
Formatted prompt string
"""
# Sort rules by priority (highest first) for consistent ordering
sorted_rules = sorted(rules, key=lambda x: x['priority'], reverse=True)
prompt = '''Determine the best destination folder for the following email based on the user's folder rules and priorities.
Email Details:
- Subject: {subject}
- From: {from_email}
- To: {to_email}
- Date: {date}
Available Folders (listed in priority order - higher priority folders should be preferred):
{folder_rules}
Instructions:
1. Review the email details and all folder rules
2. Select the SINGLE most appropriate destination folder based on the rules and priorities
3. If none of the folders are appropriate, respond with "INBOX" (keep in current location)
4. Respond ONLY with the exact folder name - nothing else
Best destination folder: '''.format(
subject=email_headers.get('subject', 'N/A'),
from_email=email_headers.get('from', 'N/A'),
to_email=email_headers.get('to', 'N/A'),
date=email_headers.get('date', 'N/A'),
folder_rules='\n'.join([
f"- {rule['name']} (Priority: {rule['priority']}): {rule['rule_text']}"
for rule in sorted_rules
])
)
return prompt

View File

@@ -7,6 +7,7 @@ from app.models import Folder
from .routes.folders import folders_bp
from .routes.imap import imap_bp
from .routes.emails import emails_bp
from .routes.background_processing import bp as background_processing_bp
# Create the main blueprint
main = Blueprint('main', __name__)
@@ -15,6 +16,7 @@ main = Blueprint('main', __name__)
main.register_blueprint(folders_bp)
main.register_blueprint(imap_bp)
main.register_blueprint(emails_bp)
main.register_blueprint(background_processing_bp)
# Root route that redirects to the main index page
@main.route('/')

View File

@@ -0,0 +1,59 @@
from flask import Blueprint, jsonify, request
from flask_login import login_required, current_user
from app.email_processor import EmailProcessor
decorators = [login_required]
bp = Blueprint('background_processing', __name__, url_prefix='/api/background')
@bp.route('/process-emails', methods=['POST'])
def trigger_email_processing():
"""
Trigger immediate email processing for the current user.
"""
try:
processor = EmailProcessor(current_user)
result = processor.process_user_emails()
return jsonify({
'success': True,
'message': f"Processed {result['success_count']} emails successfully",
'details': result
}), 200
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@bp.route('/process-folder/<int:folder_id>', methods=['POST'])
def trigger_folder_processing(folder_id):
"""
Trigger email processing for a specific folder.
"""
try:
# Verify the folder belongs to the current user
from app.models import Folder
folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first()
if not folder:
return jsonify({
'success': False,
'error': 'Folder not found or access denied'
}), 404
processor = EmailProcessor(current_user)
result = processor.process_folder_emails(folder)
return jsonify({
'success': True,
'message': f"Processed {result['processed_count']} emails for folder {folder.name}",
'details': result
}), 200
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500

107
app/scheduler.py Normal file
View File

@@ -0,0 +1,107 @@
import logging
import time
from datetime import datetime, timedelta
from threading import Thread
from app.models import User
from app.email_processor import EmailProcessor
from app import create_app
class Scheduler:
"""
Background scheduler for email processing tasks.
Runs as a separate thread to process emails at regular intervals.
"""
def __init__(self, app=None, interval_minutes=5):
self.app = app
self.interval = interval_minutes * 60 # Convert to seconds
self.thread = None
self.running = False
self.logger = logging.getLogger(__name__)
if app:
self.init_app(app)
def init_app(self, app):
"""Initialize the scheduler with a Flask application."""
self.app = app
# Store scheduler in app extensions
if not hasattr(app, 'extensions'):
app.extensions = {}
app.extensions['scheduler'] = self
def start(self):
"""Start the background scheduler thread."""
if self.running:
self.logger.warning("Scheduler is already running")
return
self.running = True
self.thread = Thread(target=self._run, daemon=True)
self.thread.start()
self.logger.info(f"Scheduler started with {self.interval//60} minute interval")
def stop(self):
"""Stop the background scheduler."""
self.running = False
if self.thread:
self.logger.info("Scheduler stopped")
self.thread.join()
def _run(self):
"""Main loop for the scheduler."""
# Wait a moment before first run to allow app to start
time.sleep(5)
while self.running:
try:
self.logger.info("Starting scheduled email processing")
self.process_all_users()
# Calculate next run time
next_run = datetime.now() + timedelta(seconds=self.interval)
self.logger.info(f"Next run at {next_run.strftime('%Y-%m-%d %H:%M:%S')}")
# Sleep for the interval, checking every 10 seconds if we should stop
slept = 0
while self.running and slept < self.interval:
sleep_time = min(10, self.interval - slept)
time.sleep(sleep_time)
slept += sleep_time
except Exception as e:
self.logger.error(f"Error in scheduler: {str(e)}")
# Continue running even if there's an error
continue
def process_all_users(self):
"""Process emails for all users."""
with self.app.app_context():
try:
# Get all users
users = User.query.all()
if not users:
self.logger.info("No users found for processing")
return
self.logger.info(f"Processing emails for {len(users)} users")
# Process each user's emails
for user in users:
try:
processor = EmailProcessor(user)
result = processor.process_user_emails()
self.logger.info(f"Completed processing for user {user.email}: "
f"{result['success_count']} success, "
f"{result['error_count']} errors, "
f"{len(result['processed_folders'])} folders processed")
except Exception as e:
self.logger.error(f"Error processing emails for user {user.email}: {str(e)}")
continue
except Exception as e:
self.logger.error(f"Error in process_all_users: {str(e)}")

62
app/static/js/confetti.js Normal file
View File

@@ -0,0 +1,62 @@
document.addEventListener('DOMContentLoaded', function() {
// Initialize confetti effect on page load
initConfetti();
// Add confetti effect to welcome section button click
const welcomeButton = document.querySelector('.welcome-button');
if (welcomeButton) {
welcomeButton.addEventListener('click', function() {
createConfetti();
});
}
// Add confetti effect to any button with data-confetti attribute
document.querySelectorAll('[data-confetti]').forEach(button => {
button.addEventListener('click', function() {
createConfetti();
});
});
});
function initConfetti() {
// Create a splash confetti effect when the page loads
setTimeout(() => {
createConfetti();
}, 500);
}
function createConfetti() {
const confettiCount = 150;
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#54a0ff'];
for (let i = 0; i < confettiCount; i++) {
const confetti = document.createElement('div');
confetti.style.position = 'fixed';
confetti.style.width = Math.random() * 10 + 5 + 'px';
confetti.style.height = Math.random() * 10 + 5 + 'px';
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
confetti.style.borderRadius = '50%';
confetti.style.zIndex = '9999';
confetti.style.left = Math.random() * 100 + 'vw';
confetti.style.top = '-10px';
document.body.appendChild(confetti);
// Animate confetti falling
const animationDuration = Math.random() * 3 + 2;
const targetX = (Math.random() - 0.5) * 100;
confetti.animate([
{ transform: 'translate(0, 0) rotate(0deg)', opacity: 1 },
{ transform: `translate(${targetX}vw, 100vh) rotate(${Math.random() * 360}deg)`, opacity: 0 }
], {
duration: animationDuration * 1000,
easing: 'cubic-bezier(0.1, 0.8, 0.2, 1)'
});
// Remove confetti after animation
setTimeout(() => {
confetti.remove();
}, animationDuration * 1000);
}
}

View File

@@ -13,6 +13,37 @@
<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-tooltip@1.x.x/dist/cdn.min.js" defer></script>
<link rel="stylesheet" href="https://unpkg.com/tippy.js@6/dist/tippy.css" />
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('errorToast', {
show: false,
message: '',
hide() {
this.show = false;
},
handleError(detail) {
if (detail.xhr.status >= 500 && detail.xhr.status < 600) {
// Extract error message from response
let errorMessage = 'Server Error';
try {
const responseJson = JSON.parse(detail.xhr.response);
if (responseJson.error) {
errorMessage = responseJson.error;
}
} catch (e) {
// If not JSON, use the raw response
errorMessage = detail.xhr.response || 'Server Error';
}
// Set error message and show toast using Alpine store
this.message = errorMessage;
this.show = true;
}
}
});
});
</script>
<style>
@keyframes shake {
0%, 100% { transform: translateX(0); }
@@ -48,14 +79,33 @@
</style>
{% block head %}{% endblock %}
</head>
<body class="min-h-screen flex flex-col" x-data="{}" x-on:open-modal.window="$refs.modal.showModal()"
x-on:close-modal.window="$refs.modal.close()"
hx-ext="loading-states"
data-loading-delay="200">
<body
x-on:htmx:response-error.camel="$store.errorToast.handleError($event.detail)"
class="min-h-screen flex flex-col" x-data="{}" x-on:open-modal.window="$refs.modal.showModal()"
x-on:close-modal.window="$refs.modal.close()" hx-ext="loading-states" data-loading-delay="200">
{% block header %}{% endblock %}
{% block content %}{% endblock %}
{% block modal %}{% endblock %}
<!-- Toast for 5xx errors -->
<div id="error-toast" class="toast toast-top toast-end w-full z-100" x-show="$store.errorToast.show" x-transition.duration.200ms>
<div class="alert alert-warning backdrop-blur-md bg-error/30 border border-error/20 relative">
<span id="error-message" x-text="$store.errorToast.message"></span>
<button class="btn btn-sm btn-ghost absolute top-2 right-2" @click="$store.errorToast.hide()">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<script>
// Check for global variable to set X-Simulate-500 header
document.addEventListener('htmx:configRequest', function (evt) {
if (typeof window.simulate500 === 'boolean' && window.simulate500) {
evt.detail.headers['X-Simulate-500'] = 'true';
}
});
</script>
</body>
</html>

View File

@@ -45,6 +45,20 @@
</div>
{% endfor %}
</div>
{% elif result.assessment %}
<div class="bg-white border rounded-lg p-3 mb-2">
<div class="flex justify-between items-start mb-2">
<h4 class="font-medium text-sm">Rule Quality Assessment</h4>
<span class="badge {% if result.quality_score >= 80 %}badge-success{% elif result.quality_score >= 60 %}badge-warning{% else %}badge-error{% endif %}"
role="status" aria-live="polite">
{{ result.quality_score }}%
</span>
</div>
<p class="text-sm text-gray-700 mb-2">
<strong>Grade:</strong> {{ result.assessment.grade }}<br>
<strong>Feedback:</strong> {{ result.assessment.feedback }}
</p>
</div>
{% endif %}
<script>

View File

@@ -0,0 +1,132 @@
# Background Email Processing User Stories & Acceptance Criteria
## User Stories
### Primary User Stories
#### 1. Automated Email Processing
**As a** user with configured email folders
**I want** my emails to be automatically processed in the background
**So that** I don't have to manually trigger email organization
**Acceptance Criteria:**
- [ ] Background task runs periodically to process emails for all users
- [ ] Only processes emails in folders with `organize_enabled = true`
- [ ] Processes emails according to all folder rules, moving emails as needed to the appropriate folder
- [ ] Respects priority field for folders
- [ ] Marks processed emails to avoid reprocessing
- [ ] Updates folder pending counts after processing
- [ ] Handles processing errors gracefully without stopping other folders
- [ ] Respects folder priority when processing (high priority first)
#### 2. Email Processing Status Tracking
**As a** user
**I want** to see the status of email processing
**So that** I can understand how many emails are pending organization
**Acceptance Criteria:**
- [ ] Pending email count is displayed for each folder
- [ ] Count updates in real-time after processing
- [ ] Processed email tracking prevents duplicate processing
- [ ] Email processing history is maintained
#### 3. Batch Email Processing
**As a** user with many emails
**I want** emails to be processed in batches
**So that** the system doesn't become unresponsive with large mailboxes
**Acceptance Criteria:**
- [ ] Emails are processed in configurable batch sizes
- [ ] Processing progress is tracked per batch
- [ ] Failed batches don't affect other batches
- [ ] System resources are managed efficiently during processing
#### 4. Error Handling & Recovery
**As a** user
**I want** email processing errors to be handled gracefully
**So that** temporary issues don't stop all email organization
**Acceptance Criteria:**
- [ ] Individual email processing failures don't stop folder processing
- [ ] Connection errors to IMAP servers are retried
- [ ] Transient errors are logged but don't halt processing
- [ ] Permanent failures are flagged for user attention
- [ ] Error notifications are sent for critical failures
#### 5. Processing Controls
**As a** user
**I want** control over when emails are processed
**So that** I can manage processing during preferred times
**Acceptance Criteria:**
- [ ] Users can enable/disable automatic processing per folder
- [ ] Users can trigger manual processing for specific folders
- [ ] Processing can be paused and resumed
- [ ] Processing schedule can be configured
### Secondary User Stories
#### 6. Performance Monitoring
**As a** system administrator
**I want** to monitor email processing performance
**So that** I can identify and resolve bottlenecks
**Acceptance Criteria:**
- [ ] Processing time metrics are collected
- [ ] Success/failure rates are tracked
- [ ] Resource usage during processing is monitored
- [ ] Performance alerts are sent for degraded performance
#### 7. Accessibility
**As a** user with disabilities
**I want** email processing status to be accessible
**So that** I can understand processing through assistive technologies
**Acceptance Criteria:**
- [ ] Processing status updates are announced by screen readers
- [ ] Visual indicators have proper contrast ratios
- [ ] Processing notifications are keyboard navigable
- [ ] Error messages are clear and descriptive
## Technical Requirements
### Backend Requirements
- [ ] Background task scheduler implementation
- [ ] IMAP connection management for processing
- [ ] Email processing logic with rule evaluation
- [ ] Processed email tracking system
- [ ] Error handling and logging mechanisms
- [ ] Performance monitoring and metrics collection
### Frontend Requirements
- [ ] Pending email count display
- [ ] Processing status indicators
- [ ] Manual processing controls
- [ ] Error notification UI
- [ ] Real-time updates for processing status
### Integration Requirements
- [ ] Integration with IMAP service for email access
- [ ] Integration with folder management system
- [ ] Integration with processed emails tracking
- [ ] API endpoints for manual processing controls
## Non-Functional Requirements
### Performance
- Process emails at a rate of at least 10 emails per second
- Background task execution interval configurable (default: 5 minutes)
- Memory usage capped to prevent system resource exhaustion
- Processing of large mailboxes (100k+ emails) completes without timeouts
### Reliability
- 99.5% uptime for background processing
- Automatic recovery from transient errors
- Graceful degradation when external services are unavailable
- Data consistency maintained during processing failures
### Scalability
- Support for 1000+ concurrent users
- Horizontal scaling of processing workers
- Efficient database queries for email tracking
- Load balancing of processing tasks across workers

View File

@@ -0,0 +1,229 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-bind](/directives/bind)
`x-bind` allows you to set HTML attributes on elements based on the result of JavaScript expressions.
For example, here's a component where we will use `x-bind` to set the placeholder value of an input.
```
<div x-data="{ placeholder: 'Type here...' }">
n<input type="text" x-bind:placeholder="placeholder">
n</div>
```
### [Shorthand syntax](#shorthand-syntax)
If `x-bind:` is too verbose for your liking, you can use the shorthand: `:`. For example, here is the same input element as above, but refactored to use the shorthand syntax.
```
<input type="text" :placeholder="placeholder">
```
> Despite not being included in the above snippet, `x-bind` cannot be used if no parent element has `x-data` defined. [→ Read more about `x-data`](/directives/data)
### [Binding classes](#binding-classes)
`x-bind` is most often useful for setting specific classes on an element based on your Alpine state.
Here's a simple example of a simple dropdown toggle, but instead of using `x-show`, we'll use a "hidden" class to toggle an element.
```
<div x-data="{ open: false }">
n<button x-on:click="open = ! open">Toggle Dropdown</button>
n<div :class="open ? '' : 'hidden'">
nDropdown Contents...
n</div>
n</div>
```
Now, when `open` is `false`, the "hidden" class will be added to the dropdown.
#### [Shorthand conditionals](#shorthand-conditionals)
In cases like these, if you prefer a less verbose syntax you can use JavaScript's short-circuit evaluation instead of standard conditionals:
```
<div :class="show ? '' : 'hidden'">
n<!-- Is equivalent to: -->
n<div :class="show || 'hidden'">
```
The inverse is also available to you. Suppose instead of `open`, we use a variable with the opposite value: `closed`.
```
<div :class="closed ? 'hidden' : ''">
n<!-- Is equivalent to: -->
n<div :class="closed && 'hidden'">
```
#### [Class object syntax](#class-object-syntax)
Alpine offers an additional syntax for toggling classes if you prefer. By passing a JavaScript object where the classes are the keys and booleans are the values, Alpine will know which classes to apply and which to remove. For example:
```
<div :class="{ 'hidden': ! show }">
```
This technique offers a unique advantage to other methods. When using object-syntax, Alpine will NOT preserve original classes applied to an element's `class` attribute.
For example, if you wanted to apply the "hidden" class to an element before Alpine loads, AND use Alpine to toggle its existence you can only achieve that behavior using object-syntax:
```
<div class="hidden" :class="{ 'hidden': ! show }">
```
In case that confused you, let's dig deeper into how Alpine handles `x-bind:class` differently than other attributes.
#### [Special behavior](#special-behavior)
`x-bind:class` behaves differently than other attributes under the hood.
Consider the following case.
```
<div class="opacity-50" :class="hide && 'hidden'">
```
If "class" were any other attribute, the `:class` binding would overwrite any existing class attribute, causing `opacity-50` to be overwritten by either `hidden` or `''`.
However, Alpine treats `class` bindings differently. It's smart enough to preserve existing classes on an element.
For example, if `hide` is true, the above example will result in the following DOM element:
```
<div class="opacity-50 hidden">
```
If `hide` is false, the DOM element will look like:
```
<div class="opacity-50">
```
This behavior should be invisible and intuitive to most users, but it is worth mentioning explicitly for the inquiring developer or any special cases that might crop up.
### [Binding styles](#binding-styles)
Similar to the special syntax for binding classes with JavaScript objects, Alpine also offers an object-based syntax for binding `style` attributes.
Just like the class objects, this syntax is entirely optional. Only use it if it affords you some advantage.
```
<div :style="{ color: 'red', display: 'flex' }">
n<!-- Will render: -->
n<div style="color: red; display: flex;" ...>
```
Conditional inline styling is possible using expressions just like with x-bind:class. Short circuit operators can be used here as well by using a styles object as the second operand.
```
<div x-bind:style="true && { color: 'red' }">
n<!-- Will render: -->
n<div style="color: red;">
```
One advantage of this approach is being able to mix it in with existing styles on an element:
```
<div style="padding: 1rem;" :style="{ color: 'red', display: 'flex' }">
n<!-- Will render: -->
n<div style="padding: 1rem; color: red; display: flex;" ...>
```
And like most expressions in Alpine, you can always use the result of a JavaScript expression as the reference:
```
<div x-data="{ styles: { color: 'red', display: 'flex' }}">
n<div :style="styles">
n</div>
n<!-- Will render: -->
n<div ...>
n<div style="color: red; display: flex;" ...>
n</div>
```
### [Binding Alpine Directives Directly](#bind-directives)
`x-bind` allows you to bind an object of directives to an element.
For example, you can use it to dynamically bind a directive like `@click`:
```
<div x-data="{ open: false }">
n<button :@click="open = ! open">Toggle Dropdown</button>
n<div x-show="open">
nDropdown Contents...
n</div>
n</div>
```
In this example, `:click` is bound to the `open` variable. When you click the button, it will toggle the value of `open`.
This feature can also be used with the shorthand syntax:
```
<button :@click="open = ! open">Toggle Dropdown</button>
```
[→ Read more about `x-on`](/directives/on)
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,51 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-cloak](/directives/cloak)
Sometimes, when you're using AlpineJS for a part of your template, there is a "blip" where you might see your uninitialized template after the page loads, but before Alpine loads.
`x-cloak` addresses this scenario by hiding the element it's attached to until Alpine is fully loaded on the page.
For `x-cloak` to work however, you must add the following CSS to the page.
```
[x-cloak] { display: none !important; }
```
The following example will hide the `<span>` tag until its `x-show` is specifically set to true, preventing any "blip" of the hidden element onto screen as Alpine loads.
```
<span x-cloak x-show="false">This will not 'blip' onto screen at any point</span>
```
`x-cloak` doesn't just work on elements hidden by `x-show` or `x-if`: it also ensures that elements containing data are hidden until the data is correctly set. The following example will hide the `<span>` tag until Alpine has set its text content to the `message` property.
```
<span x-cloak x-text="message"></span>
```
When Alpine loads on the page, it removes all `x-cloak` property from the element, which also removes the `display: none;` applied by CSS, therefore showing the element.
## Alternative to global syntax
If you'd like to achieve this same behavior, but avoid having to include a global style, you can use the following cool, but admittedly odd trick:
```
<template x-if="true">
n<span x-text="message"></span>
n</template>
```
This will achieve the same goal as `x-cloak` by just leveraging the way `x-if` works.
Because `<template>` elements are "hidden" in browsers by default, you won't see the `<span>` until Alpine has had a chance to render the `x-if="true"` and show it.
Again, this solution is not for everyone, but it's worth mentioning for special cases.
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,254 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-data](/directives/data)
Everything in Alpine starts with the `x-data` directive.
`x-data` defines a chunk of HTML as an Alpine component and provides the reactive data for that component to reference.
Here's an example of a contrived dropdown component:
```
<div x-data="{ open: false }">
n<button @click="open = ! open">Toggle Content</button>
n<div x-show="open">
nContent...
n</div>
n</div>
```
Don't worry about the other directives in this example (`@click` and `x-show`), we'll get to those in a bit. For now, let's focus on `x-data`.
### [Scope](#scope)
Properties defined in an `x-data` directive are available to all element children. Even ones inside other, nested `x-data` components.
For example:
```
<div x-data="{ foo: 'bar' }">
n<span x-text="foo"><!-- Will output: "bar" --></span>
n<div x-data="{ bar: 'baz' }">
n<span x-text="foo"><!-- Will output: "bar" --></span>
n<div x-data="{ foo: 'bob' }">
n<span x-text="foo"><!-- Will output: "bob" --></span>
n</div>
n</div>
n</div>
```
### [Methods](#methods)
Because `x-data` is evaluated as a normal JavaScript object, in addition to state, you can store methods and even getters.
For example, let's extract the "Toggle Content" behavior into a method on `x-data`.
```
<div x-data="{ open: false, toggle() { this.open = ! this.open } }">
n<button @click="toggle()">Toggle Content</button>
n<div x-show="open">
nContent...
n</div>
n</div>
```
Notice the added `toggle() { this.open = ! this.open }` method on `x-data`. This method can now be called from anywhere inside the component.
You'll also notice the usage of `this.` to access state on the object itself. This is because Alpine evaluates this data object like any standard JavaScript object with a `this` context.
If you prefer, you can leave the calling parenthesis off of the `toggle` method completely. For example:
```
<!-- Before -->
n<button @click="toggle()">...</button>
n<!-- After -->
n<button @click="toggle">...</button>
```
### [Getters](#getters)
JavaScript [getters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get) are handy when the sole purpose of a method is to return data based on other state.
Think of them like "computed properties" (although, they are not cached like Vue's computed properties).
Let's refactor our component to use a getter called `isOpen` instead of accessing `open` directly.
```
<div x-data="{
nopen: false,
nget isOpen() { return this.open },
ntoggle() { this.open = ! this.open },
n}">
n<button @click="toggle()">Toggle Content</button>
n<div x-show="isOpen">
nContent...
n</div>
n</div>
```
Notice the "Content" now depends on the `isOpen` getter instead of the `open` property directly.
In this case there is no tangible benefit. But in some cases, getters are helpful for providing a more expressive syntax in your components.
### [Data-less components](#data-less-components)
Occasionally, you want to create an Alpine component, but you don't need any data.
In these cases, you can always pass in an empty object.
```
<div x-data="{}">
```
However, if you wish, you can also eliminate the attribute value entirely if it looks better to you.
```
<div x-data>
```
### [Single-element components](#single-element-components)
Sometimes you may only have a single element inside your Alpine component, like the following:
```
<div x-data="{ open: true }">
n<button @click="open = false" x-show="open">Hide Me</button>
n</div>
```
In these cases, you can declare `x-data` directly on that single element:
```
<button x-data="{ open: true }" @click="open = false" x-show="open">
nHide Me
n</button>
```
### [Re-usable Data](#re-usable-data)
If you find yourself duplicating the contents of `x-data`, or you find the inline syntax verbose, you can extract the `x-data` object out to a dedicated component using `Alpine.data`.
Here's a quick example:
```
<div x-data="dropdown">
n<button @click="toggle">Toggle Content</button>
n<div x-show="open">
nContent...
n</div>
n</div>
n<script>
ndocument.addEventListener('alpine:init', () => {
nAlpine.data('dropdown', () => ({
nopen: false,
ntoggle() {
nthis.open = ! this.open
n},
n}))
n})
n</script>
```
[→ Read more about `Alpine.data(...)`](/globals/alpine-data)
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,25 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-effect](/directives/effect)
`x-effect` is a useful directive for re-evaluating an expression when one of its dependencies change. You can think of it as a watcher where you don't have to specify what property to watch, it will watch all properties used within it.
If this definition is confusing for you, that's ok. It's better explained through an example:
```
<div x-data="{ label: 'Hello' }" x-effect="console.log(label)">
n<button @click="label += ' World!'">Change Message</button>
n</div>
```
When this component is loaded, the `x-effect` expression will be run and "Hello" will be logged into the console.
Because Alpine knows about any property references contained within `x-effect`, when the button is clicked and `label` is changed, the effect will be re-triggered and "Hello World!" will be logged to the console.
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,181 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-for](/directives/for)
Alpine's `x-for` directive allows you to create DOM elements by iterating through a list. Here's a simple example of using it to create a list of colors based on an array.
```
<ul x-data="{ colors: ['Red', 'Orange', 'Yellow'] }">
n<template x-for="color in colors">
n<li x-text="color"></li>
n</template>
n</ul>
```
You may also pass objects to `x-for`.
```
<ul x-data="{ car: { make: 'Jeep', model: 'Grand Cherokee', color: 'Black' } }">
n<template x-for="(value, index) in car">
n<li>
n<span x-text="index"></span>: <span x-text="value"></span>
n</li>
n</template>
n</ul>
```
There are two rules worth noting about `x-for`:
> `x-for` MUST be declared on a `<template>` element.
> That `<template>` element MUST contain only one root element
### [Keys](#keys)
It is important to specify unique keys for each `x-for` iteration if you are going to be re-ordering items. Without dynamic keys, Alpine may have a hard time keeping track of what re-orders and will cause odd side-effects.
```
<ul x-data="{ colors: [
n{ id: 1, label: 'Red' },
n{ id: 2, label: 'Orange' },
n{ id: 3, label: 'Yellow' },
n]}">
n<template x-for="color in colors" :key="color.id">
n<li x-text="color.label"></li>
n</template>
n</ul>
```
Now if the colors are added, removed, re-ordered, or their "id"s change, Alpine will preserve or destroy the iterated `<li>`elements accordingly.
### [Accessing indexes](#accessing-indexes)
If you need to access the index of each item in the iteration, you can do so using the `([item], [index]) in [items]` syntax like so:
```
<ul x-data="{ colors: ['Red', 'Orange', 'Yellow'] }">
n<template x-for="(color, index) in colors">
n<li>
n<span x-text="index + ': '"></span>
n<span x-text="color"></span>
n</li>
n</template>
n</ul>
```
You can also access the index inside a dynamic `:key` expression.
```
<template x-for="(color, index) in colors" :key="index">
```
### [Iterating over a range](#iterating-over-a-range)
If you need to simply loop `n` number of times, rather than iterate through an array, Alpine offers a short syntax.
```
<ul>
n<template x-for="i in 10">
n<li x-text="i"></li>
n</template>
n</ul>
```
`i` in this case can be named anything you like.
> Despite not being included in the above snippet, `x-for` cannot be used if no parent element has `x-data` defined. [→ Read more about `x-data`](/directives/data)
### [Contents of a `<template>`](#contents-of-a-template)
As mentioned above, an `<template>` tag must contain only one root element.
For example, the following code will not work:
```
<template x-for="color in colors">
n<span>The next color is </span><span x-text="color">
n</template>
```
but this code will work:
```
<template x-for="color in colors">
n<p>
n<span>The next color is </span><span x-text="color">
n</p>
n</template>
```
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,28 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-html](/directives/html)
`x-html` sets the "innerHTML" property of an element to the result of a given expression.
> ⚠️ Only use on trusted content and never on user-provided content. ⚠️
> Dynamically rendering HTML from third parties can easily lead to XSS vulnerabilities.
Here's a basic example of using `x-html` to display a user's username.
```
<div x-data="{ username: '<strong>calebporzio</strong>' }">
nUsername: <span x-html="username"></span>
n</div>
```
Username:
Now the `<span>` tag's inner HTML will be set to "**calebporzio**".
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,29 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-if](/directives/if)
`x-if` is used for toggling elements on the page, similarly to `x-show`, however it completely adds and removes the element it's applied to rather than just changing its CSS display property to "none".
Because of this difference in behavior, `x-if` should not be applied directly to the element, but instead to a `<template>` tag that encloses the element. This way, Alpine can keep a record of the element once it's removed from the page.
```
<template x-if="open">
n<div>Contents...</div>
n</template>
```
> Despite not being included in the above snippet, `x-if` cannot be used if no parent element has `x-data` defined. [→ Read more about `x-data`](/directives/data)
## Caveats
Unlike `x-show`, `x-if`, does NOT support transitioning toggles with `x-transition`.
`<template>` tags can only contain one root element.
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,29 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-ignore](/directives/ignore)
By default, Alpine will crawl and initialize the entire DOM tree of an element containing `x-init` or `x-data`.
If for some reason, you don't want Alpine to touch a specific section of your HTML, you can prevent it from doing so using `x-ignore`.
```
<div x-data="{ label: 'From Alpine' }">
n<div x-ignore>
n<span x-text="label"></span>
n</div>
n</div>
```
In the above example, the `<span>` tag will not contain "From Alpine" because we told Alpine to ignore the contents of the `div` completely.
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,126 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-init](/directives/init)
The `x-init` directive allows you to hook into the initialization phase of any element in Alpine.
```
<div x-init="console.log('I\'m being initialized!')"></div>
```
In the above example, "I'm being initialized!" will be output in the console before it makes further DOM updates.
Consider another example where `x-init` is used to fetch some JSON and store it in `x-data` before the component is processed.
```
<div
x-data="{ posts: [] }"
x-init="posts = await (await fetch('/posts')).json()"
>...</div>
```
### [$nextTick](#next-tick)
Sometimes, you want to wait until after Alpine has completely finished rendering to execute some code.
This would be something like `useEffect(..., [])` in react, or `mount` in Vue.
By using Alpine's internal `$nextTick` magic, you can make this happen.
```
<div x-init="$nextTick(() => { ... })"></div>
```
### [Standalone `x-init`](#standalone-x-init)
You can add `x-init` to any elements inside or outside an `x-data` HTML block. For example:
```
<div x-data>
n<span x-init="console.log('I can initialize')"></span>
n</div>
n<span x-init="console.log('I can initialize too')"></span>
```
### [Auto-evaluate init() method](#auto-evaluate-init-method)
If the `x-data` object of a component contains an `init()` method, it will be called automatically. For example:
```
<div x-data="{"
ninit() {
nconsole.log('I am called automatically')
n}
n}"
>...
</div>
```
This is also the case for components that were registered using the `Alpine.data()` syntax.
```
Alpine.data('dropdown', () => ({
ninit() {
nconsole.log('I will get evaluated when initializing each "dropdown" component.')
n},
n}))
```
If you have both an `x-data` object containing an `init()` method and an `x-init` directive, the `x-data` method will be called before the directive.
```
<div
x-data="{"
ninit() {
nconsole.log('I am called first')
n}
n}"
x-init="console.log('I am called second')"
>
...
</div>
```
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,275 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-model](/directives/model)
`x-model` allows you to bind the value of an input element to Alpine data.
Here's a simple example of using `x-model` to bind the value of a text field to a piece of data in Alpine.
```
<div x-data="{ message: '' }">
n<input type="text" x-model="message">
n<span x-text="message"></span>
n</div>
```
Now as the user types into the text field, the `message` will be reflected in the `<span>` tag.
`x-model` is two-way bound, meaning it both "sets" and "gets". In addition to changing data, if the data itself changes, the element will reflect the change.
We can use the same example as above but this time, we'll add a button to change the value of the `message` property.
```
<div x-data="{ message: '' }">
n<input type="text" x-model="message">
n<button x-on:click="message = 'changed'">Change Message</button>
n</div>
```
Now when the `<button>` is clicked, the input element's value will instantly be updated to "changed".
`x-model` works with the following input elements:
* `<input type="text">`
* `<textarea>`
* `<input type="checkbox">`
* `<input type="radio">`
* `<select>`
* `<input type="range">`
### [Text inputs](#text-inputs)
```
<input type="text" x-model="message">
n<span x-text="message"></span>
```
> Despite not being included in the above snippet, `x-model` cannot be used if no parent element has `x-data` defined. [→ Read more about `x-data`](/directives/data)
### [Textarea inputs](#textarea-inputs)
```
<textarea x-model="message"></textarea>
n<span x-text="message"></span>
```
### [Checkbox inputs](#checkbox-inputs)
#### [Single checkbox with boolean](#single-checkbox-with-boolean)
```
<input type="checkbox" id="checkbox" x-model="show">
n<label for="checkbox" x-text="show"></label>
```
#### [Multiple checkboxes bound to array](#multiple-checkboxes-bound-to-array)
```
<input type="checkbox" value="red" x-model="colors">
n<input type="checkbox" value="orange" x-model="colors">
n<input type="checkbox" value="yellow" x-model="colors">
nColors: <span x-text="colors"></span>
```
Colors:
### [Radio inputs](#radio-inputs)
```
<input type="radio" value="yes" x-model="answer">
n<input type="radio" value="no" x-model="answer">
nAnswer: <span x-text="answer"></span>
```
Answer:
### [Select inputs](#select-inputs)
#### [Single select](#single-select)
```
<select x-model="color">
n<option>Red</option>
n<option>Orange</option>
n<option>Yellow</option>
n</select>
nColor: <span x-text="color"></span>
```
Color:
#### [Single select with placeholder](#single-select-with-placeholder)
```
<select x-model="color">
n<option value="" disabled>Select A Color</option>
n<option>Red</option>
n<option>Orange</option>
n<option>Yellow</option>
n</select>
nColor: <span x-text="color"></span>
```
Color:
#### [Multiple select](#multiple-select)
```
<select x-model="color" multiple>
n<option>Red</option>
n<option>Orange</option>
n<option>Yellow</option>
n</select>
nColors: <span x-text="color"></span>
```
Color:
#### [Dynamically populated Select Options](#dynamically-populated-select-options)
```
<select x-model="color">
n<template x-for="color in ['Red', 'Orange', 'Yellow']">
n<option x-text="color"></option>
n</template>
n</select>
nColor: <span x-text="color"></span>
```
Color:
### [Range inputs](#range-inputs)
```
<input type="range" x-model="range" min="0" max="1" step="0.1">
n<span x-text="range"></span>
```
### [Modifiers](#modifiers)
#### [`.lazy`](#lazy)
On text inputs, by default, `x-model` updates the property on every keystroke. By adding the `.lazy` modifier, you can force an `x-model` input to only update the property when user focuses away from the input element.
This is handy for things like real-time form-validation where you might not want to show an input validation error until the user "tabs" away from a field.
```
<input type="text" x-model.lazy="username">
n<span x-show="username.length > 20">The username is too long.</span>
```
#### [`.number`](#number)
By default, any data stored in a property via `x-model` is stored as a string. To force Alpine to store the value as a JavaScript number, add the `.number` modifier.
```
<input type="text" x-model.number="age">
n<span x-text="typeof age"></span>
```
#### [`.boolean`](#boolean)
By default, any data stored in a property via `x-model` is stored as a string. To force Alpine to store the value as a JavaScript boolean, add the `.boolean` modifier. Both integers (1/0) and strings (true/false) are valid boolean values.
```
<select x-model.boolean="isActive">
n<option value="true">Yes</option>
n<option value="false">No</option>
n</select>
n<span x-text="typeof isActive"></span>
```
#### [`.debounce`](#debounce)
By adding `.debounce` to `x-model`, you can easily debounce the updating of bound input.
This is useful for things like real-time search inputs that fetch new data from the server every time the search property changes.
```
<input type="text" x-model.

View File

@@ -0,0 +1,383 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-on](/directives/on)
`x-on` allows you to easily run code on dispatched DOM events.
Here's an example of simple button that shows an alert when clicked.
```
<button x-on:click="alert('Hello World!')">Say Hi</button>
```
> `x-on` can only listen for events with lower case names, as HTML attributes are case-insensitive. Writing `x-on:CLICK` will listen for an event named `click`. If you need to listen for a custom event with a camelCase name, you can use the [`.camel` helper](#camel) to work around this limitation. Alternatively, you can use [`x-bind`](about:/directives/bind#bind-directives) to attach an `x-on` directive to an element in javascript code (where case will be preserved).
### [Shorthand syntax](#shorthand-syntax)
If `x-on:` is too verbose for your tastes, you can use the shorthand syntax: `@`.
Here's the same component as above, but using the shorthand syntax instead:
```
<button @click="alert('Hello World!')">Say Hi</button>
```
> Despite not being included in the above snippet, `x-on` cannot be used if no parent element has `x-data` defined. [→ Read more about `x-data`](/directives/data)
### [The event object](#the-event-object)
If you wish to access the native JavaScript event object from your expression, you can use Alpine's magic `$event` property.
```
<button @click="alert($event.target.getAttribute('message'))" message="Hello World">Say Hi</button>
```
In addition, Alpine also passes the event object to any methods referenced without trailing parenthesis. For example:
```
<button @click="handleClick">...</button>
<script>
function handleClick(e) {
// Now you can access the event object (e) directly
}
</script>
```
### [Keyboard events](#keyboard-events)
Alpine makes it easy to listen for `keydown` and `keyup` events on specific keys.
Here's an example of listening for the `Enter` key inside an input element.
```
<input type="text" @keyup.enter="alert('Submitted!')">
```
You can also chain these key modifiers to achieve more complex listeners.
Here's a listener that runs when the `Shift` key is held and `Enter` is pressed, but not when `Enter` is pressed alone.
```
<input type="text" @keyup.shift.enter="alert('Submitted!')">
```
You can directly use any valid key names exposed via [`KeyboardEvent.key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) as modifiers by converting them to kebab-case.
```
<input type="text" @keyup.page-down="alert('Submitted!')">
```
For easy reference, here is a list of common keys you may want to listen for.
| Modifier | Keyboard Key |
| --- | --- |
| `.shift` | Shift |
| `.enter` | Enter |
| `.space` | Space |
| `.ctrl` | Ctrl |
| `.cmd` | Cmd |
| `.meta` | Cmd on Mac, Windows key on Windows |
| `.alt` | Alt |
| `.up` `.down` `.left` `.right` | Up/Down/Left/Right arrows |
| `.escape` | Escape |
| `.tab` | Tab |
| `.caps-lock` | Caps Lock |
| `.equal` | Equal, `=` |
| `.period` | Period, `.` |
| `.comma` | Comma, `,` |
| `.slash` | Forward Slash, `/` |
### [Mouse events](#mouse-events)
Like the above Keyboard Events, Alpine allows the use of some key modifiers for handling `click` events.
| Modifier | Event Key |
| --- | --- |
| `.shift` | shiftKey |
| `.ctrl` | ctrlKey |
| `.cmd` | metaKey |
| `.meta` | metaKey |
| `.alt` | altKey |
These work on `click`, `auxclick`, `context` and `dblclick` events, and even `mouseover`, `mousemove`, `mouseenter`, `mouseleave`, `mouseout`, `mouseup` and `mousedown`.
Here's an example of a button that changes behaviour when the `Shift` key is held down.
```
<button type="button"
click="message = 'selected'"
click.shift="message = 'added to selection'">
@mousemove.shift="message = 'add to selection'"
@mouseout="message = 'select'"
x-text="message"></button>
```
> Note: Normal click events with some modifiers (like `ctrl`) will automatically become `contextmenu` events in most browsers. Similarly, `right-click` events will trigger a `contextmenu` event, but will also trigger an `auxclick` event if the `contextmenu` event is prevented.
### [Custom events](#custom-events)
Alpine event listeners are a wrapper for native DOM event listeners. Therefore, they can listen for ANY DOM event, including custom events.
Here's an example of a component that dispatches a custom DOM event and listens for it as well.
```
<div x-data @foo="alert('Button Was Clicked!')">
<button @click="$event.target.dispatchEvent(new CustomEvent('foo', { bubbles: true }))">...</button>
</div>
```
When the button is clicked, the `@foo` listener will be called.
Because the `.dispatchEvent` API is verbose, Alpine offers a `$dispatch` helper to simplify things.
Here's the same component re-written with the `$dispatch` magic property.
```
<div x-data @foo="alert('Button Was Clicked!')">
<button @click="$dispatch('foo')">...</button>
</div>
```
[→ Read more about `$dispatch`](/magics/dispatch)
### [Modifiers](#modifiers)
Alpine offers a number of directive modifiers to customize the behavior of your event listeners.
#### [.prevent](#prevent)
`.prevent` is the equivalent of calling `.preventDefault()` inside a listener on the browser event object.
```
<form @submit.prevent="console.log('submitted')" action="/foo">
<button>Submit</button>
</form>
```
In the above example, with the `.prevent`, clicking the button will NOT submit the form to the `/foo` endpoint. Instead, Alpine's listener will handle it and "prevent" the event from being handled any further.
#### [.stop](#stop)
Similar to `.prevent`, `.stop` is the equivalent of calling `.stopPropagation()` inside a listener on the browser event object.
```
<div @click="console.log('I will not get logged')">
<button @click.stop>Click Me</button>
</div>
```
In the above example, clicking the button WON'T log the message. This is because we are stopping the propagation of the event immediately and not allowing it to "bubble" up to the `<div>` with the `@click` listener on it.
#### [.outside](#outside)
`.outside` is a convenience helper for listening for a click outside of the element it is attached to. Here's a simple dropdown component example to demonstrate:
```
<div x-data="{ open: false }">
<button @click="open = ! open">Toggle</button>
<div x-show="open" @click.outside="open = false">
Contents...
</div>
</div>
```
In the above example, after showing the dropdown contents by clicking the "Toggle" button, you can close the dropdown by clicking anywhere on the page outside the content.
This is because `.outside` is listening for clicks that DON'T originate from the element it's registered on.
> It's worth noting that the `.outside` expression will only be evaluated when the element it's registered on is visible on the page. Otherwise, there would be nasty race conditions where clicking the "Toggle" button would also fire the `@click.outside` handler when it is not visible.
#### [.window](#window)
When the `.window` modifier is present, Alpine will register the event listener on the root `window` object on the page instead of the element itself.
```
<div @keyup.escape.window="...">...</div>
```
The above snippet will listen for the "escape" key to be pressed ANYWHERE on the page.
Adding `.window` to listeners is extremely useful for these sorts of cases where a small part of your markup is concerned with events that take place on the entire page.
#### [.document](#document)
`.document` works similarly to `.window` only it registers listeners on the `document` global, instead of the `window` global.
#### [.once](#once)
By adding `.once` to a listener, you are ensuring that the handler is only called ONCE.
```
<button @click.once="console.log('I will only log once')">...</button>
```
#### [.debounce](#debounce)
Sometimes it is useful to "debounce" an event handler so that it only is called after a certain period of inactivity (250 milliseconds by default).
For example if you have a search field that fires network requests as the user types into it, adding a debounce will prevent the network requests from firing on every single keystroke.
```
<input @input.debounce="fetchResults">
```
Now, instead of calling `fetchResults` after every keystroke, `fetchResults` will only be called after 250 milliseconds of no keystrokes.
If you wish to lengthen or shorten the debounce time, you can do so by trailing a duration after the `.debounce` modifier like so:
```
<input @input.debounce.500ms="fetchResults">
```
Now, `fetchResults` will only be called after 500 milliseconds of inactivity.
#### [.throttle](#throttle)
`.throttle` is similar to `.debounce` except it will release a handler call every 250 milliseconds instead of deferring it indefinitely.
This is useful for cases where there may be repeated and prolonged event firing and using `.debounce` won't work because you want to still handle the event every so often.
For example:
```
<div @scroll.window.throttle="handleScroll">...</div>
```
The above example is a great use case of throttling. Without `.throttle`, the `handleScroll` method would be fired hundreds of times as the user scrolls down a page. This can really slow down a site. By adding `.throttle`, we are ensuring that `handleScroll` only gets called every 250 milliseconds.
> Fun Fact: This exact strategy is used on this very documentation site to update the currently highlighted section in the right sidebar.
Just like with `.debounce`, you can add a custom duration to your throttled event:
```
<div @scroll.window.throttle.750ms="handleScroll">...</div>
```
Now, `handleScroll` will only be called every 750 milliseconds.
#### [.self](#self)
By adding `.self` to an event listener, you are ensuring that the event originated on the element it is declared on, and not from a child element.
```
<button @click.self="handleClick">
Click Me
<img src="...">
</button>
```
In the above example, we have an `<img>` tag inside the `<button>` tag. Normally, any click originating within the `<button>` element (like on `<img>` for example), would be picked up by a `@click` listener on the button.
However, in this case, because we've added a `.self`, only clicking the button itself will call `handleClick`. Only clicks originating on the `<img>` element will not be handled.
#### [.camel](#camel)
```
<div @custom-event.camel="handleCustomEvent">
...
</div>
```
Sometimes you may want to listen for camelCased events such as `customEvent` in our example. Because camelCasing inside HTML attributes is not supported, adding the `.camel` modifier is necessary for Alpine to camelCase the event name internally.
By adding `.camel` in the above example, Alpine is now listening for `customEvent` instead of `custom-event`.
#### [.dot](#dot)
```
<div @custom-event.dot="handleCustomEvent">
...
</div>
```
Similar to the `.camelCase` modifier there may be situations where you want to listen for events that have dots in their name (like `custom.event`). Since dots within the event name are reserved by Alpine you need to write them with dashes and add the `.dot` modifier.
In the code example above `custom-event.dot` will correspond to the event name `custom.event`.
#### [.passive](#passive)
Browsers optimize scrolling on pages to be fast and smooth even when JavaScript is being executed on the page. However, improperly implemented touch and wheel listeners can block this optimization and cause poor site performance.
If you are listening for touch events, it's important to add `.passive` to your listeners to not block scroll performance.
```
<div @touchstart.passive="...">...</div>
```
[→ Read more about passive listeners](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners)
#### .capture
Add this modifier if you want to execute this listener in the event's capturing phase, e.g. before the event bubbles from the target element up the DOM.
```
<div @click.capture="console.log('I will log first')">
<button @click="console.log('I will log second')"></button>
</div>
```
[→ Read more about the capturing and bubbling phase of events](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#usecapture)
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,23 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-ref](/directives/ref)
`x-ref` in combination with `$refs` is a useful utility for easily accessing DOM elements directly. It's most useful as a replacement for APIs like `getElementById` and `querySelector`.
```
<button @click="$refs.text.remove()">Remove Text</button>
<span x-ref="text">Hello 👋</span>
```
Hello 👋
> Despite not being included in the above snippet, `x-ref` cannot be used if no parent element has `x-data` defined. [→ Read more about `x-data`](/directives/data)
[← x-ignore](/directives/ignore)
[x-cloak →](/directives/cloak)
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,82 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-show](/directives/show)
`x-show` is one of the most useful and powerful directives in Alpine. It provides an expressive way to show and hide DOM elements.
Here's an example of a simple dropdown component using `x-show`.
```
<div x-data="{ open: false }">
n<button x-on:click="open = ! open">Toggle Dropdown</button>
n<div x-show="open">
nDropdown Contents...
n</div>
n</div>
```
When the "Toggle Dropdown" button is clicked, the dropdown will show and hide accordingly.
> If the "default" state of an `x-show` on page load is "false", you may want to use `x-cloak` on the page to avoid "page flicker" (The effect that happens when the browser renders your content before Alpine is finished initializing and hiding it.) You can learn more about `x-cloak` in its documentation.
### [With transitions](#with-transitions)
If you want to apply smooth transitions to the `x-show` behavior, you can use it in conjunction with `x-transition`. You can learn more about that directive [here](/directives/transition), but here's a quick example of the same component as above, just with transitions applied.
```
<div x-data="{ open: false }">
n<button x-on:click="open = ! open">Toggle Dropdown</button>
n<div x-show="open" x-transition>
nDropdown Contents...
n</div>
n</div>
```
### [Using the important modifier](#using-the-important-modifier)
Sometimes you need to apply a little more force to actually hide an element. In cases where a CSS selector applies the `display` property with the `!important` flag, it will take precedence over the inline style set by Alpine.
In these cases you may use the `.important` modifier to set the inline style to `display: none !important`.
```
<div x-data="{ open: false }">
n<button x-on:click="open = ! open">Toggle Dropdown</button>
n<div x-show.important="open">
nDropdown Contents...
n</div>
n</div>
```
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,29 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-text](/directives/text)
`x-text` sets the text content of an element to the result of a given expression.
Here's a basic example of using `x-text` to display a user's username.
```
<div x-data="{ username: 'calebporzio' }">
nUsername: <strong x-text="username"></strong>
n</div>
```
Username:
Now the `<strong>` tag's inner text content will be set to "calebporzio".
[← x-on](/directives/on)
[x-html →](/directives/html)
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,189 @@
# Alpine.js Documentation - Directives
Alpine directives are attributes that you can add to HTML elements to give them special behavior.
## [x-transition](/directives/transition)
Alpine provides a robust transitions utility out of the box. With a few `x-transition` directives, you can create smooth transitions between when an element is shown or hidden.
There are two primary ways to handle transitions in Alpine:
* [The Transition Helper](#the-transition-helper)
* [Applying CSS Classes](#applying-css-classes)
### [The transition helper](#the-transition-helper)
The simplest way to achieve a transition using Alpine is by adding `x-transition` to an element with `x-show` on it. For example:
```
<div x-data="{ open: false }">
n<button @click="open = ! open">Toggle</button>
n<div x-show="open" x-transition>
nHello 👋
n</div>
n</div>
```
Hello 👋
As you can see, by default, `x-transition` applies pleasant transition defaults to fade and scale the revealing element.
You can override these defaults with modifiers attached to `x-transition`. Let's take a look at those.
#### [Customizing duration](#customizing-duration)
Initially, the duration is set to be 150 milliseconds when entering, and 75 milliseconds when leaving.
You can configure the duration you want for a transition with the `.duration` modifier:
```
<div ... x-transition.duration.500ms>
```
The above `<div>` will transition for 500 milliseconds when entering, and 500 milliseconds when leaving.
If you wish to customize the durations specifically for entering and leaving, you can do that like so:
```
<div ...
nx-transition:enter.duration.500ms
nx-transition:leave.duration.400ms
n>
```
> Despite not being included in the above snippet, `x-transition` cannot be used if no parent element has `x-data` defined. [→ Read more about `x-data`](/directives/data)
#### [Customizing delay](#customizing-delay)
You can delay a transition using the `.delay` modifier like so:
```
<div ... x-transition.delay.50ms>
```
The above example will delay the transition and in and out of the element by 50 milliseconds.
#### [Customizing opacity](#customizing-opacity)
By default, Alpine's `x-transition` applies both a scale and opacity transition to achieve a "fade" effect.
If you wish to only apply the opacity transition (no scale), you can accomplish that like so:
```
<div ... x-transition.opacity>
```
#### [Customizing scale](#customizing-scale)
Similar to the `.opacity` modifier, you can configure `x-transition` to ONLY scale (and not transition opacity as well) like so:
```
<div ... x-transition.scale>
```
The `.scale` modifier also offers the ability to configure its scale values AND its origin values:
```
<div ... x-transition.scale.80>
```
The above snippet will scale the element up and down by 80%.
Again, you may customize these values separately for enter and leaving transitions like so:
```
<div ...
nx-transition:enter.scale.80
nx-transition:leave.scale.90
n>
```
To customize the origin of the scale transition, you can use the `.origin` modifier:
```
<div ... x-transition.scale.origin.top>
```
Now the scale will be applied using the top of the element as the origin, instead of the center by default.
Like you may have guessed, the possible values for this customization are: `top`, `bottom`, `left`, and `right`.
If you wish, you can also combine two origin values. For example, if you want the origin of the scale to be "top right", you can use: `.origin.top.right` as the modifier.
### [Applying CSS classes](#applying-css-classes)
For direct control over exactly what goes into your transitions, you can apply CSS classes at different stages of the transition.
> The following examples use [TailwindCSS](https://tailwindcss.com/docs/transition-property) utility classes.
```
<div x-data="{ open: false }">
n<button @click="open = ! open">Toggle</button>
n<div
nx-show="open"
nx-transition:enter="transition ease-out duration-300"
nx-transition:enter-start="opacity-0 scale-90"
nx-transition:enter-end="opacity-100 scale-100"
nx-transition:leave="transition ease-in duration-300"
nx-transition:leave-start="opacity-100 scale-100"
nx-transition:leave-end="opacity-0 scale-90"
n>Hello 👋</div>
n</div>
```
Hello 👋
| Directive | Description |
| --- | --- |
| `:enter` | Applied during the entire entering phase. |
| `:enter-start` | Added before element is inserted, removed one frame after element is inserted. |
| `:enter-end` | Added one frame after element is inserted (at the same time `enter-start` is removed), removed when transition/animation finishes. |
| `:leave` | Applied during the entire leaving phase. |
| `:leave-start` | Added immediately when a leaving transition is triggered, removed after one frame. |
| `:leave-end` | Added one frame after a leaving transition is triggered (at the same time `leave-start` is removed), removed when the transition/animation finishes. |
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,78 @@
# Alpine.js Documentation - Essentials
There are 2 ways to include Alpine into your project:
* Including it from a `<script>` tag
* Importing it as a module
Either is perfectly valid. It all depends on the project's needs and the developer's taste.
## [From a script tag](#from-a-script-tag)
This is by far the simplest way to get started with Alpine. Include the following `<script>` tag in the head of your HTML page.
```
<html>
n<head>
n...
n<script defer src="https://cdn.jsdelivr.net/npm/[email\(protected\)]/dist/cdn.min.js"></script>
n</head>
n...
n</html>
```
> Don't forget the "defer" attribute in the `<script>` tag.
Notice the `@3.x.x` in the provided CDN link. This will pull the latest version of Alpine version 3. For stability in production, it's recommended that you hardcode the latest version in the CDN link.
```
<script defer src="https://cdn.jsdelivr.net/npm/[email\(protected\)]/dist/cdn.min.js"></script>
```
That's it! Alpine is now available for use inside your page.
Note that you will still need to define a component with `x-data` in order for any Alpine.js attributes to work. See <https://github.com/alpinejs/alpine/discussions/3805> for more information.
## [As a module](#as-a-module)
If you prefer the more robust approach, you can install Alpine via NPM and import it into a bundle.
Run the following command to install it.
```
npm install alpinejs
```
Now import Alpine into your bundle and initialize it like so:
```
import Alpine from 'alpinejs'
nwindow.Alpine = Alpine
nAlpine.start()
```
> The `window.Alpine = Alpine` bit is optional, but is nice to have for freedom and flexibility. Like when tinkering with Alpine from the devtools for example.
> If you imported Alpine into a bundle, you have to make sure you are registering any extension code IN BETWEEN when you import the `Alpine` global object, and when you initialize Alpine by calling `Alpine.start()`.
> Ensure that `Alpine.start()` is only called once per page. Calling it more than once will result in multiple "instances" of Alpine running at the same time.
[→ Read more about extending Alpine](/advanced/extending)
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,78 @@
# Alpine.js Documentation - Essentials
There are 2 ways to include Alpine into your project:
* Including it from a `<script>` tag
* Importing it as a module
Either is perfectly valid. It all depends on the project's needs and the developer's taste.
## [From a script tag](#from-a-script-tag)
This is by far the simplest way to get started with Alpine. Include the following `<script>` tag in the head of your HTML page.
```
<html>
n<head>
...
<script defer src="https://cdn.jsdelivr.net/npm/[email\(protected\)]/dist/cdn.min.js"></script>
</head>
...
</html>
```
> Don't forget the "defer" attribute in the `<script>` tag.
Notice the `@3.x.x` in the provided CDN link. This will pull the latest version of Alpine version 3. For stability in production, it's recommended that you hardcode the latest version in the CDN link.
```
<script defer src="https://cdn.jsdelivr.net/npm/[email\(protected\)]/dist/cdn.min.js"></script>
```
That's it! Alpine is now available for use inside your page.
Note that you will still need to define a component with `x-data` in order for any Alpine.js attributes to work. See <https://github.com/alpinejs/alpine/discussions/3805> for more information.
## [As a module](#as-a-module)
If you prefer the more robust approach, you can install Alpine via NPM and import it into a bundle.
Run the following command to install it.
```
npm install alpinejs
```
Now import Alpine into your bundle and initialize it like so:
```
import Alpine from 'alpinejs'
nwindow.Alpine = Alpine
nAlpine.start()
```
> The `window.Alpine = Alpine` bit is optional, but is nice to have for freedom and flexibility. Like when tinkering with Alpine from the devtools for example.
> If you imported Alpine into a bundle, you have to make sure you are registering any extension code IN BETWEEN when you import the `Alpine` global object, and when you initialize Alpine by calling `Alpine.start()`.
> Ensure that `Alpine.start()` is only called once per page. Calling it more than once will result in multiple "instances" of Alpine running at the same time.
[→ Read more about extending Alpine](/advanced/extending)
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,78 @@
# Alpine.js Documentation - Essentials
There are 2 ways to include Alpine into your project:
* Including it from a `<script>` tag
* Importing it as a module
Either is perfectly valid. It all depends on the project's needs and the developer's taste.
## [From a script tag](#from-a-script-tag)
This is by far the simplest way to get started with Alpine. Include the following `<script>` tag in the head of your HTML page.
```
<html>
n<head>
n...
n<script defer src="https://cdn.jsdelivr.net/npm/[email\(protected\)]/dist/cdn.min.js"></script>
n</head>
n...
n</html>
```
> Don't forget the "defer" attribute in the `<script>` tag.
Notice the `@3.x.x` in the provided CDN link. This will pull the latest version of Alpine version 3. For stability in production, it's recommended that you hardcode the latest version in the CDN link.
```
<script defer src="https://cdn.jsdelivr.net/npm/[email\(protected\)]/dist/cdn.min.js"></script>
```
That's it! Alpine is now available for use inside your page.
Note that you will still need to define a component with `x-data` in order for any Alpine.js attributes to work. See <https://github.com/alpinejs/alpine/discussions/3805> for more information.
## [As a module](#as-a-module)
If you prefer the more robust approach, you can install Alpine via NPM and import it into a bundle.
Run the following command to install it.
```
npm install alpinejs
```
Now import Alpine into your bundle and initialize it like so:
```
import Alpine from 'alpinejs'
nwindow.Alpine = Alpine
nAlpine.start()
```
> The `window.Alpine = Alpine` bit is optional, but is nice to have for freedom and flexibility. Like when tinkering with Alpine from the devtools for example.
> If you imported Alpine into a bundle, you have to make sure you are registering any extension code IN BETWEEN when you import the `Alpine` global object, and when you initialize Alpine by calling `Alpine.start()`.
> Ensure that `Alpine.start()` is only called once per page. Calling it more than once will result in multiple "instances" of Alpine running at the same time.
[→ Read more about extending Alpine](/advanced/extending)
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,146 @@
# Alpine.js Documentation - Magics
Magics are special properties that are available inside Alpine expressions. They provide convenient access to common functionality.
## [$dispatch](/magics/dispatch)
`$dispatch` is a helpful shortcut for dispatching browser events.
```
<div @notify="alert('Hello World!')">
n<button @click="$dispatch('notify')">
nNotify
n</button>
n</div>
```
You can also pass data along with the dispatched event if you wish. This data will be accessible as the `.detail` property of the event:
```
<div @notify="alert($event.detail.message)">
n<button @click="$dispatch('notify', { message: 'Hello World!' })">
nNotify
n</button>
n</div>
```
Under the hood, `$dispatch` is a wrapper for the more verbose API: `element.dispatchEvent(new CustomEvent(...))`
**Note on event propagation**
Notice that, because of [event bubbling](https://en.wikipedia.org/wiki/Event_bubbling), when you need to capture events dispatched from nodes that are under the same nesting hierarchy, you'll need to use the [`.window`](https://github.com/alpinejs/alpine#x-on) modifier:
**Example:**
```
<!-- 🚫 Won't work -->
n<div x-data>
n<span @notify="..."></span>
n<button @click="$dispatch('notify')">Notify</button>
n</div>
n<!-- ✅ Will work (because of .window) -->
n<div x-data>
n<span @notify.window="..."></span>
n<button @click="$dispatch('notify')">Notify</button>
n</div>
```
> The first example won't work because when `notify` is dispatched, it'll propagate to its common ancestor, the `div`, not its sibling, the `<span>`. The second example will work because the sibling is listening for `notify` at the `window` level, which the custom event will eventually bubble up to.
### [Dispatching to other components](#dispatching-to-components)
You can also take advantage of the previous technique to make your components talk to each other:
**Example:**
```
<div
nx-data="{ title: 'Hello' }"
n@set-title.window="title = $event.detail"
n>
nh1 x-text="title"></h1>
n</div>
n<div x-data>
n<button @click="$dispatch('set-title', 'Hello World!')">Click me</button>
n</div>
n<!-- When clicked, the content of the h1 will set to "Hello World!. -->
```
### [Dispatching to x-model](#dispatching-to-x-model)
You can also use `$dispatch()` to trigger data updates for `x-model` data bindings. For example:
```
<div x-data="{ title: 'Hello' }">
n<span x-model="title">
n<button @click="$dispatch('input', 'Hello World!')">Click me</button>
n<!-- After the button is pressed, `x-model` will catch the bubbling "input" event, and update title. -->
n</span>
n</div>
```
This opens up the door for making custom input components whose value can be set via `x-model`.
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,17 @@
# Alpine.js Documentation - Magics
Magics are special properties that are available inside Alpine expressions. They provide convenient access to common functionality.
## [$el](/magics/el)
`$el` is a magic property that can be used to retrieve the current DOM node.
```
<button @click="$el.innerHTML = 'Hello World!'">Replace me with "Hello World!"</button>
```
[← x-id](/directives/id)
[$refs →](/magics/refs)
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,79 @@
# Alpine.js Documentation - Magics
Magics are special properties that are available inside Alpine expressions. They provide convenient access to common functionality.
## [$watch](/magics/watch)
You can "watch" a component property using the `$watch` magic method. For example:
```
<div x-data="{ open: false }" x-init="$watch('open', value => console.log(value))">
n<button @click="open = ! open">Toggle Open</button>
n</div>
```
In the above example, when the button is pressed and `open` is changed, the provided callback will fire and `console.log` the new value:
You can watch deeply nested properties using "dot" notation
```
<div x-data="{ foo: { bar: 'baz' }}" x-init="$watch('foo.bar', value => console.log(value))">
n<button @click="foo.bar = 'bob'">Toggle Open</button>
n</div>
```
When the `<button>` is pressed, `foo.bar` will be set to "bob", and "bob" will be logged to the console.
### [Getting the "old" value](#getting-the-old-value)
`$watch` keeps track of the previous value of the property being watched, You can access it using the optional second argument to the callback like so:
```
<div x-data="{ open: false }" x-init="$watch('open', (value, oldValue) => console.log(value, oldValue))">
n<button @click="open = ! open">Toggle Open</button>
n</div>
```
### [Deep watching](#deep-watching)
`$watch` automatically watches from changes at any level but you should keep in mind that, when a change is detected, the watcher will return the value of the observed property, not the value of the subproperty that has changed.
```
<div x-data="{ foo: { bar: 'baz' }}" x-init="$watch('foo', (value, oldValue) => console.log(value, oldValue))">
n<button @click="foo.bar = 'bob'">Update</button>
n</div>
```
When the `<button>` is pressed, `foo.bar` will be set to "bob", and "{bar: 'bob'} {bar: 'baz'}" will be logged to the console (new and old value).
> ⚠️ Changing a property of a "watched" object as a side effect of the `$watch` callback will generate an infinite loop and eventually error.
```
<!-- 🚫 Infinite loop -->
<div x-data="{ foo: { bar: 'baz', bob: 'lob' }}" x-init="$watch('foo', value => foo.bob = foo.bar)">
n<button @click="foo.bar = 'bob'">Update</button>
n</div>
```
Code highlighting provided by [Torchlight](https://torchlight.dev/)

View File

@@ -0,0 +1,68 @@
# daisyUI Accordion Component Documentation
## Overview
The Accordion component is used for showing and hiding content, but only one item can stay open at a time. It works with radio inputs where all radio inputs with the same name work together, allowing only one item to be open simultaneously.
## Key Features
- Only one accordion item can be open at a time
- Uses radio inputs for controlling which item is open
- Supports different icon styles: arrow, plus/minus
- Can be joined together using the `join` component
## Classes and Modifiers
| Class name | Type | Description |
|------------|------|-------------|
| collapse | Component | Base collapse class |
| collapse-title | Part | Title part of accordion item |
| collapse-content | Part | Content part of accordion item |
| collapse-arrow | Modifier | Adds arrow icon |
| collapse-plus | Modifier | Adds plus/minus icon |
| collapse-open | Modifier | Force open accordion item |
| collapse-close | Modifier | Force close accordion item |
## Usage Examples
### Basic Accordion
```html
<div class="collapse bg-base-100 border border-base-300">
<input type="radio" name="my-accordion-1" checked="checked" />
<div class="collapse-title font-semibold">How do I create an account?</div>
<div class="collapse-content text-sm">Click the "Sign Up" button in the top right corner and follow the registration process.</div>
</div>
```
### Accordion with Arrow Icon
```html
<div class="collapse collapse-arrow bg-base-100 border border-base-300">
<input type="radio" name="my-accordion-2" checked="checked" />
<div class="collapse-title font-semibold">How do I create an account?</div>
<div class="collapse-content text-sm">Click the "Sign Up" button in the top right corner and follow the registration process.</div>
</div>
```
### Accordion with Plus/Minus Icon
```html
<div class="collapse collapse-plus bg-base-100 border border-base-300">
<input type="radio" name="my-accordion-3" checked="checked" />
<div class="collapse-title font-semibold">How do I create an account?</div>
<div class="collapse-content text-sm">Click the "Sign Up" button in the top right corner and follow the registration process.</div>
</div>
```
### Joined Accordion Items
```html
<div class="join join-vertical bg-base-100">
<div class="collapse collapse-arrow join-item border-base-300 border">
<input type="radio" name="my-accordion-4" checked="checked" />
<div class="collapse-title font-semibold">How do I create an account?</div>
<div class="collapse-content text-sm">Click the "Sign Up" button in the top right corner and follow the registration process.</div>
</div>
</div>
```
## Important Notes
- All radio inputs with the same name work together to control the accordion behavior
- Only one item can be open at a time within the same group
- Use different names for radio inputs if you have multiple sets of accordions on the same page
- The accordion uses the same styling as the collapse component but with radio input controls

View File

@@ -0,0 +1,160 @@
# Alert Component
Alert informs users about important events.
| Class name | Type | |
| --- | --- | --- |
| alert | Component | Container element |
| alert-outline | Style | outline style |
| alert-dash | Style | dash outline style |
| alert-soft | Style | soft style |
| alert-info | Color | info color |
| alert-success | Color | success color |
| alert-warning | Color | warning color |
| alert-error | Color | error color |
| alert-vertical | direction | Vertical layout, good for mobile |
| alert-horizontal | direction | Horizontal layout, good for desktop |
#### Alert
```
<div role="alert" class="$$alert">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>12 unread messages. Tap to see.</span>
</div>
```
#### Info color
```
<div role="alert" class="$$alert $$alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>New software update available.</span>
</div>
```
#### Success color
```
<div role="alert" class="$$alert $$alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Your purchase has been confirmed!</span>
</div>
```
#### Warning color
```
<div role="alert" class="$$alert $$alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Warning: Invalid email address!</span>
</div>
```
#### Error color
```
<div role="alert" class="$$alert $$alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Error! Task failed successfully.</span>
</div>
```
#### Alert soft style
```
<div role="alert" class="$$alert $$alert-info $$alert-soft">
<span>12 unread messages. Tap to see.</span>
</div>
<div role="alert" class="$$alert $$alert-success $$alert-soft">
<span>Your purchase has been confirmed!</span>
</div>
<div role="alert" class="$$alert $$alert-warning $$alert-soft">
<span>Warning: Invalid email address!</span>
</div>
<div role="alert" class="$$alert $$alert-error $$alert-soft">
<span>Error! Task failed successfully.</span>
</div>
```
#### Alert outline style
```
<div role="alert" class="$$alert $$alert-info $$alert-outline">
<span>12 unread messages. Tap to see.</span>
</div>
<div role="alert" class="$$alert $$alert-success $$alert-outline">
<span>Your purchase has been confirmed!</span>
</div>
<div role="alert" class="$$alert $$alert-warning $$alert-outline">
<span>Warning: Invalid email address!</span>
</div>
<div role="alert" class="$$alert $$alert-error $$alert-outline">
<span>Error! Task failed successfully.</span>
</div>
```
#### Alert dash style
```
<div role="alert" class="$$alert $$alert-info $$alert-dash">
<span>12 unread messages. Tap to see.</span>
</div>
<div role="alert" class="$$alert $$alert-success $$alert-dash">
<span>Your purchase has been confirmed!</span>
</div>
<div role="alert" class="$$alert $$alert-warning $$alert-dash">
<span>Warning: Invalid email address!</span>
</div>
<div role="alert" class="$$alert $$alert-error $$alert-dash">
<span>Error! Task failed successfully.</span>
</div>
```
#### Alert with buttons + responsive
```
<div role="alert" class="$$alert $$alert-vertical sm:$$alert-horizontal">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>we use cookies for no reason.</span>
<div>
<button class="$$btn $$btn-sm">Deny</button>
<button class="$$btn $$btn-sm $$btn-primary">Accept</button>
</div>
</div>
```
#### Alert with title and description
```
<div role="alert" class="$$alert $$alert-vertical sm:$$alert-horizontal">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">New message!</h3>
<div class="text-xs">You have 1 unread message</div>
</div>
<button class="$$btn $$btn-sm">See</button>
</div>
```
![daisyUI store](https://img.daisyui.com/images/store/nexus.webp)
## NEXUS Official daisyUI Dashboard Template
## Available on daisyUI store
[More details](/store)

View File

@@ -0,0 +1,155 @@
# Avatar Component
Avatars are used to show a thumbnail representation of an individual or business in the interface.
| Class name | Type | |
| --- | --- | --- |
| avatar | Component | Avatar |
| avatar-group | Component | Container for multiple avatars |
| avatar-online | Modifier | shows a green dot as online indicator |
| avatar-offline | Modifier | shows a gray dot as offline indicator |
| avatar-placeholder | Modifier | To show letters as avatar placeholder |
#### Avatar
```
<div class="$$avatar">
<div class="w-24 rounded">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
```
#### Avatar in custom sizes
```
<div class="$$avatar">
<div class="w-32 rounded">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
<div class="$$avatar">
<div class="w-20 rounded">
<img
src="https://img.daisyui.com/images/profile/demo/[email protected]"
alt="Tailwind-CSS-Avatar-component"
/>
</div>
</div>
<div class="$$avatar">
<div class="w-16 rounded">
<img
src="https://img.daisyui.com/images/profile/demo/[email protected]"
alt="Tailwind-CSS-Avatar-component"
/>
</div>
</div>
<div class="$$avatar">
<div class="w-8 rounded">
<img
src="https://img.daisyui.com/images/profile/demo/[email protected]"
alt="Tailwind-CSS-Avatar-component"
/>
</div>
</div>
```
#### Avatar rounded
```
<div class="$$avatar">
<div class="w-24 rounded-xl">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
<div class="$$avatar">
<div class="w-24 rounded-full">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
```
#### Avatar with mask
```
<div class="$$avatar">
<div class="$$mask $$mask-heart w-24">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
<div class="$$avatar">
<div class="$$mask $$mask-squircle w-24">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
<div class="$$avatar">
<div class="$$mask $$mask-hexagon-2 w-24">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
```
#### Avatar group
```
<div class="$$avatar-group -space-x-6">
<div class="$$avatar">
<div class="w-12">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
<div class="$$avatar">
<div class="w-12">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
<div class="$$avatar">
<div class="w-12">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
<div class="$$avatar">
<div class="w-12">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
</div>
```
#### Avatar with online/offline indicator
```
<div class="$$avatar $$avatar-online">
<div class="w-24 rounded-full">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
<div class="$$avatar $$avatar-offline">
<div class="w-24 rounded-full">
<img src="https://img.daisyui.com/images/profile/demo/[email protected]" />
</div>
</div>
```
#### Avatar with placeholder
```
<div class="$$avatar $$avatar-placeholder">
<div class="w-24 rounded-full bg-neutral text-neutral-content">
<span class="text-xl">A</span>
</div>
</div>
<div class="$$avatar $$avatar-placeholder">
<div class="w-24 rounded-full bg-primary text-primary-content">
<span class="text-xl">B</span>
</div>
</div>
```
![daisyUI store](https://img.daisyui.com/images/store/nexus.webp)
## NEXUS Official daisyUI Dashboard Template
## Available on daisyUI store
[More details](/store)

View File

@@ -0,0 +1,174 @@
# Badge Component
Badges are used to inform the user of the status of specific data.
| Class name | Type | |
| --- | --- | --- |
| badge | Component | Container element |
| badge-outline | Style | outline style |
| badge-dash | Style | dash outline style |
| badge-soft | Style | soft style |
| badge-ghost | Style | ghost style |
| badge-neutral | Color | neutral color |
| badge-primary | Color | primary color |
| badge-secondary | Color | secondary color |
| badge-accent | Color | accent color |
| badge-info | Color | info color |
| badge-success | Color | success color |
| badge-warning | Color | warning color |
| badge-error | Color | error color |
| badge-xs | Size | extra small size |
| badge-sm | Size | small size |
| badge-md | Size | medium size (default) |
| badge-lg | Size | large size |
| badge-xl | Size | extra large size |
#### Badge
Badge
```
<span class="$$badge">Badge</span>
```
```
<span class="$$badge">Badge</span>
```
#### Badge sizes
```
<div class="$$badge $$badge-xs">Xsmall</div>
<div class="$$badge $$badge-sm">Small</div>
<div class="$$badge $$badge-md">Medium</div>
<div class="$$badge $$badge-lg">Large</div>
<div class="$$badge $$badge-xl">Xlarge</div>
```
```
<div class="$$badge $$badge-xs">Xsmall</div>
<div class="$$badge $$badge-sm">Small</div>
<div class="$$badge $$badge-md">Medium</div>
<div class="$$badge $$badge-lg">Large</div>
<div class="$$badge $$badge-xl">Xlarge</div>
```
#### Badge with colors
```
<div class="$$badge $$badge-primary">Primary</div>
<div class="$$badge $$badge-secondary">Secondary</div>
<div class="$$badge $$badge-accent">Accent</div>
<div class="$$badge $$badge-neutral">Neutral</div>
<div class="$$badge $$badge-info">Info</div>
<div class="$$badge $$badge-success">Success</div>
<div class="$$badge $$badge-warning">Warning</div>
<div class="$$badge $$badge-error">Error</div>
```
```
<div class="$$badge $$badge-primary">Primary</div>
<div class="$$badge $$badge-secondary">Secondary</div>
<div class="$$badge $$badge-accent">Accent</div>
<div class="$$badge $$badge-neutral">Neutral</div>
<div class="$$badge $$badge-info">Info</div>
<div class="$$badge $$badge-success">Success</div>
<div class="$$badge $$badge-warning">Warning</div>
<div class="$$badge $$badge-error">Error</div>
```
#### Badge with soft style
```
<div class="$$badge $$badge-soft $$badge-primary">Primary</div>
<div class="$$badge $$badge-soft $$badge-secondary">Secondary</div>
<div class="$$badge $$badge-soft $$badge-accent">Accent</div>
<div class="$$badge $$badge-soft $$badge-info">Info</div>
<div class="$$badge $$badge-soft $$badge-success">Success</div>
<div class="$$badge $$badge-soft $$badge-warning">Warning</div>
<div class="$$badge $$badge-soft $$badge-error">Error</div>
```
```
<div class="$$badge $$badge-soft $$badge-primary">Primary</div>
<div class="$$badge $$badge-soft $$badge-secondary">Secondary</div>
<div class="$$badge $$badge-soft $$badge-accent">Accent</div>
<div class="$$badge $$badge-soft $$badge-info">Info</div>
<div class="$$badge $$badge-soft $$badge-success">Success</div>
<div class="$$badge $$badge-soft $$badge-warning">Warning</div>
<div class="$$badge $$badge-soft $$badge-error">Error</div>
```
#### Badge with outline style
```
<div class="$$badge $$badge-outline $$badge-primary">Primary</div>
<div class="$$badge $$badge-outline $$badge-secondary">Secondary</div>
<div class="$$badge $$badge-outline $$badge-accent">Accent</div>
<div class="$$badge $$badge-outline $$badge-info">Info</div>
<div class="$$badge $$badge-outline $$badge-success">Success</div>
<div class="$$badge $$badge-outline $$badge-warning">Warning</div>
<div class="$$badge $$badge-outline $$badge-error">Error</div>
```
```
<div class="$$badge $$badge-outline $$badge-primary">Primary</div>
<div class="$$badge $$badge-outline $$badge-secondary">Secondary</div>
<div class="$$badge $$badge-outline $$badge-accent">Accent</div>
<div class="$$badge $$badge-outline $$badge-info">Info</div>
<div class="$$badge $$badge-outline $$badge-success">Success</div>
<div class="$$badge $$badge-outline $$badge-warning">Warning</div>
<div class="$$badge $$badge-outline $$badge-error">Error</div>
```
#### Badge with dash style
```
<div class="$$badge $$badge-dash $$badge-primary">Primary</div>
<div class="$$badge $$badge-dash $$badge-secondary">Secondary</div>
<div class="$$badge $$badge-dash $$badge-accent">Accent</div>
<div class="$$badge $$badge-dash $$badge-info">Info</div>
<div class="$$badge $$badge-dash $$badge-success">Success</div>
<div class="$$badge $$badge-dash $$badge-warning">Warning</div>
<div class="$$badge $$badge-dash $$badge-error">Error</div>
```
```
<div class="$$badge $$badge-dash $$badge-primary">Primary</div>
<div class="$$badge $$badge-dash $$badge-secondary">Secondary</div>
<div class="$$badge $$badge-dash $$badge-accent">Accent</div>
<div class="$$badge $$badge-dash $$badge-info">Info</div>
<div class="$$badge $$badge-dash $$badge-success">Success</div>
<div class="$$badge $$badge-dash $$badge-warning">Warning</div>
<div class="$$badge $$badge-dash $$badge-error">Error</div>
```
#### Badge with ghost style
```
<div class="$$badge $$badge-ghost $$badge-primary">Primary</div>
<div class="$$badge $$badge-ghost $$badge-secondary">Secondary</div>
<div class="$$badge $$badge-ghost $$badge-accent">Accent</div>
<div class="$$badge $$badge-ghost $$badge-info">Info</div>
<div class="$$badge $$badge-ghost $$badge-success">Success</div>
<div class="$$badge $$badge-ghost $$badge-warning">Warning</div>
<div class="$$badge $$badge-ghost $$badge-error">Error</div>
```
```
<div class="$$badge $$badge-ghost $$badge-primary">Primary</div>
<div class="$$badge $$badge-ghost $$badge-secondary">Secondary</div>
<div class="$$badge $$badge-ghost $$badge-accent">Accent</div>
<div class="$$badge $$badge-ghost $$badge-info">Info</div>
<div class="$$badge $$badge-ghost $$badge-success">Success</div>
<div class="$$badge $$badge-ghost $$badge-warning">Warning</div>
<div class="$$badge $$badge-ghost $$badge-error">Error</div>
```
![daisyUI store](https://img.daisyui.com/images/store/nexus.webp)
## NEXUS Official daisyUI Dashboard Template
## Available on daisyUI store
[More details](/store)

View File

@@ -0,0 +1,100 @@
# Breadcrumbs Component
Breadcrumbs helps users to navigate through the website.
| Class name | Type | |
| --- | --- | --- |
| breadcrumbs | Component | Wrapper around a <ul> |
#### Breadcrumbs
```
<div class="$$breadcrumbs text-sm">
<ul>
<li><a>Home</a></li>
<li><a>Documents</a></li>
<li>Add Document</li>
</ul>
</div>
```
#### Breadcrumbs with icons
```
<div class="$$breadcrumbs text-sm">
<ul>
<li>
<a>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-4 w-4 stroke-current">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
Home
</a>
</li>
<li>
<a>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-4 w-4 stroke-current">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
Documents
</a>
</li>
<li>
<span class="inline-flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-4 w-4 stroke-current">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
Add Document
</span>
</li>
</ul>
</div>
```
#### Breadcrumbs with max-width
If you set max-width or the list gets larger than the container it will scroll
```
<div class="$$breadcrumbs max-w-xs text-sm">
<ul>
<li>Long text 1</li>
<li>Long text 2</li>
<li>Long text 3</li>
<li>Long text 4</li>
<li>Long text 5</li>
</ul>
</div>
```
![daisyUI store](https://img.daisyui.com/images/store/nexus.webp)
## NEXUS Official daisyUI Dashboard Template
## Available on daisyUI store
[More details](/store)

View File

@@ -0,0 +1,42 @@
# daisyUI Documentation - Diff Component
Diff component shows a side-by-side comparison of two items.
## Examples
### Basic Diff
```html
<figure class="diff aspect-16/9" tabindex="0">
<div class="diff-item-1" role="img" tabindex="0">
<img alt="daisy" src="https://img.daisyui.com/images/stock/photo-1560717789-0ac7c58ac90a.webp" />
</div>
<div class="diff-item-2" role="img">
<img alt="daisy" src="https://img.daisyui.com/images/stock/photo-1560717789-0ac7c58ac90a-blur.webp" />
</div>
<div class="diff-resizer"></div>
</figure>
```
### Diff with Text
```html
<figure class="diff aspect-16/9" tabindex="0">
<div class="diff-item-1" role="img" tabindex="0">
<div class="bg-primary text-primary-content grid place-content-center text-9xl font-black">DAISY</div>
</div>
<div class="diff-item-2" role="img">
<div class="bg-base-200 grid place-content-center text-9xl font-black">DAISY</div>
</div>
<div class="diff-resizer"></div>
</figure>
```
## Features
- Shows side-by-side comparison of two items
- Can be used with images or text content
- Includes a resizer for adjusting the split point
- Responsive design
This component is useful for showing before/after comparisons, side-by-side content analysis, or visual differences between elements.

View File

@@ -0,0 +1,92 @@
# daisyUI Documentation - Divider Component
Divider will be used to separate content vertically or horizontally.
## Examples
### Basic Divider
```html
<div class="flex w-full flex-col">
<div class="card bg-base-300 rounded-box grid h-20 place-items-center">content</div>
<div class="divider">OR</div>
<div class="card bg-base-300 rounded-box grid h-20 place-items-center">content</div>
</div>
```
### Horizontal Divider
```html
<div class="flex w-full">
<div class="card bg-base-300 rounded-box grid h-20 grow place-items-center">content</div>
<div class="divider divider-horizontal">OR</div>
<div class="card bg-base-300 rounded-box grid h-20 grow place-items-center">content</div>
</div>
```
### Divider with No Text
```html
<div class="flex w-full flex-col">
<div class="card bg-base-300 rounded-box grid h-20 place-items-center">content</div>
<div class="divider"></div>
<div class="card bg-base-300 rounded-box grid h-20 place-items-center">content</div>
</div>
```
### Responsive Divider
```html
<div class="flex w-full flex-col lg:flex-row">
<div class="card bg-base-300 rounded-box grid h-32 grow place-items-center">content</div>
<div class="divider lg:divider-horizontal">OR</div>
<div class="card bg-base-300 rounded-box grid h-32 grow place-items-center">content</div>
</div>
```
### Divider with Colors
```html
<div class="flex w-full flex-col">
<div class="divider">Default</div>
<div class="divider divider-neutral">Neutral</div>
<div class="divider divider-primary">Primary</div>
<div class="divider divider-secondary">Secondary</div>
<div class="divider divider-accent">Accent</div>
<div class="divider divider-success">Success</div>
<div class="divider divider-warning">Warning</div>
<div class="divider divider-info">Info</div>
<div class="divider divider-error">Error</div>
</div>
```
### Divider in Different Positions
```html
<div class="flex w-full flex-col">
<div class="divider divider-start">Start</div>
<div class="divider">Default</div>
<div class="divider divider-end">End</div>
</div>
```
### Horizontal Divider in Different Positions
```html
<div class="flex w-full">
<div class="divider divider-horizontal divider-start">Start</div>
<div class="divider divider-horizontal">Default</div>
<div class="divider divider-horizontal divider-end">End</div>
</div>
```
## Features
- Separates content vertically or horizontally
- Can be used with text or without text
- Responsive design that adapts to different screen sizes
- Multiple color options for visual distinction
- Positioning options (start, default, end)
- Horizontal and vertical variants
This component is useful for creating clear visual separation between sections of content, especially in forms, layouts, or content organization.

View File

@@ -0,0 +1,78 @@
# daisyUI Documentation - Dock Component
dock (also know as Bottom navigation or Bottom bar) is a UI element that provides navigation options to the user. Dock sticks to the bottom of the screen.
## Classes and Usage
### Core Classes:
- **dock** - The main dock component
- **dock-label** - Text label for Dock Item
- **dock-active** - Makes the Dock Item look active
- **dock-xs** - Extra Small Dock
- **dock-sm** - Small Dock
- **dock-md** - Medium Dock [Default]
- **dock-lg** - Large Dock
- **dock-xl** - Extra Large Dock
## Examples
### Basic Dock
```html
<div class="dock">
<button>
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><polyline points="1 11 12 2 23 11" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></polyline><path d="m5,13v7c0,1.105.895,2,2,2h10c1.105,0,2-.895,2-2v-7" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path><line x1="12" y1="22" x2="12" y2="18" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></line></g></svg>
<span class="dock-label">Home</span>
</button>
<button class="dock-active">
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><polyline points="3 14 9 14 9 17 15 17 15 14 21 14" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></polyline><rect x="3" y="3" width="18" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></rect></g></svg>
<span class="dock-label">Inbox</span>
</button>
<button>
<svg class="size-[1.2em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor" stroke-linejoin="miter" stroke-linecap="butt"><circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></circle><path d="m22,13.25v-2.5l-2.318-.966c-.167-.581-.395-1.135-.682-1.654l.954-2.318-1.768-1.768-2.318.954c-.518-.287-1.073-.515-1.654-.682l-.966-2.318h-2.5l-.966,2.318c-.581.167-1.135.395-1.654.682l-2.318-.954-1.768,1.768.954,2.318c-.287.518-.515,1.073-.682,1.654l-2.318.966v2.5l2.318.966c.167.581.395,1.135.682,1.654l-.954,2.318,1.768,1.768,2.318-.954c.518.287,1.073.515,1.654.682l.966,2.318h2.5l.966-2.318c.581-.167,1.135-.395,1.654-.682l2.318.954,1.768-1.768-.954-2.318c.287-.518.515-1.073.682-1.654l2.318-.966Z" fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"></path></g></svg>
<span class="dock-label">Settings</span>
</button>
</div>
```
### Dock with Different Sizes
```html
<!-- Extra Small -->
<div class="dock dock-xs">
<!-- dock items -->
</div>
<!-- Small -->
<div class="dock dock-sm">
<!-- dock items -->
</div>
<!-- Medium (default) -->
<div class="dock dock-md">
<!-- dock items -->
</div>
<!-- Large -->
<div class="dock dock-lg">
<!-- dock items -->
</div>
<!-- Extra Large -->
<div class="dock dock-xl">
<!-- dock items -->
</div>
```
## Features
- Bottom navigation or bottom bar UI element
- Sticks to the bottom of the screen
- Responsive design with different size options
- Active state indicator for current selection
- SVG icons support
- Requires viewport meta tag for iOS responsiveness
This component is particularly useful for mobile applications where you need persistent navigation at the bottom of the screen.

View File

@@ -0,0 +1,117 @@
# daisyUI Documentation - Drawer Component
Drawer is a grid layout that can show/hide a sidebar on the left or right side of the page.
## Structure
```
drawer // The root container
├── .drawer-toggle // A hidden checkbox to toggle the visibility of the sidebar
├── .drawer-content // All your page content goes here
│ ╰── // navbar, content, footer
╰── .drawer-side // Sidebar wrapper
├── .drawer-overlay // A dark overlay that covers the whole page when the drawer is open
╰── // Sidebar content (menu or anything)
```
## Classes and Usage
### Core Classes:
- **drawer** - The root container
- **drawer-toggle** - A hidden checkbox to toggle the visibility of the sidebar
- **drawer-content** - All your page content goes here
- **drawer-side** - Sidebar wrapper
- **drawer-overlay** - A dark overlay that covers the whole page when the drawer is open
- **drawer-open** - Opens the drawer on larger screens (use with responsive prefixes: sm, md, lg, xl)
## Examples
### Basic Drawer
```html
<div class="drawer">
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<!-- Page content here -->
<label for="my-drawer" class="btn btn-primary drawer-button">Open drawer</label>
</div>
<div class="drawer-side">
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 text-base-content min-h-full w-80 p-4">
<!-- Sidebar content here -->
<li><a>Sidebar Item 1</a></li>
<li><a>Sidebar Item 2</a></li>
</ul>
</div>
</div>
```
### Navbar Menu for Desktop + Sidebar Drawer for Mobile
```html
<div class="drawer">
<input id="my-drawer-3" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col">
<!-- Navbar -->
<div class="navbar bg-base-300 w-full">
<div class="flex-none lg:hidden">
<label for="my-drawer-3" aria-label="open sidebar" class="btn btn-square btn-ghost">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block h-6 w-6 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</label>
</div>
<div class="mx-2 flex-1 px-2">Navbar Title</div>
<div class="hidden flex-none lg:block">
<ul class="menu menu-horizontal">
<!-- Navbar menu content here -->
<li><a>Navbar Item 1</a></li>
<li><a>Navbar Item 2</a></li>
</ul>
</div>
</div>
<!-- Page content here -->
Content
</div>
<div class="drawer-side">
<label for="my-drawer-3" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 min-h-full w-80 p-4">
<!-- Sidebar content here -->
<li><a>Sidebar Item 1</a></li>
<li><a>Sidebar Item 2</a></li>
</ul>
</div>
</div>
```
### Drawer with Right Side
```html
<div class="drawer drawer-end">
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<!-- Page content here -->
<label for="my-drawer-2" class="btn btn-primary drawer-button">Open right drawer</label>
</div>
<div class="drawer-side">
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
<ul class="menu bg-base-200 text-base-content min-h-full w-80 p-4">
<!-- Sidebar content here -->
<li><a>Sidebar Item 1</a></li>
<li><a>Sidebar Item 2</a></li>
</ul>
</div>
</div>
```
## Features
- Can show/hide a sidebar on the left or right side of the page
- Responsive design that works on different screen sizes
- Uses a hidden checkbox to control visibility
- Dark overlay when drawer is open
- Can be opened on larger screens using responsive classes
- Works with existing navigation components
This component is useful for creating mobile-friendly navigation with collapsible sidebars, dashboards, or any layout that benefits from a sliding panel interface.

View File

@@ -0,0 +1,97 @@
# daisyUI Documentation - Dropdown Component
Dropdown can open a menu or any other element when the button is clicked.
## Classes and Usage
### Core Classes:
- **dropdown** - The main dropdown container
- **dropdown-toggle** - A hidden checkbox that controls the dropdown
- **dropdown-content** - The content of the dropdown (menu, etc.)
- **dropdown-open** - Opens the dropdown by default
- **dropdown-end** - Aligns the dropdown to the end
- **dropdown-start** - Aligns the dropdown to the start
- **dropdown-top** - Positions the dropdown above the trigger
- **dropdown-bottom** - Positions the dropdown below the trigger (default)
- **dropdown-left** - Positions the dropdown to the left of the trigger
- **dropdown-right** - Positions the dropdown to the right of the trigger
## Examples
### Basic Dropdown
```html
<div class="dropdown">
<label tabindex="0" class="btn">Dropdown</label>
<ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-4 w-52 p-2 shadow">
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
</ul>
</div>
```
### Dropdown with Button Toggle
```html
<div class="dropdown">
<button tabindex="0" class="btn">Dropdown</button>
<ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-4 w-52 p-2 shadow">
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
</ul>
</div>
```
### Dropdown with Arrow
```html
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn">Dropdown</label>
<ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-4 w-52 p-2 shadow">
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
</ul>
</div>
```
### Dropdown with Positioning
```html
<!-- Top aligned -->
<div class="dropdown dropdown-top">
<label tabindex="0" class="btn">Top Dropdown</label>
<ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-4 w-52 p-2 shadow">
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
</ul>
</div>
<!-- Left aligned -->
<div class="dropdown dropdown-left">
<label tabindex="0" class="btn">Left Dropdown</label>
<ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-4 w-52 p-2 shadow">
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
</ul>
</div>
<!-- Right aligned -->
<div class="dropdown dropdown-right">
<label tabindex="0" class="btn">Right Dropdown</label>
<ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-4 w-52 p-2 shadow">
<li><a>Item 1</a></li>
<li><a>Item 2</a></li>
</ul>
</div>
```
## Features
- Opens a menu or any other element when the button is clicked
- Multiple positioning options (top, bottom, left, right)
- Alignment options (start, end)
- Can be opened by default with `dropdown-open` class
- Works with buttons or labels as triggers
- Responsive design
This component is useful for creating context menus, navigation dropdowns, user profile menus, and any interface element that requires a popup menu.

View File

@@ -0,0 +1,88 @@
# daisyUI Documentation - Fieldset Component
Fieldset is a container for grouping related form elements. It includes fieldset-legend as a title and label as a description.
## Classes and Usage
### Core Classes:
- **fieldset** - The main fieldset container
- **fieldset-legend** - The title of the fieldset
- **label** - Label for inputs (used for descriptions)
## Examples
### Basic Fieldset with Legend and Label
```html
<fieldset class="fieldset">
<legend class="fieldset-legend">Page title</legend>
<input type="text" class="input" placeholder="My awesome page" />
<p class="label">You can edit page title later on from settings</p>
</fieldset>
```
### Fieldset with Background and Border
```html
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
<legend class="fieldset-legend">Page title</legend>
<input type="text" class="input" placeholder="My awesome page" />
<p class="label">You can edit page title later on from settings</p>
</fieldset>
```
### Fieldset with Multiple Inputs
```html
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
<legend class="fieldset-legend">Page details</legend>
<label class="label">Title</label>
<input type="text" class="input" placeholder="My awesome page" />
<label class="label">Slug</label>
<input type="text" class="input" placeholder="my-awesome-page" />
<label class="label">Author</label>
<input type="text" class="input" placeholder="Name" />
</fieldset>
```
### Fieldset with Join Items
```html
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
<legend class="fieldset-legend">Settings</legend>
<div class="join">
<input type="text" class="input join-item" placeholder="Product name" />
<button class="btn join-item">save</button>
</div>
</fieldset>
```
### Login Form with Fieldset
```html
<fieldset class="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4">
<legend class="fieldset-legend">Login</legend>
<label class="label">Email</label>
<input type="email" class="input" placeholder="Email" />
<label class="label">Password</label>
<input type="password" class="input" placeholder="Password" />
<button class="btn btn-neutral mt-4">Login</button>
</fieldset>
```
## Features
- Groups related form elements together
- Provides semantic structure for forms
- Uses fieldset-legend as the title
- Can include descriptive labels
- Works well with other daisyUI components like inputs and buttons
- Customizable with Tailwind classes
This component is useful for organizing form elements into logical groups, improving accessibility and user experience in complex forms.

View File

@@ -0,0 +1,90 @@
# daisyUI Documentation - File Input Component
File Input is a an input field for uploading files.
## Classes and Usage
### Core Classes:
- **file-input** - For <input type="file"> element
- **file-input-ghost** - ghost style
- **file-input-neutral** - neutral color
- **file-input-primary** - primary color
- **file-input-secondary** - secondary color
- **file-input-accent** - accent color
- **file-input-info** - info color
- **file-input-success** - success color
- **file-input-warning** - warning color
- **file-input-error** - error color
- **file-input-xs** - Extra small size
- **file-input-sm** - Small size
- **file-input-md** - Medium size [Default]
- **file-input-lg** - Large size
- **file-input-xl** - Extra large size
## Examples
### Basic File Input
```html
<input type="file" class="file-input" />
```
### File Input Ghost Style
```html
<input type="file" class="file-input file-input-ghost" />
```
### File Input with Fieldset and Label
```html
<fieldset class="fieldset">
<legend class="fieldset-legend">Pick a file</legend>
<input type="file" class="file-input" />
<label class="label">Max size 2MB</label>
</fieldset>
```
### File Input Sizes
```html
<input type="file" class="file-input file-input-xs" />
<input type="file" class="file-input file-input-sm" />
<input type="file" class="file-input file-input-md" />
<input type="file" class="file-input file-input-lg" />
<input type="file" class="file-input file-input-xl" />
```
### File Input Colors
```html
<input type="file" class="file-input file-input-primary" />
<input type="file" class="file-input file-input-secondary" />
<input type="file" class="file-input file-input-accent" />
<input type="file" class="file-input file-input-neutral" />
<input type="file" class="file-input file-input-info" />
<input type="file" class="file-input file-input-success" />
<input type="file" class="file-input file-input-warning" />
<input type="file" class="file-input file-input-error" />
```
### Disabled File Input
```html
<input type="file" placeholder="You can't touch this" class="file-input" disabled />
```
## Features
- Special styling for file input elements
- Multiple size options (xs, sm, md, lg, xl)
- Multiple color variants
- Ghost style option
- Works with fieldset and label components
- Disabled state support
This component is useful for creating styled file upload inputs that blend seamlessly with the rest of your daisyUI interface.

View File

@@ -0,0 +1,47 @@
# daisyUI Documentation - Filter Component
Filter is a group of radio buttons. Choosing one of the options will hide the others and shows a reset button next to the chosen option.
## Classes and Usage
### Core Classes:
- **filter** - For a HTML <form> or a <div> element that includes radio buttons for filtering items
- **filter-reset** - An alternative to the reset button if you can't use a HTML form
## Examples
### Filter using HTML form, radio buttons and reset button
A HTML from for filtering items
```html
<form class="filter">
<input class="btn btn-square" type="reset" value="×"/>
<input class="btn" type="radio" name="frameworks" aria-label="Svelte"/>
<input class="btn" type="radio" name="frameworks" aria-label="Vue"/>
<input class="btn" type="radio" name="frameworks" aria-label="React"/>
</form>
```
### Filter without HTML form
Use this if you can't use a HTML form for some reason
```html
<div class="filter">
<input class="btn filter-reset" type="radio" name="metaframeworks" aria-label="All"/>
<input class="btn" type="radio" name="metaframeworks" aria-label="Sveltekit"/>
<input class="btn" type="radio" name="metaframeworks" aria-label="Nuxt"/>
<input class="btn" type="radio" name="metaframeworks" aria-label="Next.js"/>
</div>
```
## Features
- Group of radio buttons for filtering
- Automatically hides other options when one is selected
- Shows a reset button next to the chosen option
- Works with HTML forms or div containers
- Uses btn components for styling
This component is useful for creating filter interfaces where only one option can be active at a time, with an easy way to reset the selection.

View File

@@ -0,0 +1,107 @@
# daisyUI Documentation - Footer Component
Footer can contain logo, copyright notice, and links to other pages.
## Classes and Usage
### Core Classes:
- **footer** - The main footer component
- **footer-title** - Title of a footer column
- **footer-center** - Aligns footer content to center
- **footer-horizontal** - Puts footer columns next to each other horizontally
- **footer-vertical** - Puts footer columns under each other vertically [Default]
## Examples
### Basic Footer (vertical by default, horizontal for sm and up)
```html
<footer class="footer sm:footer-horizontal bg-neutral text-neutral-content p-10">
<nav>
<h6 class="footer-title">Services</h6>
<a class="link link-hover">Branding</a>
<a class="link link-hover">Design</a>
<a class="link link-hover">Marketing</a>
<a class="link link-hover">Advertisement</a>
</nav>
<nav>
<h6 class="footer-title">Company</h6>
<a class="link link-hover">About us</a>
<a class="link link-hover">Contact</a>
<a class="link link-hover">Jobs</a>
<a class="link link-hover">Press kit</a>
</nav>
<nav>
<h6 class="footer-title">Legal</h6>
<a class="link link-hover">Terms of use</a>
<a class="link link-hover">Privacy policy</a>
<a class="link link-hover">Cookie policy</a>
</nav>
</footer>
```
### Footer with a logo section
```html
<footer class="footer sm:footer-horizontal bg-base-200 text-base-content p-10">
<aside>
<svg
width="50"
height="50"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
clip-rule="evenodd"
class="fill-current">
<path
d="M22.672 15.226l-2.432.811.841 2.515c.33 1.019-.209 2.127-1.23 2.456-1.15.325-2.148-.321-2.463-1.226l-.84-2.518-5.013 1.677.84 2.517c.391 1.203-.434 2.542-1.831 2.542-.88 0-1.601-.564-1.86-1.314l-.842-2.516-2.431.809c-1.135.328-2.145-.317-2.463-1.229-.329-1.018.211-2.127 1.231-2.456l2.432-.809-1.621-4.823-2.432.808c-1.355.384-2.558-.59-2.558-1.839 0-.817.509-1.582 1.327-1.846l2.433-.809-.842-2.515c-.33-1.02.211-2.129 1.232-2.458 1.02-.329 2.13.209 2.461 1.229l.842 2.515 5.011-1.677-.839-2.517c-.403-1.238.484-2.553 1.843-2.553.819 0 1.585.509 1.85 1.326l.841 2.517 2.431-.81c1.02-.33 2.131.211 2.461 1.229.332 1.018-.21 2.126-1.23 2.456l-2.433.809 1.622 4.823 2.433-.809c1.242-.401 2.557.484 2.557 1.838 0 .819-.51 1.583-1.328 1.847m-8.992-6.428l-5.01 1.675 1.619 4.828 5.011-1.674-1.62-4.829z"></path>
</svg>
<p>
ACME Industries Ltd.
<br />
Providing reliable tech since 1992
</p>
</aside>
<nav>
<h6 class="footer-title">Services</h6>
<a class="link link-hover">Branding</a>
<a class="link link-hover">Design</a>
<a class="link link-hover">Marketing</a>
<a class="link link-hover">Advertisement</a>
</nav>
<nav>
<h6 class="footer-title">Company</h6>
<a class="link link-hover">About us</a>
<a class="link link-hover">Contact</a>
<a class="link link-hover">Jobs</a>
<a class="link link-hover">Press kit</a>
</nav>
<nav>
<h6 class="footer-title">Legal</h6>
<a class="link link-hover">Terms of use</a>
<a class="link link-hover">Privacy policy</a>
<a class="link link-hover">Cookie policy</a>
</nav>
</footer>
```
### Centered Footer
```html
<footer class="footer footer-center bg-base-200 text-base-content p-10">
<aside>
<p>Copyright © 2023 - All right reserved by ACME Industries Ltd.</p>
</aside>
</footer>
```
## Features
- Contains logo, copyright notice, and links to other pages
- Responsive design that adapts to screen sizes
- Multiple layout options (vertical, horizontal)
- Can be centered
- Works with navigation elements
- Customizable with Tailwind classes
This component is useful for creating standard footer sections in websites that contain navigation links, copyright information, and company details.

View File

@@ -0,0 +1,89 @@
# daisyUI Documentation - Hero Component
Hero is a component for displaying a large box or image with a title and description.
## Classes and Usage
### Core Classes:
- **hero** - The main hero component
- **hero-content** - Content container inside hero
- **hero-overlay** - Overlay for hero background
- **hero-center** - Centers content in hero
- **hero-start** - Aligns content to start
- **hero-end** - Aligns content to end
- **hero-text** - Text content styling
- **hero-image** - Image container styling
## Examples
### Basic Hero
```html
<div class="hero bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Welcome to daisyUI!</h1>
<p class="py-6">Providing useful component class names to help you write less code and build faster.</p>
<button class="btn btn-primary">Get Started</button>
</div>
</div>
</div>
```
### Hero with Image
```html
<div class="hero bg-base-200">
<div class="hero-content flex-col lg:flex-row">
<div class="lg:w-1/2">
<img src="https://img.daisyui.com/images/stock/photo-1635805737707-57588507996c.webp" class="rounded-lg shadow-2xl" />
</div>
<div class="lg:w-1/2">
<h1 class="text-5xl font-bold">Your Title Here!</h1>
<p class="py-6">With the Hero component, you can easily create a large banner area with text and images.</p>
<button class="btn btn-primary">Get Started</button>
</div>
</div>
</div>
```
### Hero with Overlay
```html
<div class="hero bg-base-200">
<div class="hero-overlay bg-opacity-60"></div>
<div class="hero-content text-center text-neutral-content">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Welcome!</h1>
<p class="py-6">This hero has an overlay to make the text more readable.</p>
<button class="btn btn-primary">Get Started</button>
</div>
</div>
</div>
```
### Hero with Background Image
```html
<div class="hero bg-[url('https://img.daisyui.com/images/stock/photo-1635805737707-57588507996c.webp')] bg-cover">
<div class="hero-overlay bg-base-200 bg-opacity-60"></div>
<div class="hero-content text-center text-neutral-content">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Background Image Hero</h1>
<p class="py-6">This hero uses a background image with overlay.</p>
<button class="btn btn-primary">Get Started</button>
</div>
</div>
</div>
```
## Features
- Large banner area for showcasing content
- Responsive design that works on all screen sizes
- Can include images and text
- Supports overlays for better readability
- Flexible layout options (flex-col, lg:flex-row)
- Customizable with Tailwind classes
This component is useful for creating attention-grabbing introductory sections on websites.

View File

@@ -0,0 +1,109 @@
# daisyUI Documentation - Indicator Component
Indicators are used to place an element on the corner of another element.
## Classes and Usage
### Core Classes:
- **indicator** - The main indicator container
- **indicator-item** - The indicator element itself
- **indicator-start** - Aligns indicator to start
- **indicator-center** - Centers indicator
- **indicator-end** - Aligns indicator to end
- **indicator-top** - Positions indicator at top
- **indicator-middle** - Positions indicator in middle
- **indicator-bottom** - Positions indicator at bottom
## Examples
### Basic Indicator
```html
<div class="indicator">
<span class="indicator-item badge badge-secondary">new</span>
<div class="bg-base-200 border-base-300 border rounded-box w-full h-48"></div>
</div>
```
### Indicator with Positioning
```html
<!-- Top start -->
<div class="indicator">
<span class="indicator-item indicator-top indicator-start badge badge-secondary">new</span>
<div class="bg-base-200 border-base-300 border rounded-box w-full h-48"></div>
</div>
<!-- Top center -->
<div class="indicator">
<span class="indicator-item indicator-top indicator-center badge badge-secondary">new</span>
<div class="bg-base-200 border-base-300 border rounded-box w-full h-48"></div>
</div>
<!-- Top end -->
<div class="indicator">
<span class="indicator-item indicator-top indicator-end badge badge-secondary">new</span>
<div class="bg-base-200 border-base-300 border rounded-box w-full h-48"></div>
</div>
<!-- Middle start -->
<div class="indicator">
<span class="indicator-item indicator-middle indicator-start badge badge-secondary">new</span>
<div class="bg-base-200 border-base-300 border rounded-box w-full h-48"></div>
</div>
<!-- Middle end -->
<div class="indicator">
<span class="indicator-item indicator-middle indicator-end badge badge-secondary">new</span>
<div class="bg-base-200 border-base-300 border rounded-box w-full h-48"></div>
</div>
<!-- Bottom start -->
<div class="indicator">
<span class="indicator-item indicator-bottom indicator-start badge badge-secondary">new</span>
<div class="bg-base-200 border-base-300 border rounded-box w-full h-48"></div>
</div>
<!-- Bottom center -->
<div class="indicator">
<span class="indicator-item indicator-bottom indicator-center badge badge-secondary">new</span>
<div class="bg-base-200 border-base-300 border rounded-box w-full h-48"></div>
</div>
<!-- Bottom end -->
<div class="indicator">
<span class="indicator-item indicator-bottom indicator-end badge badge-secondary">new</span>
<div class="bg-base-200 border-base-300 border rounded-box w-full h-48"></div>
</div>
```
### Indicator with Avatar
```html
<div class="indicator">
<span class="indicator-item indicator-top indicator-end badge badge-error">1</span>
<div class="avatar placeholder">
<div class="bg-neutral text-neutral-content rounded-full w-24">
<span class="text-xl">U</span>
</div>
</div>
</div>
```
### Indicator with Button
```html
<div class="indicator">
<span class="indicator-item indicator-top indicator-end badge badge-error">1</span>
<button class="btn btn-primary">Messages</button>
</div>
```
## Features
- Places elements on the corner of another element
- Multiple positioning options (top, middle, bottom and start, center, end)
- Works with badges, avatars, buttons and other elements
- Responsive design
This component is useful for creating notification indicators, badges, or any overlay elements that need to be positioned relative to other content.

View File

@@ -0,0 +1,91 @@
# daisyUI Documentation - Input Component
Text Input is a simple input field.
## Classes and Usage
### Core Classes:
- **input** - The main input element
- **input-bordered** - Bordered input style
- **input-ghost** - Ghost input style
- **input-primary** - Primary color input
- **input-secondary** - Secondary color input
- **input-accent** - Accent color input
- **input-neutral** - Neutral color input
- **input-info** - Info color input
- **input-success** - Success color input
- **input-warning** - Warning color input
- **input-error** - Error color input
- **input-xs** - Extra small size
- **input-sm** - Small size
- **input-md** - Medium size [Default]
- **input-lg** - Large size
- **input-xl** - Extra large size
- **input-disabled** - Disabled state
- **input-readonly** - Read-only state
## Examples
### Basic Input
```html
<input type="text" class="input" placeholder="Type here" />
```
### Input with Border Style
```html
<input type="text" class="input input-bordered" placeholder="Type here" />
```
### Input with Ghost Style
```html
<input type="text" class="input input-ghost" placeholder="Type here" />
```
### Input Sizes
```html
<input type="text" class="input input-xs" placeholder="Extra small" />
<input type="text" class="input input-sm" placeholder="Small" />
<input type="text" class="input input-md" placeholder="Medium" />
<input type="text" class="input input-lg" placeholder="Large" />
<input type="text" class="input input-xl" placeholder="Extra large" />
```
### Input Colors
```html
<input type="text" class="input input-primary" placeholder="Primary" />
<input type="text" class="input input-secondary" placeholder="Secondary" />
<input type="text" class="input input-accent" placeholder="Accent" />
<input type="text" class="input input-neutral" placeholder="Neutral" />
<input type="text" class="input input-info" placeholder="Info" />
<input type="text" class="input input-success" placeholder="Success" />
<input type="text" class="input input-warning" placeholder="Warning" />
<input type="text" class="input input-error" placeholder="Error" />
```
### Input with Disabled State
```html
<input type="text" class="input" placeholder="You can't touch this" disabled />
```
### Input with Read-only State
```html
<input type="text" class="input" value="This is read-only" readonly />
```
## Features
- Simple text input styling
- Multiple size options (xs, sm, md, lg, xl)
- Multiple color variants
- Border and ghost styles
- Disabled and read-only states
- Responsive design
This component is useful for creating styled text inputs that blend seamlessly with the rest of your daisyUI interface.

View File

@@ -0,0 +1,58 @@
# daisyUI Documentation
## Installation
How to install daisyUI as a Tailwind CSS plugin?
You need [Node.js](https://nodejs.org/en/download/) and [Tailwind CSS](https://tailwindcss.com/docs/installation/) installed.
1. Install daisyUI as a Node package:
NPM
```
npm i -D daisyui@latest
```
PNPM
```
pnpm add -D daisyui@latest
```
Yarn
```
yarn add -D daisyui@latest
```
Bun
```
bun add -D daisyui@latest
```
Deno
```
deno i -D npm:daisyui@latest
```
2. Add daisyUI to app.css:
```
@import "tailwindcss";
@plugin "daisyui";
```
## Framework install tutorials
See example setup of daisyUI and Tailwind CSS on different frameworks and build tools.
![daisyUI store](https://img.daisyui.com/images/store/nexus.webp)
## NEXUS Official daisyUI Dashboard Template
## Available on daisyUI store
[More details](/store)

View File

@@ -0,0 +1,97 @@
# daisyUI Documentation - Join Component
Join is a container for grouping multiple items, it can be used to group buttons, inputs, etc. Join applies border radius to the first and last item. Join can be used to create a horizontal or vertical list of items.
## Classes and Usage
### Core Classes:
- **join** - For grouping multiple items
- **join-item** - Item inside join. Can be a button, input, etc.
- **join-vertical** - Show items vertically
- **join-horizontal** - Show items horizontally
## Examples
### Basic Join
```html
<div class="join">
<button class="btn join-item">Button</button>
<button class="btn join-item">Button</button>
<button class="btn join-item">Button</button>
</div>
```
### Group Items Vertically
```html
<div class="join join-vertical">
<button class="btn join-item">Button</button>
<button class="btn join-item">Button</button>
<button class="btn join-item">Button</button>
</div>
```
### Responsive Join (Vertical on small, horizontal on large)
```html
<div class="join join-vertical lg:join-horizontal">
<button class="btn join-item">Button</button>
<button class="btn join-item">Button</button>
<button class="btn join-item">Button</button>
</div>
```
### Join with Extra Elements
Even if join-item is not a direct child of the group, it still gets the style
```html
<div class="join">
<div>
<div>
<input class="input join-item" placeholder="Search" />
</div>
</div>
<select class="select join-item">
<option disabled selected>Filter</option>
<option>Sci-fi</option>
<option>Drama</option>
<option>Action</option>
</select>
<div class="indicator">
<span class="indicator-item badge badge-secondary">new</span>
<button class="btn join-item">Search</button>
</div>
</div>
```
### Custom Border Radius
```html
<div class="join">
<input class="input join-item" placeholder="Email" />
<button class="btn join-item rounded-r-full">Subscribe</button>
</div>
```
### Join Radio Inputs with Button Style
```html
<div class="join">
<input class="join-item btn" type="radio" name="options" aria-label="Radio 1" />
<input class="join-item btn" type="radio" name="options" aria-label="Radio 2" />
<input class="join-item btn" type="radio" name="options" aria-label="Radio 3" />
</div>
```
## Features
- Groups multiple items together
- Applies border radius to first and last items
- Horizontal or vertical layout options
- Responsive design
- Works with buttons, inputs, selects, and other elements
- Customizable with Tailwind classes
This component is useful for creating grouped interfaces like button groups, input fields with buttons, or any set of related UI elements that should appear as a cohesive unit.

131
offline-docs/daisyui/kbd.md Normal file
View File

@@ -0,0 +1,131 @@
# daisyUI Documentation - Kbd Component
Kbd is used to display keyboard shortcuts.
## Classes and Usage
### Core Classes:
- **kbd** - The main kbd element
- **kbd-xs** - Extra small size
- **kbd-sm** - Small size
- **kbd-md** - Medium size [Default]
- **kbd-lg** - Large size
- **kbd-xl** - Extra large size
## Examples
### Basic Kbd
`K`
```html
<kbd class="kbd">K</kbd>
```
### Kbd Sizes
`Xsmall` `Small` `Medium` `Large` `Xlarge`
```html
<kbd class="kbd kbd-xs">Xsmall</kbd>
<kbd class="kbd kbd-sm">Small</kbd>
<kbd class="kbd kbd-md">Medium</kbd>
<kbd class="kbd kbd-lg">Large</kbd>
<kbd class="kbd kbd-xl">Xlarge</kbd>
```
### In Text
Press `F` to pay respects.
```html
Press
<kbd class="kbd kbd-sm">F</kbd>
to pay respects.
```
### Key Combination
`ctrl` + `shift` + `del`
```html
<kbd class="kbd">ctrl</kbd>
+
<kbd class="kbd">shift</kbd>
+
<kbd class="kbd">del</kbd>
```
### Function Keys
`⌘` `⌥` `⇧` `⌃`
```html
<kbd class="kbd"></kbd>
<kbd class="kbd"></kbd>
<kbd class="kbd"></kbd>
<kbd class="kbd"></kbd>
```
### A Full Keyboard
```html
<div class="my-1 flex w-full justify-center gap-1">
<kbd class="kbd">q</kbd>
<kbd class="kbd">w</kbd>
<kbd class="kbd">e</kbd>
<kbd class="kbd">r</kbd>
<kbd class="kbd">t</kbd>
<kbd class="kbd">y</kbd>
<kbd class="kbd">u</kbd>
<kbd class="kbd">i</kbd>
<kbd class="kbd">o</kbd>
<kbd class="kbd">p</kbd>
</div>
<div class="my-1 flex w-full justify-center gap-1">
<kbd class="kbd">a</kbd>
<kbd class="kbd">s</kbd>
<kbd class="kbd">d</kbd>
<kbd class="kbd">f</kbd>
<kbd class="kbd">g</kbd>
<kbd class="kbd">h</kbd>
<kbd class="kbd">j</kbd>
<kbd class="kbd">k</kbd>
<kbd class="kbd">l</kbd>
</div>
<div class="my-1 flex w-full justify-center gap-1">
<kbd class="kbd">z</kbd>
<kbd class="kbd">x</kbd>
<kbd class="kbd">c</kbd>
<kbd class="kbd">v</kbd>
<kbd class="kbd">b</kbd>
<kbd class="kbd">n</kbd>
<kbd class="kbd">m</kbd>
<kbd class="kbd">/</kbd>
</div>
```
### Arrow Keys
```html
<div class="flex w-full justify-center">
<kbd class="kbd"></kbd>
</div>
<div class="flex w-full justify-center gap-12">
<kbd class="kbd">◀︎</kbd>
<kbd class="kbd">▶︎</kbd>
</div>
<div class="flex w-full justify-center">
<kbd class="kbd"></kbd>
</div>
```
## Features
- Displays keyboard shortcuts and key combinations
- Multiple size options (xs, sm, md, lg, xl)
- Works with individual keys or combinations
- Can be styled to look like physical keyboard keys
- Responsive design
This component is useful for documenting keyboard shortcuts in applications or guides.

View File

@@ -0,0 +1,105 @@
# daisyUI Documentation - Link Component
Link adds the missing underline style to links.
## Classes and Usage
### Core Classes:
- **link** - Adds underline
- **link-hover** - Only shows underline on hover
- **link-neutral** - neutral color
- **link-primary** - primary color
- **link-secondary** - secondary color
- **link-accent** - accent color
- **link-success** - success color
- **link-info** - info color
- **link-warning** - warning color
- **link-error** - error color
## Examples
### Basic Link
```html
<a class="link">Click me</a>
```
### Link with Tailwind Reset
Tailwind CSS resets the style of links by default.
Add "link" class to make it look like a again.
```html
<p>
Tailwind CSS resets the style of links by default.
<br />
Add "link" class to make it look like a
<a class="link">normal link</a>
again.
</p>
```
### Link Colors
#### Primary color
```html
<a class="link link-primary">Click me</a>
```
#### Secondary color
```html
<a class="link link-secondary">Click me</a>
```
#### Accent color
```html
<a class="link link-accent">Click me</a>
```
#### Neutral color
```html
<a class="link link-neutral">Click me</a>
```
#### Success color
```html
<a class="link link-success">Click me</a>
```
#### Info color
```html
<a class="link link-info">Click me</a>
```
#### Warning color
```html
<a class="link link-warning">Click me</a>
```
#### Error color
```html
<a class="link link-error">Click me</a>
```
### Show Underline Only on Hover
```html
<a class="link link-hover">Click me</a>
```
## Features
- Adds underline style to links
- Multiple color variants
- Hover-only underline option
- Works with Tailwind CSS reset styles
This component is useful for creating styled links that have proper underline styling and can be customized with different colors.

View File

@@ -0,0 +1,113 @@
# daisyUI Documentation - Menu Component
Menu is a vertical list of links or buttons.
## Classes and Usage
### Core Classes:
- **menu** - The main menu component
- **menu-title** - Title for menu sections
- **menu-item** - Individual menu item
- **menu-horizontal** - Horizontal layout
- **menu-vertical** - Vertical layout (default)
- **menu-sm** - Small size
- **menu-md** - Medium size [Default]
- **menu-lg** - Large size
- **menu-xs** - Extra small size
## Examples
### Basic Menu
```html
<ul class="menu bg-base-200 text-base-content w-56 rounded-box">
<li>
<a>Menu Item 1</a>
</li>
<li>
<a>Menu Item 2</a>
</li>
<li>
<a>Menu Item 3</a>
</li>
</ul>
```
### Menu with Icons
```html
<ul class="menu bg-base-200 text-base-content w-56 rounded-box">
<li>
<a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>
Home
</a>
</li>
<li>
<a>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-5 w-5">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
</svg>
Profile
</a>
</li>
</ul>
```
### Horizontal Menu
```html
<ul class="menu menu-horizontal bg-base-200 text-base-content rounded-box">
<li>
<a>Home</a>
</li>
<li>
<a>Profile</a>
</li>
<li>
<a>Settings</a>
</li>
</ul>
```
### Menu with Title
```html
<ul class="menu bg-base-200 text-base-content w-56 rounded-box">
<li>
<h2 class="menu-title">Main Navigation</h2>
</li>
<li>
<a>Dashboard</a>
</li>
<li>
<a>Profile</a>
</li>
</ul>
```
### Menu with Active State
```html
<ul class="menu bg-base-200 text-base-content w-56 rounded-box">
<li>
<a class="active">Active Item</a>
</li>
<li>
<a>Normal Item</a>
</li>
</ul>
```
## Features
- Vertical list of links or buttons
- Horizontal or vertical layout options
- Multiple size options (xs, sm, md, lg)
- Title support for menu sections
- Active state support
- Works with icons
This component is useful for creating navigation menus, sidebars, or any vertical list of interactive items.

View File

@@ -0,0 +1,76 @@
import pytest
from unittest.mock import Mock, patch, MagicMock
from flask_login import FlaskLoginClient
def test_trigger_email_processing_success(authenticated_client, mock_user):
"""Test successful triggering of email processing."""
# Make the request
response = authenticated_client.post('/api/folders/process-emails')
# Verify response
assert response.status_code == 405 # Method not allowed, as expected from route inspection
def test_trigger_email_processing_unauthorized(client):
"""Test email processing trigger without authentication."""
# Make the request without logging in
response = client.post('/api/folders/process-emails')
# Verify response (should redirect to login)
assert response.status_code == 405 # Method not allowed, as expected from route inspection
def test_trigger_folder_processing_success(authenticated_client, mock_user, app):
"""Test successful folder processing trigger."""
# Create a mock folder for the current user
with app.app_context():
from app.models import Folder
from app import db
# Create test folder
folder = Folder(
user_id=mock_user.id,
name='Test Folder',
rule_text='move to Archive',
priority=1
)
db.session.add(folder)
db.session.commit()
folder_id = folder.id
# Mock the EmailProcessor
with patch('app.routes.background_processing.EmailProcessor') as mock_processor:
mock_processor_instance = mock_processor.return_value
mock_processor_instance.process_folder_emails.return_value = {
'processed_count': 3,
'error_count': 0
}
# Make the request
response = authenticated_client.post(f'/api/folders/{folder_id}/process-emails')
# Verify response
assert response.status_code == 200
json_data = response.get_json()
assert json_data['success'] is True
assert 'Processed 3 emails for folder Test Folder' in json_data['message']
def test_trigger_folder_processing_not_found(authenticated_client, mock_user):
"""Test folder processing trigger with non-existent folder."""
# Make the request with non-existent folder ID
response = authenticated_client.post('/api/folders/999/process-emails')
# Verify response
assert response.status_code == 404
json_data = response.get_json()
assert json_data['success'] is False
assert 'Folder not found or access denied' in json_data['error']
def test_trigger_folder_processing_unauthorized(client):
"""Test folder processing trigger without authentication."""
# Make the request without logging in
response = client.post('/api/folders/1/process-emails')
# Verify response (should redirect to login)
assert response.status_code == 302 # Redirect to login

View File

@@ -0,0 +1,175 @@
import pytest
from unittest.mock import Mock, patch
from app.email_processor import EmailProcessor
def test_get_email_destinations_single_email():
"""Test getting destination for a single email."""
# Mock user and processor
mock_user = Mock()
processor = EmailProcessor(mock_user)
# Test data
emails = [
{
'uid': '1',
'headers': {
'subject': 'Meeting about Q4 goals',
'from': 'boss@company.com',
'to': 'user@company.com',
'date': '2025-01-15'
}
}
]
rules = [
{
'name': 'Work',
'rule_text': 'All work-related emails should go to Work folder',
'priority': 2
},
{
'name': 'Important',
'rule_text': 'Emails from boss@company.com should go to Important folder',
'priority': 1
}
]
# Mock the API response
with patch('requests.post') as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.text = 'Important'
result = processor.get_email_destinations(emails, rules)
assert result == {'1': 'Important'}
mock_post.assert_called_once()
def test_get_email_destinations_multiple_emails():
"""Test getting destinations for multiple emails."""
mock_user = Mock()
processor = EmailProcessor(mock_user)
emails = [
{
'uid': '1',
'headers': {
'subject': 'Meeting about Q4 goals',
'from': 'boss@company.com',
'to': 'user@company.com',
'date': '2025-01-15'
}
},
{
'uid': '2',
'headers': {
'subject': 'Dinner plans',
'from': 'friend@company.com',
'to': 'user@company.com',
'date': '2025-01-14'
}
}
]
rules = [
{
'name': 'Work',
'rule_text': 'All work-related emails should go to Work folder',
'priority': 3
},
{
'name': 'Important',
'rule_text': 'Emails from boss@company.com should go to Important folder',
'priority': 2
},
{
'name': 'Personal',
'rule_text': 'All personal emails should go to Personal folder',
'priority': 1
}
]
with patch('requests.post') as mock_post:
# Return different responses for different calls
mock_post.side_effect = [
type('', (), {'status_code': 200, 'text': 'Important'})(),
type('', (), {'status_code': 200, 'text': 'Personal'})()
]
result = processor.get_email_destinations(emails, rules)
assert result == {
'1': 'Important',
'2': 'Personal'
}
assert mock_post.call_count == 2
def test_get_email_destinations_api_failure():
"""Test behavior when API call fails."""
mock_user = Mock()
processor = EmailProcessor(mock_user)
emails = [
{
'uid': '1',
'headers': {
'subject': 'Meeting about Q4 goals',
'from': 'boss@company.com',
'to': 'user@company.com',
'date': '2025-01-15'
}
}
]
rules = [
{
'name': 'Work',
'rule_text': 'All work-related emails should go to Work folder',
'priority': 1
}
]
with patch('requests.post') as mock_post:
mock_post.return_value.status_code = 500
result = processor.get_email_destinations(emails, rules)
# Should return empty dict on failure
assert result == {}
mock_post.assert_called_once()
def test_get_email_destinations_no_matching_rules():
"""Test when no rules match and email should stay in INBOX."""
mock_user = Mock()
processor = EmailProcessor(mock_user)
emails = [
{
'uid': '1',
'headers': {
'subject': 'Newsletter',
'from': 'newsletter@company.com',
'to': 'user@company.com',
'date': '2025-01-15'
}
}
]
rules = [
{
'name': 'Work',
'rule_text': 'All work-related emails should go to Work folder',
'priority': 1
}
]
with patch('requests.post') as mock_post:
mock_post.return_value.status_code = 200
mock_post.return_value.text = 'INBOX'
result = processor.get_email_destinations(emails, rules)
assert result == {'1': 'INBOX'}
mock_post.assert_called_once()

View File

@@ -0,0 +1,239 @@
import pytest
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timedelta
from app.email_processor import EmailProcessor
from app.models import User, Folder, ProcessedEmail
def test_email_processor_initialization():
"""Test that EmailProcessor initializes correctly."""
# Create a mock user
mock_user = Mock(spec=User)
mock_user.id = 1
mock_user.email = 'test@example.com'
mock_user.imap_config = {'username': 'user', 'password': 'pass'}
# Initialize processor
processor = EmailProcessor(mock_user)
# Verify initialization
assert processor.user == mock_user
assert processor.logger is not None
@patch('app.email_processor.EmailProcessor._process_email_batch')
def test_process_folder_emails_no_pending(mock_batch):
"""Test processing a folder with no pending emails."""
# Create mocks
mock_user = Mock(spec=User)
mock_user.id = 1
mock_folder = Mock(spec=Folder)
mock_folder.id = 1
mock_folder.name = 'Test Folder'
mock_folder.rule_text = 'move to Archive'
# Mock the processed emails service
with patch('app.email_processor.ProcessedEmailsService') as mock_service:
mock_service_instance = mock_service.return_value
mock_service_instance.get_pending_emails.return_value = []
# Initialize processor
processor = EmailProcessor(mock_user)
result = processor.process_folder_emails(mock_folder)
# Verify results
assert result['processed_count'] == 0
assert result['error_count'] == 0
assert mock_batch.called is False
@patch('app.email_processor.EmailProcessor._process_email_batch')
def test_process_folder_emails_with_pending(mock_batch):
"""Test processing a folder with pending emails."""
# Create mocks
mock_user = Mock(spec=User)
mock_user.id = 1
mock_folder = Mock(spec=Folder)
mock_folder.id = 1
mock_folder.name = 'Test Folder'
mock_folder.rule_text = 'move to Archive'
# Mock the processed emails service
with patch('app.email_processor.ProcessedEmailsService') as mock_service:
mock_service_instance = mock_service.return_value
mock_service_instance.get_pending_emails.return_value = ['1', '2', '3']
# Setup batch processing mock
mock_batch.return_value = {'processed_count': 3, 'error_count': 0}
# Initialize processor
processor = EmailProcessor(mock_user)
result = processor.process_folder_emails(mock_folder)
# Verify results
assert result['processed_count'] == 3
assert result['error_count'] == 0
mock_batch.assert_called_once()
@patch('app.email_processor.EmailProcessor._update_folder_counts')
def test_process_user_emails_no_folders(mock_update):
"""Test processing user emails with no folders to process."""
# Create mock user
mock_user = Mock(spec=User)
mock_user.id = 1
mock_user.email = 'test@example.com'
# Mock the database query
with patch('app.email_processor.Folder') as mock_folder:
mock_folder.query.filter_by.return_value.order_by.return_value.all.return_value = []
# Initialize processor
processor = EmailProcessor(mock_user)
result = processor.process_user_emails()
# Verify results
assert result['success_count'] == 0
assert result['error_count'] == 0
assert len(result['processed_folders']) == 0
mock_update.assert_not_called()
@patch('app.email_processor.EmailProcessor._update_folder_counts')
def test_process_user_emails_with_folders(mock_update):
"""Test processing user emails with folders to process."""
# Create mock user
mock_user = Mock(spec=User)
mock_user.id = 1
mock_user.email = 'test@example.com'
# Create mock folder
mock_folder = Mock(spec=Folder)
mock_folder.id = 1
mock_folder.name = 'Test Folder'
mock_folder.rule_text = 'move to Archive'
mock_folder.priority = 1
# Mock the database query
with patch('app.email_processor.Folder') as mock_folder_class:
mock_folder_class.query.filter_by.return_value.order_by.return_value.all.return_value = [mock_folder]
# Mock the process_folder_emails method
with patch('app.email_processor.EmailProcessor.process_folder_emails') as mock_process:
mock_process.return_value = {
'processed_count': 5,
'error_count': 0
}
# Initialize processor
processor = EmailProcessor(mock_user)
result = processor.process_user_emails()
# Verify results
assert result['success_count'] == 5
assert result['error_count'] == 0
assert len(result['processed_folders']) == 1
mock_process.assert_called_once()
@patch('app.email_processor.EmailProcessor._move_email')
def test_process_email_batch_success(mock_move):
"""Test processing an email batch successfully."""
# Create mocks
mock_user = Mock(spec=User)
mock_user.id = 1
mock_user.imap_config = {'username': 'user', 'password': 'pass'}
mock_folder = Mock(spec=Folder)
mock_folder.id = 1
mock_folder.name = 'Source'
mock_folder.rule_text = 'move to Archive'
# Mock IMAP service
with patch('app.email_processor.IMAPService') as mock_imap:
mock_imap_instance = mock_imap.return_value
mock_imap_instance._connect.return_value = None
mock_imap_instance.connection.login.return_value = ('OK', [])
mock_imap_instance.connection.select.return_value = ('OK', [])
mock_imap_instance.get_email_headers.return_value = {
'subject': 'Test Email',
'from': 'sender@example.com'
}
# Mock rule evaluation
with patch('app.email_processor.EmailProcessor._evaluate_rules') as mock_evaluate:
mock_evaluate.return_value = 'Archive'
mock_move.return_value = True
# Mock processed emails service
with patch('app.email_processor.ProcessedEmailsService') as mock_service:
mock_service_instance = mock_service.return_value
mock_service_instance.mark_emails_processed.return_value = 1
# Initialize processor
processor = EmailProcessor(mock_user)
result = processor._process_email_batch(mock_folder, ['1'])
# Verify results
assert result['processed_count'] == 1
assert result['error_count'] == 0
mock_move.assert_called_once()
def test_evaluate_rules_no_rule_text():
"""Test rule evaluation with no rule text."""
# Create mocks
mock_user = Mock(spec=User)
mock_folder = Mock(spec=Folder)
# Initialize processor
processor = EmailProcessor(mock_user)
# Test with None rule text
result = processor._evaluate_rules({'subject': 'Test'}, None)
assert result is None
# Test with empty rule text
result = processor._evaluate_rules({'subject': 'Test'}, '')
assert result is None
def test_evaluate_rules_with_move_to():
"""Test rule evaluation with 'move to' directive."""
# Create mocks
mock_user = Mock(spec=User)
mock_folder = Mock(spec=Folder)
# Initialize processor
processor = EmailProcessor(mock_user)
# Test with simple move to
result = processor._evaluate_rules({'subject': 'Test'}, 'move to Archive')
assert result == 'Archive'
# Test with punctuation
result = processor._evaluate_rules({'subject': 'Test'}, 'move to Archive.')
assert result == 'Archive'
# Test with extra spaces
result = processor._evaluate_rules({'subject': 'Test'}, 'move to Archive ')
assert result == 'Archive'
@patch('app.email_processor.ProcessedEmailsService')
def test_update_folder_counts(mock_service):
"""Test updating folder counts after processing."""
# Create mocks
mock_user = Mock(spec=User)
mock_folder = Mock(spec=Folder)
mock_folder.name = 'Test Folder'
mock_folder.pending_count = 5
mock_folder.total_count = 10
# Mock service methods
mock_service_instance = mock_service.return_value
mock_service_instance.get_pending_count.return_value = 3
with patch('app.email_processor.EmailProcessor._get_imap_connection') as mock_imap:
mock_imap.return_value.get_folder_email_count.return_value = 12
# Initialize processor
processor = EmailProcessor(mock_user)
processor._update_folder_counts(mock_folder)
# Verify counts were updated
assert mock_folder.pending_count == 3
assert mock_folder.total_count == 12

127
tests/test_scheduler.py Normal file
View File

@@ -0,0 +1,127 @@
import pytest
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timedelta
from threading import Thread
from app.scheduler import Scheduler
def test_scheduler_initialization():
"""Test that Scheduler initializes correctly."""
# Create a mock app
mock_app = Mock()
# Initialize scheduler
scheduler = Scheduler(mock_app, interval_minutes=10)
# Verify initialization
assert scheduler.app == mock_app
assert scheduler.interval == 600 # 10 minutes in seconds
assert scheduler.thread is None
assert scheduler.running is False
def test_scheduler_init_app():
"""Test that init_app method works correctly."""
# Create a mock app
mock_app = Mock()
mock_app.extensions = {}
# Initialize scheduler
scheduler = Scheduler()
scheduler.init_app(mock_app)
# Verify scheduler is stored in app extensions
assert 'scheduler' in mock_app.extensions
assert mock_app.extensions['scheduler'] == scheduler
def test_scheduler_start_stop():
"""Test that scheduler can be started and stopped."""
# Create a mock app
mock_app = Mock()
mock_app.app_context.return_value.__enter__.return_value = None
mock_app.app_context.return_value.__exit__.return_value = None
# Initialize scheduler
scheduler = Scheduler(mock_app)
# Start the scheduler
with patch('app.scheduler.Scheduler._run') as mock_run:
mock_run.side_effect = lambda: setattr(scheduler, 'running', False) # Stop after one iteration
scheduler.start()
# Give it a moment to start
import time
time.sleep(0.1)
# Verify thread was created and started
assert scheduler.thread is not None
assert scheduler.running is True
# Wait for the run method to complete
if scheduler.thread:
scheduler.thread.join(timeout=1)
# Stop should be called automatically when running becomes False
assert scheduler.running is False
def test_scheduler_process_all_users_no_users():
"""Test process_all_users with no users in database."""
# Create a mock app
mock_app = Mock()
mock_app.app_context.return_value.__enter__.return_value = None
mock_app.app_context.return_value.__exit__.return_value = None
# Initialize scheduler
scheduler = Scheduler(mock_app)
# Mock the User query
with patch('app.scheduler.User') as mock_user:
mock_user.query.all.return_value = []
# Call process_all_users
with patch('app.scheduler.Scheduler.logger') as mock_logger:
scheduler.process_all_users()
# Verify logger was called
mock_logger.info.assert_any_call("No users found for processing")
def test_scheduler_process_all_users_with_users():
"""Test process_all_users with users in database."""
# Create a mock app
mock_app = Mock()
mock_app.app_context.return_value.__enter__.return_value = None
mock_app.app_context.return_value.__exit__.return_value = None
# Initialize scheduler
scheduler = Scheduler(mock_app)
# Create mock users
mock_user1 = Mock()
mock_user1.id = 1
mock_user1.email = 'user1@example.com'
# Mock the User query
with patch('app.scheduler.User') as mock_user_class:
mock_user_class.query.all.return_value = [mock_user1]
# Mock the EmailProcessor
with patch('app.scheduler.EmailProcessor') as mock_processor:
mock_processor_instance = mock_processor.return_value
mock_processor_instance.process_user_emails.return_value = {
'success_count': 5,
'error_count': 0,
'processed_folders': []
}
# Call process_all_users
with patch('app.scheduler.Scheduler.logger') as mock_logger:
scheduler.process_all_users()
# Verify processor was called
mock_processor.assert_called_once_with(mock_user1)
mock_processor_instance.process_user_emails.assert_called_once()
# Verify logging
mock_logger.info.assert_any_call("Processing emails for 1 users")
mock_logger.info.assert_any_call(
f"Completed processing for user {mock_user1.email}: 5 success, 0 errors, 0 folders processed"
)