Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .ai/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,20 @@
<!-- ====================================================================== -->

## Last Updated
2026-03-27 -- **Pre-Reddit Launch Cleanup.**
2026-03-28 -- **Layout Editor Debug + Sidebar Quick-Create.**

67. **Layout Editor Debug + Sidebar Quick-Create (2 items).**

- **Template editor debug logging** — Added `console.log` to `handleDrop()` and `save()` in
`static/js/widgets/template_editor.js` to diagnose layout save failures. Logs block count,
payload size, endpoint, and server error details. Code review of the full save flow
(template_editor.js → handler.go → service.go ValidateLayout → repository.go) found no
obvious bug — the `posts` block IS registered, the JSON wrapping is correct, validation
logic checks out. Issue may be in drag-and-drop interaction or rendering.
- **Sidebar entity type quick-create** — Added "+" button to sidebar Categories header
(owner-only, `GetCampaignRole >= 3`). Clicking opens an inline Alpine.js form with name
input, 24-icon picker grid, and color picker. POSTs to existing `CreateEntityType` endpoint.
Reloads page on success. No backend changes needed.

66. **Pre-Reddit Launch Cleanup (4 items).**

Expand Down
8 changes: 4 additions & 4 deletions internal/plugins/entities/show.templ
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ templ EntityShowPage(cc *campaigns.CampaignContext, entity *Entity, entityType *
@layouts.App(entity.Name + " - " + cc.Campaign.Name) {
<div class="max-w-6xl mx-auto" data-entity-type-slug={ entityType.Slug }>
<!-- Breadcrumb with ancestor chain -->
<nav class="text-sm text-fg-secondary mb-4">
<nav class="text-xs md:text-sm text-fg-secondary mb-4 overflow-x-auto whitespace-nowrap">
<a href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/entities", cc.Campaign.ID)) } class="hover:text-fg-body">Pages</a>
if entity.TypeSlug != "" {
<span class="mx-1">/</span>
Expand All @@ -37,7 +37,7 @@ templ EntityShowPage(cc *campaigns.CampaignContext, entity *Entity, entityType *
<!-- Dynamic layout from entity type template -->
if entityType != nil && len(entityType.Layout.Rows) > 0 {
for _, row := range entityType.Layout.Rows {
<div class="grid grid-cols-1 md:grid-cols-12 gap-6 mb-6">
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 md:gap-6 mb-4 md:mb-6">
for _, col := range row.Columns {
<div class={ entityColSpan(col.Width) }>
for _, block := range col.Blocks {
Expand All @@ -49,7 +49,7 @@ templ EntityShowPage(cc *campaigns.CampaignContext, entity *Entity, entityType *
}
} else {
<!-- Fallback: default two-column layout if no layout defined -->
<div class="grid grid-cols-1 md:grid-cols-12 gap-6">
<div class="grid grid-cols-1 md:grid-cols-12 gap-4 md:gap-6">
<div class="col-span-1 md:col-span-8 space-y-4">
@blockTitle(cc, entity, csrfToken)
@blockTags(cc, entity, csrfToken)
Expand Down Expand Up @@ -216,7 +216,7 @@ templ blockTitle(cc *campaigns.CampaignContext, entity *Entity, csrfToken string
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<h1 class="text-3xl font-bold text-fg">{ entity.Name }</h1>
<h1 class="text-xl md:text-3xl font-bold text-fg">{ entity.Name }</h1>
<button
class="text-fg-muted hover:text-amber-400 transition-colors"
data-favorite-toggle={ entity.ID }
Expand Down
83 changes: 82 additions & 1 deletion internal/templates/layouts/app.templ
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ templ App(title string) {
@FlashMessages()

<!-- Page content (scrollable) -->
<main class="flex-1 overflow-y-auto px-5 py-4 bg-surface" id="main-content">
<main class="flex-1 overflow-y-auto px-3 py-3 md:px-5 md:py-4 bg-surface" id="main-content">
{ children... }
</main>
</div>
Expand Down Expand Up @@ -299,7 +299,20 @@ templ Sidebar() {
>
<div class="px-4 mb-2 flex items-center justify-between sidebar-cat-list-label">
<span class="text-xs font-semibold uppercase tracking-wider text-gray-500">Categories</span>
if GetCampaignRole(ctx) >= 3 {
<button
type="button"
class="text-fg-muted hover:text-accent transition-colors p-0.5"
title="Add category"
onclick="document.getElementById('sidebar-create-category').toggleAttribute('data-open'); var el = document.getElementById('sidebar-create-category'); if (el.hasAttribute('data-open')) { el.style.display = ''; el.querySelector('input[type=text]').focus(); } else { el.style.display = 'none'; }"
>
<i class="fa-solid fa-plus text-[10px]"></i>
</button>
}
</div>
if GetCampaignRole(ctx) >= 3 {
@sidebarCreateCategory(ctx)
}
if len(GetSidebarItems(ctx)) > 0 {
<!-- Unified: render categories and other items from the items array -->
for _, item := range GetSidebarItems(ctx) {
Expand Down Expand Up @@ -793,6 +806,74 @@ templ sidebarAllPagesLink(ctx context.Context) {

// entityTypeLink renders a single category link in the sidebar icon list.
// Clicking triggers the slide-over panel via sidebar_drill.js.
// sidebarCreateCategory renders a compact inline form for creating a new
// entity type directly from the sidebar. Visible to campaign owners only.
templ sidebarCreateCategory(ctx context.Context) {
<div id="sidebar-create-category" class="px-4 mb-2" style="display: none;">
<div class="p-2.5 bg-surface-raised border border-edge rounded-md space-y-2"
x-data={ fmt.Sprintf(`{
name: '', icon: 'fa-file', color: '#6b7280', saving: false,
icons: ['fa-user','fa-users','fa-map-pin','fa-building','fa-box','fa-sticky-note',
'fa-crown','fa-shield','fa-scroll','fa-book','fa-star','fa-flag',
'fa-landmark','fa-globe','fa-mountain','fa-gem','fa-skull','fa-dragon',
'fa-hat-wizard','fa-dungeon','fa-wand-sparkles','fa-flask','fa-paw','fa-ghost'],
async create() {
if (!this.name.trim() || this.saving) return;
this.saving = true;
try {
const res = await Chronicle.apiFetch('/campaigns/%s/entity-types', {
method: 'POST',
body: { name: this.name.trim(), icon: this.icon, color: this.color }
});
if (res.ok || res.redirected) {
window.location.reload();
} else {
const err = await res.json().catch(() => ({}));
Chronicle.notify(err.message || 'Failed to create category', 'error');
}
} catch (e) {
Chronicle.notify('Failed to create category', 'error');
} finally {
this.saving = false;
}
}
}`, GetCampaignID(ctx)) }
>
<input
x-model="name"
type="text"
placeholder="Category name"
class="input w-full text-xs"
maxlength="100"
@keydown.enter="create()"
@keydown.escape="$el.closest('#sidebar-create-category').style.display='none'"
/>
<div class="flex flex-wrap gap-1 max-h-16 overflow-y-auto">
<template x-for="ic in icons" :key="ic">
<label class="cursor-pointer">
<input type="radio" name="sidebar_icon" :value="ic" x-model="icon" class="peer sr-only"/>
<span class="inline-flex items-center justify-center w-6 h-6 rounded border border-edge text-fg-secondary peer-checked:border-accent peer-checked:bg-accent/10 peer-checked:text-accent hover:border-edge transition-colors">
<i :class="'fa-solid ' + ic" class="text-[10px]"></i>
</span>
</label>
</template>
</div>
<div class="flex items-center gap-2">
<input type="color" x-model="color" class="w-6 h-6 rounded border border-edge cursor-pointer shrink-0"/>
<button
type="button"
:disabled="!name.trim() || saving"
class="btn-primary text-xs flex-1 py-1"
@click="create()"
>
<span x-show="!saving">Add</span>
<span x-show="saving">Adding...</span>
</button>
</div>
</div>
</div>
}

templ entityTypeLink(slug string, label string, color string, icon string, count int, typeID int) {
<a
href={ templ.SafeURL(fmt.Sprintf("/campaigns/%s/%s", GetCampaignID(ctx), slug)) }
Expand Down
31 changes: 31 additions & 0 deletions static/css/components/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,37 @@
bg-surface-alt text-fg-secondary hover:text-fg transition-colors;
}

/* --- Mobile touch-friendly adjustments --- */
@media (max-width: 767px) {
.chronicle-editor__toolbar {
@apply gap-1 px-1.5 py-2;
}
.chronicle-editor__btn {
@apply w-10 h-10 text-base;
}
.chronicle-editor__separator {
@apply w-px h-4 mx-0.5;
}
.chronicle-editor__btn--save {
@apply px-4 py-2 text-sm;
}
.chronicle-editor__insert-dropdown {
@apply w-64 right-0 left-auto;
}
.chronicle-editor__content .tiptap {
@apply p-3 min-h-[150px];
}
.chronicle-editor__header {
@apply px-2 py-1.5;
}
.chronicle-find-bar__row {
@apply flex-wrap;
}
.chronicle-find-bar__input {
@apply text-base py-1.5;
}
}

/* Highlight the save button when there are unsaved changes. */
.chronicle-editor__btn--save.has-changes {
@apply bg-accent text-white hover:bg-accent-hover;
Expand Down
1 change: 1 addition & 0 deletions static/js/widgets/entity_tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@
// Debounce: wait 300ms before fetching/showing.
hoverTimer = setTimeout(function () {
var url = trigger.getAttribute('data-entity-preview');
console.log('[Tooltip] Hover triggered for:', url);
showTooltip(trigger, url);
}, 300);
});
Expand Down
6 changes: 6 additions & 0 deletions static/js/widgets/template_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,8 @@ Chronicle.register('template-editor', {

this.markDirty();
this.renderCanvas();
console.log('[template-editor] Drop complete: layout now has', this.layout.rows.length, 'rows,',
this.layout.rows.reduce((n, r) => n + r.columns.reduce((m, c) => m + c.blocks.length, 0), 0), 'blocks');
},

addRow(widths) {
Expand Down Expand Up @@ -1552,15 +1554,19 @@ Chronicle.register('template-editor', {
try {
// Strip transient UI state before sending to the server.
const cleanLayout = this.cleanLayoutForSave(this.layout);
console.log('[template-editor] Saving layout:', cleanLayout.rows.length, 'rows,',
JSON.stringify(cleanLayout).length, 'bytes to', this.endpoint);
const res = await Chronicle.apiFetch(this.endpoint, {
method: 'PUT',
body: { layout: cleanLayout },
});

if (!res.ok) {
const err = await res.json().catch(() => ({}));
console.error('[template-editor] Save failed:', res.status, err);
throw new Error(err.message || 'Failed to save');
}
console.log('[template-editor] Save successful');

this.dirty = false;
if (status) status.textContent = 'Saved';
Expand Down
Loading