feat: Add complete Docker deployment with web-based setup wizard
Major additions: - Web-based setup wizard (setup.php, setup_wizard.php, setup-wizard.js) - Production Docker configuration (docker-compose.prod.yml, .env.production) - Database initialization SQL files (deploy/init_settings.sql) - Template builder system with drag-and-drop UI - Advanced features (OAuth, CDN, enhanced analytics, monetization) - Comprehensive documentation (deployment guides, quick start, feature docs) - Design system with accessibility and responsive layout - Deployment automation scripts (deploy.ps1, generate-secrets.ps1) Setup wizard allows customization of: - Platform name and branding - Domain configuration - Membership tiers and pricing - Admin credentials - Feature toggles Database includes 270+ tables for complete video streaming platform with advanced features for analytics, moderation, template building, and monetization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
BIN
f_scripts/be/fonts/codropsicons/.DS_Store
vendored
Normal file
BIN
f_scripts/be/fonts/codropsicons/.DS_Store
vendored
Normal file
Binary file not shown.
907
f_scripts/fe/css/builder/builder.css
Normal file
907
f_scripts/fe/css/builder/builder.css
Normal file
@@ -0,0 +1,907 @@
|
||||
/**
|
||||
* Template Builder Styles
|
||||
* Drag and drop template builder interface
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
Variables
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
--tb-primary: #3b82f6;
|
||||
--tb-success: #10b981;
|
||||
--tb-danger: #ef4444;
|
||||
--tb-warning: #f59e0b;
|
||||
--tb-secondary: #6b7280;
|
||||
|
||||
--tb-bg: #ffffff;
|
||||
--tb-bg-secondary: #f9fafb;
|
||||
--tb-bg-tertiary: #f3f4f6;
|
||||
--tb-border: #e5e7eb;
|
||||
--tb-text: #111827;
|
||||
--tb-text-secondary: #6b7280;
|
||||
|
||||
--tb-header-height: 60px;
|
||||
--tb-sidebar-width: 280px;
|
||||
--tb-toolbar-height: 50px;
|
||||
|
||||
--tb-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--tb-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
--tb-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
--tb-radius: 6px;
|
||||
--tb-radius-sm: 4px;
|
||||
--tb-radius-lg: 8px;
|
||||
|
||||
--tb-transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Dark Mode */
|
||||
[data-theme*="dark"] .template-builder,
|
||||
.template-builder[data-theme*="dark"] {
|
||||
--tb-bg: #1f2937;
|
||||
--tb-bg-secondary: #111827;
|
||||
--tb-bg-tertiary: #374151;
|
||||
--tb-border: #374151;
|
||||
--tb-text: #f9fafb;
|
||||
--tb-text-secondary: #9ca3af;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Layout
|
||||
========================================================================== */
|
||||
|
||||
.template-builder {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--tb-bg);
|
||||
color: var(--tb-text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.tb-header {
|
||||
height: var(--tb-header-height);
|
||||
background: var(--tb-bg-secondary);
|
||||
border-bottom: 1px solid var(--tb-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.tb-header-left,
|
||||
.tb-header-center,
|
||||
.tb-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tb-header-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tb-header-center {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.tb-header-right {
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tb-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Sidebars
|
||||
========================================================================== */
|
||||
|
||||
.tb-sidebar {
|
||||
width: var(--tb-sidebar-width);
|
||||
background: var(--tb-bg-secondary);
|
||||
border-right: 1px solid var(--tb-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
transition: var(--tb-transition);
|
||||
}
|
||||
|
||||
.tb-sidebar-right {
|
||||
border-right: none;
|
||||
border-left: 1px solid var(--tb-border);
|
||||
}
|
||||
|
||||
.tb-sidebar.collapsed {
|
||||
width: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.tb-sidebar-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--tb-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tb-sidebar-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tb-sidebar-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--tb-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: var(--tb-radius-sm);
|
||||
transition: var(--tb-transition);
|
||||
}
|
||||
|
||||
.tb-sidebar-toggle:hover {
|
||||
background: var(--tb-bg-tertiary);
|
||||
color: var(--tb-text);
|
||||
}
|
||||
|
||||
.tb-sidebar-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Canvas
|
||||
========================================================================== */
|
||||
|
||||
.tb-canvas-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--tb-bg-tertiary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tb-canvas-toolbar {
|
||||
height: var(--tb-toolbar-height);
|
||||
background: var(--tb-bg-secondary);
|
||||
border-bottom: 1px solid var(--tb-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.tb-zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tb-zoom-level {
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--tb-text-secondary);
|
||||
}
|
||||
|
||||
.tb-canvas-options {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.tb-canvas-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.tb-canvas {
|
||||
background: white;
|
||||
min-height: 100%;
|
||||
margin: 0 auto;
|
||||
box-shadow: var(--tb-shadow-lg);
|
||||
position: relative;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.tb-canvas[data-device="desktop"] {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.tb-canvas[data-device="tablet"] {
|
||||
width: 768px;
|
||||
}
|
||||
|
||||
.tb-canvas[data-device="mobile"] {
|
||||
width: 375px;
|
||||
}
|
||||
|
||||
.tb-canvas.show-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(0, 0, 0, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(0, 0, 0, 0.03) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.tb-sections-container {
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* Drop Hint */
|
||||
.tb-drop-hint {
|
||||
padding: 80px 40px;
|
||||
text-align: center;
|
||||
color: var(--tb-text-secondary);
|
||||
border: 2px dashed var(--tb-border);
|
||||
border-radius: var(--tb-radius-lg);
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
.tb-drop-hint-content i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tb-drop-hint-content p {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tb-drop-hint.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Sections
|
||||
========================================================================== */
|
||||
|
||||
.tb-section {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border: 2px dashed transparent;
|
||||
transition: var(--tb-transition);
|
||||
}
|
||||
|
||||
.tb-section:hover {
|
||||
border-color: var(--tb-border);
|
||||
}
|
||||
|
||||
.tb-section.selected {
|
||||
border-color: var(--tb-primary);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.tb-section.drag-over {
|
||||
border-color: var(--tb-success);
|
||||
background: rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
.tb-section-controls {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
right: 10px;
|
||||
background: white;
|
||||
border: 1px solid var(--tb-border);
|
||||
border-radius: var(--tb-radius);
|
||||
display: none;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
box-shadow: var(--tb-shadow);
|
||||
}
|
||||
|
||||
.tb-section:hover .tb-section-controls,
|
||||
.tb-section.selected .tb-section-controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tb-columns {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Blocks (Components)
|
||||
========================================================================== */
|
||||
|
||||
.tb-block {
|
||||
position: relative;
|
||||
min-height: 50px;
|
||||
border: 2px dashed transparent;
|
||||
border-radius: var(--tb-radius);
|
||||
transition: var(--tb-transition);
|
||||
}
|
||||
|
||||
.tb-block:hover {
|
||||
border-color: var(--tb-border);
|
||||
}
|
||||
|
||||
.tb-block.selected {
|
||||
border-color: var(--tb-primary);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.tb-block.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tb-block-controls {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
right: 10px;
|
||||
background: white;
|
||||
border: 1px solid var(--tb-border);
|
||||
border-radius: var(--tb-radius);
|
||||
display: none;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
box-shadow: var(--tb-shadow);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tb-block:hover .tb-block-controls,
|
||||
.tb-block.selected .tb-block-controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Components List (Sidebar)
|
||||
========================================================================== */
|
||||
|
||||
.tb-search-box {
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tb-search-box input {
|
||||
width: 100%;
|
||||
padding: 8px 12px 8px 36px;
|
||||
border: 1px solid var(--tb-border);
|
||||
border-radius: var(--tb-radius);
|
||||
background: var(--tb-bg);
|
||||
color: var(--tb-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.tb-search-box i {
|
||||
position: absolute;
|
||||
left: 28px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--tb-text-secondary);
|
||||
}
|
||||
|
||||
.tb-component-categories {
|
||||
padding: 0 16px 16px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tb-category-btn {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--tb-border);
|
||||
background: var(--tb-bg);
|
||||
color: var(--tb-text);
|
||||
border-radius: var(--tb-radius);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: var(--tb-transition);
|
||||
}
|
||||
|
||||
.tb-category-btn:hover {
|
||||
background: var(--tb-bg-tertiary);
|
||||
}
|
||||
|
||||
.tb-category-btn.active {
|
||||
background: var(--tb-primary);
|
||||
border-color: var(--tb-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tb-components-list {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.tb-component-item {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--tb-border);
|
||||
border-radius: var(--tb-radius);
|
||||
margin-bottom: 12px;
|
||||
cursor: grab;
|
||||
background: var(--tb-bg);
|
||||
transition: var(--tb-transition);
|
||||
}
|
||||
|
||||
.tb-component-item:hover {
|
||||
border-color: var(--tb-primary);
|
||||
box-shadow: var(--tb-shadow);
|
||||
}
|
||||
|
||||
.tb-component-item:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.tb-component-item.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tb-component-thumb {
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background: var(--tb-bg-tertiary);
|
||||
border-radius: var(--tb-radius-sm);
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tb-component-thumb img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.tb-component-thumb i {
|
||||
font-size: 32px;
|
||||
color: var(--tb-text-secondary);
|
||||
}
|
||||
|
||||
.tb-component-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.tb-component-desc {
|
||||
font-size: 12px;
|
||||
color: var(--tb-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Properties Panel
|
||||
========================================================================== */
|
||||
|
||||
.tb-no-selection {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: var(--tb-text-secondary);
|
||||
}
|
||||
|
||||
.tb-no-selection i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tb-properties-section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tb-properties-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tb-properties-header h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.tb-form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tb-form-group label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
color: var(--tb-text);
|
||||
}
|
||||
|
||||
.tb-input,
|
||||
.tb-select,
|
||||
.tb-textarea {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--tb-border);
|
||||
border-radius: var(--tb-radius);
|
||||
background: var(--tb-bg);
|
||||
color: var(--tb-text);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
transition: var(--tb-transition);
|
||||
}
|
||||
|
||||
.tb-input:focus,
|
||||
.tb-select:focus,
|
||||
.tb-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--tb-primary);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.tb-input-sm {
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tb-textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
input[type="color"].tb-input {
|
||||
height: 40px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.tb-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tb-checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tb-spacing-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Collapsible */
|
||||
.tb-collapsible {
|
||||
border-top: 1px solid var(--tb-border);
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.tb-collapsible-header {
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--tb-text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tb-collapsible-header i {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.tb-collapsible.open .tb-collapsible-header i {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.tb-collapsible-content {
|
||||
display: none;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.tb-collapsible.open .tb-collapsible-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Buttons
|
||||
========================================================================== */
|
||||
|
||||
.tb-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--tb-radius);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: var(--tb-transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tb-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tb-btn-primary {
|
||||
background: var(--tb-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tb-btn-primary:hover:not(:disabled) {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.tb-btn-success {
|
||||
background: var(--tb-success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tb-btn-success:hover:not(:disabled) {
|
||||
background: #059669;
|
||||
}
|
||||
|
||||
.tb-btn-danger {
|
||||
background: var(--tb-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tb-btn-danger:hover:not(:disabled) {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.tb-btn-secondary {
|
||||
background: var(--tb-bg-tertiary);
|
||||
color: var(--tb-text);
|
||||
border-color: var(--tb-border);
|
||||
}
|
||||
|
||||
.tb-btn-secondary:hover:not(:disabled) {
|
||||
background: var(--tb-bg-secondary);
|
||||
}
|
||||
|
||||
.tb-btn-icon {
|
||||
padding: 8px;
|
||||
min-width: 36px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tb-btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tb-btn i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Device Preview Buttons */
|
||||
.tb-device-preview {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: var(--tb-bg-tertiary);
|
||||
padding: 4px;
|
||||
border-radius: var(--tb-radius);
|
||||
}
|
||||
|
||||
.tb-device-btn {
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--tb-text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--tb-radius-sm);
|
||||
transition: var(--tb-transition);
|
||||
}
|
||||
|
||||
.tb-device-btn:hover {
|
||||
color: var(--tb-text);
|
||||
}
|
||||
|
||||
.tb-device-btn.active {
|
||||
background: var(--tb-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Template Name Input */
|
||||
.tb-template-name-input {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--tb-radius);
|
||||
background: var(--tb-bg-tertiary);
|
||||
color: var(--tb-text);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
max-width: 300px;
|
||||
transition: var(--tb-transition);
|
||||
}
|
||||
|
||||
.tb-template-name-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--tb-primary);
|
||||
background: var(--tb-bg);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Modal
|
||||
========================================================================== */
|
||||
|
||||
.tb-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
.tb-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.tb-modal-content {
|
||||
position: relative;
|
||||
background: var(--tb-bg);
|
||||
max-width: 500px;
|
||||
margin: 100px auto;
|
||||
border-radius: var(--tb-radius-lg);
|
||||
box-shadow: var(--tb-shadow-lg);
|
||||
}
|
||||
|
||||
.tb-modal-large {
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.tb-modal-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid var(--tb-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tb-modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.tb-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--tb-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: var(--tb-radius-sm);
|
||||
transition: var(--tb-transition);
|
||||
}
|
||||
|
||||
.tb-modal-close:hover {
|
||||
background: var(--tb-bg-tertiary);
|
||||
color: var(--tb-text);
|
||||
}
|
||||
|
||||
.tb-modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.tb-modal-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--tb-border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Loading
|
||||
========================================================================== */
|
||||
|
||||
.tb-loading {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
color: var(--tb-text-secondary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Utilities
|
||||
========================================================================== */
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Drag and Drop */
|
||||
.tb-dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.tb-drag-placeholder {
|
||||
border: 2px dashed var(--tb-primary);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
border-radius: var(--tb-radius);
|
||||
min-height: 100px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
.tb-sidebar-content::-webkit-scrollbar,
|
||||
.tb-canvas-wrapper::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.tb-sidebar-content::-webkit-scrollbar-track,
|
||||
.tb-canvas-wrapper::-webkit-scrollbar-track {
|
||||
background: var(--tb-bg-secondary);
|
||||
}
|
||||
|
||||
.tb-sidebar-content::-webkit-scrollbar-thumb,
|
||||
.tb-canvas-wrapper::-webkit-scrollbar-thumb {
|
||||
background: var(--tb-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tb-sidebar-content::-webkit-scrollbar-thumb:hover,
|
||||
.tb-canvas-wrapper::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--tb-text-secondary);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Responsive
|
||||
========================================================================== */
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.tb-sidebar {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tb-header {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.tb-sidebar {
|
||||
position: absolute;
|
||||
top: var(--tb-header-height);
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
box-shadow: var(--tb-shadow-lg);
|
||||
}
|
||||
|
||||
.tb-sidebar-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.tb-sidebar-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.tb-canvas-wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
1079
f_scripts/fe/js/builder/builder-core.js
Normal file
1079
f_scripts/fe/js/builder/builder-core.js
Normal file
File diff suppressed because it is too large
Load Diff
423
f_scripts/fe/js/setup-wizard.js
Normal file
423
f_scripts/fe/js/setup-wizard.js
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* EasyStream Setup Wizard - Frontend Logic
|
||||
* Handles wizard navigation, form validation, and AJAX submissions
|
||||
*/
|
||||
|
||||
let currentStep = 1;
|
||||
const totalSteps = 9;
|
||||
let setupData = {};
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateProgressBar();
|
||||
attachEventListeners();
|
||||
loadSavedData();
|
||||
});
|
||||
|
||||
function attachEventListeners() {
|
||||
// Color picker updates
|
||||
document.getElementById('primaryColor').addEventListener('input', function(e) {
|
||||
document.getElementById('primaryColorHex').textContent = e.target.value;
|
||||
});
|
||||
|
||||
document.getElementById('secondaryColor').addEventListener('input', function(e) {
|
||||
document.getElementById('secondaryColorHex').textContent = e.target.value;
|
||||
});
|
||||
|
||||
// Save form data on input
|
||||
const inputs = document.querySelectorAll('input, select, textarea');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('change', saveFormData);
|
||||
});
|
||||
}
|
||||
|
||||
function changeStep(direction) {
|
||||
const nextStep = currentStep + direction;
|
||||
|
||||
// Validate before moving forward
|
||||
if (direction > 0) {
|
||||
if (!validateCurrentStep()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for certain steps
|
||||
if (currentStep === 7 && direction > 0) {
|
||||
// Review step - show summary and install
|
||||
showReviewSummary();
|
||||
startInstallation();
|
||||
}
|
||||
|
||||
if (nextStep >= 1 && nextStep <= totalSteps) {
|
||||
// Hide current step
|
||||
document.getElementById(`step${currentStep}`).classList.remove('active');
|
||||
|
||||
// Show next step
|
||||
currentStep = nextStep;
|
||||
document.getElementById(`step${currentStep}`).classList.add('active');
|
||||
|
||||
// Update progress bar
|
||||
updateProgressBar();
|
||||
|
||||
// Update button visibility
|
||||
updateButtons();
|
||||
|
||||
// Scroll to top
|
||||
document.querySelector('.wizard-body').scrollTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
const progress = ((currentStep - 1) / (totalSteps - 1)) * 100;
|
||||
document.getElementById('progressBar').style.width = progress + '%';
|
||||
}
|
||||
|
||||
function updateButtons() {
|
||||
const prevBtn = document.getElementById('prevBtn');
|
||||
const nextBtn = document.getElementById('nextBtn');
|
||||
|
||||
// Show/hide previous button
|
||||
prevBtn.style.display = currentStep > 1 && currentStep < 8 ? 'block' : 'none';
|
||||
|
||||
// Update next button text and visibility
|
||||
if (currentStep === 1) {
|
||||
nextBtn.textContent = 'Get Started →';
|
||||
} else if (currentStep === 7) {
|
||||
nextBtn.textContent = 'Install Now →';
|
||||
} else if (currentStep === 8 || currentStep === 9) {
|
||||
nextBtn.style.display = 'none';
|
||||
} else {
|
||||
nextBtn.textContent = 'Next →';
|
||||
}
|
||||
}
|
||||
|
||||
function validateCurrentStep() {
|
||||
clearAlert();
|
||||
|
||||
switch (currentStep) {
|
||||
case 1:
|
||||
// Welcome page, no validation needed
|
||||
return true;
|
||||
|
||||
case 2:
|
||||
// Platform configuration
|
||||
const platformName = document.getElementById('platformName').value.trim();
|
||||
const domainName = document.getElementById('domainName').value.trim();
|
||||
const contactEmail = document.getElementById('contactEmail').value.trim();
|
||||
|
||||
if (!platformName) {
|
||||
showAlert('error', 'Please enter a platform name');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!domainName) {
|
||||
showAlert('error', 'Please enter a domain name');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!contactEmail || !isValidEmail(contactEmail)) {
|
||||
showAlert('error', 'Please enter a valid contact email');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case 3:
|
||||
// Branding, no strict validation
|
||||
return true;
|
||||
|
||||
case 4:
|
||||
// Membership tiers
|
||||
const tier1Name = document.getElementById('tier1Name').value.trim();
|
||||
const tier2Name = document.getElementById('tier2Name').value.trim();
|
||||
const tier3Name = document.getElementById('tier3Name').value.trim();
|
||||
|
||||
if (!tier1Name || !tier2Name || !tier3Name) {
|
||||
showAlert('error', 'Please enter names for all membership tiers');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case 5:
|
||||
// Admin account
|
||||
const username = document.getElementById('adminUsername').value.trim();
|
||||
const email = document.getElementById('adminEmail').value.trim();
|
||||
const password = document.getElementById('adminPassword').value;
|
||||
const passwordConfirm = document.getElementById('adminPasswordConfirm').value;
|
||||
|
||||
if (!username || username.length < 4) {
|
||||
showAlert('error', 'Username must be at least 4 characters');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
||||
showAlert('error', 'Username can only contain letters, numbers, and underscores');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!email || !isValidEmail(email)) {
|
||||
showAlert('error', 'Please enter a valid admin email');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
showAlert('error', 'Password must be at least 8 characters long');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/[0-9]/.test(password)) {
|
||||
showAlert('error', 'Password must contain uppercase, lowercase, and numbers');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
showAlert('error', 'Passwords do not match');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
case 6:
|
||||
// Features, no strict validation
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
function saveFormData() {
|
||||
// Save to localStorage for recovery
|
||||
const formData = collectFormData();
|
||||
localStorage.setItem('easystreamSetup', JSON.stringify(formData));
|
||||
}
|
||||
|
||||
function loadSavedData() {
|
||||
// Load from localStorage if available
|
||||
const saved = localStorage.getItem('easystreamSetup');
|
||||
if (saved) {
|
||||
try {
|
||||
const data = JSON.parse(saved);
|
||||
// Populate form fields
|
||||
Object.keys(data).forEach(key => {
|
||||
const element = document.getElementById(key);
|
||||
if (element) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = data[key];
|
||||
} else {
|
||||
element.value = data[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load saved data:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function collectFormData() {
|
||||
const formData = {};
|
||||
|
||||
// Platform config
|
||||
formData.platformName = document.getElementById('platformName').value;
|
||||
formData.platformTagline = document.getElementById('platformTagline').value;
|
||||
formData.domainName = document.getElementById('domainName').value;
|
||||
formData.contactEmail = document.getElementById('contactEmail').value;
|
||||
formData.timezone = document.getElementById('timezone').value;
|
||||
|
||||
// Branding
|
||||
formData.primaryColor = document.getElementById('primaryColor').value;
|
||||
formData.secondaryColor = document.getElementById('secondaryColor').value;
|
||||
formData.defaultTheme = document.getElementById('defaultTheme').value;
|
||||
formData.enableTheming = document.getElementById('enableTheming').checked;
|
||||
|
||||
// Membership tiers
|
||||
formData.tier1Name = document.getElementById('tier1Name').value;
|
||||
formData.tier1Upload = document.getElementById('tier1Upload').value;
|
||||
formData.tier1Storage = document.getElementById('tier1Storage').value;
|
||||
|
||||
formData.tier2Name = document.getElementById('tier2Name').value;
|
||||
formData.tier2Upload = document.getElementById('tier2Upload').value;
|
||||
formData.tier2Storage = document.getElementById('tier2Storage').value;
|
||||
formData.tier2Price = document.getElementById('tier2Price').value;
|
||||
|
||||
formData.tier3Name = document.getElementById('tier3Name').value;
|
||||
formData.tier3Upload = document.getElementById('tier3Upload').value;
|
||||
formData.tier3Storage = document.getElementById('tier3Storage').value;
|
||||
formData.tier3Price = document.getElementById('tier3Price').value;
|
||||
|
||||
// Admin account
|
||||
formData.adminUsername = document.getElementById('adminUsername').value;
|
||||
formData.adminEmail = document.getElementById('adminEmail').value;
|
||||
formData.adminPassword = document.getElementById('adminPassword').value;
|
||||
formData.adminPasswordConfirm = document.getElementById('adminPasswordConfirm').value;
|
||||
formData.adminDisplayName = document.getElementById('adminDisplayName').value;
|
||||
|
||||
// Features
|
||||
formData.enableRegistration = document.getElementById('enableRegistration').checked;
|
||||
formData.enableEmailVerification = document.getElementById('enableEmailVerification').checked;
|
||||
formData.enableLiveStreaming = document.getElementById('enableLiveStreaming').checked;
|
||||
formData.enableComments = document.getElementById('enableComments').checked;
|
||||
formData.enableDownloads = document.getElementById('enableDownloads').checked;
|
||||
formData.enableMonetization = document.getElementById('enableMonetization').checked;
|
||||
formData.enableTemplateBuilder = document.getElementById('enableTemplateBuilder').checked;
|
||||
formData.enableAnalytics = document.getElementById('enableAnalytics').checked;
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
function showReviewSummary() {
|
||||
const data = collectFormData();
|
||||
const summary = document.getElementById('reviewSummary');
|
||||
|
||||
summary.innerHTML = `
|
||||
<div style="margin-bottom: 24px;">
|
||||
<h3 style="color: #667eea; margin-bottom: 12px;">Platform Configuration</h3>
|
||||
<p><strong>Name:</strong> ${data.platformName}</p>
|
||||
<p><strong>Domain:</strong> ${data.domainName}</p>
|
||||
<p><strong>Email:</strong> ${data.contactEmail}</p>
|
||||
<p><strong>Timezone:</strong> ${data.timezone}</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 24px;">
|
||||
<h3 style="color: #667eea; margin-bottom: 12px;">Membership Tiers</h3>
|
||||
<p><strong>${data.tier1Name}:</strong> ${data.tier1Upload}MB upload, ${data.tier1Storage}GB storage</p>
|
||||
<p><strong>${data.tier2Name}:</strong> ${data.tier2Upload}MB upload, ${data.tier2Storage}GB storage ($${data.tier2Price}/month)</p>
|
||||
<p><strong>${data.tier3Name}:</strong> ${data.tier3Upload}MB upload, ${data.tier3Storage}GB storage ($${data.tier3Price}/month)</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 24px;">
|
||||
<h3 style="color: #667eea; margin-bottom: 12px;">Admin Account</h3>
|
||||
<p><strong>Username:</strong> ${data.adminUsername}</p>
|
||||
<p><strong>Email:</strong> ${data.adminEmail}</p>
|
||||
<p><strong>Display Name:</strong> ${data.adminDisplayName || 'Administrator'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style="color: #667eea; margin-bottom: 12px;">Enabled Features</h3>
|
||||
<p>${data.enableRegistration ? '✓' : '✗'} User Registration</p>
|
||||
<p>${data.enableLiveStreaming ? '✓' : '✗'} Live Streaming</p>
|
||||
<p>${data.enableComments ? '✓' : '✗'} Video Comments</p>
|
||||
<p>${data.enableMonetization ? '✓' : '✗'} Monetization</p>
|
||||
<p>${data.enableTemplateBuilder ? '✓' : '✗'} Template Builder</p>
|
||||
<p>${data.enableAnalytics ? '✓' : '✗'} Analytics</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function startInstallation() {
|
||||
// Move to installation step
|
||||
document.getElementById('step7').classList.remove('active');
|
||||
currentStep = 8;
|
||||
document.getElementById('step8').classList.add('active');
|
||||
updateProgressBar();
|
||||
updateButtons();
|
||||
|
||||
const data = collectFormData();
|
||||
setupData = data;
|
||||
|
||||
try {
|
||||
// Step 1: Save configuration
|
||||
await installStep('Saving configuration...', async () => {
|
||||
const response = await fetch('setup.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ action: 'save_configuration', ...data })
|
||||
});
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
// Step 2: Create admin user
|
||||
await installStep('Creating admin account...', async () => {
|
||||
const response = await fetch('setup.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ action: 'create_admin', ...data })
|
||||
});
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
// Step 3: Finalize setup
|
||||
await installStep('Finalizing setup...', async () => {
|
||||
const response = await fetch('setup.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ action: 'finalize', ...data })
|
||||
});
|
||||
return await response.json();
|
||||
});
|
||||
|
||||
// Success! Move to final step
|
||||
document.getElementById('step8').classList.remove('active');
|
||||
currentStep = 9;
|
||||
document.getElementById('step9').classList.add('active');
|
||||
updateProgressBar();
|
||||
|
||||
// Populate final details
|
||||
document.getElementById('finalPlatformName').textContent = data.platformName;
|
||||
document.getElementById('finalDomainName').textContent = data.domainName;
|
||||
document.getElementById('finalAdminUsername').textContent = data.adminUsername;
|
||||
|
||||
// Clear saved data
|
||||
localStorage.removeItem('easystreamSetup');
|
||||
|
||||
} catch (error) {
|
||||
document.getElementById('installStatus').textContent = 'Installation Failed';
|
||||
document.getElementById('installStep').innerHTML = `
|
||||
<div style="color: #e53e3e; padding: 20px; background: #fed7d7; border-radius: 8px;">
|
||||
<strong>Error:</strong> ${error.message}
|
||||
<br><br>
|
||||
<button onclick="location.reload()" class="btn btn-primary">Try Again</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function installStep(message, action) {
|
||||
document.getElementById('installStep').textContent = message;
|
||||
|
||||
// Add progress item
|
||||
const progress = document.getElementById('installProgress');
|
||||
const item = document.createElement('div');
|
||||
item.style.cssText = 'padding: 12px; background: #f7fafc; border-radius: 6px; margin-bottom: 8px; display: flex; align-items: center;';
|
||||
item.innerHTML = `<span class="spinner" style="width: 16px; height: 16px; margin-right: 12px; border-width: 2px;"></span><span>${message}</span>`;
|
||||
progress.appendChild(item);
|
||||
|
||||
try {
|
||||
const result = await action();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Unknown error occurred');
|
||||
}
|
||||
|
||||
// Update to success
|
||||
item.innerHTML = `<span style="color: #48bb78; font-size: 20px; margin-right: 12px;">✓</span><span>${message} <strong style="color: #48bb78;">Done!</strong></span>`;
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
item.innerHTML = `<span style="color: #e53e3e; font-size: 20px; margin-right: 12px;">✗</span><span>${message} <strong style="color: #e53e3e;">Failed!</strong></span>`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function showAlert(type, message) {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.className = `alert alert-${type} show`;
|
||||
alert.textContent = message;
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
alert.classList.remove('show');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function clearAlert() {
|
||||
const alert = document.getElementById('alert');
|
||||
alert.classList.remove('show');
|
||||
}
|
||||
515
f_scripts/shared/accessibility.css
Normal file
515
f_scripts/shared/accessibility.css
Normal file
@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* EasyStream Accessibility Enhancements
|
||||
* WCAG 2.1 AA Compliance CSS
|
||||
* Version: 1.0
|
||||
*/
|
||||
|
||||
/* ==================== FOCUS INDICATORS ==================== */
|
||||
|
||||
/* Enhanced focus styles for keyboard navigation */
|
||||
*:focus {
|
||||
outline: none; /* Remove default */
|
||||
}
|
||||
|
||||
*:focus-visible {
|
||||
outline: 3px solid var(--focus-ring-color, #06a2cb);
|
||||
outline-offset: 2px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Specific focus styles for interactive elements */
|
||||
a:focus-visible,
|
||||
button:focus-visible,
|
||||
input:focus-visible,
|
||||
textarea:focus-visible,
|
||||
select:focus-visible,
|
||||
[role="button"]:focus-visible,
|
||||
[role="link"]:focus-visible,
|
||||
[tabindex]:focus-visible {
|
||||
outline: 3px solid var(--focus-ring-color, #06a2cb);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Video player controls focus */
|
||||
.video-js button:focus-visible,
|
||||
.vjs-control:focus-visible {
|
||||
outline: 3px solid #fff;
|
||||
outline-offset: 2px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* ==================== CONTRAST IMPROVEMENTS ==================== */
|
||||
|
||||
/* Ensure minimum 4.5:1 contrast ratio for normal text */
|
||||
body {
|
||||
color: var(--color-text-primary, #111827);
|
||||
background: var(--color-bg-primary, #ffffff);
|
||||
}
|
||||
|
||||
[data-theme*="dark"] body {
|
||||
color: var(--color-text-primary, #f0f0f0);
|
||||
background: var(--color-bg-primary, #121212);
|
||||
}
|
||||
|
||||
/* Link contrast */
|
||||
a {
|
||||
color: var(--secondary-color, #0793e2);
|
||||
text-decoration-skip-ink: auto;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:focus {
|
||||
color: var(--primary-color, #06a2cb);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Button contrast */
|
||||
.btn,
|
||||
button {
|
||||
/* Ensure text contrast on buttons */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.button-blue {
|
||||
background: var(--primary-color, #06a2cb);
|
||||
color: #ffffff;
|
||||
border: 2px solid var(--primary-color, #06a2cb);
|
||||
}
|
||||
|
||||
.btn-primary:hover,
|
||||
.button-blue:hover {
|
||||
background: var(--third-color, #92cefb);
|
||||
border-color: var(--third-color, #92cefb);
|
||||
}
|
||||
|
||||
/* ==================== TEXT SIZING & READABILITY ==================== */
|
||||
|
||||
/* Responsive font sizes */
|
||||
html {
|
||||
font-size: 16px; /* Base size for 1rem */
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Line height for readability (WCAG SC 1.4.8) */
|
||||
p,
|
||||
li,
|
||||
dd,
|
||||
dt {
|
||||
line-height: 1.5;
|
||||
max-width: 80ch; /* Optimal line length for readability */
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1.3;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* Prevent text from being too small */
|
||||
.small-thumbs h2,
|
||||
.small-thumbs h3,
|
||||
.text-sm,
|
||||
small {
|
||||
font-size: max(0.875rem, 14px);
|
||||
}
|
||||
|
||||
/* ==================== TOUCH TARGETS ==================== */
|
||||
|
||||
/* Minimum 44x44px touch targets (WCAG SC 2.5.5) */
|
||||
button,
|
||||
a,
|
||||
input[type="checkbox"],
|
||||
input[type="radio"],
|
||||
select,
|
||||
.btn,
|
||||
.touch-target,
|
||||
[role="button"],
|
||||
[role="link"] {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
/* Exception for text-only links in paragraphs */
|
||||
p a,
|
||||
li a {
|
||||
min-height: auto;
|
||||
min-width: auto;
|
||||
display: inline;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* Icon buttons */
|
||||
.icon-button,
|
||||
button[class*="icon-"],
|
||||
a[class*="icon-"] {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* ==================== SKIP LINKS ==================== */
|
||||
|
||||
.skip-links {
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: 0;
|
||||
z-index: 10000;
|
||||
background: var(--primary-color, #06a2cb);
|
||||
}
|
||||
|
||||
.skip-links a {
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: 0;
|
||||
padding: 12px 16px;
|
||||
background: var(--primary-color, #06a2cb);
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border-radius: 0 0 4px 0;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.skip-links a:focus {
|
||||
top: 0;
|
||||
outline: 3px solid #ffffff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ==================== SCREEN READER ONLY ==================== */
|
||||
|
||||
.sr-only,
|
||||
.visually-hidden {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border-width: 0 !important;
|
||||
}
|
||||
|
||||
/* Allow screen reader text to be focusable when navigated to via keyboard */
|
||||
.sr-only:focus,
|
||||
.visually-hidden:focus {
|
||||
position: static !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
clip: auto !important;
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
/* ==================== FORM LABELS & INPUTS ==================== */
|
||||
|
||||
/* Ensure labels are visible and associated */
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
/* Required field indicator */
|
||||
.required::after,
|
||||
label.required::after {
|
||||
content: " *";
|
||||
color: var(--color-error, #ef4444);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Error states */
|
||||
input:invalid:not(:placeholder-shown),
|
||||
.input-error,
|
||||
.error {
|
||||
border-color: var(--color-error, #ef4444) !important;
|
||||
background: var(--color-error-bg, #fee2e2);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: var(--color-error-text, #991b1b);
|
||||
font-size: 0.875rem;
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.error-message::before {
|
||||
content: "⚠";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Success states */
|
||||
.input-success {
|
||||
border-color: var(--color-success, #10b981) !important;
|
||||
background: var(--color-success-bg, #d1fae5);
|
||||
}
|
||||
|
||||
/* ==================== REDUCED MOTION ==================== */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
|
||||
/* Keep essential animations but make them instant */
|
||||
.spinner,
|
||||
.loading,
|
||||
[class*="animate"] {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== HIGH CONTRAST MODE ==================== */
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
* {
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
button,
|
||||
a,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
border: 2px solid currentColor !important;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline-width: 4px !important;
|
||||
outline-offset: 3px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== DARK MODE PREFERENCES ==================== */
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* Respect system preference if no theme is set */
|
||||
body:not([data-theme]) {
|
||||
background: #121212;
|
||||
color: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== TABLES ACCESSIBILITY ==================== */
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
background: var(--color-bg-tertiary);
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
/* Caption for table context */
|
||||
caption {
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 8px 0;
|
||||
caption-side: top;
|
||||
}
|
||||
|
||||
/* ==================== IMAGES & MEDIA ==================== */
|
||||
|
||||
/* Ensure images don't overflow and have alt text */
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Decorative images should have empty alt */
|
||||
img[alt=""],
|
||||
img:not([alt]) {
|
||||
outline: 2px solid orange; /* Dev warning for missing alt */
|
||||
}
|
||||
|
||||
/* Video controls must be accessible */
|
||||
video:focus {
|
||||
outline: 3px solid var(--focus-ring-color, #06a2cb);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ==================== HEADINGS HIERARCHY ==================== */
|
||||
|
||||
/* Ensure proper heading hierarchy is visually clear */
|
||||
h1 {
|
||||
font-size: clamp(1.5rem, 5vw, 2.25rem);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: clamp(1.25rem, 4vw, 1.875rem);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: clamp(1.125rem, 3vw, 1.5rem);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: clamp(1rem, 2.5vw, 1.25rem);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h5, h6 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ==================== LISTS ==================== */
|
||||
|
||||
/* Ensure lists have proper spacing */
|
||||
ul, ol {
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
/* ==================== MODALS & OVERLAYS ==================== */
|
||||
|
||||
/* Modal focus trap indicator */
|
||||
[role="dialog"]:focus,
|
||||
.modal:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Modal backdrop */
|
||||
.modal-backdrop,
|
||||
[role="dialog"]::backdrop {
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
/* Prevent background scroll when modal is open */
|
||||
body.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ==================== LANGUAGE & TEXT DIRECTION ==================== */
|
||||
|
||||
/* Support for RTL languages */
|
||||
[dir="rtl"] {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir="rtl"] .sidebar {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* ==================== STATUS MESSAGES ==================== */
|
||||
|
||||
/* Live regions for dynamic content */
|
||||
[role="status"],
|
||||
[role="alert"],
|
||||
[aria-live] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-message,
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
margin: 8px 0;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: var(--color-success-bg);
|
||||
color: var(--color-success-text);
|
||||
border-color: var(--color-success);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background: var(--color-error-bg);
|
||||
color: var(--color-error-text);
|
||||
border-color: var(--color-error);
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background: var(--color-warning-bg);
|
||||
color: var(--color-warning-text);
|
||||
border-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.status-info {
|
||||
background: var(--color-info-bg);
|
||||
color: var(--color-info-text);
|
||||
border-color: var(--color-info);
|
||||
}
|
||||
|
||||
/* ==================== PRINT STYLES ==================== */
|
||||
|
||||
@media print {
|
||||
/* Remove unnecessary elements */
|
||||
nav,
|
||||
aside,
|
||||
.no-print,
|
||||
.sidebar,
|
||||
header,
|
||||
footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Optimize for print */
|
||||
* {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
box-shadow: none !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Show URLs for links */
|
||||
a[href]::after {
|
||||
content: " (" attr(href) ")";
|
||||
}
|
||||
|
||||
/* Prevent page breaks inside elements */
|
||||
img,
|
||||
pre,
|
||||
blockquote,
|
||||
table,
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
}
|
||||
1
f_scripts/shared/datepicker/dist/date-range-picker.min.js.map
vendored
Normal file
1
f_scripts/shared/datepicker/dist/date-range-picker.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
f_scripts/shared/datepicker/dist/tiny-date-picker.min.js.map
vendored
Normal file
1
f_scripts/shared/datepicker/dist/tiny-date-picker.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
427
f_scripts/shared/design-system.css
Normal file
427
f_scripts/shared/design-system.css
Normal file
@@ -0,0 +1,427 @@
|
||||
/**
|
||||
* EasyStream Design System
|
||||
* Comprehensive design tokens and variables for consistent UI/UX
|
||||
* Version: 2.0
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ==================== SPACING SYSTEM ==================== */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--space-xl: 32px;
|
||||
--space-2xl: 48px;
|
||||
--space-3xl: 64px;
|
||||
|
||||
/* ==================== TYPOGRAPHY ==================== */
|
||||
--font-family-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-family-mono: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
|
||||
|
||||
--font-size-xs: 0.75rem; /* 12px */
|
||||
--font-size-sm: 0.875rem; /* 14px */
|
||||
--font-size-md: 1rem; /* 16px */
|
||||
--font-size-lg: 1.125rem; /* 18px */
|
||||
--font-size-xl: 1.25rem; /* 20px */
|
||||
--font-size-2xl: 1.5rem; /* 24px */
|
||||
--font-size-3xl: 1.875rem; /* 30px */
|
||||
--font-size-4xl: 2.25rem; /* 36px */
|
||||
|
||||
--font-weight-light: 300;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--line-height-tight: 1.25;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.75;
|
||||
|
||||
/* ==================== COLORS - NEUTRAL ==================== */
|
||||
--color-white: #ffffff;
|
||||
--color-black: #000000;
|
||||
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-300: #d1d5db;
|
||||
--color-gray-400: #9ca3af;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
|
||||
/* ==================== SEMANTIC COLORS - LIGHT THEME ==================== */
|
||||
--color-bg-primary: var(--color-white);
|
||||
--color-bg-secondary: var(--color-gray-50);
|
||||
--color-bg-tertiary: var(--color-gray-100);
|
||||
--color-bg-elevated: var(--color-white);
|
||||
--color-bg-overlay: rgba(0, 0, 0, 0.5);
|
||||
|
||||
--color-text-primary: var(--color-gray-900);
|
||||
--color-text-secondary: var(--color-gray-600);
|
||||
--color-text-tertiary: var(--color-gray-500);
|
||||
--color-text-inverse: var(--color-white);
|
||||
--color-text-disabled: var(--color-gray-400);
|
||||
|
||||
--color-border-primary: var(--color-gray-300);
|
||||
--color-border-secondary: var(--color-gray-200);
|
||||
--color-border-focus: var(--primary-color);
|
||||
|
||||
/* ==================== STATUS COLORS ==================== */
|
||||
--color-success-bg: #d1fae5;
|
||||
--color-success-text: #065f46;
|
||||
--color-success-border: #34d399;
|
||||
--color-success: #10b981;
|
||||
|
||||
--color-warning-bg: #fef3c7;
|
||||
--color-warning-text: #92400e;
|
||||
--color-warning-border: #fbbf24;
|
||||
--color-warning: #f59e0b;
|
||||
|
||||
--color-error-bg: #fee2e2;
|
||||
--color-error-text: #991b1b;
|
||||
--color-error-border: #f87171;
|
||||
--color-error: #ef4444;
|
||||
|
||||
--color-info-bg: #dbeafe;
|
||||
--color-info-text: #1e40af;
|
||||
--color-info-border: #60a5fa;
|
||||
--color-info: #3b82f6;
|
||||
|
||||
/* ==================== BORDERS & RADIUS ==================== */
|
||||
--border-width-thin: 1px;
|
||||
--border-width-medium: 2px;
|
||||
--border-width-thick: 4px;
|
||||
|
||||
--border-radius-none: 0;
|
||||
--border-radius-sm: 4px;
|
||||
--border-radius-md: 8px;
|
||||
--border-radius-lg: 12px;
|
||||
--border-radius-xl: 16px;
|
||||
--border-radius-2xl: 24px;
|
||||
--border-radius-full: 9999px;
|
||||
|
||||
/* ==================== SHADOWS ==================== */
|
||||
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
--shadow-focus: 0 0 0 3px rgba(6, 162, 203, 0.5);
|
||||
|
||||
/* ==================== Z-INDEX ==================== */
|
||||
--z-index-dropdown: 1000;
|
||||
--z-index-sticky: 1020;
|
||||
--z-index-fixed: 1030;
|
||||
--z-index-modal-backdrop: 1040;
|
||||
--z-index-modal: 1050;
|
||||
--z-index-popover: 1060;
|
||||
--z-index-tooltip: 1070;
|
||||
--z-index-notification: 1080;
|
||||
|
||||
/* ==================== TRANSITIONS ==================== */
|
||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-slowest: 500ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* ==================== BREAKPOINTS (for reference in media queries) ==================== */
|
||||
/* Mobile: 0-639px */
|
||||
/* Tablet: 640-1023px */
|
||||
/* Desktop: 1024-1279px */
|
||||
/* Large Desktop: 1280px+ */
|
||||
|
||||
/* ==================== ACCESSIBILITY ==================== */
|
||||
--focus-ring-width: 3px;
|
||||
--focus-ring-offset: 2px;
|
||||
--focus-ring-color: var(--color-border-focus);
|
||||
|
||||
--touch-target-min: 44px; /* Minimum touch target size for mobile */
|
||||
|
||||
/* ==================== ANIMATIONS ==================== */
|
||||
--animation-duration-fast: 150ms;
|
||||
--animation-duration-normal: 300ms;
|
||||
--animation-duration-slow: 500ms;
|
||||
}
|
||||
|
||||
/* ==================== DARK THEME OVERRIDES ==================== */
|
||||
[data-theme*="dark"] {
|
||||
--color-bg-primary: #121212;
|
||||
--color-bg-secondary: #1c1c1c;
|
||||
--color-bg-tertiary: #272727;
|
||||
--color-bg-elevated: #1f1f1f;
|
||||
--color-bg-overlay: rgba(0, 0, 0, 0.75);
|
||||
|
||||
--color-text-primary: #f0f0f0;
|
||||
--color-text-secondary: #d0d0d0;
|
||||
--color-text-tertiary: #aaa;
|
||||
--color-text-inverse: #000;
|
||||
--color-text-disabled: #666;
|
||||
|
||||
--color-border-primary: #2e2e2e;
|
||||
--color-border-secondary: #232323;
|
||||
|
||||
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
||||
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.4), 0 1px 2px -1px rgba(0, 0, 0, 0.4);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.5), 0 2px 4px -2px rgba(0, 0, 0, 0.5);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.6), 0 4px 6px -4px rgba(0, 0, 0, 0.6);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.7), 0 8px 10px -6px rgba(0, 0, 0, 0.7);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
|
||||
|
||||
--color-success-bg: #064e3b;
|
||||
--color-success-text: #6ee7b7;
|
||||
--color-warning-bg: #78350f;
|
||||
--color-warning-text: #fcd34d;
|
||||
--color-error-bg: #7f1d1d;
|
||||
--color-error-text: #fca5a5;
|
||||
--color-info-bg: #1e3a8a;
|
||||
--color-info-text: #93c5fd;
|
||||
}
|
||||
|
||||
/* ==================== UTILITY CLASSES ==================== */
|
||||
|
||||
/* Spacing utilities */
|
||||
.m-0 { margin: 0; }
|
||||
.m-xs { margin: var(--space-xs); }
|
||||
.m-sm { margin: var(--space-sm); }
|
||||
.m-md { margin: var(--space-md); }
|
||||
.m-lg { margin: var(--space-lg); }
|
||||
.m-xl { margin: var(--space-xl); }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-xs { padding: var(--space-xs); }
|
||||
.p-sm { padding: var(--space-sm); }
|
||||
.p-md { padding: var(--space-md); }
|
||||
.p-lg { padding: var(--space-lg); }
|
||||
.p-xl { padding: var(--space-xl); }
|
||||
|
||||
/* Typography utilities */
|
||||
.text-xs { font-size: var(--font-size-xs); }
|
||||
.text-sm { font-size: var(--font-size-sm); }
|
||||
.text-md { font-size: var(--font-size-md); }
|
||||
.text-lg { font-size: var(--font-size-lg); }
|
||||
.text-xl { font-size: var(--font-size-xl); }
|
||||
|
||||
.font-light { font-weight: var(--font-weight-light); }
|
||||
.font-normal { font-weight: var(--font-weight-normal); }
|
||||
.font-medium { font-weight: var(--font-weight-medium); }
|
||||
.font-semibold { font-weight: var(--font-weight-semibold); }
|
||||
.font-bold { font-weight: var(--font-weight-bold); }
|
||||
|
||||
/* Border radius utilities */
|
||||
.rounded-none { border-radius: var(--border-radius-none); }
|
||||
.rounded-sm { border-radius: var(--border-radius-sm); }
|
||||
.rounded-md { border-radius: var(--border-radius-md); }
|
||||
.rounded-lg { border-radius: var(--border-radius-lg); }
|
||||
.rounded-full { border-radius: var(--border-radius-full); }
|
||||
|
||||
/* Shadow utilities */
|
||||
.shadow-xs { box-shadow: var(--shadow-xs); }
|
||||
.shadow-sm { box-shadow: var(--shadow-sm); }
|
||||
.shadow-md { box-shadow: var(--shadow-md); }
|
||||
.shadow-lg { box-shadow: var(--shadow-lg); }
|
||||
.shadow-xl { box-shadow: var(--shadow-xl); }
|
||||
|
||||
/* Transition utilities */
|
||||
.transition-fast { transition: all var(--transition-fast); }
|
||||
.transition-base { transition: all var(--transition-base); }
|
||||
.transition-slow { transition: all var(--transition-slow); }
|
||||
|
||||
/* Accessibility utilities */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.focus-visible:focus-visible {
|
||||
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
||||
outline-offset: var(--focus-ring-offset);
|
||||
border-radius: var(--border-radius-sm);
|
||||
}
|
||||
|
||||
/* Skip to content link for keyboard navigation */
|
||||
.skip-to-content {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: var(--primary-color);
|
||||
color: var(--color-white);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
text-decoration: none;
|
||||
border-radius: var(--border-radius-sm);
|
||||
z-index: var(--z-index-tooltip);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.skip-to-content:focus {
|
||||
top: var(--space-sm);
|
||||
left: var(--space-sm);
|
||||
}
|
||||
|
||||
/* Touch target minimum size for mobile */
|
||||
.touch-target {
|
||||
min-height: var(--touch-target-min);
|
||||
min-width: var(--touch-target-min);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--color-border-primary: #000;
|
||||
--focus-ring-width: 4px;
|
||||
}
|
||||
|
||||
[data-theme*="dark"] {
|
||||
--color-border-primary: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== COMPONENT PATTERNS ==================== */
|
||||
|
||||
/* Button Base */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-normal);
|
||||
border-radius: var(--border-radius-md);
|
||||
border: var(--border-width-thin) solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
text-decoration: none;
|
||||
min-height: var(--touch-target-min);
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.btn:focus-visible {
|
||||
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
||||
outline-offset: var(--focus-ring-offset);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--third-color);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-primary);
|
||||
border-color: var(--color-border-primary);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
/* Card Base */
|
||||
.card {
|
||||
background: var(--color-bg-elevated);
|
||||
border: var(--border-width-thin) solid var(--color-border-secondary);
|
||||
border-radius: var(--border-radius-lg);
|
||||
padding: var(--space-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: box-shadow var(--transition-base);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
/* Input Base */
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
font-size: var(--font-size-md);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--color-text-primary);
|
||||
background: var(--color-bg-primary);
|
||||
border: var(--border-width-thin) solid var(--color-border-primary);
|
||||
border-radius: var(--border-radius-md);
|
||||
transition: all var(--transition-base);
|
||||
min-height: var(--touch-target-min);
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-border-focus);
|
||||
box-shadow: var(--shadow-focus);
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
background: var(--color-bg-tertiary);
|
||||
color: var(--color-text-disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Alert/Message Base */
|
||||
.alert {
|
||||
padding: var(--space-md);
|
||||
border-radius: var(--border-radius-md);
|
||||
border: var(--border-width-thin) solid;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: var(--color-success-bg);
|
||||
color: var(--color-success-text);
|
||||
border-color: var(--color-success-border);
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: var(--color-warning-bg);
|
||||
color: var(--color-warning-text);
|
||||
border-color: var(--color-warning-border);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: var(--color-error-bg);
|
||||
color: var(--color-error-text);
|
||||
border-color: var(--color-error-border);
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background: var(--color-info-bg);
|
||||
color: var(--color-info-text);
|
||||
border-color: var(--color-info-border);
|
||||
}
|
||||
605
f_scripts/shared/responsive.css
Normal file
605
f_scripts/shared/responsive.css
Normal file
@@ -0,0 +1,605 @@
|
||||
/**
|
||||
* EasyStream Responsive Design System
|
||||
* Mobile-first responsive breakpoints and utilities
|
||||
* Version: 1.0
|
||||
*/
|
||||
|
||||
/* ==================== BREAKPOINT VARIABLES ==================== */
|
||||
:root {
|
||||
--breakpoint-xs: 0;
|
||||
--breakpoint-sm: 640px;
|
||||
--breakpoint-md: 768px;
|
||||
--breakpoint-lg: 1024px;
|
||||
--breakpoint-xl: 1280px;
|
||||
--breakpoint-2xl: 1536px;
|
||||
|
||||
/* Container max widths */
|
||||
--container-sm: 640px;
|
||||
--container-md: 768px;
|
||||
--container-lg: 1024px;
|
||||
--container-xl: 1280px;
|
||||
--container-2xl: 1536px;
|
||||
}
|
||||
|
||||
/* ==================== CONTAINER SYSTEM ==================== */
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: var(--space-md, 16px);
|
||||
padding-right: var(--space-md, 16px);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.container {
|
||||
max-width: var(--container-sm);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: var(--container-md);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
max-width: var(--container-lg);
|
||||
padding-left: var(--space-lg, 24px);
|
||||
padding-right: var(--space-lg, 24px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.container {
|
||||
max-width: var(--container-xl);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: var(--container-2xl);
|
||||
}
|
||||
}
|
||||
|
||||
/* Fluid container */
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
padding-left: var(--space-md, 16px);
|
||||
padding-right: var(--space-md, 16px);
|
||||
}
|
||||
|
||||
/* ==================== GRID SYSTEM ==================== */
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: var(--space-md, 16px);
|
||||
}
|
||||
|
||||
/* Auto-fit grid - responsive by default */
|
||||
.grid-auto {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
/* 12 column grid */
|
||||
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
||||
.grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)); }
|
||||
|
||||
/* Column spans */
|
||||
.col-span-1 { grid-column: span 1 / span 1; }
|
||||
.col-span-2 { grid-column: span 2 / span 2; }
|
||||
.col-span-3 { grid-column: span 3 / span 3; }
|
||||
.col-span-4 { grid-column: span 4 / span 4; }
|
||||
.col-span-6 { grid-column: span 6 / span 6; }
|
||||
.col-span-12 { grid-column: span 12 / span 12; }
|
||||
.col-span-full { grid-column: 1 / -1; }
|
||||
|
||||
/* Responsive grid columns */
|
||||
@media (min-width: 640px) {
|
||||
.sm\:grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
.sm\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.sm\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.md\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.lg\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.lg\:grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
.lg\:grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
/* ==================== FLEXBOX UTILITIES ==================== */
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* Flex direction */
|
||||
.flex-row { flex-direction: row; }
|
||||
.flex-col { flex-direction: column; }
|
||||
.flex-row-reverse { flex-direction: row-reverse; }
|
||||
.flex-col-reverse { flex-direction: column-reverse; }
|
||||
|
||||
/* Flex wrap */
|
||||
.flex-wrap { flex-wrap: wrap; }
|
||||
.flex-nowrap { flex-wrap: nowrap; }
|
||||
|
||||
/* Justify content */
|
||||
.justify-start { justify-content: flex-start; }
|
||||
.justify-end { justify-content: flex-end; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.justify-around { justify-content: space-around; }
|
||||
.justify-evenly { justify-content: space-evenly; }
|
||||
|
||||
/* Align items */
|
||||
.items-start { align-items: flex-start; }
|
||||
.items-end { align-items: flex-end; }
|
||||
.items-center { align-items: center; }
|
||||
.items-baseline { align-items: baseline; }
|
||||
.items-stretch { align-items: stretch; }
|
||||
|
||||
/* Gap utilities */
|
||||
.gap-xs { gap: var(--space-xs, 4px); }
|
||||
.gap-sm { gap: var(--space-sm, 8px); }
|
||||
.gap-md { gap: var(--space-md, 16px); }
|
||||
.gap-lg { gap: var(--space-lg, 24px); }
|
||||
.gap-xl { gap: var(--space-xl, 32px); }
|
||||
|
||||
/* ==================== DISPLAY UTILITIES ==================== */
|
||||
|
||||
.hidden { display: none; }
|
||||
.block { display: block; }
|
||||
.inline { display: inline; }
|
||||
.inline-block { display: inline-block; }
|
||||
|
||||
/* Responsive display */
|
||||
@media (max-width: 639px) {
|
||||
.xs\:hidden { display: none; }
|
||||
.xs\:block { display: block; }
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:hidden { display: none; }
|
||||
.sm\:block { display: block; }
|
||||
.sm\:flex { display: flex; }
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:hidden { display: none; }
|
||||
.md\:block { display: block; }
|
||||
.md\:flex { display: flex; }
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:hidden { display: none; }
|
||||
.lg\:block { display: block; }
|
||||
.lg\:flex { display: flex; }
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.xl\:hidden { display: none; }
|
||||
.xl\:block { display: block; }
|
||||
.xl\:flex { display: flex; }
|
||||
}
|
||||
|
||||
/* ==================== MOBILE-FIRST VIDEO GRID ==================== */
|
||||
|
||||
/* Video thumbnail grid - responsive */
|
||||
.video-grid,
|
||||
.content-grid {
|
||||
display: grid;
|
||||
gap: var(--space-md, 16px);
|
||||
grid-template-columns: 1fr; /* Mobile: 1 column */
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.video-grid,
|
||||
.content-grid {
|
||||
grid-template-columns: repeat(2, 1fr); /* Tablet: 2 columns */
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.video-grid,
|
||||
.content-grid {
|
||||
grid-template-columns: repeat(3, 1fr); /* Desktop: 3 columns */
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.video-grid,
|
||||
.content-grid {
|
||||
grid-template-columns: repeat(4, 1fr); /* Large: 4 columns */
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.video-grid,
|
||||
.content-grid {
|
||||
grid-template-columns: repeat(5, 1fr); /* XL: 5 columns */
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.video-grid,
|
||||
.content-grid {
|
||||
grid-template-columns: repeat(6, 1fr); /* 2XL: 6 columns */
|
||||
}
|
||||
}
|
||||
|
||||
/* Compact video grid for sidebars */
|
||||
.video-grid-compact {
|
||||
display: grid;
|
||||
gap: var(--space-sm, 8px);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* ==================== RESPONSIVE TYPOGRAPHY ==================== */
|
||||
|
||||
/* Fluid typography using clamp */
|
||||
.text-responsive-sm {
|
||||
font-size: clamp(0.875rem, 2vw, 1rem);
|
||||
}
|
||||
|
||||
.text-responsive-md {
|
||||
font-size: clamp(1rem, 2.5vw, 1.125rem);
|
||||
}
|
||||
|
||||
.text-responsive-lg {
|
||||
font-size: clamp(1.125rem, 3vw, 1.5rem);
|
||||
}
|
||||
|
||||
.text-responsive-xl {
|
||||
font-size: clamp(1.5rem, 4vw, 2.25rem);
|
||||
}
|
||||
|
||||
/* ==================== RESPONSIVE SPACING ==================== */
|
||||
|
||||
/* Responsive padding */
|
||||
.p-responsive {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.p-responsive {
|
||||
padding: var(--space-md);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.p-responsive {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive margin */
|
||||
.m-responsive {
|
||||
margin: var(--space-sm);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.m-responsive {
|
||||
margin: var(--space-md);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.m-responsive {
|
||||
margin: var(--space-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== SIDEBAR LAYOUTS ==================== */
|
||||
|
||||
.layout-with-sidebar {
|
||||
display: grid;
|
||||
gap: var(--space-md);
|
||||
grid-template-columns: 1fr; /* Mobile: stacked */
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.layout-with-sidebar {
|
||||
grid-template-columns: 250px 1fr; /* Desktop: sidebar + content */
|
||||
}
|
||||
|
||||
.layout-with-sidebar.sidebar-right {
|
||||
grid-template-columns: 1fr 300px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapsible sidebar */
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.sidebar.collapsed {
|
||||
width: 64px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar.collapsed + .main-content {
|
||||
margin-left: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== TOUCH OPTIMIZATIONS ==================== */
|
||||
|
||||
/* Larger touch targets on mobile */
|
||||
@media (max-width: 767px) {
|
||||
button,
|
||||
a,
|
||||
.btn,
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* Increase spacing between interactive elements */
|
||||
.button-group > *,
|
||||
.nav-menu > * {
|
||||
margin: var(--space-sm) 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== IMAGE RESPONSIVENESS ==================== */
|
||||
|
||||
img,
|
||||
video,
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.aspect-video {
|
||||
aspect-ratio: 16 / 9;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aspect-square {
|
||||
aspect-ratio: 1 / 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aspect-video img,
|
||||
.aspect-video video,
|
||||
.aspect-square img,
|
||||
.aspect-square video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* ==================== TABLE RESPONSIVENESS ==================== */
|
||||
|
||||
.table-responsive {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
/* Stack table on mobile */
|
||||
.table-stack thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.table-stack tr {
|
||||
display: block;
|
||||
margin-bottom: var(--space-md);
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: var(--border-radius-md);
|
||||
}
|
||||
|
||||
.table-stack td {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-sm);
|
||||
border-bottom: 1px solid var(--color-border-secondary);
|
||||
}
|
||||
|
||||
.table-stack td:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-stack td::before {
|
||||
content: attr(data-label);
|
||||
font-weight: 600;
|
||||
margin-right: var(--space-sm);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== MOBILE NAVIGATION ==================== */
|
||||
|
||||
/* Hamburger menu */
|
||||
.mobile-menu-toggle {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.mobile-menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-menu {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 80%;
|
||||
max-width: 300px;
|
||||
height: 100vh;
|
||||
background: var(--color-bg-primary);
|
||||
box-shadow: var(--shadow-xl);
|
||||
transition: left var(--transition-base);
|
||||
z-index: var(--z-index-modal);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mobile-menu.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity var(--transition-base), visibility var(--transition-base);
|
||||
z-index: calc(var(--z-index-modal) - 1);
|
||||
}
|
||||
|
||||
.mobile-menu-backdrop.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* ==================== VIDEO PLAYER RESPONSIVENESS ==================== */
|
||||
|
||||
.video-player-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 56.25%; /* 16:9 aspect ratio */
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-player-wrapper iframe,
|
||||
.video-player-wrapper video,
|
||||
.video-player-wrapper .video-js {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ==================== ORIENTATION SUPPORT ==================== */
|
||||
|
||||
@media (orientation: landscape) and (max-height: 500px) {
|
||||
/* Optimize for landscape mobile */
|
||||
.video-player-wrapper {
|
||||
padding-bottom: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
header,
|
||||
.sidebar {
|
||||
display: none; /* Hide in fullscreen landscape */
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== RESPONSIVE UTILITIES ==================== */
|
||||
|
||||
/* Text alignment */
|
||||
.text-left { text-align: left; }
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:text-left { text-align: left; }
|
||||
.md\:text-center { text-align: center; }
|
||||
.md\:text-right { text-align: right; }
|
||||
}
|
||||
|
||||
/* Width utilities */
|
||||
.w-full { width: 100%; }
|
||||
.w-auto { width: auto; }
|
||||
.w-screen { width: 100vw; }
|
||||
|
||||
.h-full { height: 100%; }
|
||||
.h-auto { height: auto; }
|
||||
.h-screen { height: 100vh; }
|
||||
|
||||
/* Max width utilities */
|
||||
.max-w-xs { max-width: 320px; }
|
||||
.max-w-sm { max-width: 384px; }
|
||||
.max-w-md { max-width: 448px; }
|
||||
.max-w-lg { max-width: 512px; }
|
||||
.max-w-xl { max-width: 576px; }
|
||||
.max-w-2xl { max-width: 672px; }
|
||||
.max-w-full { max-width: 100%; }
|
||||
|
||||
/* ==================== PRINT RESPONSIVENESS ==================== */
|
||||
|
||||
@media print {
|
||||
.no-print,
|
||||
.mobile-menu,
|
||||
.sidebar,
|
||||
nav,
|
||||
header,
|
||||
footer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== LANDSCAPE MODE OPTIMIZATIONS ==================== */
|
||||
|
||||
@media (max-width: 1023px) and (orientation: landscape) {
|
||||
/* Reduce vertical spacing in landscape */
|
||||
.p-responsive {
|
||||
padding-top: var(--space-sm);
|
||||
padding-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
/* Make navigation horizontal in landscape */
|
||||
.mobile-menu {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== SAFE AREAS (iPhone X+) ==================== */
|
||||
|
||||
@supports (padding: env(safe-area-inset-left)) {
|
||||
.safe-area-padding {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
header,
|
||||
.mobile-menu {
|
||||
padding-left: max(var(--space-md), env(safe-area-inset-left));
|
||||
padding-right: max(var(--space-md), env(safe-area-inset-right));
|
||||
}
|
||||
}
|
||||
514
f_scripts/shared/theme-switcher.js
Normal file
514
f_scripts/shared/theme-switcher.js
Normal file
@@ -0,0 +1,514 @@
|
||||
/**
|
||||
* EasyStream Advanced Theme Switcher
|
||||
* Smooth theme transitions with system preference detection
|
||||
* Version: 2.0
|
||||
*/
|
||||
|
||||
class ThemeSwitcher {
|
||||
constructor(options = {}) {
|
||||
this.options = {
|
||||
storageKey: 'easystream-theme',
|
||||
colorStorageKey: 'easystream-color',
|
||||
transitionDuration: 300,
|
||||
detectSystemPreference: true,
|
||||
...options
|
||||
};
|
||||
|
||||
this.themes = {
|
||||
light: ['blue', 'red', 'cyan', 'green', 'orange', 'pink', 'purple'],
|
||||
dark: ['darkblue', 'darkred', 'darkcyan', 'darkgreen', 'darkorange', 'darkpink', 'darkpurple']
|
||||
};
|
||||
|
||||
this.currentTheme = null;
|
||||
this.currentColor = null;
|
||||
this.systemPreference = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize theme switcher
|
||||
*/
|
||||
init() {
|
||||
// Detect system preference
|
||||
if (this.options.detectSystemPreference) {
|
||||
this.detectSystemPreference();
|
||||
this.watchSystemPreference();
|
||||
}
|
||||
|
||||
// Load saved theme or use default
|
||||
const savedTheme = this.getSavedTheme();
|
||||
const savedColor = this.getSavedColor();
|
||||
|
||||
if (savedTheme && savedColor) {
|
||||
this.applyTheme(savedTheme, savedColor, false);
|
||||
} else if (this.systemPreference) {
|
||||
// Use system preference
|
||||
const defaultColor = 'blue';
|
||||
const theme = this.systemPreference === 'dark' ? `dark${defaultColor}` : defaultColor;
|
||||
this.applyTheme(this.systemPreference, defaultColor, false);
|
||||
} else {
|
||||
// Default to light blue
|
||||
this.applyTheme('light', 'blue', false);
|
||||
}
|
||||
|
||||
// Setup UI controls if they exist
|
||||
this.setupControls();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect system color scheme preference
|
||||
*/
|
||||
detectSystemPreference() {
|
||||
if (window.matchMedia) {
|
||||
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
this.systemPreference = darkModeQuery.matches ? 'dark' : 'light';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for system preference changes
|
||||
*/
|
||||
watchSystemPreference() {
|
||||
if (window.matchMedia) {
|
||||
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
darkModeQuery.addEventListener('change', (e) => {
|
||||
this.systemPreference = e.matches ? 'dark' : 'light';
|
||||
|
||||
// Only auto-switch if user hasn't manually set a theme
|
||||
const savedTheme = this.getSavedTheme();
|
||||
if (!savedTheme || savedTheme === 'auto') {
|
||||
this.applySystemTheme();
|
||||
}
|
||||
|
||||
this.dispatchEvent('system-preference-change', {
|
||||
preference: this.systemPreference
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply system theme based on preference
|
||||
*/
|
||||
applySystemTheme() {
|
||||
const color = this.currentColor || 'blue';
|
||||
const theme = this.systemPreference === 'dark' ? `dark${color}` : color;
|
||||
this.applyTheme(this.systemPreference, color, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get saved theme from storage
|
||||
*/
|
||||
getSavedTheme() {
|
||||
try {
|
||||
return localStorage.getItem(this.options.storageKey);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get saved color from storage
|
||||
*/
|
||||
getSavedColor() {
|
||||
try {
|
||||
return localStorage.getItem(this.options.colorStorageKey);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save theme to storage
|
||||
*/
|
||||
saveTheme(mode, color) {
|
||||
try {
|
||||
localStorage.setItem(this.options.storageKey, mode);
|
||||
localStorage.setItem(this.options.colorStorageKey, color);
|
||||
} catch (e) {
|
||||
console.warn('Failed to save theme preference', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme with smooth transition
|
||||
* @param {string} mode - 'light' or 'dark'
|
||||
* @param {string} color - color name (blue, red, etc.)
|
||||
* @param {boolean} animate - whether to animate the transition
|
||||
*/
|
||||
applyTheme(mode, color, animate = true) {
|
||||
const fullTheme = mode === 'dark' ? `dark${color}` : color;
|
||||
|
||||
// Prevent unnecessary updates
|
||||
if (this.currentTheme === mode && this.currentColor === color) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousMode = this.currentTheme;
|
||||
this.currentTheme = mode;
|
||||
this.currentColor = color;
|
||||
|
||||
// Add transition class for smooth animation
|
||||
if (animate) {
|
||||
this.enableTransitions();
|
||||
}
|
||||
|
||||
// Update body/html data attribute
|
||||
document.documentElement.setAttribute('data-theme', fullTheme);
|
||||
document.body.setAttribute('data-theme', fullTheme);
|
||||
|
||||
// Update meta theme-color for mobile browsers
|
||||
this.updateMetaThemeColor();
|
||||
|
||||
// Save preference
|
||||
this.saveTheme(mode, color);
|
||||
|
||||
// Remove transition class after animation
|
||||
if (animate) {
|
||||
setTimeout(() => {
|
||||
this.disableTransitions();
|
||||
}, this.options.transitionDuration);
|
||||
}
|
||||
|
||||
// Update UI controls
|
||||
this.updateControls();
|
||||
|
||||
// Dispatch custom event
|
||||
this.dispatchEvent('theme-change', {
|
||||
mode,
|
||||
color,
|
||||
fullTheme,
|
||||
previousMode
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable smooth transitions during theme switch
|
||||
*/
|
||||
enableTransitions() {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'theme-transition-styles';
|
||||
style.textContent = `
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
transition: background-color ${this.options.transitionDuration}ms ease,
|
||||
color ${this.options.transitionDuration}ms ease,
|
||||
border-color ${this.options.transitionDuration}ms ease,
|
||||
box-shadow ${this.options.transitionDuration}ms ease !important;
|
||||
}
|
||||
|
||||
/* Disable transitions for animations that shouldn't be affected */
|
||||
.no-theme-transition,
|
||||
[class*="animate"],
|
||||
.video-js,
|
||||
.spinner {
|
||||
transition: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable transitions after theme switch
|
||||
*/
|
||||
disableTransitions() {
|
||||
const style = document.getElementById('theme-transition-styles');
|
||||
if (style) {
|
||||
style.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update meta theme-color for mobile browsers
|
||||
*/
|
||||
updateMetaThemeColor() {
|
||||
let themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||
|
||||
if (!themeColorMeta) {
|
||||
themeColorMeta = document.createElement('meta');
|
||||
themeColorMeta.name = 'theme-color';
|
||||
document.head.appendChild(themeColorMeta);
|
||||
}
|
||||
|
||||
// Color mapping
|
||||
const colors = {
|
||||
blue: '#06a2cb',
|
||||
red: '#dd1e2f',
|
||||
cyan: '#00997a',
|
||||
green: '#199900',
|
||||
orange: '#f28410',
|
||||
pink: '#ec7ab9',
|
||||
purple: '#b25c8b'
|
||||
};
|
||||
|
||||
const bgColors = {
|
||||
light: '#ffffff',
|
||||
dark: '#121212'
|
||||
};
|
||||
|
||||
// Use primary color for theme
|
||||
const primaryColor = colors[this.currentColor] || colors.blue;
|
||||
const bgColor = bgColors[this.currentTheme] || bgColors.light;
|
||||
|
||||
themeColorMeta.content = this.currentTheme === 'dark' ? bgColor : primaryColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle between light and dark mode
|
||||
*/
|
||||
toggleMode() {
|
||||
const newMode = this.currentTheme === 'light' ? 'dark' : 'light';
|
||||
this.applyTheme(newMode, this.currentColor, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set color theme
|
||||
* @param {string} color - color name
|
||||
*/
|
||||
setColor(color) {
|
||||
if (!this.themes.light.includes(color) && !this.themes.dark.includes(color.replace('dark', ''))) {
|
||||
console.warn(`Invalid color: ${color}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseColor = color.replace('dark', '');
|
||||
this.applyTheme(this.currentTheme, baseColor, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup UI controls
|
||||
*/
|
||||
setupControls() {
|
||||
// Theme toggle button
|
||||
const toggleBtn = document.getElementById('theme-toggle');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', () => this.toggleMode());
|
||||
}
|
||||
|
||||
// Color picker buttons
|
||||
const colorBtns = document.querySelectorAll('[data-color-theme]');
|
||||
colorBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const color = btn.getAttribute('data-color-theme');
|
||||
this.setColor(color);
|
||||
});
|
||||
});
|
||||
|
||||
// Auto theme switch (follow system)
|
||||
const autoSwitch = document.getElementById('theme-auto');
|
||||
if (autoSwitch) {
|
||||
autoSwitch.addEventListener('change', (e) => {
|
||||
if (e.target.checked) {
|
||||
this.applySystemTheme();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI controls to reflect current theme
|
||||
*/
|
||||
updateControls() {
|
||||
// Update toggle button
|
||||
const toggleBtn = document.getElementById('theme-toggle');
|
||||
if (toggleBtn) {
|
||||
const icon = toggleBtn.querySelector('i, .icon');
|
||||
if (icon) {
|
||||
if (this.currentTheme === 'dark') {
|
||||
icon.className = 'icon-sun';
|
||||
toggleBtn.setAttribute('aria-label', 'Switch to light mode');
|
||||
} else {
|
||||
icon.className = 'icon-moon';
|
||||
toggleBtn.setAttribute('aria-label', 'Switch to dark mode');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update color buttons
|
||||
const colorBtns = document.querySelectorAll('[data-color-theme]');
|
||||
colorBtns.forEach(btn => {
|
||||
const color = btn.getAttribute('data-color-theme');
|
||||
if (color === this.currentColor) {
|
||||
btn.classList.add('active');
|
||||
btn.setAttribute('aria-pressed', 'true');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
btn.setAttribute('aria-pressed', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current theme info
|
||||
*/
|
||||
getCurrentTheme() {
|
||||
return {
|
||||
mode: this.currentTheme,
|
||||
color: this.currentColor,
|
||||
fullTheme: this.currentTheme === 'dark' ? `dark${this.currentColor}` : this.currentColor,
|
||||
systemPreference: this.systemPreference
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch custom event
|
||||
*/
|
||||
dispatchEvent(eventName, detail) {
|
||||
const event = new CustomEvent(`easystream:${eventName}`, {
|
||||
detail,
|
||||
bubbles: true
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create theme picker UI
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
static createThemePicker() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'theme-picker';
|
||||
container.setAttribute('role', 'toolbar');
|
||||
container.setAttribute('aria-label', 'Theme controls');
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="theme-picker-header">
|
||||
<h3>Appearance</h3>
|
||||
</div>
|
||||
|
||||
<div class="theme-mode-toggle">
|
||||
<label class="theme-label">
|
||||
<span>Mode</span>
|
||||
<button id="theme-toggle" class="btn btn-secondary touch-target" aria-label="Toggle theme mode">
|
||||
<i class="icon-moon"></i>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="theme-color-picker">
|
||||
<span class="theme-label">Color</span>
|
||||
<div class="color-options" role="group" aria-label="Color themes">
|
||||
<button class="color-btn color-blue" data-color-theme="blue" aria-label="Blue theme">
|
||||
<span class="sr-only">Blue</span>
|
||||
</button>
|
||||
<button class="color-btn color-red" data-color-theme="red" aria-label="Red theme">
|
||||
<span class="sr-only">Red</span>
|
||||
</button>
|
||||
<button class="color-btn color-cyan" data-color-theme="cyan" aria-label="Cyan theme">
|
||||
<span class="sr-only">Cyan</span>
|
||||
</button>
|
||||
<button class="color-btn color-green" data-color-theme="green" aria-label="Green theme">
|
||||
<span class="sr-only">Green</span>
|
||||
</button>
|
||||
<button class="color-btn color-orange" data-color-theme="orange" aria-label="Orange theme">
|
||||
<span class="sr-only">Orange</span>
|
||||
</button>
|
||||
<button class="color-btn color-pink" data-color-theme="pink" aria-label="Pink theme">
|
||||
<span class="sr-only">Pink</span>
|
||||
</button>
|
||||
<button class="color-btn color-purple" data-color-theme="purple" aria-label="Purple theme">
|
||||
<span class="sr-only">Purple</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.theme-picker {
|
||||
padding: var(--space-lg, 24px);
|
||||
background: var(--color-bg-elevated);
|
||||
border-radius: var(--border-radius-lg, 12px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.theme-picker-header h3 {
|
||||
margin: 0 0 var(--space-md, 16px) 0;
|
||||
font-size: var(--font-size-lg, 1.125rem);
|
||||
font-weight: var(--font-weight-semibold, 600);
|
||||
}
|
||||
|
||||
.theme-mode-toggle {
|
||||
margin-bottom: var(--space-lg, 24px);
|
||||
}
|
||||
|
||||
.theme-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-weight: var(--font-weight-medium, 500);
|
||||
margin-bottom: var(--space-sm, 8px);
|
||||
}
|
||||
|
||||
.color-options {
|
||||
display: flex;
|
||||
gap: var(--space-sm, 8px);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.color-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--border-radius-full, 9999px);
|
||||
border: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base, 200ms);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.color-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.color-btn:focus-visible {
|
||||
outline: 3px solid var(--focus-ring-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.color-btn.active {
|
||||
border-color: var(--color-text-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.color-btn.active::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.color-blue { background: #06a2cb; }
|
||||
.color-red { background: #dd1e2f; }
|
||||
.color-cyan { background: #00997a; }
|
||||
.color-green { background: #199900; }
|
||||
.color-orange { background: #f28410; }
|
||||
.color-pink { background: #ec7ab9; }
|
||||
.color-purple { background: #b25c8b; }
|
||||
</style>
|
||||
`;
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize on page load
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
window.themeSwitcher = new ThemeSwitcher();
|
||||
|
||||
// Make it globally accessible
|
||||
window.EasyStream = window.EasyStream || {};
|
||||
window.EasyStream.ThemeSwitcher = ThemeSwitcher;
|
||||
});
|
||||
}
|
||||
|
||||
// Export for module systems
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = ThemeSwitcher;
|
||||
}
|
||||
Reference in New Issue
Block a user