The duplicate functions at lines 455-486 were redefining cacheElements and bindEvents with wrong element IDs (kebab-case vs camelCase in HTML). This caused 'Cannot read properties of null' error on slides app init.
543 lines
14 KiB
HTML
543 lines
14 KiB
HTML
<!-- =============================================================================
|
|
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");
|
|
if (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");
|
|
if (slashMenu) {
|
|
slashMenu.classList.remove("hidden");
|
|
}
|
|
}
|
|
if (e.key === "Escape") {
|
|
const slashMenu = document.getElementById("slash-menu");
|
|
if (slashMenu) {
|
|
slashMenu.classList.add("hidden");
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|