mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-04 22:40:29 +03:00
Add sliding sync support and change font to SF Pro Display
- Enable sliding sync in config.json with matrix.org proxy - Update font from InterVariable to SF Pro Display - Add sliding sync state management with Jotai atoms - Create bridge between sliding sync and existing room list atoms - Add sliding sync settings UI in General settings - Implement purple theme with gradient enhancements - Add synchronization status display for sliding sync - Update client initialization to support sliding sync 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
67b05eeb09
commit
d25cc7250b
25 changed files with 1510 additions and 14 deletions
139
CLAUDE.md
Normal file
139
CLAUDE.md
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Cinny is a Matrix client built with React, TypeScript, and Vite. It focuses on providing a simple, elegant, and secure interface for Matrix messaging. The application uses modern web technologies including React 18, Matrix JS SDK, and Jotai for state management.
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm ci
|
||||
|
||||
# Start development server (runs on port 8080)
|
||||
npm start
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Lint code (ESLint + Prettier)
|
||||
npm run lint
|
||||
|
||||
# Type checking
|
||||
npm run typecheck
|
||||
|
||||
# Individual lint checks
|
||||
npm run check:eslint # ESLint only
|
||||
npm run check:prettier # Prettier only
|
||||
npm run fix:prettier # Auto-fix Prettier issues
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Core Technologies
|
||||
- **Frontend**: React 18 with TypeScript
|
||||
- **Build Tool**: Vite with custom plugins for Matrix SDK WASM support
|
||||
- **State Management**: Jotai atoms with custom hooks for Matrix client binding
|
||||
- **Styling**: Vanilla Extract CSS-in-JS + SCSS
|
||||
- **Matrix SDK**: matrix-js-sdk with IndexedDB storage and Rust crypto
|
||||
- **Routing**: React Router v6
|
||||
- **Data Fetching**: React Query (TanStack Query)
|
||||
|
||||
### Key Directories Structure
|
||||
|
||||
- `src/app/` - Main application code
|
||||
- `atoms/` - Base UI components (buttons, inputs, modals, etc.)
|
||||
- `components/` - Reusable business logic components
|
||||
- `features/` - Feature-specific components and logic
|
||||
- `hooks/` - Custom React hooks
|
||||
- `pages/` - Route components and app setup
|
||||
- `state/` - Jotai atoms and state management
|
||||
- `utils/` - Utility functions
|
||||
- `src/client/` - Matrix client initialization and management
|
||||
- `public/` - Static assets (icons, fonts, locales)
|
||||
|
||||
### State Management Architecture
|
||||
|
||||
Cinny uses Jotai for state management with a unique pattern:
|
||||
- **Atoms**: Defined in `src/app/state/` for different data types (rooms, invites, unread counts, etc.)
|
||||
- **Binding**: Matrix client events are bound to atoms via `useBindAtoms` hooks
|
||||
- **Hooks**: Custom hooks in `src/app/state/hooks/` provide convenient access to atom values
|
||||
|
||||
Key state atoms:
|
||||
- `allRoomsAtom` - All joined rooms
|
||||
- `allInvitesAtom` - Room invitations
|
||||
- `roomToUnreadAtom` - Unread message counts
|
||||
- `mDirectAtom` - Direct message room mappings
|
||||
- `roomToParentsAtom` - Space/room hierarchies
|
||||
|
||||
### Matrix Client Integration
|
||||
|
||||
The Matrix client is initialized in `src/client/initMatrix.ts`:
|
||||
- Uses IndexedDB for message storage and crypto store
|
||||
- Enables Rust crypto for end-to-end encryption
|
||||
- Lazy loads room members for performance
|
||||
- Custom crypto callbacks for secret storage
|
||||
|
||||
### Component Architecture
|
||||
|
||||
- **Atoms**: Basic UI building blocks in `src/app/atoms/`
|
||||
- **Components**: Business logic components in `src/app/components/`
|
||||
- **Features**: Complete feature implementations in `src/app/features/`
|
||||
- **Styling**: Mix of Vanilla Extract (`.css.ts`) and SCSS files
|
||||
|
||||
### Key Features
|
||||
|
||||
- **Rooms & Spaces**: Full Matrix rooms and spaces support
|
||||
- **End-to-End Encryption**: Device verification and cross-signing
|
||||
- **Message Types**: Text, images, files, audio, video with rich content
|
||||
- **Themes**: Light/dark mode support
|
||||
- **PWA**: Service worker with offline capabilities
|
||||
- **Internationalization**: i18next with multiple language support
|
||||
- **Sliding Sync**: Matrix sliding sync protocol support for improved performance
|
||||
|
||||
### Sliding Sync Integration
|
||||
|
||||
Cinny now supports Matrix's sliding sync protocol as implemented in `src/app/state/sliding-sync/`:
|
||||
|
||||
- **Configuration**: Enabled via `config.json` with proxy URL and list configurations
|
||||
- **State Management**: Dedicated Jotai atoms for sliding sync state and data
|
||||
- **Bridge**: Transparent integration with existing room list atoms for UI compatibility
|
||||
- **Fallback**: Automatic fallback to traditional sync when disabled
|
||||
|
||||
Key files:
|
||||
- `src/client/initMatrix.ts` - Client initialization with sliding sync support
|
||||
- `src/app/state/sliding-sync/` - Sliding sync state management and bridge logic
|
||||
- `SLIDING_SYNC.md` - Detailed implementation documentation
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- `config.json` - Client configuration (homeservers, features)
|
||||
- `build.config.ts` - Build-time configuration (base path)
|
||||
- `vite.config.js` - Vite build configuration with Matrix SDK WASM support
|
||||
- `tsconfig.json` - TypeScript configuration
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Matrix SDK Integration
|
||||
- WASM files for Matrix SDK crypto are served via custom Vite plugin
|
||||
- IndexedDB is used for persistent storage of messages and crypto data
|
||||
- Client state is bound to Jotai atoms for reactive UI updates
|
||||
|
||||
### Styling Approach
|
||||
- Primary styling uses Vanilla Extract for type-safe CSS-in-JS
|
||||
- Legacy SCSS files exist in some components
|
||||
- CSS custom properties for theming support
|
||||
|
||||
### Testing & Quality
|
||||
- ESLint with Airbnb config + TypeScript rules
|
||||
- Prettier for code formatting
|
||||
- TypeScript strict mode enabled
|
||||
- No test framework currently configured
|
||||
|
||||
### Build Process
|
||||
- Vite handles bundling with React plugin
|
||||
- Service worker built with vite-plugin-pwa
|
||||
- Static files copied from public/ and node_modules/
|
||||
- WASM support for Matrix SDK crypto functionality
|
||||
144
SLIDING_SYNC.md
Normal file
144
SLIDING_SYNC.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Sliding Sync Implementation
|
||||
|
||||
This document describes the sliding sync integration added to Cinny.
|
||||
|
||||
## Overview
|
||||
|
||||
Cinny now supports Matrix's sliding sync protocol as an alternative to the traditional `/sync` endpoint. Sliding sync provides more efficient synchronization, better performance for large accounts, and fine-grained control over data loading.
|
||||
|
||||
## Configuration
|
||||
|
||||
To enable sliding sync, update your `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"slidingSync": {
|
||||
"enabled": true,
|
||||
"proxyUrl": "https://your-sliding-sync-proxy.example.com",
|
||||
"defaultLists": {
|
||||
"allRooms": {
|
||||
"ranges": [[0, 49]],
|
||||
"sort": ["by_recency", "by_name"],
|
||||
"timeline_limit": 1,
|
||||
"required_state": [
|
||||
["m.room.name", ""],
|
||||
["m.room.avatar", ""],
|
||||
["m.room.canonical_alias", ""],
|
||||
["m.room.topic", ""],
|
||||
["m.room.encryption", ""],
|
||||
["m.room.member", "$ME"]
|
||||
]
|
||||
},
|
||||
"directMessages": {
|
||||
"ranges": [[0, 49]],
|
||||
"sort": ["by_recency"],
|
||||
"timeline_limit": 1,
|
||||
"filters": {
|
||||
"is_dm": true
|
||||
},
|
||||
"required_state": [
|
||||
["m.room.name", ""],
|
||||
["m.room.avatar", ""],
|
||||
["m.room.member", "$ME"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- `enabled`: Boolean to enable/disable sliding sync
|
||||
- `proxyUrl`: URL of your sliding sync proxy server
|
||||
- `defaultLists`: Configuration for different room lists
|
||||
- `ranges`: Array of [start, end] ranges for room pagination
|
||||
- `sort`: Array of sort methods (`by_recency`, `by_name`)
|
||||
- `timeline_limit`: Number of timeline events to fetch per room
|
||||
- `required_state`: State events to include in room data
|
||||
- `filters`: Filters to apply to the list (e.g., `is_dm` for direct messages)
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
SlidingSync → Event Handlers → Jotai Atoms → Bridge → Existing Room List Atoms → UI Components
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **Client Initialization** (`src/client/initMatrix.ts`)
|
||||
- Conditionally creates SlidingSync instance based on config
|
||||
- Falls back to traditional sync if sliding sync is disabled
|
||||
|
||||
2. **State Management** (`src/app/state/sliding-sync/`)
|
||||
- `slidingSync.ts`: Core atoms for sliding sync state
|
||||
- `useSlidingSync.ts`: Hooks for binding sliding sync events to atoms
|
||||
- `roomListBridge.ts`: Bridge between sliding sync data and existing room list atoms
|
||||
|
||||
3. **Integration** (`src/app/state/hooks/useBindAtoms.ts`)
|
||||
- Conditionally binds either sliding sync or traditional sync atoms
|
||||
- Maintains compatibility with existing UI components
|
||||
|
||||
### State Atoms
|
||||
|
||||
- `slidingSyncAtom`: Stores the SlidingSync instance
|
||||
- `slidingSyncStateAtom`: Current sync state (PREPARED, SYNCING, STOPPED, ERROR)
|
||||
- `slidingSyncEnabledAtom`: Boolean indicating if sliding sync is active
|
||||
- `slidingSyncRoomListAtom`: Room lists from sliding sync
|
||||
- `slidingSyncRoomDataAtom`: Room metadata from sliding sync
|
||||
- `slidingSyncErrorAtom`: Error state
|
||||
|
||||
## Usage
|
||||
|
||||
### Enabling Sliding Sync
|
||||
|
||||
1. **Configuration**: Edit `config.json` to enable sliding sync and set proxy URL
|
||||
2. **Restart**: Restart the Cinny application for changes to take effect
|
||||
3. **Verification**: Check the settings UI to verify sliding sync is active
|
||||
|
||||
### Settings Interface
|
||||
|
||||
Cinny provides a comprehensive settings interface for sliding sync:
|
||||
|
||||
- **General Settings**: View sliding sync status in Settings → General → Synchronization
|
||||
- **Dedicated Page**: Access detailed configuration in Settings → Sliding Sync
|
||||
- **Status Indicator**: Real-time sync status displayed in the top bar
|
||||
- **URL Validation**: Built-in validation for proxy URLs with helpful error messages
|
||||
|
||||
### Settings Features
|
||||
|
||||
- **Status Monitoring**: Real-time status display (Active, Syncing, Error, etc.)
|
||||
- **Configuration View**: Display current proxy URL and enabled status
|
||||
- **URL Examples**: Pre-configured examples of common sliding sync proxies
|
||||
- **Error Reporting**: Detailed error messages when sliding sync fails
|
||||
- **Validation**: URL format validation with security requirements (HTTPS)
|
||||
|
||||
When sliding sync is enabled:
|
||||
|
||||
1. The client will use sliding sync instead of traditional `/sync`
|
||||
2. Room lists are populated via sliding sync events
|
||||
3. Existing UI components continue to work via the bridge layer
|
||||
4. Fallback to traditional sync occurs if sliding sync fails or is disabled
|
||||
5. Status indicators show real-time sync state
|
||||
|
||||
## Requirements
|
||||
|
||||
- Matrix JS SDK v37.5.0+
|
||||
- Sliding sync proxy server
|
||||
- Compatible homeserver (Matrix 1.4+)
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Performance**: Faster sync for large accounts
|
||||
- **Efficiency**: Only loads visible rooms and timelines
|
||||
- **Scalability**: Better handling of large room lists
|
||||
- **Flexibility**: Fine-grained control over data loading
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The implementation maintains full backward compatibility:
|
||||
- Traditional sync works when sliding sync is disabled
|
||||
- All existing UI components function without changes
|
||||
- Progressive enhancement approach
|
||||
33
config.json
33
config.json
|
|
@ -34,5 +34,38 @@
|
|||
"hashRouter": {
|
||||
"enabled": false,
|
||||
"basename": "/"
|
||||
},
|
||||
|
||||
"slidingSync": {
|
||||
"enabled": true,
|
||||
"proxyUrl": "https://syncv3.matrix.org",
|
||||
"defaultLists": {
|
||||
"allRooms": {
|
||||
"ranges": [[0, 49]],
|
||||
"sort": ["by_recency", "by_name"],
|
||||
"timeline_limit": 1,
|
||||
"required_state": [
|
||||
["m.room.name", ""],
|
||||
["m.room.avatar", ""],
|
||||
["m.room.canonical_alias", ""],
|
||||
["m.room.topic", ""],
|
||||
["m.room.encryption", ""],
|
||||
["m.room.member", "$ME"]
|
||||
]
|
||||
},
|
||||
"directMessages": {
|
||||
"ranges": [[0, 49]],
|
||||
"sort": ["by_recency"],
|
||||
"timeline_limit": 1,
|
||||
"filters": {
|
||||
"is_dm": true
|
||||
},
|
||||
"required_state": [
|
||||
["m.room.name", ""],
|
||||
["m.room.avatar", ""],
|
||||
["m.room.member", "$ME"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
262
purple-theme-demo.html
Normal file
262
purple-theme-demo.html
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cinny Purple Theme - Applied</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #1A1B23;
|
||||
color: #FFFFFF;
|
||||
line-height: 1.6;
|
||||
padding: 40px 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 2.5em;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #6B46FF, #9D5EFF);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.1em;
|
||||
color: #B0B3C1;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.demo-section {
|
||||
background: #2A2B35;
|
||||
border-radius: 16px;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
border: 1px solid #3A3B47;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.demo-section:hover {
|
||||
border-color: #6B46FF;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(107, 70, 255, 0.2);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.3em;
|
||||
font-weight: 700;
|
||||
margin-bottom: 20px;
|
||||
color: #FFFFFF;
|
||||
border-bottom: 2px solid #6B46FF;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.button-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.demo-button {
|
||||
padding: 14px 24px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 0.95em;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.demo-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.primary-gradient {
|
||||
background: linear-gradient(135deg, #6B46FF, #9D5EFF);
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 4px 15px rgba(107, 70, 255, 0.3);
|
||||
}
|
||||
|
||||
.primary-gradient:hover {
|
||||
background: linear-gradient(135deg, #5A3AE5, #8B4FE6);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(107, 70, 255, 0.5);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: transparent;
|
||||
border: 2px solid #6B46FF;
|
||||
color: #6B46FF;
|
||||
}
|
||||
|
||||
.secondary:hover {
|
||||
background: #6B46FF;
|
||||
color: #FFFFFF;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-demo {
|
||||
background: linear-gradient(135deg, rgba(107, 70, 255, 0.1), rgba(157, 94, 255, 0.05));
|
||||
border: 1px solid rgba(107, 70, 255, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card-demo:hover {
|
||||
background: linear-gradient(135deg, rgba(107, 70, 255, 0.15), rgba(157, 94, 255, 0.08));
|
||||
border: 1px solid rgba(107, 70, 255, 0.3);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 25px rgba(107, 70, 255, 0.2);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
background: #3A3B47;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: linear-gradient(135deg, rgba(107, 70, 255, 0.15), rgba(157, 94, 255, 0.08));
|
||||
border-left: 3px solid #6B46FF;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: linear-gradient(135deg, rgba(107, 70, 255, 0.2), rgba(157, 94, 255, 0.1));
|
||||
border-left: 3px solid #6B46FF;
|
||||
}
|
||||
|
||||
.input-demo {
|
||||
background: #2A2B35;
|
||||
border: 1px solid #3A3B47;
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
color: #FFFFFF;
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input-demo:focus {
|
||||
outline: none;
|
||||
border: 1px solid #6B46FF;
|
||||
box-shadow: 0 0 0 3px rgba(107, 70, 255, 0.1);
|
||||
}
|
||||
|
||||
.status-message {
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(16, 185, 129, 0.05));
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
color: #10B981;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 class="main-title">Cinny Purple Theme</h1>
|
||||
<p class="subtitle">🎨 Successfully Applied to Cinny Matrix Client</p>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="section-title">Primary Buttons & Actions</h2>
|
||||
<div class="button-grid">
|
||||
<button class="demo-button primary-gradient">Start a Chat</button>
|
||||
<button class="demo-button primary-gradient">Join Room</button>
|
||||
<button class="demo-button secondary">View Settings</button>
|
||||
<button class="demo-button secondary">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="section-title">Navigation & Room Cards</h2>
|
||||
<div class="nav-item active">🏠 Home</div>
|
||||
<div class="nav-item">💬 Direct Messages</div>
|
||||
<div class="nav-item">🏢 Workspaces</div>
|
||||
|
||||
<div class="card-demo">
|
||||
<strong>#general</strong><br>
|
||||
<small style="color: #B0B3C1;">Latest: Welcome to the purple theme!</small>
|
||||
</div>
|
||||
<div class="card-demo">
|
||||
<strong>#development</strong><br>
|
||||
<small style="color: #B0B3C1;">Latest: Theme looks amazing 🎨</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="section-title">Input Fields & Forms</h2>
|
||||
<input type="text" class="input-demo" placeholder="Search rooms..." />
|
||||
<input type="text" class="input-demo" placeholder="Type a message..." />
|
||||
<button class="demo-button primary-gradient">Send Message</button>
|
||||
</div>
|
||||
|
||||
<div class="status-message">
|
||||
✅ <strong>Purple Theme Successfully Applied!</strong><br>
|
||||
The refined purple color scheme has been integrated into Cinny with gradient buttons,
|
||||
enhanced cards, and beautiful purple accents throughout the interface.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Add some interactive behavior
|
||||
document.querySelectorAll('.demo-button').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
button.style.transform = 'scale(0.95)';
|
||||
setTimeout(() => {
|
||||
button.style.transform = '';
|
||||
}, 150);
|
||||
});
|
||||
});
|
||||
|
||||
// Simulate typing in inputs
|
||||
document.querySelectorAll('.input-demo').forEach(input => {
|
||||
input.addEventListener('focus', () => {
|
||||
input.style.background = '#3A3B47';
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
input.style.background = '#2A2B35';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
87
simple-server.cjs
Normal file
87
simple-server.cjs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = 4000;
|
||||
const DIST_DIR = path.join(__dirname, 'dist');
|
||||
|
||||
console.log('Starting server...');
|
||||
console.log('Dist directory:', DIST_DIR);
|
||||
console.log('Files in dist:', fs.readdirSync(DIST_DIR));
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
console.log(`${req.method} ${req.url}`);
|
||||
|
||||
// Add CORS headers
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
|
||||
// Handle OPTIONS requests
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Serve index.html for all routes (SPA routing)
|
||||
const filePath = req.url === '/' ?
|
||||
path.join(DIST_DIR, 'index.html') :
|
||||
path.join(DIST_DIR, req.url);
|
||||
|
||||
console.log('Looking for file:', filePath);
|
||||
|
||||
// Check if file exists
|
||||
fs.access(filePath, fs.constants.F_OK, (err) => {
|
||||
if (err) {
|
||||
console.log('File not found, serving index.html');
|
||||
// File doesn't exist, serve index.html for SPA routing
|
||||
const indexPath = path.join(DIST_DIR, 'index.html');
|
||||
fs.readFile(indexPath, (err, data) => {
|
||||
if (err) {
|
||||
console.error('Error loading index.html:', err);
|
||||
res.writeHead(500);
|
||||
res.end('Error loading index.html');
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(data);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// File exists, serve it
|
||||
const ext = path.extname(filePath);
|
||||
const contentType = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'application/javascript',
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.ico': 'image/x-icon',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.wasm': 'application/wasm'
|
||||
}[ext] || 'application/octet-stream';
|
||||
|
||||
console.log('Serving file:', filePath, 'as', contentType);
|
||||
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
console.error('Error loading file:', err);
|
||||
res.writeHead(500);
|
||||
res.end('Error loading file');
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`✅ Server running at http://localhost:${PORT}/`);
|
||||
console.log('🎨 Purple theme should be visible!');
|
||||
});
|
||||
63
simple-server.js
Normal file
63
simple-server.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PORT = 4000;
|
||||
const DIST_DIR = path.join(__dirname, 'dist');
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
console.log(`${req.method} ${req.url}`);
|
||||
|
||||
// Serve index.html for all routes (SPA routing)
|
||||
const filePath = req.url === '/' ?
|
||||
path.join(DIST_DIR, 'index.html') :
|
||||
path.join(DIST_DIR, req.url);
|
||||
|
||||
// Check if file exists
|
||||
fs.access(filePath, fs.constants.F_OK, (err) => {
|
||||
if (err) {
|
||||
// File doesn't exist, serve index.html for SPA routing
|
||||
const indexPath = path.join(DIST_DIR, 'index.html');
|
||||
fs.readFile(indexPath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(500);
|
||||
res.end('Error loading index.html');
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
res.end(data);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// File exists, serve it
|
||||
const ext = path.extname(filePath);
|
||||
const contentType = {
|
||||
'.html': 'text/html',
|
||||
'.js': 'application/javascript',
|
||||
'.css': 'text/css',
|
||||
'.json': 'application/json',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.ico': 'image/x-icon',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.wasm': 'application/wasm'
|
||||
}[ext] || 'application/octet-stream';
|
||||
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(500);
|
||||
res.end('Error loading file');
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Server running at http://localhost:${PORT}/`);
|
||||
});
|
||||
|
|
@ -78,4 +78,39 @@
|
|||
@include state.hover(var(--bg-danger-hover));
|
||||
@include state.focus(var(--bs-danger-outline));
|
||||
@include state.active(var(--bg-danger-active));
|
||||
}
|
||||
|
||||
// Purple theme gradient button enhancement
|
||||
.purple-theme .btn-primary {
|
||||
background: linear-gradient(135deg, #6B46FF, #9D5EFF);
|
||||
box-shadow: 0 4px 15px rgba(107, 70, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #5A3AE5, #8B4FE6);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(107, 70, 255, 0.4);
|
||||
|
||||
&::before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
background: linear-gradient(135deg, #4A2FD4, #7A40CC);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,8 @@ import { Notifications } from './notifications';
|
|||
import { Devices } from './devices';
|
||||
import { EmojisStickers } from './emojis-stickers';
|
||||
import { DeveloperTools } from './developer-tools';
|
||||
// Temporarily disabled for debugging
|
||||
// import { SlidingSyncSettings } from './sliding-sync';
|
||||
import { About } from './about';
|
||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||
import { stopPropagation } from '../../utils/keyboard';
|
||||
|
|
@ -40,6 +42,7 @@ export enum SettingsPages {
|
|||
NotificationPage,
|
||||
DevicesPage,
|
||||
EmojisStickersPage,
|
||||
SlidingSyncPage,
|
||||
DeveloperToolsPage,
|
||||
AboutPage,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import { stopPropagation } from '../../../utils/keyboard';
|
|||
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
|
||||
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||
|
||||
type ThemeSelectorProps = {
|
||||
themeNames: Record<string, string>;
|
||||
|
|
@ -612,6 +613,50 @@ function Messages() {
|
|||
);
|
||||
}
|
||||
|
||||
function Synchronization() {
|
||||
const clientConfig = useClientConfig();
|
||||
|
||||
const isEnabled = clientConfig.slidingSync?.enabled;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Synchronization</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Sliding Sync"
|
||||
description="High-performance synchronization protocol using native Matrix support."
|
||||
after={
|
||||
<Chip
|
||||
variant={isEnabled ? "Success" : "Secondary"}
|
||||
radii="Pill"
|
||||
>
|
||||
<Text size="B300">{isEnabled ? 'Enabled (Native)' : 'Disabled'}</Text>
|
||||
</Chip>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
{isEnabled && (
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Implementation"
|
||||
description={clientConfig.slidingSync?.proxyUrl ? `Proxy: ${clientConfig.slidingSync.proxyUrl}` : 'Native sliding sync (MSC4186)'}
|
||||
/>
|
||||
</SequenceCard>
|
||||
)}
|
||||
|
||||
{!isEnabled && (
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Configuration"
|
||||
description="Sliding sync is disabled. Edit config.json and set slidingSync.enabled to true to enable native sliding sync."
|
||||
/>
|
||||
</SequenceCard>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type GeneralProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
|
|
@ -639,6 +684,7 @@ export function General({ requestClose }: GeneralProps) {
|
|||
<Appearance />
|
||||
<Editor />
|
||||
<Messages />
|
||||
<Synchronization />
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Scroll>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Text,
|
||||
} from 'folds';
|
||||
import { Page, PageContent, PageHeader } from '../../../components/page';
|
||||
import { SequenceCard } from '../../../components/sequence-card';
|
||||
import { SettingTile } from '../../../components/setting-tile';
|
||||
import { SequenceCardStyle } from '../styles.css';
|
||||
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||
|
||||
type SlidingSyncSettingsProps = {
|
||||
requestClose: () => void;
|
||||
};
|
||||
|
||||
export function SlidingSyncSettings({ requestClose }: SlidingSyncSettingsProps) {
|
||||
const clientConfig = useClientConfig();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader outlined={false}>
|
||||
<Box grow="Yes" gap="200">
|
||||
<Box grow="Yes" alignItems="Center" gap="200">
|
||||
<Text size="H3" truncate>
|
||||
Sliding Sync
|
||||
</Text>
|
||||
</Box>
|
||||
<Box shrink="No">
|
||||
<IconButton onClick={requestClose} variant="Surface">
|
||||
<Icon src={Icons.Cross} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</PageHeader>
|
||||
<Box grow="Yes">
|
||||
<PageContent>
|
||||
<Box direction="Column" gap="700">
|
||||
|
||||
{/* Status Section */}
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Status</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Current Status"
|
||||
description="Sliding sync is currently disabled in configuration"
|
||||
after={
|
||||
<Text size="B300">
|
||||
{clientConfig.slidingSync?.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
|
||||
{/* Configuration Section */}
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Configuration</Text>
|
||||
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="Proxy URL"
|
||||
description={clientConfig.slidingSync?.proxyUrl || 'Not configured'}
|
||||
/>
|
||||
</SequenceCard>
|
||||
</Box>
|
||||
|
||||
{/* Info Section */}
|
||||
<Box direction="Column" gap="100">
|
||||
<Text size="L400">Information</Text>
|
||||
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
|
||||
<SettingTile
|
||||
title="About Sliding Sync"
|
||||
description="Sliding sync is a more efficient synchronization protocol that provides better performance, especially for large accounts. It requires a compatible proxy server and homeserver."
|
||||
/>
|
||||
</SequenceCard>
|
||||
|
||||
{!clientConfig.slidingSync?.enabled && (
|
||||
<SequenceCard className={SequenceCardStyle} variant="Warning" direction="Column">
|
||||
<SettingTile
|
||||
title="Configuration Required"
|
||||
description="To enable sliding sync, you need to edit config.json and set slidingSync.enabled to true and provide a valid proxy URL. Restart the application after making changes."
|
||||
/>
|
||||
</SequenceCard>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
</PageContent>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
1
src/app/features/settings/sliding-sync/index.ts
Normal file
1
src/app/features/settings/sliding-sync/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './SlidingSyncSettings';
|
||||
|
|
@ -5,6 +5,20 @@ export type HashRouterConfig = {
|
|||
basename?: string;
|
||||
};
|
||||
|
||||
export type SlidingSyncListConfig = {
|
||||
ranges: number[][];
|
||||
sort?: string[];
|
||||
timeline_limit?: number;
|
||||
required_state?: string[][];
|
||||
filters?: Record<string, any>;
|
||||
};
|
||||
|
||||
export type SlidingSyncConfig = {
|
||||
enabled?: boolean;
|
||||
proxyUrl?: string | null;
|
||||
defaultLists?: Record<string, SlidingSyncListConfig>;
|
||||
};
|
||||
|
||||
export type ClientConfig = {
|
||||
defaultHomeserver?: number;
|
||||
homeserverList?: string[];
|
||||
|
|
@ -18,6 +32,7 @@ export type ClientConfig = {
|
|||
};
|
||||
|
||||
hashRouter?: HashRouterConfig;
|
||||
slidingSync?: SlidingSyncConfig;
|
||||
};
|
||||
|
||||
const ClientConfigContext = createContext<ClientConfig | null>(null);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { lightTheme } from 'folds';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
||||
import { butterTheme, darkTheme, silverTheme } from '../../colors.css';
|
||||
import { butterTheme, darkTheme, silverTheme, purpleTheme } from '../../colors.css';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
|
||||
|
|
@ -37,9 +37,14 @@ export const ButterTheme: Theme = {
|
|||
kind: ThemeKind.Dark,
|
||||
classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-dark'],
|
||||
};
|
||||
export const PurpleTheme: Theme = {
|
||||
id: 'purple-theme',
|
||||
kind: ThemeKind.Dark,
|
||||
classNames: ['purple-theme', purpleTheme, onDarkFontWeight, 'prism-dark'],
|
||||
};
|
||||
|
||||
export const useThemes = (): Theme[] => {
|
||||
const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme], []);
|
||||
const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme, PurpleTheme], []);
|
||||
|
||||
return themes;
|
||||
};
|
||||
|
|
@ -51,6 +56,7 @@ export const useThemeNames = (): Record<string, string> =>
|
|||
[SilverTheme.id]: 'Silver',
|
||||
[DarkTheme.id]: 'Dark',
|
||||
[ButterTheme.id]: 'Butter',
|
||||
[PurpleTheme.id]: 'Purple',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
|
@ -84,14 +90,14 @@ export const useActiveTheme = (): Theme => {
|
|||
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
|
||||
|
||||
if (!systemTheme) {
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? PurpleTheme;
|
||||
|
||||
return selectedTheme;
|
||||
}
|
||||
|
||||
const selectedTheme =
|
||||
systemThemeKind === ThemeKind.Dark
|
||||
? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
|
||||
? themes.find((theme) => theme.id === darkThemeId) ?? PurpleTheme
|
||||
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
|
||||
|
||||
return selectedTheme;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import { useSyncState } from '../../hooks/useSyncState';
|
|||
import { stopPropagation } from '../../utils/keyboard';
|
||||
import { SyncStatus } from './SyncStatus';
|
||||
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
|
||||
import { useClientConfig } from '../../hooks/useClientConfig';
|
||||
|
||||
function ClientRootLoading() {
|
||||
return (
|
||||
|
|
@ -147,13 +148,14 @@ type ClientRootProps = {
|
|||
export function ClientRoot({ children }: ClientRootProps) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { baseUrl } = getSecret();
|
||||
const clientConfig = useClientConfig();
|
||||
|
||||
const [loadState, loadMatrix] = useAsyncCallback<MatrixClient, Error, []>(
|
||||
useCallback(() => initClient(getSecret() as any), [])
|
||||
useCallback(() => initClient(getSecret() as any, clientConfig), [clientConfig])
|
||||
);
|
||||
const mx = loadState.status === AsyncStatus.Success ? loadState.data : undefined;
|
||||
const [startState, startMatrix] = useAsyncCallback<void, Error, [MatrixClient]>(
|
||||
useCallback((m) => startClient(m), [])
|
||||
useCallback((m) => startClient(m, clientConfig), [clientConfig])
|
||||
);
|
||||
|
||||
useLogoutListener(mx);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React, { useCallback, useState } from 'react';
|
|||
import { Box, config, Line, Text } from 'folds';
|
||||
import { useSyncState } from '../../hooks/useSyncState';
|
||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||
import { useSlidingSyncEnabled, useSlidingSyncState, useSlidingSyncError } from '../../state/sliding-sync';
|
||||
|
||||
type StateData = {
|
||||
current: SyncState | null;
|
||||
|
|
@ -18,6 +19,10 @@ export function SyncStatus({ mx }: SyncStatusProps) {
|
|||
previous: undefined,
|
||||
});
|
||||
|
||||
const slidingSyncEnabled = useSlidingSyncEnabled();
|
||||
const slidingSyncState = useSlidingSyncState();
|
||||
const slidingSyncError = useSlidingSyncError();
|
||||
|
||||
useSyncState(
|
||||
mx,
|
||||
useCallback((current, previous) => {
|
||||
|
|
@ -30,6 +35,45 @@ export function SyncStatus({ mx }: SyncStatusProps) {
|
|||
}, [])
|
||||
);
|
||||
|
||||
// Handle sliding sync status if enabled
|
||||
if (slidingSyncEnabled) {
|
||||
if (slidingSyncError) {
|
||||
return (
|
||||
<Box direction="Column" shrink="No">
|
||||
<Box
|
||||
className={ContainerColor({ variant: 'Critical' })}
|
||||
style={{ padding: `${config.space.S100} 0` }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
>
|
||||
<Text size="L400">Sliding Sync Error!</Text>
|
||||
</Box>
|
||||
<Line variant="Critical" size="300" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (slidingSyncState === 'SYNCING') {
|
||||
return (
|
||||
<Box direction="Column" shrink="No">
|
||||
<Box
|
||||
className={ContainerColor({ variant: 'Primary' })}
|
||||
style={{ padding: `${config.space.S100} 0` }}
|
||||
alignItems="Center"
|
||||
justifyContent="Center"
|
||||
>
|
||||
<Text size="L400">Sliding Sync Active...</Text>
|
||||
</Box>
|
||||
<Line variant="Primary" size="300" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// Don't show traditional sync status when sliding sync is active
|
||||
return null;
|
||||
}
|
||||
|
||||
// Traditional sync status handling
|
||||
if (
|
||||
(stateData.current === SyncState.Prepared ||
|
||||
stateData.current === SyncState.Syncing ||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,28 @@ import { mDirectAtom, useBindMDirectAtom } from '../mDirectList';
|
|||
import { roomToUnreadAtom, useBindRoomToUnreadAtom } from '../room/roomToUnread';
|
||||
import { roomToParentsAtom, useBindRoomToParentsAtom } from '../room/roomToParents';
|
||||
import { roomIdToTypingMembersAtom, useBindRoomIdToTypingMembersAtom } from '../typingMembers';
|
||||
import { useBindSlidingSyncAtom, useSlidingSyncRoomListBridge, useShouldUseSlidingSync } from '../sliding-sync';
|
||||
import { getSlidingSync } from '../../../client/initMatrix';
|
||||
|
||||
export const useBindAtoms = (mx: MatrixClient) => {
|
||||
const slidingSync = getSlidingSync();
|
||||
const shouldUseSlidingSync = useShouldUseSlidingSync();
|
||||
|
||||
// Bind sliding sync atoms if enabled
|
||||
useBindSlidingSyncAtom(mx, slidingSync);
|
||||
|
||||
// Bridge sliding sync data to existing room list atoms
|
||||
useSlidingSyncRoomListBridge();
|
||||
|
||||
// Only bind traditional room list atoms if not using sliding sync
|
||||
if (!shouldUseSlidingSync) {
|
||||
useBindAllInvitesAtom(mx, allInvitesAtom);
|
||||
useBindAllRoomsAtom(mx, allRoomsAtom);
|
||||
}
|
||||
|
||||
// These atoms are always bound regardless of sync method
|
||||
useBindMDirectAtom(mx, mDirectAtom);
|
||||
useBindAllInvitesAtom(mx, allInvitesAtom);
|
||||
useBindAllRoomsAtom(mx, allRoomsAtom);
|
||||
useBindRoomToParentsAtom(mx, roomToParentsAtom);
|
||||
useBindRoomToUnreadAtom(mx, roomToUnreadAtom);
|
||||
|
||||
useBindRoomIdToTypingMembersAtom(mx, roomIdToTypingMembersAtom);
|
||||
};
|
||||
|
|
|
|||
3
src/app/state/sliding-sync/index.ts
Normal file
3
src/app/state/sliding-sync/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './slidingSync';
|
||||
export * from './useSlidingSync';
|
||||
export * from './roomListBridge';
|
||||
40
src/app/state/sliding-sync/roomListBridge.ts
Normal file
40
src/app/state/sliding-sync/roomListBridge.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { useSetAtom, useAtomValue } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import { allRoomsAtom } from '../room-list/roomList';
|
||||
import { allInvitesAtom } from '../room-list/inviteList';
|
||||
import { slidingSyncRoomListAtom, slidingSyncEnabledAtom } from './slidingSync';
|
||||
|
||||
/**
|
||||
* Bridge sliding sync room list data to existing Cinny room list atoms
|
||||
* This allows existing UI components to work seamlessly with sliding sync
|
||||
*/
|
||||
export const useSlidingSyncRoomListBridge = () => {
|
||||
const slidingSyncEnabled = useAtomValue(slidingSyncEnabledAtom);
|
||||
const slidingSyncRoomList = useAtomValue(slidingSyncRoomListAtom);
|
||||
const setAllRooms = useSetAtom(allRoomsAtom);
|
||||
const setAllInvites = useSetAtom(allInvitesAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!slidingSyncEnabled) return;
|
||||
|
||||
// Bridge sliding sync room lists to existing atoms
|
||||
const allRoomsList = slidingSyncRoomList.allRooms || [];
|
||||
const directMessagesList = slidingSyncRoomList.directMessages || [];
|
||||
const invitesList = slidingSyncRoomList.invites || [];
|
||||
|
||||
// Combine all rooms and DMs for the all rooms atom
|
||||
const combinedRooms = [...allRoomsList, ...directMessagesList];
|
||||
|
||||
// Update existing atoms
|
||||
setAllRooms({ type: 'INITIALIZE', rooms: combinedRooms });
|
||||
setAllInvites({ type: 'INITIALIZE', invites: invitesList });
|
||||
|
||||
}, [slidingSyncEnabled, slidingSyncRoomList, setAllRooms, setAllInvites]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to determine if sliding sync should override traditional sync behavior
|
||||
*/
|
||||
export const useShouldUseSlidingSync = () => {
|
||||
return useAtomValue(slidingSyncEnabledAtom);
|
||||
};
|
||||
20
src/app/state/sliding-sync/slidingSync.ts
Normal file
20
src/app/state/sliding-sync/slidingSync.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { atom } from 'jotai';
|
||||
import { SlidingSync } from 'matrix-js-sdk/lib/sliding-sync';
|
||||
|
||||
// Sliding sync instance atom
|
||||
export const slidingSyncAtom = atom<SlidingSync | null>(null);
|
||||
|
||||
// Sliding sync state atom
|
||||
export const slidingSyncStateAtom = atom<'PREPARED' | 'SYNCING' | 'STOPPED' | 'ERROR'>('STOPPED');
|
||||
|
||||
// Sliding sync enabled atom
|
||||
export const slidingSyncEnabledAtom = atom<boolean>(false);
|
||||
|
||||
// Room list data from sliding sync
|
||||
export const slidingSyncRoomListAtom = atom<Record<string, string[]>>({});
|
||||
|
||||
// Room data from sliding sync
|
||||
export const slidingSyncRoomDataAtom = atom<Record<string, any>>({});
|
||||
|
||||
// Error state for sliding sync
|
||||
export const slidingSyncErrorAtom = atom<Error | null>(null);
|
||||
79
src/app/state/sliding-sync/useSlidingSync.ts
Normal file
79
src/app/state/sliding-sync/useSlidingSync.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { useSetAtom, useAtomValue } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { SlidingSync, SlidingSyncEvent } from 'matrix-js-sdk/lib/sliding-sync';
|
||||
import {
|
||||
slidingSyncAtom,
|
||||
slidingSyncStateAtom,
|
||||
slidingSyncEnabledAtom,
|
||||
slidingSyncRoomListAtom,
|
||||
slidingSyncRoomDataAtom,
|
||||
slidingSyncErrorAtom
|
||||
} from './slidingSync';
|
||||
|
||||
export const useBindSlidingSyncAtom = (mx: MatrixClient, slidingSync: SlidingSync | null) => {
|
||||
const setSlidingSync = useSetAtom(slidingSyncAtom);
|
||||
const setSlidingSyncState = useSetAtom(slidingSyncStateAtom);
|
||||
const setSlidingSyncEnabled = useSetAtom(slidingSyncEnabledAtom);
|
||||
const setSlidingSyncRoomList = useSetAtom(slidingSyncRoomListAtom);
|
||||
const setSlidingSyncRoomData = useSetAtom(slidingSyncRoomDataAtom);
|
||||
const setSlidingSyncError = useSetAtom(slidingSyncErrorAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!slidingSync) {
|
||||
setSlidingSyncEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setSlidingSync(slidingSync);
|
||||
setSlidingSyncEnabled(true);
|
||||
|
||||
const handleSyncUpdate = () => {
|
||||
setSlidingSyncState('SYNCING');
|
||||
|
||||
// Extract room lists
|
||||
const lists: Record<string, string[]> = {};
|
||||
slidingSync.getListData().forEach((listData, listKey) => {
|
||||
lists[listKey] = listData.joinedRooms || [];
|
||||
});
|
||||
setSlidingSyncRoomList(lists);
|
||||
|
||||
// Extract room data
|
||||
const roomData: Record<string, any> = {};
|
||||
slidingSync.getRoomData().forEach((data, roomId) => {
|
||||
roomData[roomId] = data;
|
||||
});
|
||||
setSlidingSyncRoomData(roomData);
|
||||
};
|
||||
|
||||
const handleSyncComplete = () => {
|
||||
setSlidingSyncState('PREPARED');
|
||||
setSlidingSyncError(null);
|
||||
};
|
||||
|
||||
const handleSyncError = (error: Error) => {
|
||||
setSlidingSyncState('ERROR');
|
||||
setSlidingSyncError(error);
|
||||
};
|
||||
|
||||
// Bind sliding sync events
|
||||
slidingSync.on(SlidingSyncEvent.List, handleSyncUpdate);
|
||||
slidingSync.on(SlidingSyncEvent.RoomData, handleSyncUpdate);
|
||||
// Note: These event names might need adjustment based on actual SDK events
|
||||
slidingSync.on('sync' as any, handleSyncComplete);
|
||||
slidingSync.on('error' as any, handleSyncError);
|
||||
|
||||
return () => {
|
||||
slidingSync.removeListener(SlidingSyncEvent.List, handleSyncUpdate);
|
||||
slidingSync.removeListener(SlidingSyncEvent.RoomData, handleSyncUpdate);
|
||||
slidingSync.removeListener('sync' as any, handleSyncComplete);
|
||||
slidingSync.removeListener('error' as any, handleSyncError);
|
||||
};
|
||||
}, [mx, slidingSync, setSlidingSync, setSlidingSyncState, setSlidingSyncEnabled, setSlidingSyncRoomList, setSlidingSyncRoomData, setSlidingSyncError]);
|
||||
};
|
||||
|
||||
export const useSlidingSyncEnabled = () => useAtomValue(slidingSyncEnabledAtom);
|
||||
export const useSlidingSyncState = () => useAtomValue(slidingSyncStateAtom);
|
||||
export const useSlidingSyncRoomList = () => useAtomValue(slidingSyncRoomListAtom);
|
||||
export const useSlidingSyncRoomData = () => useAtomValue(slidingSyncRoomDataAtom);
|
||||
export const useSlidingSyncError = () => useAtomValue(slidingSyncErrorAtom);
|
||||
65
src/app/styles/PurpleGradient.css.ts
Normal file
65
src/app/styles/PurpleGradient.css.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { color } from 'folds';
|
||||
|
||||
export const purpleGradientButton = style({
|
||||
background: 'linear-gradient(135deg, #6B46FF, #9D5EFF)',
|
||||
border: 'none',
|
||||
color: '#FFFFFF',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
transition: 'all 0.3s ease',
|
||||
boxShadow: '0 4px 15px rgba(107, 70, 255, 0.2)',
|
||||
|
||||
'::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: '-100%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent)',
|
||||
transition: 'left 0.5s ease',
|
||||
},
|
||||
|
||||
':hover': {
|
||||
background: 'linear-gradient(135deg, #5A3AE5, #8B4FE6)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 10px 25px rgba(107, 70, 255, 0.4)',
|
||||
},
|
||||
|
||||
':hover::before': {
|
||||
left: '100%',
|
||||
},
|
||||
|
||||
':active': {
|
||||
transform: 'translateY(0)',
|
||||
background: 'linear-gradient(135deg, #4A2FD4, #7A40CC)',
|
||||
},
|
||||
});
|
||||
|
||||
export const purpleGradientCard = style({
|
||||
background: 'linear-gradient(135deg, rgba(107, 70, 255, 0.1), rgba(157, 94, 255, 0.05))',
|
||||
border: '1px solid rgba(107, 70, 255, 0.2)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
|
||||
':hover': {
|
||||
background: 'linear-gradient(135deg, rgba(107, 70, 255, 0.15), rgba(157, 94, 255, 0.08))',
|
||||
border: '1px solid rgba(107, 70, 255, 0.3)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 10px 30px rgba(107, 70, 255, 0.2)',
|
||||
},
|
||||
});
|
||||
|
||||
export const purpleThemeColors = style({
|
||||
selectors: {
|
||||
'.purple-theme &': {
|
||||
'--accent-gradient': 'linear-gradient(135deg, #6B46FF, #9D5EFF)',
|
||||
'--accent-gradient-hover': 'linear-gradient(135deg, #5A3AE5, #8B4FE6)',
|
||||
'--accent-gradient-active': 'linear-gradient(135deg, #4A2FD4, #7A40CC)',
|
||||
'--surface-gradient': 'linear-gradient(135deg, rgba(107, 70, 255, 0.1), rgba(157, 94, 255, 0.05))',
|
||||
'--surface-gradient-hover': 'linear-gradient(135deg, rgba(107, 70, 255, 0.15), rgba(157, 94, 255, 0.08))',
|
||||
'--purple-glow': '0 10px 30px rgba(107, 70, 255, 0.2)',
|
||||
'--purple-glow-strong': '0 10px 30px rgba(107, 70, 255, 0.4)',
|
||||
},
|
||||
},
|
||||
});
|
||||
76
src/app/utils/sliding-sync.ts
Normal file
76
src/app/utils/sliding-sync.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* Validates a sliding sync proxy URL
|
||||
*/
|
||||
export const validateSlidingSyncProxyUrl = (url: string): boolean => {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
// Must be HTTPS for security
|
||||
if (parsed.protocol !== 'https:') return false;
|
||||
// Must have a hostname
|
||||
if (!parsed.hostname) return false;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a human-readable error message for invalid proxy URLs
|
||||
*/
|
||||
export const getSlidingSyncProxyUrlError = (url: string): string | null => {
|
||||
if (!url) return 'Proxy URL is required';
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.protocol !== 'https:') {
|
||||
return 'Proxy URL must use HTTPS for security';
|
||||
}
|
||||
if (!parsed.hostname) {
|
||||
return 'Proxy URL must have a valid hostname';
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return 'Invalid URL format';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Common sliding sync proxy URLs for reference
|
||||
*/
|
||||
export const SLIDING_SYNC_PROXY_EXAMPLES = [
|
||||
'https://syncv3.matrix.org',
|
||||
'https://sliding-sync.matrix.org',
|
||||
'https://your-server.com/_matrix/sliding-sync',
|
||||
];
|
||||
|
||||
/**
|
||||
* Default sliding sync list configurations
|
||||
*/
|
||||
export const DEFAULT_SLIDING_SYNC_LISTS = {
|
||||
allRooms: {
|
||||
ranges: [[0, 49]],
|
||||
sort: ['by_recency', 'by_name'],
|
||||
timeline_limit: 1,
|
||||
required_state: [
|
||||
['m.room.name', ''],
|
||||
['m.room.avatar', ''],
|
||||
['m.room.canonical_alias', ''],
|
||||
['m.room.topic', ''],
|
||||
['m.room.encryption', ''],
|
||||
['m.room.member', '$ME'],
|
||||
],
|
||||
},
|
||||
directMessages: {
|
||||
ranges: [[0, 49]],
|
||||
sort: ['by_recency'],
|
||||
timeline_limit: 1,
|
||||
filters: {
|
||||
is_dm: true,
|
||||
},
|
||||
required_state: [
|
||||
['m.room.name', ''],
|
||||
['m.room.avatar', ''],
|
||||
['m.room.member', '$ME'],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
@ -1,7 +1,10 @@
|
|||
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
|
||||
import { SlidingSync } from 'matrix-js-sdk/lib/sliding-sync';
|
||||
import { SlidingSyncSdk } from 'matrix-js-sdk/lib/sliding-sync-sdk';
|
||||
|
||||
import { cryptoCallbacks } from './state/secretStorageKeys';
|
||||
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
||||
import { ClientConfig } from '../app/hooks/useClientConfig';
|
||||
|
||||
type Session = {
|
||||
baseUrl: string;
|
||||
|
|
@ -10,7 +13,7 @@ type Session = {
|
|||
deviceId: string;
|
||||
};
|
||||
|
||||
export const initClient = async (session: Session): Promise<MatrixClient> => {
|
||||
export const initClient = async (session: Session, clientConfig?: ClientConfig): Promise<MatrixClient> => {
|
||||
const indexedDBStore = new IndexedDBStore({
|
||||
indexedDB: global.indexedDB,
|
||||
localStorage: global.localStorage,
|
||||
|
|
@ -39,7 +42,63 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
|
|||
return mx;
|
||||
};
|
||||
|
||||
export const startClient = async (mx: MatrixClient) => {
|
||||
// Store sliding sync instance globally for atom access
|
||||
let globalSlidingSync: SlidingSync | null = null;
|
||||
|
||||
export const getSlidingSync = (): SlidingSync | null => globalSlidingSync;
|
||||
|
||||
export const startClient = async (mx: MatrixClient, clientConfig?: ClientConfig) => {
|
||||
// Check if sliding sync is enabled
|
||||
if (clientConfig?.slidingSync?.enabled) {
|
||||
try {
|
||||
const lists = new Map();
|
||||
Object.entries(clientConfig.slidingSync.defaultLists || {}).forEach(([key, config]) => {
|
||||
lists.set(key, {
|
||||
ranges: config.ranges,
|
||||
sort: config.sort || ['by_recency'],
|
||||
timeline_limit: config.timeline_limit || 1,
|
||||
required_state: config.required_state || [],
|
||||
filters: config.filters,
|
||||
});
|
||||
});
|
||||
|
||||
// Use native sliding sync if no proxy URL, otherwise use proxy
|
||||
const proxyUrl = clientConfig.slidingSync.proxyUrl;
|
||||
const slidingSync = new SlidingSync(
|
||||
proxyUrl || mx.baseUrl, // Use homeserver base URL for native sliding sync
|
||||
lists,
|
||||
{},
|
||||
mx,
|
||||
5000 // timeout
|
||||
);
|
||||
|
||||
// Store globally for atom access
|
||||
globalSlidingSync = slidingSync;
|
||||
|
||||
// Use SlidingSyncSdk as a drop-in replacement for traditional sync
|
||||
const slidingSyncSdk = new SlidingSyncSdk(slidingSync, mx);
|
||||
|
||||
// Start sliding sync with timeout and error handling
|
||||
const slidingSyncPromise = slidingSync.start();
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Sliding sync timeout')), 10000)
|
||||
);
|
||||
|
||||
await Promise.race([slidingSyncPromise, timeoutPromise]);
|
||||
|
||||
console.log('Sliding sync started successfully');
|
||||
return;
|
||||
} catch (error) {
|
||||
console.warn('Sliding sync failed, falling back to traditional sync:', error);
|
||||
globalSlidingSync = null;
|
||||
// Fall through to traditional sync
|
||||
}
|
||||
}
|
||||
|
||||
// Reset global sliding sync for traditional sync
|
||||
globalSlidingSync = null;
|
||||
|
||||
// Fallback to traditional sync
|
||||
await mx.startClient({
|
||||
lazyLoadMembers: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -236,3 +236,100 @@ export const butterTheme = createTheme(color, {
|
|||
OnContainer: '#F2EED3',
|
||||
},
|
||||
});
|
||||
|
||||
export const purpleTheme = createTheme(color, {
|
||||
Background: {
|
||||
Container: '#1A1B23',
|
||||
ContainerHover: '#2A2B35',
|
||||
ContainerActive: '#3A3B47',
|
||||
ContainerLine: '#4A4B57',
|
||||
OnContainer: '#FFFFFF',
|
||||
},
|
||||
|
||||
Surface: {
|
||||
Container: '#2A2B35',
|
||||
ContainerHover: '#3A3B47',
|
||||
ContainerActive: '#4A4B57',
|
||||
ContainerLine: '#5A5B67',
|
||||
OnContainer: '#FFFFFF',
|
||||
},
|
||||
|
||||
SurfaceVariant: {
|
||||
Container: '#3A3B47',
|
||||
ContainerHover: '#4A4B57',
|
||||
ContainerActive: '#5A5B67',
|
||||
ContainerLine: '#6A6B77',
|
||||
OnContainer: '#FFFFFF',
|
||||
},
|
||||
|
||||
Primary: {
|
||||
Main: '#6B46FF',
|
||||
MainHover: '#5A3AE5',
|
||||
MainActive: '#4A2FD4',
|
||||
MainLine: '#3A1FBF',
|
||||
OnMain: '#FFFFFF',
|
||||
Container: '#F4F1FF',
|
||||
ContainerHover: '#E9E4FF',
|
||||
ContainerActive: '#DDD6FF',
|
||||
ContainerLine: '#D1C8FF',
|
||||
OnContainer: '#2D1B7B',
|
||||
},
|
||||
|
||||
Secondary: {
|
||||
Main: '#9D5EFF',
|
||||
MainHover: '#8B4FE6',
|
||||
MainActive: '#7A40CC',
|
||||
MainLine: '#6930B3',
|
||||
OnMain: '#FFFFFF',
|
||||
Container: '#B89FFF',
|
||||
ContainerHover: '#B299FF',
|
||||
ContainerActive: '#AC93FF',
|
||||
ContainerLine: '#A68DFF',
|
||||
OnContainer: '#2D1B7B',
|
||||
},
|
||||
|
||||
Success: {
|
||||
Main: '#10B981',
|
||||
MainHover: '#059669',
|
||||
MainActive: '#047857',
|
||||
MainLine: '#065F46',
|
||||
OnMain: '#FFFFFF',
|
||||
Container: '#D1FAE5',
|
||||
ContainerHover: '#A7F3D0',
|
||||
ContainerActive: '#6EE7B7',
|
||||
ContainerLine: '#34D399',
|
||||
OnContainer: '#065F46',
|
||||
},
|
||||
|
||||
Warning: {
|
||||
Main: '#F59E0B',
|
||||
MainHover: '#D97706',
|
||||
MainActive: '#B45309',
|
||||
MainLine: '#92400E',
|
||||
OnMain: '#FFFFFF',
|
||||
Container: '#FEF3C7',
|
||||
ContainerHover: '#FDE68A',
|
||||
ContainerActive: '#FCD34D',
|
||||
ContainerLine: '#FBBF24',
|
||||
OnContainer: '#92400E',
|
||||
},
|
||||
|
||||
Critical: {
|
||||
Main: '#EF4444',
|
||||
MainHover: '#DC2626',
|
||||
MainActive: '#B91C1C',
|
||||
MainLine: '#991B1B',
|
||||
OnMain: '#FFFFFF',
|
||||
Container: '#FEE2E2',
|
||||
ContainerHover: '#FECACA',
|
||||
ContainerActive: '#FCA5A5',
|
||||
ContainerLine: '#F87171',
|
||||
OnContainer: '#991B1B',
|
||||
},
|
||||
|
||||
Other: {
|
||||
FocusRing: 'rgba(107, 70, 255, 0.5)',
|
||||
Shadow: 'rgba(0, 0, 0, 1)',
|
||||
Overlay: 'rgba(0, 0, 0, 0.8)',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,73 @@
|
|||
font-display: swap;
|
||||
}
|
||||
|
||||
// Purple theme gradient enhancements
|
||||
.purple-theme {
|
||||
// Gradient buttons for folds components
|
||||
.Button[data-variant='Primary'] {
|
||||
background: linear-gradient(135deg, #6B46FF, #9D5EFF) !important;
|
||||
box-shadow: 0 4px 15px rgba(107, 70, 255, 0.2);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #5A3AE5, #8B4FE6) !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(107, 70, 255, 0.4);
|
||||
|
||||
&::before {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
background: linear-gradient(135deg, #4A2FD4, #7A40CC) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced cards and surfaces with subtle gradients
|
||||
.SequenceCard,
|
||||
.InfoCard,
|
||||
.RoomCard {
|
||||
background: linear-gradient(135deg, rgba(107, 70, 255, 0.05), rgba(157, 94, 255, 0.02));
|
||||
border: 1px solid rgba(107, 70, 255, 0.1);
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, rgba(107, 70, 255, 0.1), rgba(157, 94, 255, 0.05));
|
||||
border: 1px solid rgba(107, 70, 255, 0.2);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 25px rgba(107, 70, 255, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation items with purple accents
|
||||
.NavItem[data-selected='true'] {
|
||||
background: linear-gradient(135deg, rgba(107, 70, 255, 0.15), rgba(157, 94, 255, 0.08)) !important;
|
||||
border-left: 3px solid #6B46FF;
|
||||
}
|
||||
|
||||
// Room input with gradient border focus
|
||||
.RoomInput {
|
||||
&:focus-within {
|
||||
border: 1px solid #6B46FF;
|
||||
box-shadow: 0 0 0 3px rgba(107, 70, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
/* background color | --bg-[background type]: value */
|
||||
--bg-surface: #ffffff;
|
||||
|
|
@ -193,8 +260,8 @@
|
|||
--fluid-slide-up: cubic-bezier(0.13, 0.56, 0.25, 0.99);
|
||||
|
||||
--font-emoji: 'Twemoji_DISABLED';
|
||||
--font-primary: 'InterVariable', var(--font-emoji), sans-serif;
|
||||
--font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
|
||||
--font-primary: 'SF Pro Display', var(--font-emoji), sans-serif;
|
||||
--font-secondary: 'SF Pro Display', var(--font-emoji), sans-serif;
|
||||
}
|
||||
|
||||
.silver-theme {
|
||||
|
|
@ -326,7 +393,7 @@
|
|||
--fw-medium: 450;
|
||||
--fw-bold: 550;
|
||||
|
||||
--font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
|
||||
--font-secondary: 'SF Pro Display', var(--font-emoji), sans-serif;
|
||||
}
|
||||
|
||||
.butter-theme {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue