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:
Rodrigo Rodriguez (Pragmatismo) 2026-01-09 20:56:59 -03:00
parent cb33a75d39
commit 80c91f6304
15 changed files with 7210 additions and 834 deletions

View file

@ -6,9 +6,7 @@ mod shared;
mod ui_server; mod ui_server;
fn init_logging() { fn init_logging() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) botlib::logging::init_compact_logger("info");
.format_timestamp_millis()
.init();
} }
fn get_port() -> u16 { fn get_port() -> u16 {

View file

@ -46,6 +46,16 @@ const SUITE_DIRS: &[&str] = &[
"tools", "tools",
"assets", "assets",
"partials", "partials",
"video",
"learn",
"social",
"dashboards",
"designer",
"workspace",
"project",
"goals",
"player",
"canvas",
]; ];
pub async fn index() -> impl IntoResponse { pub async fn index() -> impl IntoResponse {

992
ui/suite/canvas/canvas.html Normal file
View 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.

View file

@ -1117,7 +1117,7 @@ body {
.app-grid { .app-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 2px; gap: 2px;
} }

View file

@ -588,7 +588,7 @@ body.no-animations .spinner {
.apps-grid, .apps-grid,
.app-grid { .app-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 2px; gap: 2px;
} }

View file

@ -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 { .home-container {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 32px 40px;
min-height: 100%;
} }
.home-header { /* Welcome Section */
text-align: center; .welcome-section {
margin-bottom: 3rem; display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 48px;
gap: 24px;
flex-wrap: wrap;
} }
.home-logo { .welcome-content {
width: 80px; flex: 1;
height: 80px; min-width: 280px;
margin: 0 auto 1.5rem; }
background: linear-gradient(135deg, var(--primary), #8b5cf6);
border-radius: 20px; .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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 2.5rem; flex-shrink: 0;
} }
.home-title { .recent-icon.doc {
font-size: 2.5rem; background: linear-gradient(135deg, #3b82f6, #1d4ed8);
font-weight: 700; color: white;
margin-bottom: 0.75rem;
background: linear-gradient(135deg, var(--text), var(--primary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
} }
.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); color: var(--text-secondary);
font-size: 1.125rem; }
max-width: 500px;
margin: 0 auto; /* Apps Section */
.apps-section {
margin-bottom: 48px;
} }
.apps-grid { .apps-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem; gap: 20px;
margin-bottom: 3rem;
} }
.app-card { .app-card {
background: var(--surface); display: flex;
border: 1px solid var(--border); gap: 16px;
padding: 24px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 16px; border-radius: 16px;
padding: 1.5rem;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s; transition: all 0.25s ease;
display: block;
} }
.app-card:hover { .app-card:hover {
border-color: var(--accent-color);
transform: translateY(-4px); transform: translateY(-4px);
border-color: var(--primary); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12);
box-shadow: 0 8px 32px rgba(59, 130, 246, 0.15);
} }
.app-icon { .app-icon {
@ -93,135 +230,168 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 1.75rem; flex-shrink: 0;
margin-bottom: 1rem; color: white;
} }
.app-icon.chat { background: linear-gradient(135deg, #3b82f6, #1d4ed8); } .app-icon.chat {
.app-icon.drive { background: linear-gradient(135deg, #f59e0b, #d97706); } background: linear-gradient(135deg, #3b82f6, #1d4ed8);
.app-icon.tasks { background: linear-gradient(135deg, #22c55e, #16a34a); } }
.app-icon.mail { background: linear-gradient(135deg, #ef4444, #dc2626); } .app-icon.people {
.app-icon.calendar { background: linear-gradient(135deg, #a855f7, #7c3aed); } background: linear-gradient(135deg, #8b5cf6, #7c3aed);
.app-icon.meet { background: linear-gradient(135deg, #06b6d4, #0891b2); } }
.app-icon.paper { background: linear-gradient(135deg, #eab308, #ca8a04); } .app-icon.mail {
.app-icon.research { background: linear-gradient(135deg, #ec4899, #db2777); } background: linear-gradient(135deg, #ef4444, #dc2626);
.app-icon.analytics { background: linear-gradient(135deg, #6366f1, #4f46e5); } }
.app-icon.meet {
.app-name { background: linear-gradient(135deg, #06b6d4, #0891b2);
font-size: 1.25rem; }
font-weight: 600; .app-icon.calendar {
margin-bottom: 0.5rem; 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 { .app-content {
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 {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.5rem; gap: 8px;
min-width: 0;
} }
.recent-item { .app-content h3 {
display: flex; font-size: 1.125rem;
align-items: center; font-weight: 600;
gap: 1rem; margin: 0;
padding: 0.75rem; color: var(--text-primary);
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
} }
.recent-item:hover { .app-content p {
background: var(--bg); font-size: 0.875rem;
} line-height: 1.5;
margin: 0;
.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;
color: var(--text-secondary); color: var(--text-secondary);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
} }
/* Responsive */
@media (max-width: 768px) { @media (max-width: 768px) {
.home-container { .home-container {
padding: 1rem; padding: 20px 16px;
} }
.home-title { .welcome-section {
flex-direction: column;
align-items: stretch;
}
.welcome-title {
font-size: 1.75rem; font-size: 1.75rem;
} }
.quick-search {
min-width: 100%;
}
.apps-grid { .apps-grid {
grid-template-columns: 1fr; 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
View 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>

File diff suppressed because it is too large Load diff

View file

@ -95,7 +95,9 @@
<div class="header-left"> <div class="header-left">
<button <button
class="logo-wrapper" class="logo-wrapper"
onclick="window.location.href='/#chat'" hx-get="/suite/home.html"
hx-target="#main-content"
hx-push-url="/#home"
title="General Bots" title="General Bots"
aria-label="General Bots - Home" aria-label="General Bots - Home"
> >
@ -450,6 +452,69 @@
<span data-i18n="nav-chat">Chat</span> <span data-i18n="nav-chat">Chat</span>
</a> </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 --> <!-- Docs -->
<a <a
class="app-item" class="app-item"
@ -945,16 +1010,16 @@
<span data-i18n="nav-sources">Sources</span> <span data-i18n="nav-sources">Sources</span>
</a> </a>
<!-- Tools --> <!-- Compliance -->
<a <a
class="app-item" class="app-item"
href="#tools" href="#compliance"
data-section="tools" data-section="compliance"
role="menuitem" role="menuitem"
aria-label="Tools" aria-label="Compliance"
hx-get="/suite/tools/compliance.html" hx-get="/suite/tools/compliance.html"
hx-target="#main-content" hx-target="#main-content"
hx-push-url="/#tools" hx-push-url="/#compliance"
> >
<div class="app-icon" aria-hidden="true"> <div class="app-icon" aria-hidden="true">
<svg <svg
@ -966,11 +1031,14 @@
stroke-width="2" stroke-width="2"
> >
<path <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> </svg>
</div> </div>
<span data-i18n="nav-tools">Tools</span> <span data-i18n="nav-compliance"
>Compliance</span
>
</a> </a>
<!-- Designer (.bas Editor) --> <!-- Designer (.bas Editor) -->
@ -1030,6 +1098,261 @@
</div> </div>
<span data-i18n="nav-attendant">Attendant</span> <span data-i18n="nav-attendant">Attendant</span>
</a> </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> </div>
</nav> </nav>
</div> </div>

View file

@ -28,6 +28,12 @@
"nav-tools": "Tools", "nav-tools": "Tools",
"nav-attendant": "Attendant", "nav-attendant": "Attendant",
"nav-settings": "Settings", "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-search": "Search...",
"nav-all-apps": "All Applications", "nav-all-apps": "All Applications",
"dashboard-title": "Dashboard", "dashboard-title": "Dashboard",
@ -461,6 +467,66 @@
"a11y-loading": "Loading, please wait", "a11y-loading": "Loading, please wait",
"a11y-menu-open": "Open menu", "a11y-menu-open": "Open menu",
"a11y-menu-close": "Close 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": { "pt-BR": {
"app-name": "General Bots", "app-name": "General Bots",
@ -484,6 +550,12 @@
"nav-tools": "Ferramentas", "nav-tools": "Ferramentas",
"nav-attendant": "Atendente", "nav-attendant": "Atendente",
"nav-settings": "Configurações", "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-search": "Buscar...",
"nav-all-apps": "Todos os Aplicativos", "nav-all-apps": "Todos os Aplicativos",
"dashboard-title": "Painel", "dashboard-title": "Painel",
@ -922,6 +994,66 @@
"a11y-loading": "Carregando, por favor aguarde", "a11y-loading": "Carregando, por favor aguarde",
"a11y-menu-open": "Abrir menu", "a11y-menu-open": "Abrir menu",
"a11y-menu-close": "Fechar 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
View 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
View 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

File diff suppressed because it is too large Load diff

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

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