Redesign home page with beautiful layout, add People/Contacts, rename Tools to Compliance
- Complete home page redesign with large icons, full descriptions, recent documents - Add People (Contacts) menu item and page with contacts management - Move Paper right after Chat in menu order - Rename Tools to Compliance with shield icon - Settings moved to end of menu - Logo click now shows home page - Add Project, Canvas, Goals, Player, Workspace, Video, Learn to menu - New CSS for home page with modern card layout
This commit is contained in:
parent
cb33a75d39
commit
80c91f6304
15 changed files with 7210 additions and 834 deletions
|
|
@ -6,9 +6,7 @@ mod shared;
|
|||
mod ui_server;
|
||||
|
||||
fn init_logging() {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
botlib::logging::init_compact_logger("info");
|
||||
}
|
||||
|
||||
fn get_port() -> u16 {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,16 @@ const SUITE_DIRS: &[&str] = &[
|
|||
"tools",
|
||||
"assets",
|
||||
"partials",
|
||||
"video",
|
||||
"learn",
|
||||
"social",
|
||||
"dashboards",
|
||||
"designer",
|
||||
"workspace",
|
||||
"project",
|
||||
"goals",
|
||||
"player",
|
||||
"canvas",
|
||||
];
|
||||
|
||||
pub async fn index() -> impl IntoResponse {
|
||||
|
|
|
|||
992
ui/suite/canvas/canvas.html
Normal file
992
ui/suite/canvas/canvas.html
Normal file
|
|
@ -0,0 +1,992 @@
|
|||
<!-- =============================================================================
|
||||
CANVAS APP - Collaborative Whiteboard & Drawing
|
||||
Respects Theme Manager - No hardcoded theme
|
||||
============================================================================= -->
|
||||
|
||||
<div class="canvas-app">
|
||||
<!-- Toolbar -->
|
||||
<aside class="canvas-toolbar">
|
||||
<div class="toolbar-section">
|
||||
<button class="tool-btn active" data-tool="select" onclick="selectTool('select')" title="Select (V)">
|
||||
<span>↖</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-tool="pan" onclick="selectTool('pan')" title="Pan (H)">
|
||||
<span>✋</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-section">
|
||||
<button class="tool-btn" data-tool="pencil" onclick="selectTool('pencil')" title="Pencil (P)">
|
||||
<span>✏️</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-tool="brush" onclick="selectTool('brush')" title="Brush (B)">
|
||||
<span>🖌️</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-tool="eraser" onclick="selectTool('eraser')" title="Eraser (E)">
|
||||
<span>🧹</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-section">
|
||||
<button class="tool-btn" data-tool="rectangle" onclick="selectTool('rectangle')" title="Rectangle (R)">
|
||||
<span>▢</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-tool="ellipse" onclick="selectTool('ellipse')" title="Ellipse (O)">
|
||||
<span>◯</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-tool="line" onclick="selectTool('line')" title="Line (L)">
|
||||
<span>╱</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-tool="arrow" onclick="selectTool('arrow')" title="Arrow (A)">
|
||||
<span>→</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-section">
|
||||
<button class="tool-btn" data-tool="text" onclick="selectTool('text')" title="Text (T)">
|
||||
<span>T</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-tool="sticky" onclick="selectTool('sticky')" title="Sticky Note (S)">
|
||||
<span>📝</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-tool="image" onclick="selectTool('image')" title="Image (I)">
|
||||
<span>🖼️</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-section">
|
||||
<button class="tool-btn" data-tool="connector" onclick="selectTool('connector')" title="Connector (C)">
|
||||
<span>⤳</span>
|
||||
</button>
|
||||
<button class="tool-btn" data-tool="frame" onclick="selectTool('frame')" title="Frame (F)">
|
||||
<span>⬚</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-divider"></div>
|
||||
|
||||
<div class="toolbar-section colors">
|
||||
<input type="color" id="stroke-color" value="#000000" title="Stroke Color" onchange="setStrokeColor(this.value)">
|
||||
<input type="color" id="fill-color" value="#ffffff" title="Fill Color" onchange="setFillColor(this.value)">
|
||||
</div>
|
||||
|
||||
<div class="toolbar-section">
|
||||
<select id="stroke-width" onchange="setStrokeWidth(this.value)" title="Stroke Width">
|
||||
<option value="1">1px</option>
|
||||
<option value="2" selected>2px</option>
|
||||
<option value="4">4px</option>
|
||||
<option value="6">6px</option>
|
||||
<option value="8">8px</option>
|
||||
</select>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Canvas Area -->
|
||||
<main class="canvas-main">
|
||||
<!-- Top Bar -->
|
||||
<header class="canvas-header">
|
||||
<div class="header-left">
|
||||
<input type="text" id="canvas-name" value="Untitled Canvas" class="canvas-name-input"
|
||||
onblur="renameCanvas(this.value)">
|
||||
<span class="save-status" id="save-status">Saved</span>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<div class="zoom-controls">
|
||||
<button class="zoom-btn" onclick="zoomOut()" title="Zoom Out">−</button>
|
||||
<span id="zoom-level">100%</span>
|
||||
<button class="zoom-btn" onclick="zoomIn()" title="Zoom In">+</button>
|
||||
<button class="zoom-btn" onclick="resetZoom()" title="Reset Zoom">⟲</button>
|
||||
<button class="zoom-btn" onclick="fitToScreen()" title="Fit to Screen">⤢</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="btn-icon" onclick="undo()" title="Undo (Ctrl+Z)" data-i18n-title="canvas-undo">
|
||||
<span>↶</span>
|
||||
</button>
|
||||
<button class="btn-icon" onclick="redo()" title="Redo (Ctrl+Y)" data-i18n-title="canvas-redo">
|
||||
<span>↷</span>
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<button class="btn-icon" onclick="clearCanvas()" title="Clear Canvas" data-i18n-title="canvas-clear">
|
||||
<span>🗑️</span>
|
||||
</button>
|
||||
<button class="btn-secondary" onclick="exportCanvas()" data-i18n="canvas-export">
|
||||
<span>📤</span> Export
|
||||
</button>
|
||||
<button class="btn-primary" onclick="shareCanvas()" data-i18n="canvas-collaborate">
|
||||
<span>👥</span> Share
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Canvas Container -->
|
||||
<div class="canvas-container" id="canvas-container">
|
||||
<canvas id="main-canvas"></canvas>
|
||||
<div id="selection-box" class="selection-box hidden"></div>
|
||||
<div id="cursor-indicators" class="cursor-indicators"></div>
|
||||
</div>
|
||||
|
||||
<!-- Mini Map -->
|
||||
<div class="mini-map" id="mini-map">
|
||||
<canvas id="mini-map-canvas"></canvas>
|
||||
<div class="viewport-indicator" id="viewport-indicator"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Properties Panel -->
|
||||
<aside class="properties-panel collapsed" id="properties-panel">
|
||||
<button class="panel-toggle" onclick="togglePropertiesPanel()">
|
||||
<span>⚙️</span>
|
||||
</button>
|
||||
<div class="panel-content">
|
||||
<h3>Properties</h3>
|
||||
|
||||
<div id="element-properties" class="properties-group hidden">
|
||||
<div class="property-row">
|
||||
<label>Position</label>
|
||||
<div class="input-group">
|
||||
<input type="number" id="prop-x" placeholder="X" onchange="updateElementProperty('x', this.value)">
|
||||
<input type="number" id="prop-y" placeholder="Y" onchange="updateElementProperty('y', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<label>Size</label>
|
||||
<div class="input-group">
|
||||
<input type="number" id="prop-width" placeholder="W" onchange="updateElementProperty('width', this.value)">
|
||||
<input type="number" id="prop-height" placeholder="H" onchange="updateElementProperty('height', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<label>Rotation</label>
|
||||
<input type="number" id="prop-rotation" placeholder="0°" onchange="updateElementProperty('rotation', this.value)">
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<label>Opacity</label>
|
||||
<input type="range" id="prop-opacity" min="0" max="100" value="100" onchange="updateElementProperty('opacity', this.value)">
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<label>Stroke</label>
|
||||
<input type="color" id="prop-stroke" onchange="updateElementProperty('strokeColor', this.value)">
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<label>Fill</label>
|
||||
<input type="color" id="prop-fill" onchange="updateElementProperty('fillColor', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="canvas-properties" class="properties-group">
|
||||
<h4>Canvas</h4>
|
||||
<div class="property-row">
|
||||
<label>Background</label>
|
||||
<input type="color" id="canvas-bg" value="#ffffff" onchange="setCanvasBackground(this.value)">
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<label>Grid</label>
|
||||
<select id="grid-type" onchange="setGridType(this.value)">
|
||||
<option value="none">None</option>
|
||||
<option value="dots" selected>Dots</option>
|
||||
<option value="lines">Lines</option>
|
||||
<option value="squares">Squares</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="property-row">
|
||||
<label>Snap to Grid</label>
|
||||
<input type="checkbox" id="snap-grid" onchange="toggleSnapToGrid(this.checked)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="properties-group">
|
||||
<h4>Layers</h4>
|
||||
<div id="layers-list" class="layers-list">
|
||||
<div class="layer-item active">
|
||||
<span class="layer-visibility">👁️</span>
|
||||
<span class="layer-name">Layer 1</span>
|
||||
<span class="layer-lock">🔓</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-sm" onclick="addLayer()">+ Add Layer</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Collaborators Panel -->
|
||||
<div class="collaborators-panel" id="collaborators-panel">
|
||||
<div class="collaborators-list" id="collaborators-list">
|
||||
<!-- Collaborator avatars will be added here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Context Menu -->
|
||||
<div id="context-menu" class="context-menu hidden">
|
||||
<button onclick="duplicateSelected()">Duplicate</button>
|
||||
<button onclick="copySelected()">Copy</button>
|
||||
<button onclick="pasteClipboard()">Paste</button>
|
||||
<div class="menu-divider"></div>
|
||||
<button onclick="bringToFront()">Bring to Front</button>
|
||||
<button onclick="sendToBack()">Send to Back</button>
|
||||
<div class="menu-divider"></div>
|
||||
<button onclick="deleteSelected()">Delete</button>
|
||||
</div>
|
||||
|
||||
<!-- Export Modal -->
|
||||
<div id="export-modal" class="modal hidden">
|
||||
<div class="modal-content">
|
||||
<h2 data-i18n="canvas-export">Export Canvas</h2>
|
||||
<div class="export-options">
|
||||
<div class="export-format">
|
||||
<label>Format</label>
|
||||
<select id="export-format">
|
||||
<option value="png">PNG Image</option>
|
||||
<option value="svg">SVG Vector</option>
|
||||
<option value="pdf">PDF Document</option>
|
||||
<option value="json">JSON Data</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="export-scale">
|
||||
<label>Scale</label>
|
||||
<select id="export-scale">
|
||||
<option value="1">1x</option>
|
||||
<option value="2" selected>2x</option>
|
||||
<option value="3">3x</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="export-bg">
|
||||
<label>
|
||||
<input type="checkbox" id="export-bg" checked>
|
||||
Include Background
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn-secondary" onclick="closeExportModal()">Cancel</button>
|
||||
<button class="btn-primary" onclick="doExport()">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.canvas-app {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.canvas-toolbar {
|
||||
width: 56px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.toolbar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.toolbar-section.colors {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.toolbar-section.colors input[type="color"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.125rem;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.tool-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tool-btn.active {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.toolbar-section select {
|
||||
width: 100%;
|
||||
padding: 0.25rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.canvas-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.canvas-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.canvas-name-input {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.canvas-name-input:hover,
|
||||
.canvas-name-input:focus {
|
||||
background: var(--bg-primary);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.save-status {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.header-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
background: var(--bg-primary);
|
||||
padding: 0.25rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.zoom-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.zoom-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#zoom-level {
|
||||
font-size: 0.875rem;
|
||||
min-width: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--border-color);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
#main-canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.selection-box {
|
||||
position: absolute;
|
||||
border: 2px dashed var(--accent-color);
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.selection-box.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cursor-indicators {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cursor-indicator {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cursor-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.cursor-name {
|
||||
font-size: 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mini-map {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
#mini-map-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.viewport-indicator {
|
||||
position: absolute;
|
||||
border: 2px solid var(--accent-color);
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.properties-panel {
|
||||
width: 280px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border-color);
|
||||
transition: width 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.properties-panel.collapsed {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.properties-panel.collapsed .panel-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel-toggle {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.panel-content h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.properties-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.properties-group.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.properties-group h4 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 0.75rem 0;
|
||||
}
|
||||
|
||||
.property-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.property-row label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.property-row input[type="number"],
|
||||
.property-row input[type="text"],
|
||||
.property-row select {
|
||||
width: 100px;
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.property-row input[type="color"] {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.property-row input[type="range"] {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.property-row input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
width: 48px !important;
|
||||
}
|
||||
|
||||
.layers-list {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.layer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.layer-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.layer-item.active {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.layer-visibility,
|
||||
.layer-lock {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.layer-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.collaborators-panel {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
gap: -0.5rem;
|
||||
}
|
||||
|
||||
.collaborators-list {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.collaborator-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--bg-secondary);
|
||||
margin-left: -8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.collaborator-avatar:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 0.5rem 0;
|
||||
min-width: 160px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.context-menu.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.context-menu button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.context-menu button:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
min-width: 400px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.export-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.export-format,
|
||||
.export-scale,
|
||||
.export-bg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.export-format select,
|
||||
.export-scale select {
|
||||
width: 150px;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.export-bg label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.properties-panel {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
z-index: 50;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.canvas-toolbar {
|
||||
width: 48px;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.mini-map {
|
||||
width: 120px;
|
||||
height: 90px;
|
||||
}
|
||||
|
||||
.properties-panel.collapsed {
|
||||
width: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.canvas-header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
order: 3;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentTool = 'select';
|
||||
let isDrawing = false;
|
||||
let startX, startY;
|
||||
let elements = [];
|
||||
let selectedElement = null;
|
||||
let history = [];
|
||||
let historyIndex = -1;
|
||||
let zoom = 1;
|
||||
let panX = 0, panY = 0;
|
||||
let strokeColor = '#000000';
|
||||
let fillColor = '#ffffff';
|
||||
let strokeWidth = 2;
|
||||
let canvas, ctx;
|
||||
let gridType = 'dots';
|
||||
let snapToGrid = false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
canvas = document.getElementById('main-canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
resizeCanvas();
|
||||
drawGrid();
|
||||
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
canvas.addEventListener('mousedown', handleMouseDown);
|
||||
canvas.addEventListener('mousemove', handleMouseMove);
|
||||
canvas.addEventListener('mouseup', handleMouseUp);
|
||||
canvas.addEventListener('wheel', handleWheel);
|
||||
canvas.addEventListener('contextmenu', handleContextMenu);
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
});
|
||||
|
||||
function resizeCanvas() {
|
||||
const container = document.getElementById('canvas-container');
|
||||
canvas.width = container.clientWidth;
|
||||
canvas.height = container.clientHeight;
|
||||
redraw();
|
||||
}
|
||||
|
||||
function selectTool(tool) {
|
||||
currentTool = tool;
|
||||
document.querySelectorAll('.tool-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.tool === tool);
|
||||
});
|
||||
|
||||
const container = document.getElementById('canvas-container');
|
||||
switch (tool) {
|
||||
case 'select':
|
||||
container.style.cursor = 'default';
|
||||
break;
|
||||
case 'pan':
|
||||
container.style.cursor = 'grab';
|
||||
break;
|
||||
case 'text':
|
||||
container.style.cursor = 'text';
|
||||
break;
|
||||
default:
|
||||
container.style.cursor = 'crosshair';
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseDown(e) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
startX = (e.clientX - rect.left - panX) / zoom;
|
||||
startY = (e.clientY - rect.top - panY) / zoom;
|
||||
isDrawing = true;
|
||||
|
||||
if (currentTool === 'select') {
|
||||
selectedElement = findElementAt(startX, startY);
|
||||
if (selectedElement) {
|
||||
showElementProperties(selectedElement);
|
||||
} else {
|
||||
hideElementProperties();
|
||||
}
|
||||
} else if (currentTool === 'pan') {
|
||||
document.getElementById('canvas-container').style.cursor = 'grabbing';
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseMove(e) {
|
||||
if (!isDrawing) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const currentX = (e.
|
||||
|
|
@ -1117,7 +1117,7 @@ body {
|
|||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -588,7 +588,7 @@ body.no-animations .spinner {
|
|||
.apps-grid,
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,89 +1,226 @@
|
|||
/* Home page styles */
|
||||
|
||||
:root {
|
||||
--primary: #3b82f6;
|
||||
--primary-hover: #2563eb;
|
||||
--bg: #0f172a;
|
||||
--surface: #1e293b;
|
||||
--border: #334155;
|
||||
--text: #f8fafc;
|
||||
--text-secondary: #94a3b8;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.home-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
padding: 32px 40px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.home-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
/* Welcome Section */
|
||||
.welcome-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 48px;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.home-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 1.5rem;
|
||||
background: linear-gradient(135deg, var(--primary), #8b5cf6);
|
||||
border-radius: 20px;
|
||||
.welcome-content {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 8px 0;
|
||||
color: var(--text-primary);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--text-primary),
|
||||
var(--accent-color)
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 1.125rem;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quick-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 20px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
min-width: 320px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.quick-search:hover {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px var(--accent-color-alpha, rgba(99, 102, 241, 0.1));
|
||||
}
|
||||
|
||||
.quick-search svg {
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-search input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9375rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.quick-search input::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.quick-search kbd {
|
||||
padding: 4px 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.75rem;
|
||||
font-family: inherit;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Section Headers */
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.view-all {
|
||||
font-size: 0.875rem;
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.view-all:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Recent Section */
|
||||
.recent-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.recent-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.recent-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px 20px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.recent-card:hover {
|
||||
border-color: var(--accent-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.recent-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.home-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.75rem;
|
||||
background: linear-gradient(135deg, var(--text), var(--primary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
.recent-icon.doc {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.home-subtitle {
|
||||
.recent-icon.sheet {
|
||||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.recent-icon.slides {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.recent-icon.paper {
|
||||
background: linear-gradient(135deg, #ec4899, #db2777);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.recent-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recent-name {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.recent-meta {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.125rem;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Apps Section */
|
||||
.apps-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.apps-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||
display: block;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.app-card:hover {
|
||||
border-color: var(--accent-color);
|
||||
transform: translateY(-4px);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 8px 32px rgba(59, 130, 246, 0.15);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
|
|
@ -93,135 +230,168 @@ body {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.75rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-shrink: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.app-icon.chat { background: linear-gradient(135deg, #3b82f6, #1d4ed8); }
|
||||
.app-icon.drive { background: linear-gradient(135deg, #f59e0b, #d97706); }
|
||||
.app-icon.tasks { background: linear-gradient(135deg, #22c55e, #16a34a); }
|
||||
.app-icon.mail { background: linear-gradient(135deg, #ef4444, #dc2626); }
|
||||
.app-icon.calendar { background: linear-gradient(135deg, #a855f7, #7c3aed); }
|
||||
.app-icon.meet { background: linear-gradient(135deg, #06b6d4, #0891b2); }
|
||||
.app-icon.paper { background: linear-gradient(135deg, #eab308, #ca8a04); }
|
||||
.app-icon.research { background: linear-gradient(135deg, #ec4899, #db2777); }
|
||||
.app-icon.analytics { background: linear-gradient(135deg, #6366f1, #4f46e5); }
|
||||
|
||||
.app-name {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
.app-icon.chat {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
.app-icon.people {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
}
|
||||
.app-icon.mail {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
}
|
||||
.app-icon.meet {
|
||||
background: linear-gradient(135deg, #06b6d4, #0891b2);
|
||||
}
|
||||
.app-icon.calendar {
|
||||
background: linear-gradient(135deg, #a855f7, #9333ea);
|
||||
}
|
||||
.app-icon.tasks {
|
||||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||
}
|
||||
.app-icon.project {
|
||||
background: linear-gradient(135deg, #14b8a6, #0d9488);
|
||||
}
|
||||
.app-icon.goals {
|
||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
||||
}
|
||||
.app-icon.paper {
|
||||
background: linear-gradient(135deg, #ec4899, #db2777);
|
||||
}
|
||||
.app-icon.docs {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
}
|
||||
.app-icon.sheet {
|
||||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||
}
|
||||
.app-icon.slides {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
.app-icon.drive {
|
||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||||
}
|
||||
.app-icon.video {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
}
|
||||
.app-icon.player {
|
||||
background: linear-gradient(135deg, #a855f7, #9333ea);
|
||||
}
|
||||
.app-icon.social {
|
||||
background: linear-gradient(135deg, #06b6d4, #0891b2);
|
||||
}
|
||||
.app-icon.learn {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
}
|
||||
.app-icon.research {
|
||||
background: linear-gradient(135deg, #14b8a6, #0d9488);
|
||||
}
|
||||
.app-icon.analytics {
|
||||
background: linear-gradient(135deg, #6366f1, #4f46e5);
|
||||
}
|
||||
.app-icon.dashboards {
|
||||
background: linear-gradient(135deg, #8b5cf6, #7c3aed);
|
||||
}
|
||||
.app-icon.sources {
|
||||
background: linear-gradient(135deg, #a855f7, #9333ea);
|
||||
}
|
||||
.app-icon.canvas {
|
||||
background: linear-gradient(135deg, #ec4899, #db2777);
|
||||
}
|
||||
.app-icon.workspace {
|
||||
background: linear-gradient(135deg, #64748b, #475569);
|
||||
}
|
||||
.app-icon.designer {
|
||||
background: linear-gradient(135deg, #f97316, #ea580c);
|
||||
}
|
||||
.app-icon.editor {
|
||||
background: linear-gradient(135deg, #10b981, #059669);
|
||||
}
|
||||
.app-icon.attendant {
|
||||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||
}
|
||||
.app-icon.compliance {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
.app-icon.monitoring {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||
}
|
||||
.app-icon.settings {
|
||||
background: linear-gradient(135deg, #71717a, #52525b);
|
||||
}
|
||||
|
||||
.app-description {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
color: var(--text);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.quick-action-btn:hover {
|
||||
border-color: var(--primary);
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.recent-section {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.recent-list {
|
||||
.app-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recent-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
.app-content h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.recent-item:hover {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.recent-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.recent-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.recent-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.recent-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.recent-time {
|
||||
font-size: 0.75rem;
|
||||
.app-content p {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.home-container {
|
||||
padding: 1rem;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.home-title {
|
||||
.welcome-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.quick-search {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.apps-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.recent-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.app-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.app-content h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.app-content p {
|
||||
font-size: 0.8125rem;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
818
ui/suite/goals/goals.html
Normal file
818
ui/suite/goals/goals.html
Normal file
|
|
@ -0,0 +1,818 @@
|
|||
<!-- =============================================================================
|
||||
GOALS APP - OKR (Objectives & Key Results) Management
|
||||
Respects Theme Manager - No hardcoded theme
|
||||
============================================================================= -->
|
||||
|
||||
<div class="goals-app">
|
||||
<!-- Sidebar -->
|
||||
<aside class="goals-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2 data-i18n="goals-title">Goals (OKR)</h2>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="new-objective-btn"
|
||||
title="New Objective"
|
||||
hx-get="/api/goals/objectives/new"
|
||||
hx-target="#goals-modal"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span>+</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="period-selector">
|
||||
<select
|
||||
id="period-select"
|
||||
hx-get="/api/goals/objectives"
|
||||
hx-trigger="change"
|
||||
hx-target="#objectives-list"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="this"
|
||||
>
|
||||
<option value="q1-2025">Q1 2025</option>
|
||||
<option value="q2-2025" selected>Q2 2025</option>
|
||||
<option value="q3-2025">Q3 2025</option>
|
||||
<option value="q4-2025">Q4 2025</option>
|
||||
<option value="annual-2025">Annual 2025</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<h3 data-i18n="goals-objectives">Objectives</h3>
|
||||
<div
|
||||
id="objectives-list"
|
||||
hx-get="/api/goals/objectives"
|
||||
hx-trigger="load, objectiveCreated from:body, objectiveDeleted from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="loading-placeholder">Loading objectives...</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button
|
||||
class="sidebar-btn"
|
||||
hx-get="/api/goals/alignment"
|
||||
hx-target="#main-content"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span class="btn-icon">🔗</span>
|
||||
<span data-i18n="goals-alignment">Alignment</span>
|
||||
</button>
|
||||
<button
|
||||
class="sidebar-btn"
|
||||
hx-get="/api/goals/ai/suggest"
|
||||
hx-target="#goals-modal"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span class="btn-icon">✨</span>
|
||||
<span>AI Suggestions</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="goals-main">
|
||||
<!-- Dashboard Header -->
|
||||
<header class="goals-header">
|
||||
<div class="header-left">
|
||||
<h1 data-i18n="goals-dashboard">OKR Dashboard</h1>
|
||||
<span class="period-badge" id="current-period">Q2 2025</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" data-view="dashboard" onclick="switchGoalsView('dashboard')">
|
||||
<span>📊</span> Dashboard
|
||||
</button>
|
||||
<button class="view-btn" data-view="tree" onclick="switchGoalsView('tree')">
|
||||
<span>🌳</span> Tree View
|
||||
</button>
|
||||
<button class="view-btn" data-view="list" onclick="switchGoalsView('list')">
|
||||
<span>📋</span> List
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="btn-primary"
|
||||
hx-get="/api/goals/objectives/new"
|
||||
hx-target="#goals-modal"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span>+</span> New Objective
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid">
|
||||
<div
|
||||
class="stat-card"
|
||||
hx-get="/api/goals/dashboard"
|
||||
hx-trigger="load, objectiveUpdated from:body"
|
||||
hx-swap="innerHTML"
|
||||
hx-select=".stat-objectives"
|
||||
>
|
||||
<div class="stat-icon">🎯</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value" id="total-objectives">--</div>
|
||||
<div class="stat-label">Objectives</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🔑</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value" id="total-krs">--</div>
|
||||
<div class="stat-label">Key Results</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card progress-card">
|
||||
<div class="stat-icon">📈</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value" id="avg-progress">--</div>
|
||||
<div class="stat-label">Avg Progress</div>
|
||||
</div>
|
||||
<div class="progress-ring" id="progress-ring">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path class="progress-bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
||||
<path class="progress-bar" stroke-dasharray="0, 100" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">⚠️</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value at-risk" id="at-risk-count">--</div>
|
||||
<div class="stat-label">At Risk</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View Containers -->
|
||||
<div id="main-content" class="goals-views">
|
||||
<!-- Dashboard View -->
|
||||
<div id="dashboard-view" class="view-container active">
|
||||
<div class="objectives-grid"
|
||||
hx-get="/api/goals/objectives"
|
||||
hx-trigger="load, objectiveCreated from:body, objectiveUpdated from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="loading-placeholder">Loading objectives...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tree View -->
|
||||
<div id="tree-view" class="view-container">
|
||||
<div
|
||||
class="alignment-tree"
|
||||
hx-get="/api/goals/alignment"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="loading-placeholder">Loading alignment tree...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<div id="list-view" class="view-container">
|
||||
<div class="objectives-table">
|
||||
<div class="table-header">
|
||||
<div class="col-objective">Objective</div>
|
||||
<div class="col-owner">Owner</div>
|
||||
<div class="col-progress">Progress</div>
|
||||
<div class="col-status">Status</div>
|
||||
<div class="col-krs">Key Results</div>
|
||||
<div class="col-actions">Actions</div>
|
||||
</div>
|
||||
<div
|
||||
class="table-body"
|
||||
hx-get="/api/goals/objectives?view=list"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="loading-placeholder">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="goals-empty" class="empty-state">
|
||||
<div class="empty-state-icon">🎯</div>
|
||||
<h2>No Objectives Yet</h2>
|
||||
<p>Create your first objective to start tracking your OKRs</p>
|
||||
<button
|
||||
class="btn-primary"
|
||||
hx-get="/api/goals/objectives/new"
|
||||
hx-target="#goals-modal"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span>+</span> Create Objective
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Details Panel -->
|
||||
<aside class="details-panel collapsed" id="details-panel">
|
||||
<button class="panel-toggle" onclick="toggleGoalsPanel()">
|
||||
<span>📋</span>
|
||||
</button>
|
||||
<div class="panel-content">
|
||||
<div id="objective-details">
|
||||
<p class="empty-message">Select an objective to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Modal Container -->
|
||||
<div id="goals-modal" class="modal-container"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.goals-app {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.goals-sidebar {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.period-selector {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.period-selector select {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.nav-section h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sidebar-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.goals-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goals-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.period-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-value.at-risk {
|
||||
color: var(--color-warning, #f59e0b);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.progress-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-ring {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.progress-ring svg {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.progress-bg {
|
||||
fill: none;
|
||||
stroke: var(--border-color);
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
fill: none;
|
||||
stroke: var(--accent-color);
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dasharray 0.5s;
|
||||
}
|
||||
|
||||
/* Views */
|
||||
.goals-views {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.view-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.view-container.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.objectives-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Objective Card */
|
||||
.objective-card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.objective-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.objective-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.objective-owner {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.objective-status {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-on-track {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.status-at-risk {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.status-behind {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.objective-progress {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.progress-bar-container {
|
||||
height: 8px;
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--accent-color);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.key-results-list {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.key-result-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.key-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.kr-progress {
|
||||
width: 40px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.kr-title {
|
||||
flex: 1;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.kr-check-in {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.kr-check-in:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Table View */
|
||||
.objectives-table {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 120px 100px 80px 80px;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Details Panel */
|
||||
.details-panel {
|
||||
width: 360px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border-color);
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.details-panel.collapsed {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.details-panel.collapsed .panel-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel-toggle {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 48px);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.goals-app:not(.has-objectives) .goals-views {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.goals-app:not(.has-objectives) .empty-state {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.goals-sidebar {
|
||||
position: absolute;
|
||||
left: -280px;
|
||||
height: 100%;
|
||||
z-index: 50;
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
.goals-sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.details-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.objectives-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentGoalsView = 'dashboard';
|
||||
|
||||
function switchGoalsView(view) {
|
||||
currentGoalsView = view;
|
||||
|
||||
document.querySelectorAll('.goals-app .view-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.view === view);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.goals-app .view-container').forEach(container => {
|
||||
container.classList.toggle('active', container.id === `${view}-view`);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGoalsPanel() {
|
||||
const panel = document.getElementById('details-panel');
|
||||
panel.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
function selectObjective(objectiveId) {
|
||||
htmx.ajax('GET', `/api/goals/objectives/${objectiveId}`, {
|
||||
target: '#objective-details',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
|
||||
const panel = document.getElementById('details-panel');
|
||||
panel.classList.remove('collapsed');
|
||||
}
|
||||
|
||||
function updateProgressRing(percentage) {
|
||||
const ring = document.querySelector('.progress-bar');
|
||||
if (ring) {
|
||||
ring.setAttribute('stroke-dasharray', `${percentage}, 100`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkIn(krId) {
|
||||
htmx.ajax('GET', `/api/goals/key-results/${krId}/check-in`, {
|
||||
target: '#goals-modal',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.detail.target.id === 'objectives-list' ||
|
||||
event.detail.target.classList.contains('objectives-grid')) {
|
||||
const hasObjectives = event.detail.target.children.length > 0;
|
||||
document.querySelector('.goals-app').classList.toggle('has-objectives', hasObjectives);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
1800
ui/suite/home.html
1800
ui/suite/home.html
File diff suppressed because it is too large
Load diff
|
|
@ -95,7 +95,9 @@
|
|||
<div class="header-left">
|
||||
<button
|
||||
class="logo-wrapper"
|
||||
onclick="window.location.href='/#chat'"
|
||||
hx-get="/suite/home.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#home"
|
||||
title="General Bots"
|
||||
aria-label="General Bots - Home"
|
||||
>
|
||||
|
|
@ -450,6 +452,69 @@
|
|||
<span data-i18n="nav-chat">Chat</span>
|
||||
</a>
|
||||
|
||||
<!-- People (Contacts) -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#people"
|
||||
data-section="people"
|
||||
role="menuitem"
|
||||
aria-label="People - Contacts"
|
||||
hx-get="/suite/people/people.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#people"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"
|
||||
/>
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-people">People</span>
|
||||
</a>
|
||||
|
||||
<!-- Paper -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#paper"
|
||||
data-section="paper"
|
||||
role="menuitem"
|
||||
aria-label="Paper"
|
||||
hx-get="/suite/paper/paper.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#paper"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"
|
||||
/>
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<line x1="10" y1="9" x2="8" y2="9" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-paper">Paper</span>
|
||||
</a>
|
||||
|
||||
<!-- Docs -->
|
||||
<a
|
||||
class="app-item"
|
||||
|
|
@ -945,16 +1010,16 @@
|
|||
<span data-i18n="nav-sources">Sources</span>
|
||||
</a>
|
||||
|
||||
<!-- Tools -->
|
||||
<!-- Compliance -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#tools"
|
||||
data-section="tools"
|
||||
href="#compliance"
|
||||
data-section="compliance"
|
||||
role="menuitem"
|
||||
aria-label="Tools"
|
||||
aria-label="Compliance"
|
||||
hx-get="/suite/tools/compliance.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#tools"
|
||||
hx-push-url="/#compliance"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
|
|
@ -966,11 +1031,14 @@
|
|||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z"
|
||||
d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"
|
||||
/>
|
||||
<path d="M9 12l2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-tools">Tools</span>
|
||||
<span data-i18n="nav-compliance"
|
||||
>Compliance</span
|
||||
>
|
||||
</a>
|
||||
|
||||
<!-- Designer (.bas Editor) -->
|
||||
|
|
@ -1030,6 +1098,261 @@
|
|||
</div>
|
||||
<span data-i18n="nav-attendant">Attendant</span>
|
||||
</a>
|
||||
|
||||
<!-- Project -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#project"
|
||||
data-section="project"
|
||||
role="menuitem"
|
||||
aria-label="Project Management"
|
||||
hx-get="/suite/project/project.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#project"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<path d="M3 9h18" />
|
||||
<path d="M9 21V9" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-project">Project</span>
|
||||
</a>
|
||||
|
||||
<!-- Canvas -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#canvas"
|
||||
data-section="canvas"
|
||||
role="menuitem"
|
||||
aria-label="Canvas Whiteboard"
|
||||
hx-get="/suite/canvas/canvas.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#canvas"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="3"
|
||||
y="3"
|
||||
width="18"
|
||||
height="18"
|
||||
rx="2"
|
||||
/>
|
||||
<path d="M8 12h8" />
|
||||
<path d="M12 8v8" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-canvas">Canvas</span>
|
||||
</a>
|
||||
|
||||
<!-- Goals -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#goals"
|
||||
data-section="goals"
|
||||
role="menuitem"
|
||||
aria-label="Goals & OKRs"
|
||||
hx-get="/suite/goals/goals.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#goals"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="6" />
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-goals">Goals</span>
|
||||
</a>
|
||||
|
||||
<!-- Player -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#player"
|
||||
data-section="player"
|
||||
role="menuitem"
|
||||
aria-label="Media Player"
|
||||
hx-get="/suite/player/player.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#player"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polygon points="5 3 19 12 5 21 5 3" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-player">Player</span>
|
||||
</a>
|
||||
|
||||
<!-- Workspace -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#workspace"
|
||||
data-section="workspace"
|
||||
role="menuitem"
|
||||
aria-label="Workspace"
|
||||
hx-get="/suite/workspace/workspace.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#workspace"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="2"
|
||||
y="7"
|
||||
width="20"
|
||||
height="14"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<path
|
||||
d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-workspace">Workspace</span>
|
||||
</a>
|
||||
|
||||
<!-- Video -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#video"
|
||||
data-section="video"
|
||||
role="menuitem"
|
||||
aria-label="Video"
|
||||
hx-get="/suite/video/video.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#video"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="20"
|
||||
height="20"
|
||||
rx="2.18"
|
||||
ry="2.18"
|
||||
/>
|
||||
<line x1="7" y1="2" x2="7" y2="22" />
|
||||
<line x1="17" y1="2" x2="17" y2="22" />
|
||||
<line x1="2" y1="12" x2="22" y2="12" />
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-video">Video</span>
|
||||
</a>
|
||||
|
||||
<!-- Learn -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#learn"
|
||||
data-section="learn"
|
||||
role="menuitem"
|
||||
aria-label="Learn"
|
||||
hx-get="/suite/learn/learn.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#learn"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"
|
||||
/>
|
||||
<path
|
||||
d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-learn">Learn</span>
|
||||
</a>
|
||||
|
||||
<!-- Settings (last) -->
|
||||
<a
|
||||
class="app-item"
|
||||
href="#settings"
|
||||
data-section="settings"
|
||||
role="menuitem"
|
||||
aria-label="Settings"
|
||||
hx-get="/suite/settings/index.html"
|
||||
hx-target="#main-content"
|
||||
hx-push-url="/#settings"
|
||||
>
|
||||
<div class="app-icon" aria-hidden="true">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span data-i18n="nav-settings">Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@
|
|||
"nav-tools": "Tools",
|
||||
"nav-attendant": "Attendant",
|
||||
"nav-settings": "Settings",
|
||||
"nav-workspace": "Workspace",
|
||||
"nav-project": "Project",
|
||||
"nav-goals": "Goals",
|
||||
"nav-security": "Security",
|
||||
"nav-player": "Player",
|
||||
"nav-canvas": "Canvas",
|
||||
"nav-search": "Search...",
|
||||
"nav-all-apps": "All Applications",
|
||||
"dashboard-title": "Dashboard",
|
||||
|
|
@ -461,6 +467,66 @@
|
|||
"a11y-loading": "Loading, please wait",
|
||||
"a11y-menu-open": "Open menu",
|
||||
"a11y-menu-close": "Close menu",
|
||||
"workspace-title": "Workspace",
|
||||
"workspace-new-page": "New Page",
|
||||
"workspace-search-pages": "Search pages...",
|
||||
"workspace-recent": "Recent",
|
||||
"workspace-favorites": "Favorites",
|
||||
"workspace-shared": "Shared with me",
|
||||
"workspace-trash": "Trash",
|
||||
"workspace-settings": "Workspace Settings",
|
||||
"workspace-members": "Members",
|
||||
"workspace-templates": "Templates",
|
||||
"project-title": "Project",
|
||||
"project-new": "New Project",
|
||||
"project-tasks": "Tasks",
|
||||
"project-timeline": "Timeline",
|
||||
"project-gantt": "Gantt Chart",
|
||||
"project-resources": "Resources",
|
||||
"project-milestones": "Milestones",
|
||||
"project-critical-path": "Critical Path",
|
||||
"project-progress": "Progress",
|
||||
"goals-title": "Goals (OKR)",
|
||||
"goals-objectives": "Objectives",
|
||||
"goals-key-results": "Key Results",
|
||||
"goals-new-objective": "New Objective",
|
||||
"goals-new-key-result": "New Key Result",
|
||||
"goals-progress": "Progress",
|
||||
"goals-check-in": "Check-in",
|
||||
"goals-alignment": "Alignment",
|
||||
"goals-dashboard": "OKR Dashboard",
|
||||
"goals-periods": "Periods",
|
||||
"security-title": "Security",
|
||||
"security-overview": "Security Overview",
|
||||
"security-tls": "TLS Settings",
|
||||
"security-cors": "CORS Settings",
|
||||
"security-rate-limit": "Rate Limiting",
|
||||
"security-api-keys": "API Keys",
|
||||
"security-audit": "Audit Log",
|
||||
"security-mfa": "Multi-Factor Auth",
|
||||
"security-sessions": "Active Sessions",
|
||||
"security-password-policy": "Password Policy",
|
||||
"security-scan": "Run Security Scan",
|
||||
"player-title": "Player",
|
||||
"player-play": "Play",
|
||||
"player-pause": "Pause",
|
||||
"player-stop": "Stop",
|
||||
"player-volume": "Volume",
|
||||
"player-fullscreen": "Fullscreen",
|
||||
"player-download": "Download",
|
||||
"player-next": "Next",
|
||||
"player-previous": "Previous",
|
||||
"canvas-title": "Canvas",
|
||||
"canvas-new": "New Canvas",
|
||||
"canvas-draw": "Draw",
|
||||
"canvas-shapes": "Shapes",
|
||||
"canvas-text": "Text",
|
||||
"canvas-image": "Image",
|
||||
"canvas-undo": "Undo",
|
||||
"canvas-redo": "Redo",
|
||||
"canvas-clear": "Clear",
|
||||
"canvas-export": "Export",
|
||||
"canvas-collaborate": "Collaborate",
|
||||
},
|
||||
"pt-BR": {
|
||||
"app-name": "General Bots",
|
||||
|
|
@ -484,6 +550,12 @@
|
|||
"nav-tools": "Ferramentas",
|
||||
"nav-attendant": "Atendente",
|
||||
"nav-settings": "Configurações",
|
||||
"nav-workspace": "Área de Trabalho",
|
||||
"nav-project": "Projeto",
|
||||
"nav-goals": "Metas",
|
||||
"nav-security": "Segurança",
|
||||
"nav-player": "Player",
|
||||
"nav-canvas": "Canvas",
|
||||
"nav-search": "Buscar...",
|
||||
"nav-all-apps": "Todos os Aplicativos",
|
||||
"dashboard-title": "Painel",
|
||||
|
|
@ -922,6 +994,66 @@
|
|||
"a11y-loading": "Carregando, por favor aguarde",
|
||||
"a11y-menu-open": "Abrir menu",
|
||||
"a11y-menu-close": "Fechar menu",
|
||||
"workspace-title": "Área de Trabalho",
|
||||
"workspace-new-page": "Nova Página",
|
||||
"workspace-search-pages": "Buscar páginas...",
|
||||
"workspace-recent": "Recentes",
|
||||
"workspace-favorites": "Favoritos",
|
||||
"workspace-shared": "Compartilhados comigo",
|
||||
"workspace-trash": "Lixeira",
|
||||
"workspace-settings": "Configurações do Espaço",
|
||||
"workspace-members": "Membros",
|
||||
"workspace-templates": "Modelos",
|
||||
"project-title": "Projeto",
|
||||
"project-new": "Novo Projeto",
|
||||
"project-tasks": "Tarefas",
|
||||
"project-timeline": "Linha do Tempo",
|
||||
"project-gantt": "Gráfico de Gantt",
|
||||
"project-resources": "Recursos",
|
||||
"project-milestones": "Marcos",
|
||||
"project-critical-path": "Caminho Crítico",
|
||||
"project-progress": "Progresso",
|
||||
"goals-title": "Metas (OKR)",
|
||||
"goals-objectives": "Objetivos",
|
||||
"goals-key-results": "Resultados-Chave",
|
||||
"goals-new-objective": "Novo Objetivo",
|
||||
"goals-new-key-result": "Novo Resultado-Chave",
|
||||
"goals-progress": "Progresso",
|
||||
"goals-check-in": "Check-in",
|
||||
"goals-alignment": "Alinhamento",
|
||||
"goals-dashboard": "Painel OKR",
|
||||
"goals-periods": "Períodos",
|
||||
"security-title": "Segurança",
|
||||
"security-overview": "Visão Geral de Segurança",
|
||||
"security-tls": "Configurações TLS",
|
||||
"security-cors": "Configurações CORS",
|
||||
"security-rate-limit": "Limitação de Taxa",
|
||||
"security-api-keys": "Chaves de API",
|
||||
"security-audit": "Log de Auditoria",
|
||||
"security-mfa": "Autenticação Multi-Fator",
|
||||
"security-sessions": "Sessões Ativas",
|
||||
"security-password-policy": "Política de Senhas",
|
||||
"security-scan": "Executar Verificação",
|
||||
"player-title": "Player",
|
||||
"player-play": "Reproduzir",
|
||||
"player-pause": "Pausar",
|
||||
"player-stop": "Parar",
|
||||
"player-volume": "Volume",
|
||||
"player-fullscreen": "Tela Cheia",
|
||||
"player-download": "Baixar",
|
||||
"player-next": "Próximo",
|
||||
"player-previous": "Anterior",
|
||||
"canvas-title": "Canvas",
|
||||
"canvas-new": "Novo Canvas",
|
||||
"canvas-draw": "Desenhar",
|
||||
"canvas-shapes": "Formas",
|
||||
"canvas-text": "Texto",
|
||||
"canvas-image": "Imagem",
|
||||
"canvas-undo": "Desfazer",
|
||||
"canvas-redo": "Refazer",
|
||||
"canvas-clear": "Limpar",
|
||||
"canvas-export": "Exportar",
|
||||
"canvas-collaborate": "Colaborar",
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
622
ui/suite/people/people.css
Normal file
622
ui/suite/people/people.css
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
.people-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.people-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.people-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-box svg {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: var(--text-secondary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
padding: 10px 12px 10px 40px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
width: 280px;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.search-box input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px var(--accent-color-alpha, rgba(99, 102, 241, 0.1));
|
||||
}
|
||||
|
||||
.search-box input::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.people-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.alphabet-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 12px 24px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.alpha-btn {
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.alpha-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.alpha-btn.active {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.contacts-list {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.contact-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.group-contacts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.contact-card:hover {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.contact-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-avatar.large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.contact-name {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.contact-detail {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.contact-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.contact-card:hover .contact-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.icon-btn.small {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.contact-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 400px;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
border-left: 1px solid var(--border-color);
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.contact-panel.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.contact-header {
|
||||
text-align: center;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.contact-header h2 {
|
||||
margin: 16px 0 4px 0;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.contact-title {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.contact-company {
|
||||
margin: 4px 0 0 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.contact-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.field a,
|
||||
.field p {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.field a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.field a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.contact-quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.modal {
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
background: var(--bg-primary);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.modal::backdrop {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px var(--accent-color-alpha, rgba(99, 102, 241, 0.1));
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 24px;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-state svg {
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1.125rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
margin: 0 0 24px 0;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 24px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid var(--border-color);
|
||||
border-top-color: var(--accent-color);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.groups-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.directory-tree {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.recent-list {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.people-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-box input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.contact-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.alphabet-filter {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
527
ui/suite/people/people.html
Normal file
527
ui/suite/people/people.html
Normal file
|
|
@ -0,0 +1,527 @@
|
|||
<link rel="stylesheet" href="people/people.css" />
|
||||
|
||||
<div class="people-container">
|
||||
<!-- Header -->
|
||||
<header class="people-header">
|
||||
<div class="header-left">
|
||||
<h1 data-i18n="people-title">People</h1>
|
||||
<p class="header-subtitle" data-i18n="people-subtitle">
|
||||
Contacts, Groups & Directory
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<div class="search-box">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search contacts..."
|
||||
data-i18n-placeholder="people-search"
|
||||
id="people-search"
|
||||
/>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="openAddContact()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="8.5" cy="7" r="4"/>
|
||||
<line x1="20" y1="8" x2="20" y2="14"/>
|
||||
<line x1="23" y1="11" x2="17" y2="11"/>
|
||||
</svg>
|
||||
<span data-i18n="people-add">Add Contact</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<nav class="tab-nav" role="tablist">
|
||||
<button class="tab-btn active" role="tab" aria-selected="true" onclick="showTab('contacts', this)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
</svg>
|
||||
<span data-i18n="people-tab-contacts">Contacts</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('groups', this)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
<span data-i18n="people-tab-groups">Groups</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('directory', this)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
||||
</svg>
|
||||
<span data-i18n="people-tab-directory">Directory</span>
|
||||
</button>
|
||||
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('recent', this)">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
<span data-i18n="people-tab-recent">Recent</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="people-content">
|
||||
<!-- Contacts Tab -->
|
||||
<div id="contacts-tab" class="tab-content active">
|
||||
<!-- Alphabet Filter -->
|
||||
<div class="alphabet-filter">
|
||||
<button class="alpha-btn active" onclick="filterByLetter('all', this)">All</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('A', this)">A</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('B', this)">B</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('C', this)">C</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('D', this)">D</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('E', this)">E</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('F', this)">F</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('G', this)">G</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('H', this)">H</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('I', this)">I</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('J', this)">J</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('K', this)">K</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('L', this)">L</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('M', this)">M</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('N', this)">N</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('O', this)">O</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('P', this)">P</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('Q', this)">Q</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('R', this)">R</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('S', this)">S</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('T', this)">T</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('U', this)">U</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('V', this)">V</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('W', this)">W</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('X', this)">X</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('Y', this)">Y</button>
|
||||
<button class="alpha-btn" onclick="filterByLetter('Z', this)">Z</button>
|
||||
</div>
|
||||
|
||||
<!-- Contacts List -->
|
||||
<div class="contacts-list" id="contacts-list">
|
||||
<!-- Contacts will be loaded here -->
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p data-i18n="people-loading">Loading contacts...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Groups Tab -->
|
||||
<div id="groups-tab" class="tab-content">
|
||||
<div class="groups-grid" id="groups-list">
|
||||
<!-- Groups will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Directory Tab -->
|
||||
<div id="directory-tab" class="tab-content">
|
||||
<div class="directory-tree" id="directory-tree">
|
||||
<!-- Organization directory will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Tab -->
|
||||
<div id="recent-tab" class="tab-content">
|
||||
<div class="recent-list" id="recent-list">
|
||||
<!-- Recent contacts will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Detail Panel (slides in from right) -->
|
||||
<div class="contact-panel" id="contact-panel">
|
||||
<div class="panel-header">
|
||||
<button class="close-btn" onclick="closeContactPanel()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="panel-actions">
|
||||
<button class="icon-btn" title="Edit" onclick="editContact()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-btn" title="Delete" onclick="deleteContact()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-content" id="contact-detail">
|
||||
<!-- Contact details will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Contact Modal -->
|
||||
<dialog id="contact-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="modal-title" data-i18n="people-add-contact">Add Contact</h2>
|
||||
<button class="close-btn" onclick="closeModal()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form id="contact-form" onsubmit="saveContact(event)">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label data-i18n="people-first-name">First Name</label>
|
||||
<input type="text" name="firstName" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="people-last-name">Last Name</label>
|
||||
<input type="text" name="lastName" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="people-email">Email</label>
|
||||
<input type="email" name="email" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="people-phone">Phone</label>
|
||||
<input type="tel" name="phone" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="people-company">Company</label>
|
||||
<input type="text" name="company" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="people-title">Title</label>
|
||||
<input type="text" name="title" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label data-i18n="people-notes">Notes</label>
|
||||
<textarea name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal()" data-i18n="cancel">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" data-i18n="save">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script>
|
||||
let currentContact = null;
|
||||
let contacts = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadContacts();
|
||||
});
|
||||
|
||||
async function loadContacts() {
|
||||
try {
|
||||
const response = await fetch('/api/contacts');
|
||||
if (response.ok) {
|
||||
contacts = await response.json();
|
||||
renderContacts(contacts);
|
||||
} else {
|
||||
renderEmptyState();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load contacts:', error);
|
||||
renderEmptyState();
|
||||
}
|
||||
}
|
||||
|
||||
function renderContacts(contactsList) {
|
||||
const container = document.getElementById('contacts-list');
|
||||
if (!contactsList || contactsList.length === 0) {
|
||||
renderEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
const grouped = groupByLetter(contactsList);
|
||||
let html = '';
|
||||
|
||||
for (const [letter, group] of Object.entries(grouped)) {
|
||||
html += `<div class="contact-group" data-letter="${letter}">
|
||||
<div class="group-header">${letter}</div>
|
||||
<div class="group-contacts">`;
|
||||
|
||||
for (const contact of group) {
|
||||
html += renderContactCard(contact);
|
||||
}
|
||||
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderContactCard(contact) {
|
||||
const initials = getInitials(contact.firstName, contact.lastName);
|
||||
const name = `${contact.firstName} ${contact.lastName}`;
|
||||
|
||||
return `<div class="contact-card" onclick="showContact('${contact.id}')">
|
||||
<div class="contact-avatar" style="background: ${getAvatarColor(name)}">${initials}</div>
|
||||
<div class="contact-info">
|
||||
<div class="contact-name">${name}</div>
|
||||
<div class="contact-detail">${contact.email || contact.phone || ''}</div>
|
||||
</div>
|
||||
<div class="contact-actions">
|
||||
<button class="icon-btn small" onclick="event.stopPropagation(); startChat('${contact.id}')" title="Chat">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="icon-btn small" onclick="event.stopPropagation(); sendEmail('${contact.email}')" title="Email">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||
<polyline points="22,6 12,13 2,6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderEmptyState() {
|
||||
document.getElementById('contacts-list').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
<h3 data-i18n="people-empty-title">No contacts yet</h3>
|
||||
<p data-i18n="people-empty-desc">Add your first contact to get started</p>
|
||||
<button class="btn btn-primary" onclick="openAddContact()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
<span data-i18n="people-add">Add Contact</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function groupByLetter(contactsList) {
|
||||
const grouped = {};
|
||||
for (const contact of contactsList) {
|
||||
const letter = (contact.lastName || contact.firstName || '#').charAt(0).toUpperCase();
|
||||
if (!grouped[letter]) grouped[letter] = [];
|
||||
grouped[letter].push(contact);
|
||||
}
|
||||
return Object.fromEntries(Object.entries(grouped).sort());
|
||||
}
|
||||
|
||||
function getInitials(firstName, lastName) {
|
||||
return ((firstName?.charAt(0) || '') + (lastName?.charAt(0) || '')).toUpperCase() || '?';
|
||||
}
|
||||
|
||||
function getAvatarColor(name) {
|
||||
const colors = ['#6366f1', '#8b5cf6', '#ec4899', '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6'];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
}
|
||||
|
||||
function showTab(tabId, btn) {
|
||||
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.classList.remove('active');
|
||||
b.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
|
||||
document.getElementById(tabId + '-tab').classList.add('active');
|
||||
btn.classList.add('active');
|
||||
btn.setAttribute('aria-selected', 'true');
|
||||
}
|
||||
|
||||
function filterByLetter(letter, btn) {
|
||||
document.querySelectorAll('.alpha-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
|
||||
document.querySelectorAll('.contact-group').forEach(group => {
|
||||
if (letter === 'all' || group.dataset.letter === letter) {
|
||||
group.style.display = '';
|
||||
} else {
|
||||
group.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showContact(id) {
|
||||
currentContact = contacts.find(c => c.id === id);
|
||||
if (!currentContact) return;
|
||||
|
||||
const panel = document.getElementById('contact-panel');
|
||||
const detail = document.getElementById('contact-detail');
|
||||
|
||||
detail.innerHTML = `
|
||||
<div class="contact-header">
|
||||
<div class="contact-avatar large" style="background: ${getAvatarColor(currentContact.firstName + ' ' + currentContact.lastName)}">
|
||||
${getInitials(currentContact.firstName, currentContact.lastName)}
|
||||
</div>
|
||||
<h2>${currentContact.firstName} ${currentContact.lastName}</h2>
|
||||
${currentContact.title ? `<p class="contact-title">${currentContact.title}</p>` : ''}
|
||||
${currentContact.company ? `<p class="contact-company">${currentContact.company}</p>` : ''}
|
||||
</div>
|
||||
<div class="contact-fields">
|
||||
${currentContact.email ? `
|
||||
<div class="field">
|
||||
<label>Email</label>
|
||||
<a href="mailto:${currentContact.email}">${currentContact.email}</a>
|
||||
</div>
|
||||
` : ''}
|
||||
${currentContact.phone ? `
|
||||
<div class="field">
|
||||
<label>Phone</label>
|
||||
<a href="tel:${currentContact.phone}">${currentContact.phone}</a>
|
||||
</div>
|
||||
` : ''}
|
||||
${currentContact.notes ? `
|
||||
<div class="field">
|
||||
<label>Notes</label>
|
||||
<p>${currentContact.notes}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="contact-quick-actions">
|
||||
<button class="action-btn" onclick="startChat('${currentContact.id}')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
Chat
|
||||
</button>
|
||||
<button class="action-btn" onclick="sendEmail('${currentContact.email}')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||
<polyline points="22,6 12,13 2,6"/>
|
||||
</svg>
|
||||
Email
|
||||
</button>
|
||||
<button class="action-btn" onclick="scheduleMeeting('${currentContact.id}')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||
</svg>
|
||||
Meeting
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
panel.classList.add('open');
|
||||
}
|
||||
|
||||
function closeContactPanel() {
|
||||
document.getElementById('contact-panel').classList.remove('open');
|
||||
currentContact = null;
|
||||
}
|
||||
|
||||
function openAddContact() {
|
||||
currentContact = null;
|
||||
document.getElementById('modal-title').textContent = 'Add Contact';
|
||||
document.getElementById('contact-form').reset();
|
||||
document.getElementById('contact-modal').showModal();
|
||||
}
|
||||
|
||||
function editContact() {
|
||||
if (!currentContact) return;
|
||||
document.getElementById('modal-title').textContent = 'Edit Contact';
|
||||
const form = document.getElementById('contact-form');
|
||||
form.firstName.value = currentContact.firstName || '';
|
||||
form.lastName.value = currentContact.lastName || '';
|
||||
form.email.value = currentContact.email || '';
|
||||
form.phone.value = currentContact.phone || '';
|
||||
form.company.value = currentContact.company || '';
|
||||
form.title.value = currentContact.title || '';
|
||||
form.notes.value = currentContact.notes || '';
|
||||
document.getElementById('contact-modal').showModal();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('contact-modal').close();
|
||||
}
|
||||
|
||||
async function saveContact(event) {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
const data = {
|
||||
firstName: form.firstName.value,
|
||||
lastName: form.lastName.value,
|
||||
email: form.email.value,
|
||||
phone: form.phone.value,
|
||||
company: form.company.value,
|
||||
title: form.title.value,
|
||||
notes: form.notes.value
|
||||
};
|
||||
|
||||
try {
|
||||
const url = currentContact ? `/api/contacts/${currentContact.id}` : '/api/contacts';
|
||||
const method = currentContact ? 'PUT' : 'POST';
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
closeModal();
|
||||
loadContacts();
|
||||
if (currentContact) closeContactPanel();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save contact:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteContact() {
|
||||
if (!currentContact || !confirm('Delete this contact?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/contacts/${currentContact.id}`, { method: 'DELETE' });
|
||||
if (response.ok) {
|
||||
closeContactPanel();
|
||||
loadContacts();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete contact:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function startChat(contactId) {
|
||||
window.location.href = `/#chat?contact=${contactId}`;
|
||||
}
|
||||
|
||||
function sendEmail(email) {
|
||||
if (email) window.location.href = `mailto:${email}`;
|
||||
}
|
||||
|
||||
function scheduleMeeting(contactId) {
|
||||
window.location.href = `/#calendar?new=meeting&contact=${contactId}`;
|
||||
}
|
||||
|
||||
// Search functionality
|
||||
document.getElementById('people-search')?.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase();
|
||||
const filtered = contacts.filter(c =>
|
||||
(c.firstName + ' ' + c.lastName).toLowerCase().includes(query) ||
|
||||
(c.email || '').toLowerCase().includes(query) ||
|
||||
(c.company || '').toLowerCase().includes(query)
|
||||
);
|
||||
renderContacts(filtered);
|
||||
});
|
||||
</script>
|
||||
1003
ui/suite/player/player.html
Normal file
1003
ui/suite/player/player.html
Normal file
File diff suppressed because it is too large
Load diff
758
ui/suite/project/project.html
Normal file
758
ui/suite/project/project.html
Normal file
|
|
@ -0,0 +1,758 @@
|
|||
<!-- =============================================================================
|
||||
PROJECT APP - Project Management with Gantt Chart
|
||||
Respects Theme Manager - No hardcoded theme
|
||||
============================================================================= -->
|
||||
|
||||
<div class="project-app">
|
||||
<!-- Sidebar -->
|
||||
<aside class="project-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2 data-i18n="project-title">Projects</h2>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="new-project-btn"
|
||||
title="New Project"
|
||||
hx-get="/api/ui/project/new"
|
||||
hx-target="#project-modal"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span>+</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-search">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search projects..."
|
||||
hx-get="/projects"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#project-list"
|
||||
hx-swap="innerHTML"
|
||||
name="q"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div
|
||||
id="project-list"
|
||||
hx-get="/projects"
|
||||
hx-trigger="load, projectCreated from:body, projectDeleted from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="loading-placeholder">Loading projects...</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="project-main">
|
||||
<!-- Project Header -->
|
||||
<header class="project-header">
|
||||
<div class="project-info">
|
||||
<h1 id="project-name">Select a Project</h1>
|
||||
<div class="project-meta">
|
||||
<span class="meta-item" id="project-status">
|
||||
<span class="status-dot"></span>
|
||||
<span>No project selected</span>
|
||||
</span>
|
||||
<span class="meta-item" id="project-progress">
|
||||
<span class="progress-label">Progress:</span>
|
||||
<span class="progress-value">--</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-actions">
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" data-view="gantt" onclick="switchView('gantt')">
|
||||
<span>📊</span> Gantt
|
||||
</button>
|
||||
<button class="view-btn" data-view="timeline" onclick="switchView('timeline')">
|
||||
<span>📅</span> Timeline
|
||||
</button>
|
||||
<button class="view-btn" data-view="list" onclick="switchView('list')">
|
||||
<span>📋</span> List
|
||||
</button>
|
||||
<button class="view-btn" data-view="board" onclick="switchView('board')">
|
||||
<span>📌</span> Board
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="btn-primary"
|
||||
id="add-task-btn"
|
||||
hx-get="/api/ui/project/task/new"
|
||||
hx-target="#project-modal"
|
||||
hx-swap="innerHTML"
|
||||
disabled
|
||||
>
|
||||
<span>+</span> Add Task
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- View Containers -->
|
||||
<div class="project-views">
|
||||
<!-- Gantt Chart View -->
|
||||
<div id="gantt-view" class="view-container active">
|
||||
<div class="gantt-toolbar">
|
||||
<div class="gantt-zoom">
|
||||
<button class="zoom-btn" onclick="zoomGantt('day')">Day</button>
|
||||
<button class="zoom-btn active" onclick="zoomGantt('week')">Week</button>
|
||||
<button class="zoom-btn" onclick="zoomGantt('month')">Month</button>
|
||||
<button class="zoom-btn" onclick="zoomGantt('quarter')">Quarter</button>
|
||||
</div>
|
||||
<div class="gantt-filters">
|
||||
<label>
|
||||
<input type="checkbox" id="show-critical" checked onchange="toggleCriticalPath()">
|
||||
<span data-i18n="project-critical-path">Show Critical Path</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="show-milestones" checked onchange="toggleMilestones()">
|
||||
<span data-i18n="project-milestones">Show Milestones</span>
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn-secondary" onclick="fitGanttToScreen()">
|
||||
Fit to Screen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="gantt-container">
|
||||
<div class="gantt-table">
|
||||
<div class="gantt-table-header">
|
||||
<div class="col-name">Task Name</div>
|
||||
<div class="col-start">Start</div>
|
||||
<div class="col-end">End</div>
|
||||
<div class="col-duration">Duration</div>
|
||||
<div class="col-progress">Progress</div>
|
||||
<div class="col-assignee">Assignee</div>
|
||||
</div>
|
||||
<div
|
||||
id="gantt-table-body"
|
||||
class="gantt-table-body"
|
||||
hx-get="/api/ui/project/tasks"
|
||||
hx-trigger="projectSelected from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="empty-state-inline">
|
||||
Select a project to view tasks
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gantt-chart">
|
||||
<div class="gantt-timeline-header" id="gantt-timeline-header">
|
||||
<!-- Timeline headers generated by JS -->
|
||||
</div>
|
||||
<div
|
||||
id="gantt-chart-body"
|
||||
class="gantt-chart-body"
|
||||
hx-get="/api/ui/project/gantt"
|
||||
hx-trigger="projectSelected from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="empty-state-inline">
|
||||
<p>No tasks to display</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline View -->
|
||||
<div id="timeline-view" class="view-container">
|
||||
<div
|
||||
class="timeline-container"
|
||||
hx-get="/api/ui/project/timeline"
|
||||
hx-trigger="projectSelected from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="empty-state-inline">Select a project to view timeline</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<div id="list-view" class="view-container">
|
||||
<div
|
||||
class="list-container"
|
||||
hx-get="/api/ui/project/tasks/list"
|
||||
hx-trigger="projectSelected from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="empty-state-inline">Select a project to view tasks</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Board View -->
|
||||
<div id="board-view" class="view-container">
|
||||
<div class="board-columns">
|
||||
<div class="board-column" data-status="not-started">
|
||||
<h3>Not Started</h3>
|
||||
<div class="column-tasks" hx-get="/api/ui/project/tasks?status=not_started" hx-trigger="projectSelected from:body" hx-swap="innerHTML"></div>
|
||||
</div>
|
||||
<div class="board-column" data-status="in-progress">
|
||||
<h3>In Progress</h3>
|
||||
<div class="column-tasks" hx-get="/api/ui/project/tasks?status=in_progress" hx-trigger="projectSelected from:body" hx-swap="innerHTML"></div>
|
||||
</div>
|
||||
<div class="board-column" data-status="completed">
|
||||
<h3>Completed</h3>
|
||||
<div class="column-tasks" hx-get="/api/ui/project/tasks?status=completed" hx-trigger="projectSelected from:body" hx-swap="innerHTML"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="project-empty" class="empty-state">
|
||||
<div class="empty-state-icon">📋</div>
|
||||
<h2>No Project Selected</h2>
|
||||
<p>Select a project from the sidebar or create a new one</p>
|
||||
<button
|
||||
class="btn-primary"
|
||||
hx-get="/api/ui/project/new"
|
||||
hx-target="#project-modal"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<span>+</span> Create Project
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Details Panel -->
|
||||
<aside class="details-panel collapsed" id="details-panel">
|
||||
<button class="panel-toggle" onclick="toggleDetailsPanel()">
|
||||
<span>ℹ️</span>
|
||||
</button>
|
||||
<div class="panel-content">
|
||||
<div id="task-details">
|
||||
<p class="empty-message">Select a task to view details</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Modal Container -->
|
||||
<div id="project-modal" class="modal-container"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.project-app {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.project-sidebar {
|
||||
width: 280px;
|
||||
min-width: 280px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-search {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.sidebar-search input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.project-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.project-info h1 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.project-meta {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
margin-right: 0.375rem;
|
||||
}
|
||||
|
||||
.project-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.view-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.view-btn.active {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.project-views {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.view-container {
|
||||
display: none;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.view-container.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Gantt Chart Styles */
|
||||
.gantt-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.gantt-zoom {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.zoom-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.zoom-btn.active {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.gantt-filters {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gantt-filters label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gantt-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gantt-table {
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
border-right: 2px solid var(--border-color);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.gantt-table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 80px 80px 70px 70px 90px;
|
||||
padding: 0.75rem 0.5rem;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.gantt-table-body {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.gantt-chart {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gantt-timeline-header {
|
||||
display: flex;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.gantt-chart-body {
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* Board View */
|
||||
.board-columns {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.board-column {
|
||||
width: 300px;
|
||||
min-width: 300px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.board-column h3 {
|
||||
padding: 1rem;
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.column-tasks {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Details Panel */
|
||||
.details-panel {
|
||||
width: 320px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border-color);
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.details-panel.collapsed {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.details-panel.collapsed .panel-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel-toggle {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
height: calc(100% - 48px);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.project-app:not(.has-project) .project-views {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.project-app:not(.has-project) .empty-state {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state-inline {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.gantt-table {
|
||||
width: 300px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.gantt-table-header {
|
||||
grid-template-columns: 1fr 70px 70px 60px;
|
||||
}
|
||||
|
||||
.gantt-table-header .col-progress,
|
||||
.gantt-table-header .col-assignee {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.project-sidebar {
|
||||
position: absolute;
|
||||
left: -280px;
|
||||
height: 100%;
|
||||
z-index: 50;
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
.project-sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.details-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.view-toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let currentView = 'gantt';
|
||||
let currentZoom = 'week';
|
||||
|
||||
function switchView(view) {
|
||||
currentView = view;
|
||||
|
||||
document.querySelectorAll('.view-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.view === view);
|
||||
});
|
||||
|
||||
document.querySelectorAll('.view-container').forEach(container => {
|
||||
container.classList.toggle('active', container.id === `${view}-view`);
|
||||
});
|
||||
}
|
||||
|
||||
function zoomGantt(level) {
|
||||
currentZoom = level;
|
||||
|
||||
document.querySelectorAll('.zoom-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.textContent.toLowerCase() === level);
|
||||
});
|
||||
|
||||
htmx.trigger('#gantt-chart-body', 'ganttZoomChanged');
|
||||
}
|
||||
|
||||
function toggleCriticalPath() {
|
||||
const show = document.getElementById('show-critical').checked;
|
||||
document.querySelectorAll('.gantt-bar.critical').forEach(bar => {
|
||||
bar.style.display = show ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function toggleMilestones() {
|
||||
const show = document.getElementById('show-milestones').checked;
|
||||
document.querySelectorAll('.gantt-milestone').forEach(ms => {
|
||||
ms.style.display = show ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function fitGanttToScreen() {
|
||||
const container = document.querySelector('.gantt-chart');
|
||||
if (container) {
|
||||
container.scrollLeft = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDetailsPanel() {
|
||||
const panel = document.getElementById('details-panel');
|
||||
panel.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
function selectProject(projectId) {
|
||||
document.querySelector('.project-app').classList.add('has-project');
|
||||
document.getElementById('add-task-btn').disabled = false;
|
||||
htmx.trigger(document.body, 'projectSelected', { projectId: projectId });
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
generateTimelineHeaders();
|
||||
});
|
||||
|
||||
function generateTimelineHeaders() {
|
||||
const header = document.getElementById('gantt-timeline-header');
|
||||
if (!header) return;
|
||||
|
||||
const today = new Date();
|
||||
let html = '';
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const date = new Date(today);
|
||||
date.setDate(date.getDate() + i);
|
||||
const day = date.getDate();
|
||||
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' });
|
||||
const isWeekend = date.getDay() === 0 || date.getDay() === 6;
|
||||
|
||||
html += `
|
||||
<div class="timeline-day ${isWeekend ? 'weekend' : ''}" style="width: 40px; text-align: center; padding: 0.5rem 0; border-right: 1px solid var(--border-color);">
|
||||
<div style="font-size: 0.625rem; color: var(--text-muted);">${dayName}</div>
|
||||
<div style="font-size: 0.75rem; font-weight: 600;">${day}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
header.innerHTML = html;
|
||||
}
|
||||
</script>
|
||||
535
ui/suite/workspace/workspace.html
Normal file
535
ui/suite/workspace/workspace.html
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
<!-- =============================================================================
|
||||
WORKSPACE APP - Notion-style Pages & Blocks
|
||||
Respects Theme Manager - No hardcoded theme
|
||||
============================================================================= -->
|
||||
|
||||
<div class="workspace-app">
|
||||
<!-- Sidebar -->
|
||||
<aside class="workspace-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h2 data-i18n="workspace-title">Workspace</h2>
|
||||
<button
|
||||
class="btn-icon"
|
||||
id="new-page-btn"
|
||||
title="New Page"
|
||||
hx-post="/api/workspaces/current/pages"
|
||||
hx-vals='{"title": "Untitled"}'
|
||||
hx-target="#page-tree"
|
||||
hx-swap="beforeend"
|
||||
>
|
||||
<span>+</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-search">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search pages..."
|
||||
data-i18n-placeholder="workspace-search-pages"
|
||||
hx-get="/api/workspaces/current/search"
|
||||
hx-trigger="keyup changed delay:300ms"
|
||||
hx-target="#search-results"
|
||||
hx-swap="innerHTML"
|
||||
name="q"
|
||||
/>
|
||||
<div id="search-results" class="search-results"></div>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-section">
|
||||
<h3 data-i18n="workspace-recent">Recent</h3>
|
||||
<div
|
||||
id="recent-pages"
|
||||
hx-get="/api/workspaces/current/pages?filter=recent"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="loading-placeholder">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3 data-i18n="workspace-favorites">Favorites</h3>
|
||||
<div
|
||||
id="favorite-pages"
|
||||
hx-get="/api/workspaces/current/pages?filter=favorites"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="loading-placeholder">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Pages</h3>
|
||||
<div
|
||||
id="page-tree"
|
||||
hx-get="/api/workspaces/current/pages"
|
||||
hx-trigger="load, pageCreated from:body, pageDeleted from:body"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="loading-placeholder">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button class="sidebar-btn" data-i18n="workspace-templates">
|
||||
<span class="btn-icon">📄</span>
|
||||
Templates
|
||||
</button>
|
||||
<button class="sidebar-btn" data-i18n="workspace-trash">
|
||||
<span class="btn-icon">🗑️</span>
|
||||
Trash
|
||||
</button>
|
||||
<button
|
||||
class="sidebar-btn"
|
||||
hx-get="/api/workspaces/current/settings"
|
||||
hx-target="#workspace-modal"
|
||||
hx-swap="innerHTML"
|
||||
data-i18n="workspace-settings"
|
||||
>
|
||||
<span class="btn-icon">⚙️</span>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="workspace-main">
|
||||
<div id="page-content" class="page-content">
|
||||
<!-- Page Header -->
|
||||
<header class="page-header">
|
||||
<div class="page-icon" id="page-icon">📄</div>
|
||||
<input
|
||||
type="text"
|
||||
class="page-title"
|
||||
id="page-title"
|
||||
value="Untitled"
|
||||
placeholder="Untitled"
|
||||
hx-put="/api/pages/current"
|
||||
hx-trigger="blur changed"
|
||||
hx-vals='js:{title: document.getElementById("page-title").value}'
|
||||
/>
|
||||
<div class="page-meta">
|
||||
<span class="meta-item">Last edited: just now</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Page Body - Block Editor -->
|
||||
<div class="page-body">
|
||||
<div
|
||||
id="blocks-container"
|
||||
class="blocks-container"
|
||||
hx-get="/api/pages/current/blocks"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<!-- Empty state -->
|
||||
<div class="empty-page">
|
||||
<p>Press <kbd>/</kbd> for commands or start typing...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slash Command Menu -->
|
||||
<div id="slash-menu" class="slash-menu hidden">
|
||||
<div class="slash-menu-header">
|
||||
<span data-i18n="paper-commands">Commands</span>
|
||||
</div>
|
||||
<div
|
||||
class="slash-menu-items"
|
||||
hx-get="/api/workspaces/commands"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<!-- Commands loaded dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State (no page selected) -->
|
||||
<div id="empty-state" class="empty-state hidden">
|
||||
<div class="empty-state-icon">📝</div>
|
||||
<h2>Select a page or create a new one</h2>
|
||||
<p>Your workspace pages will appear in the sidebar</p>
|
||||
<button
|
||||
class="btn-primary"
|
||||
hx-post="/api/workspaces/current/pages"
|
||||
hx-vals='{"title": "Untitled"}'
|
||||
hx-target="#page-tree"
|
||||
hx-swap="beforeend"
|
||||
>
|
||||
<span>+</span> New Page
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Members Panel (collapsible) -->
|
||||
<aside class="members-panel collapsed" id="members-panel">
|
||||
<button class="panel-toggle" onclick="toggleMembersPanel()">
|
||||
<span>👥</span>
|
||||
</button>
|
||||
<div class="panel-content">
|
||||
<h3 data-i18n="workspace-members">Members</h3>
|
||||
<div
|
||||
id="members-list"
|
||||
hx-get="/api/workspaces/current/members"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="loading-placeholder">Loading...</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn-secondary btn-sm"
|
||||
hx-get="/api/workspaces/current/invite"
|
||||
hx-target="#workspace-modal"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
Invite Members
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Modal Container -->
|
||||
<div id="workspace-modal" class="modal-container"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.workspace-app {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.workspace-sidebar {
|
||||
width: 260px;
|
||||
min-width: 260px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-search {
|
||||
padding: 0.75rem 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sidebar-search input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-section h3 {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sidebar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sidebar-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.workspace-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.page-title::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.page-meta {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.blocks-container {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.empty-page {
|
||||
color: var(--text-muted);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.empty-page kbd {
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.slash-menu {
|
||||
position: absolute;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
min-width: 280px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.slash-menu.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.slash-menu-header {
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.members-panel {
|
||||
width: 280px;
|
||||
background: var(--bg-secondary);
|
||||
border-left: 1px solid var(--border-color);
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.members-panel.collapsed {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.members-panel.collapsed .panel-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.panel-toggle {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.panel-content h3 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.empty-state.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.empty-state-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.modal-container:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.search-results:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.workspace-sidebar {
|
||||
position: absolute;
|
||||
left: -260px;
|
||||
height: 100%;
|
||||
z-index: 50;
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
.workspace-sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.members-panel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function toggleMembersPanel() {
|
||||
const panel = document.getElementById('members-panel');
|
||||
panel.classList.toggle('collapsed');
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const blocksContainer = document.getElementById('blocks-container');
|
||||
if (blocksContainer) {
|
||||
blocksContainer.addEventListener('keydown', function(e) {
|
||||
if (e.key === '/') {
|
||||
const slashMenu = document.getElementById('slash-menu');
|
||||
slashMenu.classList.remove('hidden');
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
const slashMenu = document.getElementById('slash-menu');
|
||||
slashMenu.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Loading…
Add table
Reference in a new issue