Storyline: split the chapter page, dock the notes at the bottom

The chapter page now fills the viewport as two horizontal panes: the
chapter scrolls in the top ~2/3, the notes sit in a fixed dock in the
bottom ~1/3, always visible while you read. Drag the divider to change
the ratio, click the "Chapter notes" header to collapse the dock to a
bar; both the height and the collapsed state persist in localStorage.

- base.html.twig: add a {% block body_class %} hook on <body>.
- chapter.html.twig sets body class "storyline-chapter-page"; the split
  layout CSS is scoped to it and falls back to normal stacked flow under
  768px wide or 560px tall.
- app.js publishes the navbar height as --app-nav-h so the split can size
  itself to viewport-minus-navbar even when the navbar wraps.
- new chapter_notes Stimulus controller: drag-resize + collapse + persist.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-09-08 19:00:49 +02:00
co-authored by Claude Sonnet 5
parent 5c821bd7ad
commit 90345ecd38
5 changed files with 318 additions and 74 deletions
+118 -14
View File
@@ -192,7 +192,7 @@ table.dataTable {
/* --- Storyline: chapter layout + table of contents ------------------- */
.storyline-toc {
position: sticky;
top: 1.5rem;
top: 0.25rem;
font-size: 0.9375rem;
}
.storyline-toc-link {
@@ -429,22 +429,126 @@ table.dataTable {
}
.storyline-pager-title { font-weight: 600; }
/* Chapter notes: collapsible card above the chapter body */
.storyline-notes > summary {
cursor: pointer;
list-style: none;
/* --- Storyline chapter page: split view (content above, notes docked) --- */
/* On wide/tall screens the chapter page fills the viewport: the top pane
scrolls the chapter, the bottom pane is a fixed-height notes dock. Falls
back to normal document flow on small screens (see media query below). */
.storyline-chapter-page { overflow: hidden; }
.storyline-chapter-page main.app-main {
min-height: 0;
padding: 0;
height: calc(100vh - var(--app-nav-h, 3.5rem));
height: calc(100dvh - var(--app-nav-h, 3.5rem));
}
.storyline-chapter-page main.app-main > .container {
height: 100%;
display: flex;
flex-direction: column;
padding-top: 0.75rem;
}
.storyline-split {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
/* --notes-h is set by the resize handle; default is one third */
--notes-h: 33%;
}
.storyline-split-main {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-bottom: 1.5rem;
}
.storyline-notes-resize {
flex: 0 0 auto;
height: 10px;
margin: 0 -0.5rem;
cursor: row-resize;
background:
linear-gradient(var(--bs-border-color), var(--bs-border-color)) center/2.5rem 2px no-repeat;
border-top: 1px solid var(--bs-border-color);
}
.storyline-notes-resize:hover { background-color: var(--bs-tertiary-bg); }
.storyline-notes-dock {
flex: 0 0 var(--notes-h);
min-height: 2.75rem;
display: flex;
flex-direction: column;
background: var(--bs-body-bg);
border-top: 1px solid var(--bs-border-color);
}
.storyline-notes-bar {
flex: 0 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.4rem 0.1rem;
}
.storyline-notes-toggle {
border: 0;
background: none;
padding: 0.15rem 0.25rem;
font-weight: 600;
color: var(--bs-body-color);
display: inline-flex;
align-items: center;
gap: 0.4rem;
cursor: pointer;
}
.storyline-notes > summary::-webkit-details-marker { display: none; }
.storyline-notes > summary::before {
content: "\25B8"; /* right-pointing triangle */
display: inline-block;
margin-right: 0.5rem;
color: var(--bs-secondary-color);
transition: transform 0.15s ease;
.storyline-notes-caret { transition: transform 0.15s ease; display: inline-block; }
.storyline-notes-dock.is-collapsed { flex: 0 0 auto !important; }
.storyline-notes-dock.is-collapsed .storyline-notes-body { display: none; }
.storyline-notes-dock.is-collapsed .storyline-notes-caret { transform: rotate(-90deg); }
.storyline-notes-resize:has(+ .storyline-notes-dock.is-collapsed) { display: none; }
.storyline-notes-body {
flex: 1 1 auto;
min-height: 0;
padding-bottom: 0.5rem;
}
.storyline-notes-form {
height: 100%;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.storyline-notes-text {
flex: 1 1 auto;
min-height: 0;
resize: none;
font-size: 0.95rem;
line-height: 1.55;
}
.storyline-notes-actions {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 0.75rem;
}
/* Small or short screens: no fixed split, just stack notes under the chapter. */
@media (max-width: 767.98px), (max-height: 560px) {
.storyline-chapter-page { overflow: visible; }
.storyline-chapter-page main.app-main {
height: auto;
padding-top: 1.5rem;
padding-bottom: 3rem;
}
.storyline-chapter-page main.app-main > .container { display: block; height: auto; }
.storyline-split { display: block; }
.storyline-split-main { overflow: visible; }
.storyline-notes-resize { display: none; }
.storyline-notes-dock {
flex-basis: auto;
margin-top: 1.5rem;
border-top-width: 2px;
padding-top: 0.5rem;
}
.storyline-notes-text { min-height: 9rem; }
}
.storyline-notes[open] > summary::before { transform: rotate(90deg); }
.storyline-notes textarea { font-size: 0.95rem; line-height: 1.6; }
/* Chapter list: kicker line above each title */
.storyline-chapter-kicker {