]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'development' into default-templates
authorDan Brown <redacted>
Mon, 11 Dec 2023 11:41:43 +0000 (11:41 +0000)
committerDan Brown <redacted>
Mon, 11 Dec 2023 11:41:43 +0000 (11:41 +0000)
1  2 
app/Entities/Controllers/BookController.php
app/Entities/Controllers/PageController.php
app/Entities/Models/Book.php
app/Entities/Repos/PageRepo.php
lang/en/entities.php
resources/sass/_forms.scss
resources/views/books/parts/form.blade.php

index 9d8db27e93c29d85c60a4bbde9cca4cc192f565e,55d28c6847e32a1227cb4663902e3388fbb27524..9b938d89a41c7f23fb311d5eedd86c2a0770a36a
@@@ -1,12 -1,12 +1,13 @@@
  <?php
  
- namespace BookStack\Http\Controllers;
+ namespace BookStack\Entities\Controllers;
  
- use BookStack\Actions\ActivityQueries;
- use BookStack\Actions\ActivityType;
- use BookStack\Actions\View;
+ use BookStack\Activity\ActivityQueries;
+ use BookStack\Activity\ActivityType;
+ use BookStack\Activity\Models\View;
+ use BookStack\Activity\Tools\UserEntityWatchOptions;
  use BookStack\Entities\Models\Bookshelf;
 +use BookStack\Entities\Models\Page;
  use BookStack\Entities\Repos\BookRepo;
  use BookStack\Entities\Tools\BookContents;
  use BookStack\Entities\Tools\Cloner;
@@@ -15,6 -15,7 +16,7 @@@ use BookStack\Entities\Tools\ShelfConte
  use BookStack\Exceptions\ImageUploadException;
  use BookStack\Exceptions\NotFoundException;
  use BookStack\Facades\Activity;
+ use BookStack\Http\Controller;
  use BookStack\References\ReferenceFetcher;
  use BookStack\Util\SimpleListOptions;
  use Illuminate\Http\Request;
@@@ -80,14 -81,8 +82,14 @@@ class BookController extends Controlle
  
          $this->setPageTitle(trans('entities.books_create'));
  
 +        $templates = Page::visible()
 +            ->where('template', '=', true)
 +            ->orderBy('name', 'asc')
 +            ->get();
 +
          return view('books.create', [
              'bookshelf' => $bookshelf,
 +            'templates' => $templates,
          ]);
      }
  
              'description' => ['string', 'max:1000'],
              'image'       => array_merge(['nullable'], $this->getImageValidationRules()),
              'tags'        => ['array'],
 +            'default_template'  => ['nullable', 'exists:pages,id'], 
          ]);
  
          $bookshelf = null;
              'current'           => $book,
              'bookChildren'      => $bookChildren,
              'bookParentShelves' => $bookParentShelves,
