Add Calendar integration (read-only)
@@ -0,0 +1,45 @@
|
||||
<h2 id="maintenance">Maintenance</h2>
|
||||
{{if .Maintenance}}
|
||||
<div class="cal-scroll"><table class="data-table panel cal-table">
|
||||
<thead><tr><th>Task</th><th>Repeats</th><th>Last</th><th>Next</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Maintenance}}
|
||||
<tr><td>{{.Summary}}</td><td class="muted">{{.Rule}}</td><td class="nowrap">{{.Last}}</td><td class="nowrap">{{.Next}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{{else if .HasMaintenance}}
|
||||
<p class="muted">No maintenance tasks within a year of today.</p>
|
||||
{{else}}
|
||||
<p class="muted">No maintenance calendar. Set <code>maintenance = <name></code> in this folder's <code>.page-settings</code>.</p>
|
||||
{{end}}
|
||||
|
||||
<h2 id="calendars">Calendars</h2>
|
||||
{{if .Calendars}}
|
||||
<div class="cal-scroll"><table class="data-table panel cal-table">
|
||||
<thead><tr><th>Calendar</th><th>Role</th><th>Events</th><th>Updated</th></tr></thead>
|
||||
<tbody>
|
||||
{{range .Calendars}}
|
||||
<tr>
|
||||
<td>{{.Name}}{{if .Error}}<div class="cal-error">{{.Error}}</div>{{end}}</td>
|
||||
<td class="muted">{{.Roles}}</td>
|
||||
<td>{{.Events}}</td>
|
||||
<td class="nowrap">{{.Updated}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table></div>
|
||||
{{else}}
|
||||
<p class="muted">No <code>.ics</code> files in this folder yet.</p>
|
||||
{{end}}
|
||||
{{if .IsSyncDir}}
|
||||
{{if .SyncEnabled}}
|
||||
<form class="cal-sync row" method="post" action="?calsync">
|
||||
<button type="submit" class="btn">Sync now</button>
|
||||
<span class="muted">{{if .LastRun}}Last sync: {{.LastRun}}{{else}}Not synced yet{{end}}</span>
|
||||
</form>
|
||||
{{if .LastError}}<p class="cal-error">{{.LastError}}</p>{{end}}
|
||||
{{else}}
|
||||
<p class="muted">Sync is off. Set <code>caldav-url</code>, <code>caldav-user</code> and <code>caldav-pass</code> in this folder's <code>.page-settings</code>.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,16 @@
|
||||
<span class="dropdown maint">
|
||||
<button type="button" class="btn maint-toggle{{if not .Count}} is-idle{{end}}" data-action="maint-menu" aria-haspopup="true" aria-expanded="false" title="Maintenance (H)">{{icon "maintenance"}}<span class="maint-count">{{.Count}}</span></button>
|
||||
<div class="dropdown-menu align-right maint-menu">
|
||||
<div class="panel-header">Today</div>
|
||||
<div class="maint-list">
|
||||
{{range .Today}}<span class="maint-when">{{.When}}</span><span>{{.Summary}}</span>
|
||||
{{else}}<span class="muted">Nothing due</span>{{end}}
|
||||
</div>
|
||||
<div class="panel-header">Next 7 days</div>
|
||||
<div class="maint-list">
|
||||
{{range .Upcoming}}<span class="maint-when">{{.When}}</span><span>{{.Summary}}</span>
|
||||
{{else}}<span class="muted">Nothing planned</span>{{end}}
|
||||
</div>
|
||||
{{if .PageURL}}<a class="btn btn-block maint-all" href="{{.PageURL}}">All maintenance →</a>{{end}}
|
||||
</div>
|
||||
</span>
|
||||
@@ -10,4 +10,8 @@
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if or .Events .Weight}}<div class="diary-day-cal muted">
|
||||
{{if .Events}}<p>{{.Events}}</p>{{end}}
|
||||
{{if .Weight}}<p>{{.Weight}}</p>{{end}}
|
||||
</div>{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
e.preventDefault();
|
||||
if (window.location.pathname !== '/' && typeof movePage === 'function') movePage();
|
||||
break;
|
||||
case 'H':
|
||||
var maint = document.querySelector('[data-action="maint-menu"]');
|
||||
if (!maint) return;
|
||||
e.preventDefault();
|
||||
maint.click();
|
||||
break;
|
||||
case 'F':
|
||||
var input = document.querySelector('.search-input');
|
||||
if (!input) return;
|
||||
@@ -58,12 +64,14 @@ document.addEventListener('keydown', function (e) {
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var trigger = document.querySelector('[data-action="actions-menu"]');
|
||||
if (!trigger) return;
|
||||
wireDropdown(trigger);
|
||||
var menu = trigger.parentElement.querySelector('.dropdown-menu');
|
||||
if (!menu) return;
|
||||
trigger.addEventListener('click', function () {
|
||||
trigger.setAttribute('aria-expanded', menu.classList.contains('is-open') ? true : false);
|
||||
['actions-menu', 'maint-menu'].forEach(function (action) {
|
||||
var trigger = document.querySelector('[data-action="' + action + '"]');
|
||||
if (!trigger) return;
|
||||
wireDropdown(trigger);
|
||||
var menu = trigger.parentElement.querySelector('.dropdown-menu');
|
||||
if (!menu) return;
|
||||
trigger.addEventListener('click', function () {
|
||||
trigger.setAttribute('aria-expanded', menu.classList.contains('is-open') ? true : false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em"
|
||||
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" stroke-linecap="square">
|
||||
<path d="M2 4h12M6 4V2h4v2M4 4l1 10h6l1-10M7 7v4M9 7v4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 259 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em"
|
||||
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" stroke-linecap="square">
|
||||
<path d="M11 2l3 3-8 8H3v-3zM9 4l3 3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 241 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em"
|
||||
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" stroke-linecap="square">
|
||||
<path d="M2 3h12v11H2zM2 6h12M5 1v2M11 1v2M5 10l2 2 4-4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 260 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em"
|
||||
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" stroke-linecap="square">
|
||||
<path d="M2 8h8M7 5l3 3-3 3M13 2v12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 240 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em"
|
||||
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" stroke-linecap="square">
|
||||
<path d="M8 3v10M3 8h10"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 228 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em"
|
||||
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" stroke-linecap="square">
|
||||
<path d="M13 8a5 5 0 1 1-1.5-3.5M12 1v4H8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 246 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em"
|
||||
fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="miter" stroke-linecap="square">
|
||||
<path d="M1 14V3h5l1 2h8v9zM8 12V8M6 10l2-2 2 2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 252 B |
@@ -31,6 +31,7 @@
|
||||
{{end}}
|
||||
<div class="header-actions row">
|
||||
{{if .EditMode}}{{block "headerActions" .}}{{end}}{{else}}
|
||||
{{maintenanceWidget}}
|
||||
<span class="dropdown actions">
|
||||
<button type="button" class="btn actions-toggle" data-action="actions-menu" aria-haspopup="true" aria-expanded="false" title="Actions">ACTIONS</button>
|
||||
<div class="dropdown-menu align-right actions-menu">
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{{define "headScripts"}}<script src="/_/page/actions.js"></script>{{end}}
|
||||
|
||||
{{define "headerActions"}}{{if .CanEdit}}
|
||||
<button class="btn btn-block" onclick="newPage()" title="New page (N)">NEW</button>
|
||||
<button class="btn btn-block" data-companion-reveal hidden title="Reveal in file manager">REVEAL</button>
|
||||
<button class="btn btn-block" onclick="newPage()" title="New page (N)"><span class="btn-icon">{{icon "new"}}</span><span class="btn-label">NEW</span></button>
|
||||
<button class="btn btn-block" data-companion-reveal hidden title="Reveal in file manager"><span class="btn-icon">{{icon "reveal"}}</span><span class="btn-label">REVEAL</span></button>
|
||||
{{if not .IsRoot}}
|
||||
<button class="btn btn-block" onclick="movePage()" title="Move page (M)">MOVE</button>
|
||||
<button class="btn btn-block danger" onclick="deletePage()" title="Delete page">DELETE</button>
|
||||
<button class="btn btn-block" onclick="movePage()" title="Move page (M)"><span class="btn-icon">{{icon "move"}}</span><span class="btn-label">MOVE</span></button>
|
||||
<button class="btn btn-block danger" onclick="deletePage()" title="Delete page"><span class="btn-icon">{{icon "delete"}}</span><span class="btn-label">DELETE</span></button>
|
||||
{{end}}
|
||||
<a class="btn btn-block" href="?edit" title="Edit page (E)">EDIT</a>
|
||||
<a class="btn btn-block" href="?edit" title="Edit page (E)"><span class="btn-icon">{{icon "edit"}}</span><span class="btn-label">EDIT</span></a>
|
||||
{{end}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
{{end}}
|
||||
|
||||
{{define "headerActions"}}
|
||||
<button class="btn btn-block" onclick="rebuildIndex()" title="Rebuild search index">REBUILD INDEX</button>
|
||||
<button class="btn btn-block" onclick="rebuildIndex()" title="Rebuild search index"><span class="btn-icon">{{icon "rebuild"}}</span><span class="btn-label">REBUILD INDEX</span></button>
|
||||
{{end}}
|
||||
|
||||
{{define "footerExtras"}}
|
||||
|
||||
@@ -389,6 +389,10 @@ main > h2 {
|
||||
.dropdown-menu.scrollable { max-height: 23rem; overflow-y: auto; }
|
||||
|
||||
.actions-special > .btn {text-align: left;}
|
||||
/* Header actions carry an icon and a label: the mobile dropdown shows the
|
||||
label, the inline desktop bar (see responsive block) the icon. */
|
||||
.btn-icon { display: none; }
|
||||
.btn-icon svg { display: block; }
|
||||
|
||||
/* === Suggestion dropdown (header search + editor link picker) ===
|
||||
Anchored to a position:relative host. Mirrors .dropdown-menu visuals with
|
||||
@@ -797,6 +801,37 @@ aside.sidebar:empty { display: none; }
|
||||
.diary-cal-grid td.cal-current,
|
||||
.diary-cal-grid td.cal-current a { color: var(--primary-hover); }
|
||||
.btn-block.cal-current { color: var(--primary-hover); }
|
||||
/* Calendar events and weight under a diary day heading. */
|
||||
.diary-day-cal { margin: var(--space-3) 0 0; }
|
||||
.diary-day-cal p { margin: 0; }
|
||||
|
||||
/* === Calendars ===
|
||||
Header maintenance chip + dropdown, and the calendars folder page. */
|
||||
.maint-toggle { display: inline-flex; align-items: center; }
|
||||
.maint-count { margin-left: 0.3rem; }
|
||||
.maint-toggle.is-idle { color: var(--text-muted); }
|
||||
.maint-menu {
|
||||
min-width: 20rem;
|
||||
max-width: calc(100vw - 2rem);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
white-space: normal;
|
||||
}
|
||||
/* Two columns so dates line up; an empty when-cell keeps the grid intact. */
|
||||
.maint-list {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
column-gap: var(--space-2);
|
||||
row-gap: 0.15rem;
|
||||
font-size: var(--font-sm);
|
||||
}
|
||||
.maint-list > .muted { grid-column: 1 / -1; }
|
||||
.maint-list + .panel-header { margin-top: var(--space-3); }
|
||||
.maint-when { color: var(--text-muted); white-space: nowrap; }
|
||||
.maint-all { margin-top: var(--space-3); padding: 0.3rem 0 0; border-top: var(--border-dashed); }
|
||||
.cal-scroll { overflow-x: auto; }
|
||||
.cal-table .nowrap { white-space: nowrap; }
|
||||
.cal-error { color: var(--danger); font-size: var(--font-sm); }
|
||||
.cal-sync { margin-top: var(--space-3); }
|
||||
|
||||
/* === Fitness dashboard ===
|
||||
Server-rendered inline SVG charts. Geometry comes precomputed from Go;
|
||||
@@ -840,7 +875,9 @@ aside.sidebar:empty { display: none; }
|
||||
@media (min-width: 1101px) {
|
||||
.actions-toggle, .actions-special {display: none;}
|
||||
.actions, .actions-menu, .actions.dropdown-menu.is-open {display: contents;}
|
||||
.actions-menu .btn-block { width: auto; padding: 0;}
|
||||
.actions-menu .btn-block { width: auto; padding: 0; align-items: center; }
|
||||
.actions-menu .btn-icon { display: inline-block; }
|
||||
.actions-menu .btn-label { display: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
||||