Files
flowdeck/app/templates/gitea_workspace.html
T
bruno 1fedafa808
FlowDeck CI / lint (push) Successful in 1m37s
FlowDeck CI / docker (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 1h9m58s
fix: sidebar Repository et pages forge — erreurs console, cache assets (v7.70.1)
2026-10-09 23:33:48 -04:00

359 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block page_title %}{{ workspace_name }}{% endblock %}
{% block title_prefix %}{{ workspace_name }}{% endblock %}
{% block page_icon %}{{ fd_icon("link",18) }}{% endblock %}
{% block topbar %}
{% set page_icon = "link" %}
{% set page_title = workspace_name %}
{% include '_header.html' %}
{% endblock %}
{% block content %}
<!-- Shared forge layout — identical structure to /local-workspace:
ws-split > ws-main, breadcrumb-row + Library-style toolbar
(View/Sort/Filter/Search, New file), search bar, filter chips,
then tree / list / details / cards / content views bound to the
forge tree (filteredTreeItems). File view + editor + commit bar
preserved below. -->
<link rel="stylesheet" href="/static/css/highlight.css?v={{ asset_version }}">
<script data-cfasync="false" src="/static/js/highlight.min.js?v={{ asset_version }}"></script>
<script data-cfasync="false" src="/static/js/highlight-extra.min.js?v={{ asset_version }}"></script>
<script data-cfasync="false" src="/static/js/code-highlight.js?v={{ asset_version }}"></script>
<style>
.ws-split{display:flex;max-width:100%;margin:0 auto;padding:0 16px 0;gap:0;min-height:calc(100vh - var(--topbar-height));}
.ws-main{flex:1;min-width:0;overflow:auto;}
.breadcrumb-row{display:flex;align-items:center;gap:4px;padding:2px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px 8px 0 0;margin-bottom:0;flex-wrap:wrap;}
.breadcrumb-row .nav-arrow{width:22px;height:22px;padding:0;font-size:13px;}
.toolbar-row{display:flex;align-items:center;gap:4px;padding:2px 8px;background:var(--bg-secondary);border:1px solid var(--border);border-top:none;border-radius:0 0 8px 8px;margin-bottom:2px;flex-wrap:wrap;}
.nav-arrow{width:28px;height:28px;font-size:13px;flex-shrink:0;}
.unified-breadcrumb{flex:1;display:flex;align-items:center;gap:2px;min-width:0;font-size:13px;overflow:hidden;}
.unified-breadcrumb .bc-root{color:var(--text-secondary);text-decoration:none;white-space:nowrap;font-weight:500;flex-shrink:0;}
.unified-breadcrumb .bc-root:hover{color:var(--accent);}
.unified-breadcrumb .bc-sep{color:var(--text-tertiary);margin:0 2px;flex-shrink:0;}
.unified-breadcrumb a{color:var(--text-secondary);text-decoration:none;white-space:nowrap;}
.unified-breadcrumb a:hover{color:var(--accent);}
.ws-toolbar{display:flex;align-items:center;gap:2px;flex-shrink:0;margin-left:auto;}
.ws-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:6px;background:transparent;border:none;color:var(--text-secondary);cursor:pointer;}
.ws-icon-btn:hover,.ws-icon-btn.active{background:var(--bg-hover);color:var(--text-primary);}
.ws-icon-btn svg{width:16px;height:16px;}
.ws-dropdown{position:absolute;top:calc(100% + 4px);right:0;min-width:210px;background:var(--bg-modal);border:1px solid var(--border);border-radius:8px;box-shadow:var(--shadow-popover);z-index:200;padding:6px;}
.ws-dropdown .dd-label{padding:6px 8px 2px;font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.3px;}
.ws-dropdown .dd-item{padding:6px 8px;font-size:13px;color:var(--text-primary);cursor:pointer;border-radius:4px;display:flex;align-items:center;gap:8px;}
.ws-dropdown .dd-item:hover{background:var(--bg-hover);}
.ws-dropdown .dd-item.active{color:var(--accent);}
.ws-dropdown .dd-item .check{width:14px;flex-shrink:0;color:var(--accent);}
.lib-toolbar-wrap{position:relative;}
.ws-search-bar{margin-bottom:10px;}
.ws-search-bar input{width:100%;padding:8px 12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-secondary);color:var(--text-primary);font-size:13px;outline:none;}
.ws-search-bar input:focus{border-color:var(--accent);}
.filter-row{display:flex;align-items:center;gap:4px;padding:2px 0;margin-bottom:4px;flex-wrap:wrap;}
.filter-chip{padding:4px 10px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:6px;font-size:12px;color:var(--text-secondary);cursor:pointer;}
.filter-chip:hover{background:var(--bg-hover);color:var(--text);}
.filter-chip.active{background:var(--accent);color:#fff;border-color:var(--accent);}
.filter-clear{font-size:11px;color:var(--text-tertiary);cursor:pointer;padding:2px 6px;border-radius:4px;}
.filter-clear:hover{color:var(--danger);background:var(--bg-hover);}
.ws-tree{list-style:none;margin:0;padding:0;}
.ws-tree-item{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:6px;cursor:pointer;transition:background 150ms;border:1px solid transparent;margin-bottom:2px;}
.ws-tree-item:hover{background:var(--bg-tertiary);border-color:var(--border);}
.ws-tree-item .name{flex:1;font-size:14px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.ws-tree-item .name.folder{font-weight:500;}
.tree-chevron{width:16px;height:16px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;background:none;border:none;color:var(--text-tertiary);cursor:pointer;font-size:8px;padding:0;border-radius:3px;}
.tree-chevron:hover{color:var(--text);background:var(--bg-hover);}
.tree-chevron.open{transform:rotate(90deg);}
.ws-table-compact,.ws-table-enhanced{width:100%;border-collapse:collapse;font-size:13px;}
.ws-table-compact th,.ws-table-enhanced th{text-align:left;padding:6px 10px;color:var(--text-tertiary);font-weight:500;border-bottom:2px solid var(--border);font-size:11px;text-transform:uppercase;}
.ws-table-compact td,.ws-table-enhanced td{padding:5px 10px;border-bottom:1px solid var(--border);}
.ws-table-compact tr:hover td,.ws-table-enhanced tr:hover td{background:var(--bg-hover);}
.title-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px;margin-top:8px;}
.title-card{display:flex;flex-direction:column;align-items:center;padding:16px 8px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:10px;cursor:pointer;min-height:100px;justify-content:center;}
.title-card:hover{background:var(--bg-hover);border-color:var(--accent);}
.title-card-name{font-size:13px;font-weight:500;text-align:center;word-break:break-word;}
.title-card-meta{display:flex;gap:8px;margin-top:6px;font-size:11px;color:var(--text-tertiary);}
.content-list{display:flex;flex-direction:column;gap:4px;margin-top:8px;}
.content-item{padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;cursor:pointer;}
.content-item:hover{background:var(--bg-hover);border-color:var(--accent);}
.empty-state{text-align:center;padding:80px 20px;color:var(--text-dim);}
.empty-state h2{font-size:18px;margin-bottom:8px;color:var(--text);}
.gw-file-toolbar{display:flex;align-items:center;gap:8px;padding:8px 24px;background:var(--bg-secondary);border-bottom:1px solid var(--border);font-size:13px;color:var(--text-dim);}
.gw-file-toolbar strong{color:var(--text);margin-right:12px;}
.gw-content{padding:24px;overflow-y:auto;flex:1;}
.gw-content pre{background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;padding:16px;overflow-x:auto;font-size:13px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:70vh;overflow-y:auto;}
.gw-content textarea{width:100%;min-height:50vh;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;padding:12px;font-family:monospace;resize:vertical;outline:none;}
.gw-commit-bar{display:flex;align-items:center;gap:8px;padding:12px 24px;background:var(--bg-secondary);border-top:1px solid var(--border);flex-wrap:wrap;}
.gw-commit-bar input{flex:1;min-width:180px;padding:8px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:13px;outline:none;}
.ctx-item{display:flex;align-items:center;gap:8px;padding:7px 14px;font-size:13px;color:var(--text);cursor:pointer;}
.ctx-item:hover{background:var(--bg-hover);}
.ctx-item.ctx-danger{color:var(--danger);}
.btn{padding:10px 20px;border-radius:8px;font-size:13px;cursor:pointer;border:none;font-weight:500;}
.btn-primary{background:var(--accent);color:#fff;}
.btn-danger{background:rgba(255,80,80,.1);color:var(--danger);}
</style>
<script data-cfasync="false" nonce="{{ csp_nonce() }}" src="/static/js/gitea_workspace.js?v={{ asset_version }}"></script>
<div class="ws-split" x-data="giteaWorkspace()">
<div class="ws-main">
<!-- File view -->
<div x-show="showFile && !showEditor" x-transition>
<div class="gw-file-toolbar">
<strong x-text="filePath || ''"></strong>
<span x-text="formatSize(fileSize)"></span>
<span style="flex:1;"></span>
<button class="btn" @click="openEditor()">{{ fd_icon("edit",14) }} Edit</button>
<button class="btn btn-danger" @click="deleteCurrentFile()">{{ fd_icon("trash",14) }} Delete</button>
</div>
<div class="gw-content">
<div class="gw-code-head" x-show="!fileLoading" style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
<span class="gw-code-lang" x-text="fileLanguageLabel" style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-tertiary);"></span>
</div>
<pre x-show="!fileLoading"><code class="hljs" :class="'language-'+fileLanguage" :data-hljs-lang="fileLanguage" x-ref="codeBlock" x-text="fileContent"></code></pre>
</div>
</div>
<!-- Editor view -->
<template x-if="showEditor">
<div>
<div class="gw-file-toolbar">
<strong>Editing: <span x-text="filePath || 'new file'"></span></strong>
<span style="flex:1;"></span>
<button class="btn" @click="showEditor=false">Cancel</button>
</div>
<div class="gw-content">
<textarea x-model="editContent"></textarea>
</div>
<div class="gw-commit-bar">
<input type="text" x-model="commitMessage" placeholder="Commit message" list="commit-history">
<datalist id="commit-history">
<template x-for="msg in commitHistory" :key="msg">
<option :value="msg">
</template>
</datalist>
<button class="btn-primary" @click="saveFile()">{{ fd_icon("save",14) }} Commit</button>
</div>
</div>
</template>
<!-- Browser (same chrome as /local-workspace) -->
<template x-if="!showFile && !showEditor && !showPrivate">
<div>
<div class="breadcrumb-row">
<a href="/workspaces" class="btn-icon nav-arrow" title="All workspaces">{{ fd_icon("home",14) }}</a>
<div class="unified-breadcrumb">
<a href="#" @click.prevent="navigateToRoot()" class="bc-root" style="cursor:pointer;" x-text="owner + '/' + repo"></a>
<template x-for="(b, i) in folderStack" :key="i">
<span style="display:contents;">
<span class="bc-sep">/</span>
<a href="#" @click.prevent="navigateToFolder(i)" x-text="b"></a>
</span>
</template>
<span x-show="treeLoading" style="color:var(--text-tertiary);">Loading...</span>
</div>
<div class="ws-toolbar">
<div class="lib-toolbar-wrap" style="position:relative;">
<button class="ws-icon-btn" :class="{active: viewMenuOpen}" title="View" @click.stop="toggleViewMenu()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="20" y2="18"/></svg>
</button>
<div class="ws-dropdown" x-show="viewMenuOpen" @click.outside="viewMenuOpen=false" x-transition style="right:0;">
<div class="dd-label">View</div>
<div class="dd-item" :class="{active: viewMode==='tree'}" @click="viewMode='tree', viewMenuOpen=false"><span class="check" x-text="viewMode==='tree'?'✓':''"></span>{{ fd_icon("folder",14) }} Tree</div>
<div class="dd-item" :class="{active: viewMode==='list'}" @click="viewMode='list', viewMenuOpen=false"><span class="check" x-text="viewMode==='list'?'✓':''"></span>{{ fd_icon("list",14) }} List</div>
<div class="dd-item" :class="{active: viewMode==='details'}" @click="viewMode='details', viewMenuOpen=false"><span class="check" x-text="viewMode==='details'?'✓':''"></span>{{ fd_icon("bar-chart",14) }} Details</div>
<div class="dd-item" :class="{active: viewMode==='title'}" @click="viewMode='title', viewMenuOpen=false"><span class="check" x-text="viewMode==='title'?'✓':''"></span>{{ fd_icon("grid",14) }} Cards</div>
<div class="dd-item" :class="{active: viewMode==='content'}" @click="viewMode='content', viewMenuOpen=false"><span class="check" x-text="viewMode==='content'?'✓':''"></span>{{ fd_icon("align-left",14) }} Content</div>
</div>
</div>
<div class="lib-toolbar-wrap" style="position:relative;">
<button class="ws-icon-btn" :class="{active: sortOpen}" title="Sort" @click.stop="toggleSortMenu()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="9" x2="14" y2="9"/><line x1="4" y1="15" x2="10" y2="15"/></svg>
</button>
<div class="ws-dropdown" x-show="sortOpen" @click.outside="sortOpen=false" x-transition style="right:0;">
<div class="dd-label">Sort by</div>
<div class="dd-item" :class="{active: sortBy===''}" @click="sortBy='', doSort(), sortOpen=false"><span class="check" x-text="sortBy===''?'✓':''"></span> Name A→Z</div>
<div class="dd-item" :class="{active: sortBy==='name-desc'}" @click="sortBy='name-desc', doSort(), sortOpen=false"><span class="check" x-text="sortBy==='name-desc'?'✓':''"></span> Name Z→A</div>
<div class="dd-item" :class="{active: sortBy==='type'}" @click="sortBy='type', doSort(), sortOpen=false"><span class="check" x-text="sortBy==='type'?'✓':''"></span> Type</div>
</div>
</div>
<div class="lib-toolbar-wrap" style="position:relative;">
<button class="ws-icon-btn" :class="{active: filterOpen || filterType}" title="Filter" @click.stop="toggleFilterMenu()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></svg>
</button>
<div class="ws-dropdown" x-show="filterOpen" @click.outside="filterOpen=false" x-transition style="right:0;">
<div class="dd-label">File type</div>
<div class="dd-item" :class="{active: filterType==='folder'}" @click="filterByType('folder'), filterOpen=false"><span class="check" x-text="filterType==='folder'?'✓':''"></span>{{ fd_icon("folder",14) }} Folders</div>
<div class="dd-item" :class="{active: filterType==='page'}" @click="filterByType('page'), filterOpen=false"><span class="check" x-text="filterType==='page'?'✓':''"></span>{{ fd_icon("file",14) }} Files</div>
<div class="dd-item" :class="{active: filterType==='image'}" @click="filterByType('image'), filterOpen=false"><span class="check" x-text="filterType==='image'?'✓':''"></span>{{ fd_icon("image",14) }} Images</div>
<div class="dd-item" :class="{active: filterType==='code'}" @click="filterByType('code'), filterOpen=false"><span class="check" x-text="filterType==='code'?'✓':''"></span>{{ fd_icon("file",14) }} Code</div>
<div class="more-sep" x-show="filterType"></div>
<div class="dd-item" x-show="filterType" @click="filterType='', doFilter(), filterOpen=false" style="color:var(--text-dim);">{{ fd_icon("x",14) }} Clear filter</div>
</div>
</div>
<button class="ws-icon-btn" :class="{active: searchOpen}" title="Search" @click.stop="toggleSearch()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</button>
<button class="ws-icon-btn" @click="refreshTree()" title="Refresh">{{ fd_icon("refresh",16) }}</button>
<button class="ws-icon-btn" @click="showNewFile=!showNewFile" title="New file">{{ fd_icon("file",14) }}+</button>
<button class="ws-icon-btn" @click="createFolderAtRoot()" title="New folder">{{ fd_icon("folder",14) }}+</button>
<button class="ws-icon-btn" @click="triggerUpload()" title="Upload file">{{ fd_icon("upload",14) }}</button>
<input type="file" id="gitea-upload-input" style="display:none" @change="doUpload($event)" multiple>
</div>
</div>
<div class="ws-search-bar" x-show="searchOpen" x-transition>
<input type="text" placeholder="Filter files and folders..." x-model="searchQuery" @input="doFilter()" autocomplete="off">
</div>
<div class="filter-row" x-show="filterType" style="padding:4px 0;margin-bottom:6px;">
<span class="filter-clear" @click="filterType='', doFilter()">clear filter ×</span>
</div>
<div class="gw-content" style="padding:12px 4px 24px;">
<div x-show="!treeLoading">
<template x-if="treeError">
<div class="empty-state">
<h2>{{ fd_icon("link",16) }} Repository unavailable</h2>
<p x-text="treeErrorText || 'Could not load the repository tree.'"></p>
<div class="empty-actions">
<button class="btn btn-primary" @click="refreshTree()">Retry</button>
</div>
</div>
</template>
<template x-if="!treeError && (filteredTreeItems || []).length === 0">
<div class="empty-state">
<h2>{{ fd_icon("folder",16) }} Empty</h2>
<p>No files in this folder</p>
<button class="btn btn-primary" @click="showNewFile=!showNewFile">{{ fd_icon("file",14) }}+ New file</button>
</div>
</template>
<!-- Tree view -->
<div class="ws-tree" x-show="viewMode==='tree'">
<template x-for="item in (filteredTreeItems || [])" :key="item.path">
<div class="ws-tree-item"
@click="item.type==='folder' ? drillDown(item.path) : openFile(item.path, item.sha)"
@contextmenu.prevent="openGwContext($event, item)">
<span x-init="bindGwIcon($el, item)" style="font-size:16px;"></span>
<span x-text="item.name" style="flex:1;font-size:14px;" :style="{ fontWeight: item.type==='folder' ? '500' : '400' }"></span>
<span x-text="item.type==='folder' ? '' : formatSize(item.size)" style="font-size:12px;color:var(--text-tertiary);"></span>
</div>
</template>
</div>
<!-- List view -->
<table class="ws-table-compact" x-show="viewMode==='list' && (filteredTreeItems || []).length>0">
<thead><tr><th>Name</th><th style="width:80px">Type</th><th style="width:80px">Size</th></tr></thead>
<tbody>
<template x-for="item in (filteredTreeItems || [])" :key="'l'+item.path">
<tr @click="item.type==='folder' ? drillDown(item.path) : openFile(item.path, item.sha)" style="cursor:pointer">
<td x-text="item.name"></td>
<td style="font-size:12px;color:var(--text-tertiary)" x-text="item.type"></td>
<td style="font-size:12px;color:var(--text-secondary)" x-text="formatSize(item.size)"></td>
</tr>
</template>
</tbody>
</table>
<!-- Details view -->
<table class="ws-table-enhanced" x-show="viewMode==='details' && (filteredTreeItems || []).length>0">
<thead><tr><th>Name</th><th>Path</th><th>Size</th><th>Type</th></tr></thead>
<tbody>
<template x-for="item in (filteredTreeItems || [])" :key="'d'+item.path">
<tr @click="item.type==='folder' ? drillDown(item.path) : openFile(item.path, item.sha)" style="cursor:pointer">
<td x-text="item.name"></td>
<td style="font-size:12px;color:var(--text-tertiary);font-family:monospace;" x-text="item.path"></td>
<td style="font-size:12px;" x-text="formatSize(item.size)"></td>
<td style="font-size:12px;" x-text="item.type"></td>
</tr>
</template>
</tbody>
</table>
<!-- Cards view -->
<div class="title-grid" x-show="viewMode==='title' && (filteredTreeItems || []).length>0">
<template x-for="item in (filteredTreeItems || [])" :key="'g'+item.path">
<div class="title-card" @click="item.type==='folder' ? drillDown(item.path) : openFile(item.path, item.sha)">
<div class="title-card-icon" x-init="bindGwIcon($el, item)"></div>
<div class="title-card-name" x-text="item.name"></div>
<div class="title-card-meta"><span x-text="formatSize(item.size)"></span></div>
</div>
</template>
</div>
<!-- Content view -->
<div class="content-list" x-show="viewMode==='content' && (filteredTreeItems || []).length>0">
<template x-for="item in (filteredTreeItems || [])" :key="'c'+item.path">
<div class="content-item" @click="item.type==='folder' ? drillDown(item.path) : openFile(item.path, item.sha)">
<div class="content-item-header">
<span x-init="bindGwIcon($el, item)"></span>
<span x-text="item.name"></span>
<span style="font-size:11px;color:var(--text-tertiary);margin-left:auto" x-text="formatSize(item.size)"></span>
</div>
</div>
</template>
</div>
<!-- Context menu -->
<div class="gw-context-menu"
x-show="gwCtx.visible"
:style="'left:' + gwCtx.x + 'px; top:' + gwCtx.y + 'px'"
@click.outside="gwCtx.visible = false"
style="display:none;position:fixed;background:var(--bg-modal);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:var(--shadow-popover);z-index:1100;min-width:160px;padding:4px;">
<div class="ctx-item" @click="gwRename()">{{ fd_icon("edit",14) }} Rename</div>
<div class="ctx-item ctx-danger" @click="gwDelete()">{{ fd_icon("trash",14) }} Delete</div>
</div>
</div>
<!-- New file form -->
<div x-show="showNewFile" style="margin-top:16px;max-width:400px;">
<input type="text" x-model="newFilePath" placeholder="path/to/file.md" class="settings-input" style="margin-bottom:8px;">
<textarea x-model="newFileContent" placeholder="File content..." class="settings-input" style="height:120px;margin-bottom:8px;"></textarea>
<input type="text" x-model="newFileMsg" placeholder="Commit message" class="settings-input" style="margin-bottom:8px;">
<button class="btn btn-primary" @click="createNewFile()">{{ fd_icon("save",14) }} Create file</button>
</div>
</div>
</div>
</template>
<!-- Private pages view -->
<template x-if="showPrivate && !editingPrivate">
<div class="gw-content">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
<h3>{{ fd_icon("lock",14) }} Private Pages</h3>
<button class="btn btn-primary" onclick="window.FlowDeck.createPage()">+ New Page</button>
</div>
<template x-if="privatePages.length === 0">
<div class="empty-state" style="padding:40px 20px;">
<p>No private pages yet. Create one to store notes linked to this project.</p>
</div>
</template>
<template x-for="pp in privatePages" :key="pp.id">
<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:8px;cursor:pointer;display:flex;align-items:center;justify-content:space-between;"
@click="openPrivate(pp.id)">
<div>
<div style="font-weight:500;" x-text="pp.title"></div>
<div style="font-size:12px;color:var(--text-dim);" x-text="fmtGwDate(pp)"></div>
</div>
<button class="btn btn-danger" style="padding:4px 8px;font-size:11px;" @click.stop="deletePrivate(pp.id)">{{ fd_icon('trash',14) }}</button>
</div>
</template>
</div>
</template>
<!-- Private page editor -->
<template x-if="editingPrivate">
<div>
<div class="gw-file-toolbar">
<strong>Editing: <span x-text="editingPrivateTitle || 'Untitled'"></span></strong>
<span style="flex:1;"></span>
<button class="btn" @click="savePrivate()">{{ fd_icon("save",14) }} Save</button>
<button class="btn" @click="closePrivateEdit()">Cancel</button>
</div>
<div class="gw-content">
<input x-model="editingPrivateTitle" placeholder="Page title" style="width:100%;padding:10px 12px;background:var(--bg-tertiary);border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:16px;margin-bottom:12px;outline:none;">
<textarea x-model="editingPrivateContent" placeholder="Start writing…" style="min-height:60vh;"></textarea>
</div>
</div>
</template>
</div>
</div>
{% endblock %}