+             'watchOptions'      => new UserEntityWatchOptions(user(), $book),
              'activity'          => $activities->entityActivity($book, 20, 1),
              'referenceCount'    => $this->referenceFetcher->getPageReferenceCountToEntity($book),
          ]);
          $this->checkOwnablePermission('book-update', $book);
          $this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()]));
  
 -        return view('books.edit', ['book' => $book, 'current' => $book]);
 +        $templates = Page::visible()
 +            ->where('template', '=', true)
 +            ->orderBy('name', 'asc')
 +            ->get();
 +
 +        return view('books.edit', ['book' => $book, 'current' => $book, 'templates' => $templates]);
      }
  
      /**
              'description' => ['string', 'max:1000'],
              'image'       => array_merge(['nullable'], $this->getImageValidationRules()),
              'tags'        => ['array'],
 +            'default_template'  => ['nullable', 'exists:pages,id'], 
          ]);
  
          if ($request->has('image_reset')) {
index 8b131c4f3d8d1f2bd0decc2c855142029745677c,4d8c7e809f9b087a12d5dff8702ec0c5a5d0fbc8..ad75448b34a30fb3fc04a10e435d3607644432e7
@@@ -1,9 -1,10 +1,11 @@@
  <?php
  
- namespace BookStack\Http\Controllers;
+ namespace BookStack\Entities\Controllers;
  
- use BookStack\Actions\View;
+ use BookStack\Activity\Models\View;
+ use BookStack\Activity\Tools\CommentTree;
+ use BookStack\Activity\Tools\UserEntityWatchOptions;
 +use BookStack\Entities\Models\Book;
  use BookStack\Entities\Models\Page;
  use BookStack\Entities\Repos\PageRepo;
  use BookStack\Entities\Tools\BookContents;
@@@ -14,6 -15,7 +16,7 @@@ use BookStack\Entities\Tools\PageEditAc
  use BookStack\Entities\Tools\PageEditorData;
  use BookStack\Exceptions\NotFoundException;
  use BookStack\Exceptions\PermissionsException;
+ use BookStack\Http\Controller;
  use BookStack\References\ReferenceFetcher;
  use Exception;
  use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@@ -23,16 -25,10 +26,10 @@@ use Throwable
  
  class PageController extends Controller
  {
-     protected PageRepo $pageRepo;
-     protected ReferenceFetcher $referenceFetcher;
-     /**
-      * PageController constructor.
-      */
-     public function __construct(PageRepo $pageRepo, ReferenceFetcher $referenceFetcher)
-     {
-         $this->pageRepo = $pageRepo;
-         $this->referenceFetcher = $referenceFetcher;
+     public function __construct(
+         protected PageRepo $pageRepo,
+         protected ReferenceFetcher $referenceFetcher
+     ) {
      }
  
      /**
@@@ -75,6 -71,7 +72,6 @@@
          $page = $this->pageRepo->getNewDraftPage($parent);
          $this->pageRepo->publishDraft($page, [
              'name' => $request->get('name'),
 -            'html' => '',
          ]);
  
          return redirect($page->getUrl('/edit'));
  
          $pageContent = (new PageContent($page));
          $page->html = $pageContent->render();
-         $sidebarTree = (new BookContents($page->book))->getTree();
          $pageNav = $pageContent->getNavigation($page->html);
  
-         // Check if page comments are enabled
-         $commentsEnabled = !setting('app-disable-comments');
-         if ($commentsEnabled) {
-             $page->load(['comments.createdBy']);
-         }
+         $sidebarTree = (new BookContents($page->book))->getTree();
+         $commentTree = (new CommentTree($page));
          $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
  
          View::incrementFor($page);
              'book'            => $page->book,
              'current'         => $page,
              'sidebarTree'     => $sidebarTree,
-             'commentsEnabled' => $commentsEnabled,
+             'commentTree'     => $commentTree,
              'pageNav'         => $pageNav,
+             'watchOptions'    => new UserEntityWatchOptions(user(), $page),
              'next'            => $nextPreviousLocator->getNext(),
              'previous'        => $nextPreviousLocator->getPrevious(),
              'referenceCount'  => $this->referenceFetcher->getPageReferenceCountToEntity($page),
          $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
          $this->checkOwnablePermission('page-delete', $page);
          $this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
 +        $times_used_as_template = Book::where('default_template', '=', $page->id)->count();
  
          return view('pages.delete', [
              'book'    => $page->book,
              'page'    => $page,
              'current' => $page,
 +            'times_used_as_template' => $times_used_as_template,
          ]);
      }
  
          }
  
          try {
-             $parent = $this->pageRepo->move($page, $entitySelection);
+             $this->pageRepo->move($page, $entitySelection);
          } catch (PermissionsException $exception) {
              $this->showPermissionError();
          } catch (Exception $exception) {
              $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
  
-             return redirect()->back();
+             return redirect($page->getUrl('/move'));
          }
  
-         $this->showSuccessNotification(trans('entities.pages_move_success', ['parentName' => $parent->name]));
          return redirect($page->getUrl());
      }
  
          if (is_null($newParent)) {
              $this->showErrorNotification(trans('errors.selected_book_chapter_not_found'));
  
-             return redirect()->back();
+             return redirect($page->getUrl('/copy'));
          }
  
          $this->checkOwnablePermission('page-create', $newParent);
index b84a351f8416fbada6538c38e636b14b2b5a90d4,f54a0bf2d6a464d859848ac1f97e674322562a5a..8584e755e60c38fe5d250235096e339935d2ceb2
@@@ -27,7 -27,7 +27,7 @@@ class Book extends Entity implements Ha
  
      public $searchFactor = 1.2;
  
 -    protected $fillable = ['name', 'description'];
 +    protected $fillable = ['name', 'description', 'default_template'];
      protected $hidden = ['pivot', 'image_id', 'deleted_at'];
  
      /**
  
      /**
       * Returns book cover image, if book cover not exists return default cover image.
-      *
-      * @param int $width  - Width of the image
-      * @param int $height - Height of the image
-      *
-      * @return string
       */
-     public function getBookCover($width = 440, $height = 250)
+     public function getBookCover(int $width = 440, int $height = 250): string
      {
          $default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
-         if (!$this->image_id) {
+         if (!$this->image_id || !$this->cover) {
              return $default;
          }
  
          try {
-             $cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
+             return $this->cover->getThumb($width, $height, false) ?? $default;
          } catch (Exception $err) {
-             $cover = $default;
+             return $default;
          }
-         return $cover;
      }
  
      /**
          return 'cover_book';
      }
  
 +    /**
 +     * Get the Page that is used as default template for newly created pages within this Book.
 +     */
 +    public function defaultTemplate(): BelongsTo
 +    {
 +        return $this->belongsTo(Page::class, 'default_template');
 +    }
 +
      /**
       * Get all pages within this book.
       */
index a1558b85db8fe4157e440988a2926af43a0a511b,dbd4a47d243e23f3b0f5870e9d14de42ec83a705..5269a0bccb613ed43e90128f6c286c277d066959
@@@ -2,7 -2,7 +2,7 @@@
  
  namespace BookStack\Entities\Repos;
  
- use BookStack\Actions\ActivityType;
+ use BookStack\Activity\ActivityType;
  use BookStack\Entities\Models\Book;
  use BookStack\Entities\Models\Chapter;
  use BookStack\Entities\Models\Entity;
@@@ -23,24 -23,12 +23,12 @@@ use Illuminate\Pagination\LengthAwarePa
  
  class PageRepo
  {
-     protected BaseRepo $baseRepo;
-     protected RevisionRepo $revisionRepo;
-     protected ReferenceStore $referenceStore;
-     protected ReferenceUpdater $referenceUpdater;
-     /**
-      * PageRepo constructor.
-      */
      public function __construct(
-         BaseRepo $baseRepo,
-         RevisionRepo $revisionRepo,
-         ReferenceStore $referenceStore,
-         ReferenceUpdater $referenceUpdater
+         protected BaseRepo $baseRepo,
+         protected RevisionRepo $revisionRepo,
+         protected ReferenceStore $referenceStore,
+         protected ReferenceUpdater $referenceUpdater
      ) {
-         $this->baseRepo = $baseRepo;
-         $this->revisionRepo = $revisionRepo;
-         $this->referenceStore = $referenceStore;
-         $this->referenceUpdater = $referenceUpdater;
      }
  
      /**
              $page->book_id = $parent->id;
          }
  
 +        if ($page->book->defaultTemplate) {
 +            $page->forceFill([
 +                'html'  => $page->book->defaultTemplate->html,
 +            ]);
 +        }
 +
          $page->save();
          $page->refresh()->rebuildPermissions();
  
       */
      public function publishDraft(Page $draft, array $input): Page
      {
-         $this->updateTemplateStatusAndContentFromInput($draft, $input);
-         $this->baseRepo->update($draft, $input);
          $draft->draft = false;
          $draft->revision_count = 1;
          $draft->priority = $this->getNewPriority($draft);
-         $draft->save();
+         $this->updateTemplateStatusAndContentFromInput($draft, $input);
+         $this->baseRepo->update($draft, $input);
  
          $this->revisionRepo->storeNewForPage($draft, trans('entities.pages_initial_revision'));
          $this->referenceStore->updateForPage($draft);
          $inputEmpty = empty($input['markdown']) && empty($input['html']);
  
          if ($haveInput && $inputEmpty) {
-             $pageContent->setNewHTML('');
+             $pageContent->setNewHTML('', user());
          } elseif (!empty($input['markdown']) && is_string($input['markdown'])) {
              $newEditor = 'markdown';
-             $pageContent->setNewMarkdown($input['markdown']);
+             $pageContent->setNewMarkdown($input['markdown'], user());
          } elseif (isset($input['html'])) {
              $newEditor = 'wysiwyg';
-             $pageContent->setNewHTML($input['html']);
+             $pageContent->setNewHTML($input['html'], user());
          }
  
          if ($newEditor !== $currentEditor && userCan('editor-change')) {
          $content = new PageContent($page);
  
          if (!empty($revision->markdown)) {
-             $content->setNewMarkdown($revision->markdown);
+             $content->setNewMarkdown($revision->markdown, user());
          } else {
-             $content->setNewHTML($revision->html);
+             $content->setNewHTML($revision->html, user());
          }
  
          $page->updated_by = user()->id;
diff --combined lang/en/entities.php
index 4af6120f820ae674e37e8d72c8f666c2bc6ac9f9,cfb5aae1a78c4218f43f9ad12de8b75838099279..e4c67f5ca8114b539712cfd42945ceac88276c60
@@@ -23,7 -23,7 +23,7 @@@ return 
      'meta_updated' => 'Updated :timeLength',
      'meta_updated_name' => 'Updated :timeLength by :user',
      'meta_owned_name' => 'Owned by :user',
-     'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
+     'meta_reference_page_count' => 'Referenced on :count page|Referenced on :count pages',
      'entity_select' => 'Entity Select',
      'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
      'images' => 'Images',
      'shelves_permissions_updated' => 'Shelf Permissions Updated',
      'shelves_permissions_active' => 'Shelf Permissions Active',
      'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
+     'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',
      'shelves_copy_permissions_to_books' => 'Copy Permissions to Books',
      'shelves_copy_permissions' => 'Copy Permissions',
      'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
      'books_search_this' => 'Search this book',
      'books_navigation' => 'Book Navigation',
      'books_sort' => 'Sort Book Contents',
+     'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books.',
      'books_sort_named' => 'Sort Book :bookName',
      'books_sort_name' => 'Sort by Name',
      'books_sort_created' => 'Sort by Created Date',
      'books_sort_chapters_last' => 'Chapters Last',
      'books_sort_show_other' => 'Show Other Books',
      'books_sort_save' => 'Save New Order',
+     'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',
+     'books_sort_move_up' => 'Move Up',
+     'books_sort_move_down' => 'Move Down',
+     'books_sort_move_prev_book' => 'Move to Previous Book',
+     'books_sort_move_next_book' => 'Move to Next Book',
+     'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',
+     'books_sort_move_next_chapter' => 'Move Into Next Chapter',
+     'books_sort_move_book_start' => 'Move to Start of Book',
+     'books_sort_move_book_end' => 'Move to End of Book',
+     'books_sort_move_before_chapter' => 'Move to Before Chapter',
+     'books_sort_move_after_chapter' => 'Move to After Chapter',
      'books_copy' => 'Copy Book',
      'books_copy_success' => 'Book successfully copied',
  
      'chapters_save' => 'Save Chapter',
      'chapters_move' => 'Move Chapter',
      'chapters_move_named' => 'Move Chapter :chapterName',
-     'chapter_move_success' => 'Chapter moved to :bookName',
      'chapters_copy' => 'Copy Chapter',
      'chapters_copy_success' => 'Chapter successfully copied',
      'chapters_permissions' => 'Chapter Permissions',
      'pages_delete_draft' => 'Delete Draft Page',
      'pages_delete_success' => 'Page deleted',
      'pages_delete_draft_success' => 'Draft page deleted',
 +    'pages_delete_warning_template' => '{0}|{1}Be careful: this page is used as a template for :count book.|[2,*]Be careful: this page is used as a template for :count books.',
      'pages_delete_confirm' => 'Are you sure you want to delete this page?',
      'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?',
      'pages_editing_named' => 'Editing Page :pageName',
      'pages_editing_page' => 'Editing Page',
      'pages_edit_draft_save_at' => 'Draft saved at ',
      'pages_edit_delete_draft' => 'Delete Draft',
+     'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',
      'pages_edit_discard_draft' => 'Discard Draft',
      'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
      'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
      'pages_md_insert_drawing' => 'Insert Drawing',
      'pages_md_show_preview' => 'Show preview',
      'pages_md_sync_scroll' => 'Sync preview scroll',
+     'pages_drawing_unsaved' => 'Unsaved Drawing Found',
+     'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',
      'pages_not_in_chapter' => 'Page is not in a chapter',
      'pages_move' => 'Move Page',
-     'pages_move_success' => 'Page moved to ":parentName"',
      'pages_copy' => 'Copy Page',
      'pages_copy_desination' => 'Copy Destination',
      'pages_copy_success' => 'Page successfully copied',
      'pages_revisions_restore' => 'Restore',
      'pages_revisions_none' => 'This page has no revisions',
      'pages_copy_link' => 'Copy Link',
-     'pages_edit_content_link' => 'Edit Content',
+     'pages_edit_content_link' => 'Jump to section in editor',
+     'pages_pointer_enter_mode' => 'Enter section select mode',
+     'pages_pointer_label' => 'Page Section Options',
+     'pages_pointer_permalink' => 'Page Section Permalink',
+     'pages_pointer_include_tag' => 'Page Section Include Tag',
+     'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',
+     'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',
      'pages_permissions_active' => 'Page Permissions Active',
      'pages_initial_revision' => 'Initial publish',
      'pages_references_update_revision' => 'System auto-update of internal links',
          'time_b' => 'in the last :minCount minutes',
          'message' => ':start :time. Take care not to overwrite each other\'s updates!',
      ],
-     'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content',
+     'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',
+     'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',
      'pages_specific' => 'Specific Page',
      'pages_is_template' => 'Page Template',
  
      // Editor Sidebar
+     'toggle_sidebar' => 'Toggle Sidebar',
      'page_tags' => 'Page Tags',
      'chapter_tags' => 'Chapter Tags',
      'book_tags' => 'Book Tags',
      'attachments' => 'Attachments',
      'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.',
      'attachments_explain_instant_save' => 'Changes here are saved instantly.',
-     'attachments_items' => 'Attached Items',
      'attachments_upload' => 'Upload File',
      'attachments_link' => 'Attach Link',
+     'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
      'attachments_set_link' => 'Set Link',
      'attachments_delete' => 'Are you sure you want to delete this attachment?',
-     'attachments_dropzone' => 'Drop files or click here to attach a file',
+     'attachments_dropzone' => 'Drop files here to upload',
      'attachments_no_files' => 'No files have been uploaded',
      'attachments_explain_link' => 'You can attach a link if you\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.',
      'attachments_link_name' => 'Link Name',
      'templates_replace_content' => 'Replace page content',
      'templates_append_content' => 'Append to page content',
      'templates_prepend_content' => 'Prepend to page content',
 +    'default_template' => 'Default Page Template',
 +    'default_template_explain' => "Assign a default template that will be used for all new pages in this book.",
  
      // Profile View
      'profile_user_for_x' => 'User for :time',
      'comment_placeholder' => 'Leave a comment here',
      'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
      'comment_save' => 'Save Comment',
-     'comment_saving' => 'Saving comment...',
-     'comment_deleting' => 'Deleting comment...',
      'comment_new' => 'New Comment',
      'comment_created' => 'commented :createDiff',
      'comment_updated' => 'Updated :updateDiff by :username',
+     'comment_updated_indicator' => 'Updated',
      'comment_deleted_success' => 'Comment deleted',
      'comment_created_success' => 'Comment added',
      'comment_updated_success' => 'Comment updated',
      'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
      'comment_in_reply_to' => 'In reply to :commentId',
+     'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
  
      // Revision
      'revision_delete_confirm' => 'Are you sure you want to delete this revision?',
      'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
-     'revision_delete_success' => 'Revision deleted',
      'revision_cannot_delete_latest' => 'Cannot delete the latest revision.',
  
      // Copy view
      'references' => 'References',
      'references_none' => 'There are no tracked references to this item.',
      'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
+     // Watch Options
+     'watch' => 'Watch',
+     'watch_title_default' => 'Default Preferences',
+     'watch_desc_default' => 'Revert watching to just your default notification preferences.',
+     'watch_title_ignore' => 'Ignore',
+     'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',
+     'watch_title_new' => 'New Pages',
+     'watch_desc_new' => 'Notify when any new page is created within this item.',
+     'watch_title_updates' => 'All Page Updates',
+     'watch_desc_updates' => 'Notify upon all new pages and page changes.',
+     'watch_desc_updates_page' => 'Notify upon all page changes.',
+     'watch_title_comments' => 'All Page Updates & Comments',
+     'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',
+     'watch_desc_comments_page' => 'Notify upon page changes and new comments.',
+     'watch_change_default' => 'Change default notification preferences',
+     'watch_detail_ignore' => 'Ignoring notifications',
+     'watch_detail_new' => 'Watching for new pages',
+     'watch_detail_updates' => 'Watching new pages and updates',
+     'watch_detail_comments' => 'Watching new pages, updates & comments',
+     'watch_detail_parent_book' => 'Watching via parent book',
+     'watch_detail_parent_book_ignore' => 'Ignoring via parent book',
+     'watch_detail_parent_chapter' => 'Watching via parent chapter',
+     'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',
  ];
index 6f1a81d12ea58ab9566a7b853763109dcd68ce4c,2257e8000d0fc7c50fe95c1f1ed0dfa13a493881..cd5d929f4b57835e5e8da3629d2988645f2ed5e9
    max-width: 100%;
  
    &.neg, &.invalid {
-     border: 1px solid $negative;
+     border: 1px solid var(--color-negative);
    }
    &.pos, &.valid {
-     border: 1px solid $positive;
+     border: 1px solid var(--color-positive);
    }
    &.disabled, &[disabled] {
      background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAYAAADEUlfTAAAAMUlEQVQIW2NkwAGuXbv2nxGbHEhCS0uLEUMSJgHShCKJLIEiiS4Bl8QmAZbEJQGSBAC62BuJ+tt7zgAAAABJRU5ErkJggg==);
@@@ -75,6 -75,7 +75,7 @@@
    @include lightDark(border-color, #ddd, #000);
    position: relative;
    flex: 1;
+   min-width: 0;
  }
  .markdown-editor-wrap + .markdown-editor-wrap {
    flex-basis: 50%;
    flex-grow: 0;
  }
  
+ .markdown-editor-wrap .cm-editor {
+   flex: 1;
+   max-width: 100%;
+   border: 0;
+   margin: 0;
+ }
  .markdown-panel-divider {
    width: 2px;
    @include lightDark(background-color, #ddd, #000);
      max-width: 100%;
      flex-grow: 1;
      flex-basis: auto !important;
+     min-height: 0;
    }
    .editor-toolbar-label {
      float: none !important;
    #markdown-editor .markdown-editor-wrap:not(.active) {
      flex-grow: 0;
      flex: none;
-     min-height: 0;
    }
  }
  
@@@ -140,10 -148,9 +148,9 @@@ html.markdown-editor-display.dark-mode 
    width: 100%;
    font-size: 11px;
    line-height: 1.6;
-   border-bottom: 1px solid #DDD;
-   background-color: #EEE;
-   @include lightDark(background-color, #eee, #111);
-   @include lightDark(border-color, #ddd, #000);
+   border-bottom: 1px solid #CCC;
+   @include lightDark(background-color, #FFF, #333);
+   @include lightDark(border-color, #CCC, #000);
    flex: none;
    @include whenDark {
      button {
@@@ -258,7 -265,6 +265,6 @@@ input[type=color] 
      border-radius: 2px;
      display: inline-block;
      border: 2px solid currentColor;
-     opacity: 0.6;
      overflow: hidden;
      fill: currentColor;
      .svg-icon {
    height: auto;
  }
  
- .title-input.page-title {
-   font-size: 0.8em;
-   @include lightDark(background-color, #fff, #333);
-   .input {
-     border: 0;
-     margin-bottom: -1px;
-   }
-   input[type="text"] {
-     max-width: 840px;
-     margin: 0 auto;
-     border: none;
-     height: auto;
-   }
- }
- .page-title input {
-   display: block;
-   width: 100%;
-   font-size: 1.4em;
- }
  .description-input textarea {
    display: block;
    width: 100%;
    height: auto;
  }
  
- div[editor-type="markdown"] .title-input.page-title input[type="text"] {
-   max-width: 100%;
-   border-radius: 0;
- }
  .search-box {
    max-width: 100%;
    position: relative;
    &.flexible input {
      width: 100%;
    }
 -  .search-box-cancel {
 +  button.search-box-cancel {
      left: auto;
      right: 0;
    }
  }
  
+ .contained-search-box {
+   display: flex;
+   height: 38px;
+   z-index: -1;
+   &.floating {
+     box-shadow: $bs-med;
+     border-radius: 4px;
+     overflow: hidden;
+     @include whenDark {
+       border: 1px solid #000;
+     }
+   }
+   input, button {
+     height: 100%;
+     border-radius: 0;
+     border: 1px solid #ddd;
+     @include lightDark(border-color, #ddd, #000);
+     margin-inline-start: -1px;
+     &:last-child {
+       border-inline-end: 0;
+     }
+   }
+   input {
+     border: 0;
+     flex: 5;
+     padding: $-xs $-s;
+     &:focus, &:active {
+       outline: 1px dotted var(--color-primary);
+       outline-offset: -2px;
+       border: 0;
+     }
+   }
+   button {
+     border: 0;
+     width: 48px;
+     border-inline-start: 1px solid #DDD;
+     background-color: #FFF;
+     @include lightDark(background-color, #FFF, #333);
+     @include lightDark(color, #444, #AAA);
+   }
+   button:focus {
+     outline: 1px dotted var(--color-primary);
+     outline-offset: -2px;
+   }
+   svg {
+     margin: 0;
+   }
+   @include smaller-than($s) {
+     width: 180px;
+   }
+ }
  .outline > input {
    border: 0;
    border-bottom: 2px solid #DDD;
index c6ef7d171f1ffc94c50be3e210073d306572e91e,56d385c9e2a936065eb27c28e677e32ea84b1dcf..9b66b8ac87a73b4d1a2e1d3f240665bd1bbfdb03
@@@ -11,7 -11,7 +11,7 @@@
  </div>
  
  <div class="form-group collapsible" component="collapsible" id="logo-control">
-     <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
+     <button refs="collapsible@trigger" type="button" class="collapse-title text-link" aria-expanded="false">
          <label>{{ trans('common.cover_image') }}</label>
      </button>
      <div refs="collapsible@content" class="collapse-content">
@@@ -27,7 -27,7 +27,7 @@@
  </div>
  
  <div class="form-group collapsible" component="collapsible" id="tags-control">
-     <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
+     <button refs="collapsible@trigger" type="button" class="collapse-title text-link" aria-expanded="false">
          <label for="tag-manager">{{ trans('entities.book_tags') }}</label>
      </button>
      <div refs="collapsible@content" class="collapse-content">
      </div>
  </div>
  
 +<div class="form-group collapsible" component="collapsible" id="template-control">
 +    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
 +        <label for="template-manager">{{ trans('entities.default_template') }}</label>
 +    </button>
 +    <div refs="collapsible@content" class="collapse-content">
 +        @include('entities.template-manager', ['entity' => $book ?? null, 'templates' => $templates])
 +    </div>
 +</div>
 +
  <div class="form-group text-right">
      <a href="{{ $returnLocation }}" class="button outline">{{ trans('common.cancel') }}</a>
      <button type="submit" class="button">{{ trans('entities.books_save') }}</button>