]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'development' into release
authorDan Brown <redacted>
Wed, 30 Nov 2022 12:29:52 +0000 (12:29 +0000)
committerDan Brown <redacted>
Wed, 30 Nov 2022 12:29:52 +0000 (12:29 +0000)
501 files changed:
.github/translators.txt
.github/workflows/analyse-php.yml
.github/workflows/test-migrations.yml
.github/workflows/test-php.yml
app/Actions/Queries/WebhooksAllPaginatedAndSorted.php [new file with mode: 0644]
app/Actions/TagRepo.php
app/Api/ListingResponseBuilder.php
app/Auth/Access/Oidc/OidcJwtSigningKey.php
app/Auth/Access/Oidc/OidcProviderSettings.php
app/Auth/Access/Oidc/OidcService.php
app/Auth/Permissions/JointPermissionBuilder.php
app/Auth/Queries/RolesAllPaginatedAndSorted.php [new file with mode: 0644]
app/Auth/Queries/UsersAllPaginatedAndSorted.php [moved from app/Auth/Queries/AllUsersPaginatedAndSorted.php with 63% similarity]
app/Auth/Role.php
app/Auth/UserRepo.php
app/Config/app.php
app/Config/setting-defaults.php
app/Entities/Models/Page.php
app/Entities/Repos/BaseRepo.php
app/Entities/Tools/BookContents.php
app/Entities/Tools/Cloner.php
app/Http/Controllers/AuditLogController.php
app/Http/Controllers/Auth/ConfirmEmailController.php
app/Http/Controllers/BookController.php
app/Http/Controllers/BookshelfController.php
app/Http/Controllers/HomeController.php
app/Http/Controllers/PageRevisionController.php
app/Http/Controllers/RoleController.php
app/Http/Controllers/SearchController.php
app/Http/Controllers/TagController.php
app/Http/Controllers/UserController.php
app/Http/Controllers/UserPreferencesController.php [new file with mode: 0644]
app/Http/Controllers/WebhookController.php
app/Providers/RouteServiceProvider.php
app/Search/SearchRunner.php
app/Settings/SettingService.php
app/Settings/UserShortcutMap.php [new file with mode: 0644]
app/Uploads/ImageService.php
app/Util/SimpleListOptions.php [new file with mode: 0644]
composer.lock
dev/docs/javascript-code.md
package-lock.json
package.json
resources/icons/image.svg
resources/icons/shortcuts.svg [new file with mode: 0644]
resources/js/app.js
resources/js/code.mjs
resources/js/components/add-remove-rows.js
resources/js/components/ajax-delete-row.js
resources/js/components/ajax-form.js
resources/js/components/attachments-list.js
resources/js/components/attachments.js
resources/js/components/auto-submit.js [new file with mode: 0644]
resources/js/components/auto-suggest.js
resources/js/components/back-to-top.js
resources/js/components/book-sort.js
resources/js/components/chapter-contents.js
resources/js/components/code-editor.js
resources/js/components/code-highlighter.js
resources/js/components/code-textarea.js
resources/js/components/collapsible.js
resources/js/components/component.js [new file with mode: 0644]
resources/js/components/confirm-dialog.js
resources/js/components/custom-checkbox.js
resources/js/components/details-highlighter.js
resources/js/components/dropdown-search.js
resources/js/components/dropdown.js
resources/js/components/dropzone.js
resources/js/components/editor-toolbox.js
resources/js/components/entity-permissions.js
resources/js/components/entity-search.js
resources/js/components/entity-selector-popup.js
resources/js/components/entity-selector.js
resources/js/components/event-emit-select.js
resources/js/components/expand-toggle.js
resources/js/components/global-search.js [new file with mode: 0644]
resources/js/components/header-mobile-toggle.js
resources/js/components/image-manager.js
resources/js/components/image-picker.js
resources/js/components/index.js
resources/js/components/list-sort-control.js
resources/js/components/markdown-editor.js
resources/js/components/new-user-password.js
resources/js/components/notification.js
resources/js/components/optional-input.js
resources/js/components/page-comments.js
resources/js/components/page-display.js
resources/js/components/page-editor.js
resources/js/components/page-picker.js
resources/js/components/permissions-table.js
resources/js/components/pointer.js
resources/js/components/popup.js
resources/js/components/setting-app-color-picker.js
resources/js/components/setting-color-picker.js
resources/js/components/setting-homepage-control.js [moved from resources/js/components/homepage-control.js with 55% similarity]
resources/js/components/shelf-sort.js
resources/js/components/shortcut-input.js [new file with mode: 0644]
resources/js/components/shortcuts.js [new file with mode: 0644]
resources/js/components/sidebar.js [deleted file]
resources/js/components/sortable-list.js
resources/js/components/submit-on-change.js
resources/js/components/tabs.js
resources/js/components/tag-manager.js
resources/js/components/template-manager.js
resources/js/components/toggle-switch.js
resources/js/components/tri-layout.js
resources/js/components/user-select.js
resources/js/components/webhook-events.js
resources/js/components/wysiwyg-editor.js
resources/js/markdown/actions.js [new file with mode: 0644]
resources/js/markdown/codemirror.js [new file with mode: 0644]
resources/js/markdown/common-events.js [new file with mode: 0644]
resources/js/markdown/display.js [new file with mode: 0644]
resources/js/markdown/editor.js [new file with mode: 0644]
resources/js/markdown/markdown.js [new file with mode: 0644]
resources/js/markdown/settings.js [new file with mode: 0644]
resources/js/markdown/shortcuts.js [new file with mode: 0644]
resources/js/services/components.js [new file with mode: 0644]
resources/js/services/dom.js
resources/js/services/keyboard-navigation.js [new file with mode: 0644]
resources/js/services/text.js [new file with mode: 0644]
resources/js/services/util.js
resources/js/wysiwyg/config.js
resources/js/wysiwyg/plugin-codeeditor.js
resources/js/wysiwyg/plugin-drawio.js
resources/js/wysiwyg/plugins-customhr.js
resources/js/wysiwyg/plugins-imagemanager.js
resources/js/wysiwyg/shortcuts.js
resources/js/wysiwyg/toolbars.js
resources/lang/ar/auth.php
resources/lang/ar/common.php
resources/lang/ar/editor.php
resources/lang/ar/entities.php
resources/lang/ar/preferences.php [new file with mode: 0644]
resources/lang/ar/settings.php
resources/lang/bg/auth.php
resources/lang/bg/common.php
resources/lang/bg/editor.php
resources/lang/bg/entities.php
resources/lang/bg/preferences.php [new file with mode: 0644]
resources/lang/bg/settings.php
resources/lang/bs/auth.php
resources/lang/bs/common.php
resources/lang/bs/editor.php
resources/lang/bs/entities.php
resources/lang/bs/preferences.php [new file with mode: 0644]
resources/lang/bs/settings.php
resources/lang/ca/auth.php
resources/lang/ca/common.php
resources/lang/ca/editor.php
resources/lang/ca/entities.php
resources/lang/ca/preferences.php [new file with mode: 0644]
resources/lang/ca/settings.php
resources/lang/cs/activities.php
resources/lang/cs/auth.php
resources/lang/cs/common.php
resources/lang/cs/editor.php
resources/lang/cs/entities.php
resources/lang/cs/errors.php
resources/lang/cs/passwords.php
resources/lang/cs/preferences.php [new file with mode: 0644]
resources/lang/cs/settings.php
resources/lang/cs/validation.php
resources/lang/cy/auth.php
resources/lang/cy/common.php
resources/lang/cy/editor.php
resources/lang/cy/entities.php
resources/lang/cy/preferences.php [new file with mode: 0644]
resources/lang/cy/settings.php
resources/lang/da/auth.php
resources/lang/da/common.php
resources/lang/da/editor.php
resources/lang/da/entities.php
resources/lang/da/preferences.php [new file with mode: 0644]
resources/lang/da/settings.php
resources/lang/de/auth.php
resources/lang/de/common.php
resources/lang/de/editor.php
resources/lang/de/entities.php
resources/lang/de/preferences.php [new file with mode: 0644]
resources/lang/de/settings.php
resources/lang/de_informal/auth.php
resources/lang/de_informal/common.php
resources/lang/de_informal/editor.php
resources/lang/de_informal/entities.php
resources/lang/de_informal/preferences.php [new file with mode: 0644]
resources/lang/de_informal/settings.php
resources/lang/el/auth.php
resources/lang/el/common.php
resources/lang/el/editor.php
resources/lang/el/entities.php
resources/lang/el/preferences.php [new file with mode: 0644]
resources/lang/el/settings.php
resources/lang/en/auth.php
resources/lang/en/common.php
resources/lang/en/editor.php
resources/lang/en/entities.php
resources/lang/en/preferences.php [new file with mode: 0644]
resources/lang/en/settings.php
resources/lang/es/auth.php
resources/lang/es/common.php
resources/lang/es/editor.php
resources/lang/es/entities.php
resources/lang/es/preferences.php [new file with mode: 0644]
resources/lang/es/settings.php
resources/lang/es_AR/auth.php
resources/lang/es_AR/common.php
resources/lang/es_AR/editor.php
resources/lang/es_AR/entities.php
resources/lang/es_AR/preferences.php [new file with mode: 0644]
resources/lang/es_AR/settings.php
resources/lang/et/auth.php
resources/lang/et/common.php
resources/lang/et/editor.php
resources/lang/et/entities.php
resources/lang/et/preferences.php [new file with mode: 0644]
resources/lang/et/settings.php
resources/lang/eu/auth.php
resources/lang/eu/common.php
resources/lang/eu/editor.php
resources/lang/eu/entities.php
resources/lang/eu/preferences.php [new file with mode: 0644]
resources/lang/eu/settings.php
resources/lang/fa/activities.php
resources/lang/fa/auth.php
resources/lang/fa/common.php
resources/lang/fa/editor.php
resources/lang/fa/entities.php
resources/lang/fa/preferences.php [new file with mode: 0644]
resources/lang/fa/settings.php
resources/lang/fr/auth.php
resources/lang/fr/common.php
resources/lang/fr/editor.php
resources/lang/fr/entities.php
resources/lang/fr/preferences.php [new file with mode: 0644]
resources/lang/fr/settings.php
resources/lang/he/auth.php
resources/lang/he/common.php
resources/lang/he/editor.php
resources/lang/he/entities.php
resources/lang/he/preferences.php [new file with mode: 0644]
resources/lang/he/settings.php
resources/lang/hr/auth.php
resources/lang/hr/common.php
resources/lang/hr/editor.php
resources/lang/hr/entities.php
resources/lang/hr/preferences.php [new file with mode: 0644]
resources/lang/hr/settings.php
resources/lang/hu/auth.php
resources/lang/hu/common.php
resources/lang/hu/editor.php
resources/lang/hu/entities.php
resources/lang/hu/preferences.php [new file with mode: 0644]
resources/lang/hu/settings.php
resources/lang/id/auth.php
resources/lang/id/common.php
resources/lang/id/editor.php
resources/lang/id/entities.php
resources/lang/id/preferences.php [new file with mode: 0644]
resources/lang/id/settings.php
resources/lang/it/auth.php
resources/lang/it/common.php
resources/lang/it/editor.php
resources/lang/it/entities.php
resources/lang/it/preferences.php [new file with mode: 0644]
resources/lang/it/settings.php
resources/lang/ja/auth.php
resources/lang/ja/common.php
resources/lang/ja/editor.php
resources/lang/ja/entities.php
resources/lang/ja/preferences.php [new file with mode: 0644]
resources/lang/ja/settings.php
resources/lang/ka/activities.php [new file with mode: 0644]
resources/lang/ka/auth.php [new file with mode: 0644]
resources/lang/ka/common.php [new file with mode: 0644]
resources/lang/ka/components.php [new file with mode: 0644]
resources/lang/ka/editor.php [new file with mode: 0644]
resources/lang/ka/entities.php [new file with mode: 0644]
resources/lang/ka/errors.php [new file with mode: 0644]
resources/lang/ka/pagination.php [new file with mode: 0644]
resources/lang/ka/passwords.php [new file with mode: 0644]
resources/lang/ka/preferences.php [new file with mode: 0644]
resources/lang/ka/settings.php [new file with mode: 0644]
resources/lang/ka/validation.php [new file with mode: 0644]
resources/lang/ko/auth.php
resources/lang/ko/common.php
resources/lang/ko/editor.php
resources/lang/ko/entities.php
resources/lang/ko/preferences.php [new file with mode: 0644]
resources/lang/ko/settings.php
resources/lang/lt/auth.php
resources/lang/lt/common.php
resources/lang/lt/editor.php
resources/lang/lt/entities.php
resources/lang/lt/preferences.php [new file with mode: 0644]
resources/lang/lt/settings.php
resources/lang/lv/auth.php
resources/lang/lv/common.php
resources/lang/lv/editor.php
resources/lang/lv/entities.php
resources/lang/lv/preferences.php [new file with mode: 0644]
resources/lang/lv/settings.php
resources/lang/nb/auth.php
resources/lang/nb/common.php
resources/lang/nb/editor.php
resources/lang/nb/entities.php
resources/lang/nb/preferences.php [new file with mode: 0644]
resources/lang/nb/settings.php
resources/lang/nl/auth.php
resources/lang/nl/common.php
resources/lang/nl/editor.php
resources/lang/nl/entities.php
resources/lang/nl/preferences.php [new file with mode: 0644]
resources/lang/nl/settings.php
resources/lang/pl/auth.php
resources/lang/pl/common.php
resources/lang/pl/editor.php
resources/lang/pl/entities.php
resources/lang/pl/preferences.php [new file with mode: 0644]
resources/lang/pl/settings.php
resources/lang/pt/auth.php
resources/lang/pt/common.php
resources/lang/pt/editor.php
resources/lang/pt/entities.php
resources/lang/pt/preferences.php [new file with mode: 0644]
resources/lang/pt/settings.php
resources/lang/pt_BR/auth.php
resources/lang/pt_BR/common.php
resources/lang/pt_BR/editor.php
resources/lang/pt_BR/entities.php
resources/lang/pt_BR/preferences.php [new file with mode: 0644]
resources/lang/pt_BR/settings.php
resources/lang/ro/auth.php
resources/lang/ro/common.php
resources/lang/ro/editor.php
resources/lang/ro/entities.php
resources/lang/ro/preferences.php [new file with mode: 0644]
resources/lang/ro/settings.php
resources/lang/ru/auth.php
resources/lang/ru/common.php
resources/lang/ru/editor.php
resources/lang/ru/entities.php
resources/lang/ru/preferences.php [new file with mode: 0644]
resources/lang/ru/settings.php
resources/lang/sk/auth.php
resources/lang/sk/common.php
resources/lang/sk/editor.php
resources/lang/sk/entities.php
resources/lang/sk/preferences.php [new file with mode: 0644]
resources/lang/sk/settings.php
resources/lang/sl/auth.php
resources/lang/sl/common.php
resources/lang/sl/editor.php
resources/lang/sl/entities.php
resources/lang/sl/preferences.php [new file with mode: 0644]
resources/lang/sl/settings.php
resources/lang/sv/auth.php
resources/lang/sv/common.php
resources/lang/sv/editor.php
resources/lang/sv/entities.php
resources/lang/sv/errors.php
resources/lang/sv/preferences.php [new file with mode: 0644]
resources/lang/sv/settings.php
resources/lang/tr/auth.php
resources/lang/tr/common.php
resources/lang/tr/editor.php
resources/lang/tr/entities.php
resources/lang/tr/preferences.php [new file with mode: 0644]
resources/lang/tr/settings.php
resources/lang/uk/auth.php
resources/lang/uk/common.php
resources/lang/uk/editor.php
resources/lang/uk/entities.php
resources/lang/uk/preferences.php [new file with mode: 0644]
resources/lang/uk/settings.php
resources/lang/uz/auth.php
resources/lang/uz/common.php
resources/lang/uz/editor.php
resources/lang/uz/entities.php
resources/lang/uz/preferences.php [new file with mode: 0644]
resources/lang/uz/settings.php
resources/lang/vi/auth.php
resources/lang/vi/common.php
resources/lang/vi/editor.php
resources/lang/vi/entities.php
resources/lang/vi/preferences.php [new file with mode: 0644]
resources/lang/vi/settings.php
resources/lang/zh_CN/auth.php
resources/lang/zh_CN/common.php
resources/lang/zh_CN/editor.php
resources/lang/zh_CN/entities.php
resources/lang/zh_CN/preferences.php [new file with mode: 0644]
resources/lang/zh_CN/settings.php
resources/lang/zh_TW/auth.php
resources/lang/zh_TW/common.php
resources/lang/zh_TW/editor.php
resources/lang/zh_TW/entities.php
resources/lang/zh_TW/errors.php
resources/lang/zh_TW/preferences.php [new file with mode: 0644]
resources/lang/zh_TW/settings.php
resources/lang/zh_TW/validation.php
resources/sass/_animations.scss
resources/sass/_blocks.scss
resources/sass/_components.scss
resources/sass/_forms.scss
resources/sass/_header.scss
resources/sass/_layout.scss
resources/sass/_lists.scss
resources/sass/_opacity.scss [new file with mode: 0644]
resources/sass/_pages.scss
resources/sass/_tables.scss
resources/sass/_text.scss
resources/sass/_variables.scss
resources/sass/styles.scss
resources/views/api-docs/index.blade.php
resources/views/api-docs/parts/endpoint.blade.php
resources/views/attachments/manager.blade.php
resources/views/auth/login.blade.php
resources/views/auth/parts/login-message.blade.php [new file with mode: 0644]
resources/views/auth/parts/register-message.blade.php [new file with mode: 0644]
resources/views/auth/register-confirm-accept.blade.php [new file with mode: 0644]
resources/views/auth/register.blade.php
resources/views/books/index.blade.php
resources/views/books/parts/form.blade.php
resources/views/books/parts/list.blade.php
resources/views/books/parts/sort-box.blade.php
resources/views/books/show.blade.php
resources/views/books/sort.blade.php
resources/views/chapters/parts/form.blade.php
resources/views/chapters/show.blade.php
resources/views/common/dark-mode-toggle.blade.php
resources/views/common/header.blade.php
resources/views/common/notifications.blade.php
resources/views/common/sort.blade.php [moved from resources/views/entities/sort.blade.php with 52% similarity]
resources/views/common/status-indicator.blade.php [new file with mode: 0644]
resources/views/entities/export-menu.blade.php
resources/views/entities/favourite-action.blade.php
resources/views/entities/search-form.blade.php
resources/views/entities/sibling-navigation.blade.php
resources/views/entities/view-toggle.blade.php
resources/views/form/custom-checkbox.blade.php
resources/views/form/entity-permissions-row.blade.php
resources/views/form/entity-permissions.blade.php
resources/views/form/image-picker.blade.php
resources/views/form/toggle-switch.blade.php
resources/views/home/parts/expand-toggle.blade.php
resources/views/home/shelves.blade.php
resources/views/home/specific-page.blade.php
resources/views/layouts/base.blade.php
resources/views/pages/parts/code-editor.blade.php
resources/views/pages/parts/editor-toolbox.blade.php
resources/views/pages/parts/markdown-editor.blade.php
resources/views/pages/parts/revisions-index-row.blade.php [moved from resources/views/pages/parts/revision-table-row.blade.php with 62% similarity]
resources/views/pages/parts/template-manager.blade.php
resources/views/pages/revisions.blade.php
resources/views/pages/show.blade.php
resources/views/search/parts/entity-selector-list.blade.php [moved from resources/views/search/parts/entity-ajax-list.blade.php with 100% similarity]
resources/views/search/parts/entity-suggestion-list.blade.php [new file with mode: 0644]
resources/views/search/parts/term-list.blade.php
resources/views/settings/audit.blade.php
resources/views/settings/customization.blade.php
resources/views/settings/parts/page-picker.blade.php
resources/views/settings/parts/setting-entity-color-picker.blade.php
resources/views/settings/parts/table-user.blade.php
resources/views/settings/recycle-bin/index.blade.php
resources/views/settings/recycle-bin/parts/recycle-bin-list-item.blade.php [new file with mode: 0644]
resources/views/settings/roles/index.blade.php
resources/views/settings/roles/parts/asset-permissions-row.blade.php [new file with mode: 0644]
resources/views/settings/roles/parts/form.blade.php
resources/views/settings/roles/parts/related-asset-permissions-row.blade.php [new file with mode: 0644]
resources/views/settings/roles/parts/roles-list-item.blade.php [new file with mode: 0644]
resources/views/settings/webhooks/index.blade.php
resources/views/settings/webhooks/parts/webhooks-list-item.blade.php [new file with mode: 0644]
resources/views/shelves/index.blade.php
resources/views/shelves/parts/form.blade.php
resources/views/shelves/parts/list.blade.php
resources/views/shelves/show.blade.php
resources/views/tags/index.blade.php
resources/views/tags/parts/table-row.blade.php [deleted file]
resources/views/tags/parts/tags-list-item.blade.php [new file with mode: 0644]
resources/views/users/api-tokens/parts/list.blade.php
resources/views/users/index.blade.php
resources/views/users/parts/form.blade.php
resources/views/users/parts/users-list-item.blade.php [new file with mode: 0644]
resources/views/users/preferences/parts/shortcut-control.blade.php [new file with mode: 0644]
resources/views/users/preferences/shortcuts.blade.php [new file with mode: 0644]
routes/web.php
tests/Actions/AuditLogTest.php
tests/Auth/OidcTest.php
tests/Auth/RegistrationTest.php
tests/Entity/BookTest.php
tests/Entity/EntitySearchTest.php
tests/Entity/PageRevisionTest.php
tests/Entity/TagTest.php
tests/Permissions/EntityPermissionsTest.php
tests/Settings/RecycleBinTest.php
tests/TestCase.php
tests/ThemeTest.php
tests/Uploads/ImageTest.php
tests/User/UserManagementTest.php
tests/User/UserPreferencesTest.php

index 064fc06725e07588537bec3a6ee11b701b0a621c..e21d3c6e9ea7c4b523c755f7148cfd9ad6a11d70 100644 (file)
@@ -291,3 +291,10 @@ Fabrice Boyer (FabriceBoyer) :: French
 mikael (bitcanon) :: Swedish
 Matthias Mai (schnapsidee) :: German
 Ufuk Ayyıldız (ufukayyildiz) :: Turkish
+Jan Mitrof (jan.kachlik) :: Czech
+edwardsmirnov :: Russian
+Mr_OSS117 :: French
+shotu :: French
+Cesar_Lopez_Aguillon :: Spanish
+bdewoop :: German
+dina davoudi (dina.davoudi) :: Persian
index 191399d78b8a54f8565c06619a3767dffd58f496..fd56a53ef9ac645dd81a0def99862914b9cae77d 100644 (file)
@@ -18,10 +18,10 @@ jobs:
     - name: Get Composer Cache Directory
       id: composer-cache
       run: |
-        echo "::set-output name=dir::$(composer config cache-files-dir)"
+        echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
 
     - name: Cache composer packages
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       with:
         path: ${{ steps.composer-cache.outputs.dir }}
         key: ${{ runner.os }}-composer-8.1
index e9b66a0a65d09001372084761589ab66be8ba851..82f5ae258b3c82491a0c0feb2a1061a50330102f 100644 (file)
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-22.04
     strategy:
       matrix:
-        php: ['7.4', '8.0', '8.1']
+        php: ['7.4', '8.0', '8.1', '8.2']
     steps:
       - uses: actions/checkout@v1
 
@@ -21,10 +21,10 @@ jobs:
       - name: Get Composer Cache Directory
         id: composer-cache
         run: |
-          echo "::set-output name=dir::$(composer config cache-files-dir)"
+          echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
 
       - name: Cache composer packages
-        uses: actions/cache@v2
+        uses: actions/cache@v3
         with:
           path: ${{ steps.composer-cache.outputs.dir }}
           key: ${{ runner.os }}-composer-${{ matrix.php }}
index 917038f599dbf97896e2abedcbdacaba8dff75d7..903b676cd9607652bddb115cf6f807a3138093f5 100644 (file)
@@ -8,7 +8,7 @@ jobs:
     runs-on: ubuntu-22.04
     strategy:
       matrix:
-        php: ['7.4', '8.0', '8.1']
+        php: ['7.4', '8.0', '8.1', '8.2']
     steps:
     - uses: actions/checkout@v1
 
@@ -21,10 +21,10 @@ jobs:
     - name: Get Composer Cache Directory
       id: composer-cache
       run: |
-        echo "::set-output name=dir::$(composer config cache-files-dir)"
+        echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
 
     - name: Cache composer packages
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       with:
         path: ${{ steps.composer-cache.outputs.dir }}
         key: ${{ runner.os }}-composer-${{ matrix.php }}
diff --git a/app/Actions/Queries/WebhooksAllPaginatedAndSorted.php b/app/Actions/Queries/WebhooksAllPaginatedAndSorted.php
new file mode 100644 (file)
index 0000000..4958b60
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace BookStack\Actions\Queries;
+
+use BookStack\Actions\Webhook;
+use BookStack\Util\SimpleListOptions;
+use Illuminate\Pagination\LengthAwarePaginator;
+
+/**
+ * Get all the webhooks in the system in a paginated format.
+ */
+class WebhooksAllPaginatedAndSorted
+{
+    public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
+    {
+        $query = Webhook::query()->select(['*'])
+            ->withCount(['trackedEvents'])
+            ->orderBy($listOptions->getSort(), $listOptions->getOrder());
+
+        if ($listOptions->getSearch()) {
+            $term = '%' . $listOptions->getSearch() . '%';
+            $query->where(function ($query) use ($term) {
+                $query->where('name', 'like', $term)
+                    ->orWhere('endpoint', 'like', $term);
+            });
+        }
+
+        return $query->paginate($count);
+    }
+}
index 2618ed2e902329512defd05545e355bc22c9c286..cece30de003b3a48b0d72f2d0e30b28e7dfa828e 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Actions;
 
 use BookStack\Auth\Permissions\PermissionApplicator;
 use BookStack\Entities\Models\Entity;
+use BookStack\Util\SimpleListOptions;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Support\Collection;
 use Illuminate\Support\Facades\DB;
@@ -20,8 +21,14 @@ class TagRepo
     /**
      * Start a query against all tags in the system.
      */
-    public function queryWithTotals(string $searchTerm, string $nameFilter): Builder
+    public function queryWithTotals(SimpleListOptions $listOptions, string $nameFilter): Builder
     {
+        $searchTerm = $listOptions->getSearch();
+        $sort = $listOptions->getSort();
+        if ($sort === 'name' && $nameFilter) {
+            $sort = 'value';
+        }
+
         $query = Tag::query()
             ->select([
                 'name',
@@ -32,7 +39,7 @@ class TagRepo
                 DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'),
                 DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
             ])
-            ->orderBy($nameFilter ? 'value' : 'name');
+            ->orderBy($sort, $listOptions->getOrder());
 
         if ($nameFilter) {
             $query->where('name', '=', $nameFilter);
index 39752e6d4618dc299f848d0b378db71e1e86fed8..44117bad9759e5108502485003c66fd3a599e96f 100644 (file)
@@ -4,21 +4,29 @@ namespace BookStack\Api;
 
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Model;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 
 class ListingResponseBuilder
 {
-    protected $query;
-    protected $request;
-    protected $fields;
+    protected Builder $query;
+    protected Request $request;
+
+    /**
+     * @var string[]
+     */
+    protected array $fields;
 
     /**
      * @var array<callable>
      */
-    protected $resultModifiers = [];
+    protected array $resultModifiers = [];
 
-    protected $filterOperators = [
+    /**
+     * @var array<string, string>
+     */
+    protected array $filterOperators = [
         'eq'   => '=',
         'ne'   => '!=',
         'gt'   => '>',
@@ -62,9 +70,9 @@ class ListingResponseBuilder
     /**
      * Add a callback to modify each element of the results.
      *
-     * @param (callable(Model)) $modifier
+     * @param (callable(Model): void) $modifier
      */
-    public function modifyResults($modifier): void
+    public function modifyResults(callable $modifier): void
     {
         $this->resultModifiers[] = $modifier;
     }
index 012a6cbf9c10c72a7a27a26883f8a23173e58f72..f003ec93ca1c1df3f2c80ef9a3a940e2d90697fe 100644 (file)
@@ -67,11 +67,10 @@ class OidcJwtSigningKey
             throw new OidcInvalidKeyException("Only RS256 keys are currently supported. Found key using {$alg}");
         }
 
-        if (empty($jwk['use'])) {
-            throw new OidcInvalidKeyException('A "use" parameter on the provided key is expected');
-        }
-
-        if ($jwk['use'] !== 'sig') {
+        // 'use' is optional for a JWK but we assume 'sig' where no value exists since that's what
+        // the OIDC discovery spec infers since 'sig' MUST be set if encryption keys come into play.
+        $use = $jwk['use'] ?? 'sig';
+        if ($use !== 'sig') {
             throw new OidcInvalidKeyException("Only signature keys are currently supported. Found key for use {$jwk['use']}");
         }
 
index d15705782721f0963cfc670060a7d7476b904fe0..30ec8cac8b8e867438a0630b9097f2ab6eeaf53e 100644 (file)
@@ -15,40 +15,17 @@ use Psr\Http\Client\ClientInterface;
  */
 class OidcProviderSettings
 {
-    /**
-     * @var string
-     */
-    public $issuer;
-
-    /**
-     * @var string
-     */
-    public $clientId;
-
-    /**
-     * @var string
-     */
-    public $clientSecret;
-
-    /**
-     * @var string
-     */
-    public $redirectUri;
-
-    /**
-     * @var string
-     */
-    public $authorizationEndpoint;
-
-    /**
-     * @var string
-     */
-    public $tokenEndpoint;
+    public string $issuer;
+    public string $clientId;
+    public string $clientSecret;
+    public ?string $redirectUri;
+    public ?string $authorizationEndpoint;
+    public ?string $tokenEndpoint;
 
     /**
      * @var string[]|array[]
      */
-    public $keys = [];
+    public ?array $keys = [];
 
     public function __construct(array $settings)
     {
@@ -164,9 +141,10 @@ class OidcProviderSettings
     protected function filterKeys(array $keys): array
     {
         return array_filter($keys, function (array $key) {
-            $alg = $key['alg'] ?? null;
+            $alg = $key['alg'] ?? 'RS256';
+            $use = $key['use'] ?? 'sig';
 
-            return $key['kty'] === 'RSA' && $key['use'] === 'sig' && (is_null($alg) || $alg === 'RS256');
+            return $key['kty'] === 'RSA' && $use === 'sig' && $alg === 'RS256';
         });
     }
 
index c9c3cc51180277c754625eb0617b9663e23176d2..a9323d4233109e544bd94ec044fa4e57530a2cd9 100644 (file)
@@ -52,7 +52,6 @@ class OidcService
     {
         $settings = $this->getProviderSettings();
         $provider = $this->getProvider($settings);
-
         return [
             'url'   => $provider->getAuthorizationUrl(),
             'state' => $provider->getState(),
index 129b4a04d149df87e1f0e74cc34ce09be4a26f0d..114cff6191a35edc6e20494bd3305664c8a6eddd 100644 (file)
@@ -22,7 +22,7 @@ class JointPermissionBuilder
     /**
      * @var array<string, array<int, SimpleEntityData>>
      */
-    protected $entityCache;
+    protected array $entityCache;
 
     /**
      * Re-generate all entity permission from scratch.
@@ -230,7 +230,7 @@ class JointPermissionBuilder
     /**
      * Create & Save entity jointPermissions for many entities and roles.
      *
-     * @param Entity[] $entities
+     * @param Entity[] $originalEntities
      * @param Role[]   $roles
      */
     protected function createManyJointPermissions(array $originalEntities, array $roles)
diff --git a/app/Auth/Queries/RolesAllPaginatedAndSorted.php b/app/Auth/Queries/RolesAllPaginatedAndSorted.php
new file mode 100644 (file)
index 0000000..9ee4f6c
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+
+namespace BookStack\Auth\Queries;
+
+use BookStack\Auth\Role;
+use BookStack\Util\SimpleListOptions;
+use Illuminate\Pagination\LengthAwarePaginator;
+
+/**
+ * Get all the roles in the system in a paginated format.
+ */
+class RolesAllPaginatedAndSorted
+{
+    public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
+    {
+        $sort = $listOptions->getSort();
+        if ($sort === 'created_at') {
+            $sort = 'users.created_at';
+        }
+
+        $query = Role::query()->select(['*'])
+            ->withCount(['users', 'permissions'])
+            ->orderBy($sort, $listOptions->getOrder());
+
+        if ($listOptions->getSearch()) {
+            $term = '%' . $listOptions->getSearch() . '%';
+            $query->where(function ($query) use ($term) {
+                $query->where('display_name', 'like', $term)
+                    ->orWhere('description', 'like', $term);
+            });
+        }
+
+        return $query->paginate($count);
+    }
+}
similarity index 63%
rename from app/Auth/Queries/AllUsersPaginatedAndSorted.php
rename to app/Auth/Queries/UsersAllPaginatedAndSorted.php
index 7b849eaf4c1af9f84036cf8eeb1aa24f89716836..29b6a89697bc880daf0788f66158fe8431b861db 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Auth\Queries;
 
 use BookStack\Auth\User;
+use BookStack\Util\SimpleListOptions;
 use Illuminate\Pagination\LengthAwarePaginator;
 
 /**
@@ -11,23 +12,23 @@ use Illuminate\Pagination\LengthAwarePaginator;
  * user is assumed to be trusted. (Admin users).
  * Email search can be abused to extract email addresses.
  */
-class AllUsersPaginatedAndSorted
+class UsersAllPaginatedAndSorted
 {
-    /**
-     * @param array{sort: string, order: string, search: string} $sortData
-     */
-    public function run(int $count, array $sortData): LengthAwarePaginator
+    public function run(int $count, SimpleListOptions $listOptions): LengthAwarePaginator
     {
-        $sort = $sortData['sort'];
+        $sort = $listOptions->getSort();
+        if ($sort === 'created_at') {
+            $sort = 'users.created_at';
+        }
 
         $query = User::query()->select(['*'])
             ->scopes(['withLastActivityAt'])
             ->with(['roles', 'avatar'])
             ->withCount('mfaValues')
-            ->orderBy($sort, $sortData['order']);
+            ->orderBy($sort, $listOptions->getOrder());
 
-        if ($sortData['search']) {
-            $term = '%' . $sortData['search'] . '%';
+        if ($listOptions->getSearch()) {
+            $term = '%' . $listOptions->getSearch() . '%';
             $query->where(function ($query) use ($term) {
                 $query->where('name', 'like', $term)
                     ->orWhere('email', 'like', $term);
index 17a4edcc020b0a84c5e1da880d364747069e2abc..b293d1af256aabd1d01574c4732ad729c698401a 100644 (file)
@@ -110,14 +110,6 @@ class Role extends Model implements Loggable
         return static::query()->where('system_name', '=', $systemName)->first();
     }
 
-    /**
-     * Get all visible roles.
-     */
-    public static function visible(): Collection
-    {
-        return static::query()->where('hidden', '=', false)->orderBy('name')->get();
-    }
-
     /**
      * {@inheritdoc}
      */
index c589fd9647bcbca5a3e7228157d02626fb05aee3..78bcb978ebc997f3346d0e8dfb8a319cbfe641a4 100644 (file)
@@ -158,6 +158,9 @@ class UserRepo
         // Delete user profile images
         $this->userAvatar->destroyAllForUser($user);
 
+        // Delete related activities
+        setting()->deleteUserSettings($user->id);
+
         if (!empty($newOwnerId)) {
             $newOwner = User::query()->find($newOwnerId);
             if (!is_null($newOwner)) {
index 90726c90407bf1bea09b14d6f9e5ec4c11a7054c..ed8bcf9001d3099ff92d1eb9eb981814bba10a79 100644 (file)
@@ -75,7 +75,7 @@ return [
     'locale' => env('APP_LANG', 'en'),
 
     // Locales available
-    'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'de_informal', 'el', 'es', 'es_AR', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl',  'ro', 'ru', 'tr', 'uk', 'uz', 'vi', 'zh_CN', 'zh_TW'],
+    'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'cy', 'da', 'de', 'de_informal', 'el', 'es', 'es_AR', 'et', 'eu', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ka', 'ko', 'lt', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl',  'ro', 'ru', 'tr', 'uk', 'uz', 'vi', 'zh_CN', 'zh_TW'],
 
     //  Application Fallback Locale
     'fallback_locale' => 'en',
index cb6082c528619ba420d8e1d657704d9350038b0d..5e1e4348ab4e7e351d6067af59179ff83bf8ff42 100644 (file)
@@ -26,6 +26,8 @@ return [
 
     // User-level default settings
     'user' => [
+        'ui-shortcuts'          => '{}',
+        'ui-shortcuts-enabled'  => false,
         'dark-mode-enabled'     => env('APP_DEFAULT_DARK_MODE', false),
         'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
         'bookshelf_view_type'   => env('APP_VIEWS_BOOKSHELF', 'grid'),
index 7a60b3ada05a2c78f5e4c95f7886b1d31ed42619..a224071ccc144611ceb1befc104d9c421f65210a 100644 (file)
@@ -88,8 +88,6 @@ class Page extends BookChild
 
     /**
      * Get the current revision for the page if existing.
-     *
-     * @return PageRevision|null
      */
     public function currentRevision(): HasOne
     {
index da939e10281792ffbd78a9e4fa030470f4e2b0f8..815c0bb2ec503522ec9962f959e60e5fad079292 100644 (file)
@@ -87,14 +87,14 @@ class BaseRepo
     {
         if ($coverImage) {
             $imageType = $entity->coverImageTypeKey();
-            $this->imageRepo->destroyImage($entity->cover);
+            $this->imageRepo->destroyImage($entity->cover()->first());
             $image = $this->imageRepo->saveNew($coverImage, $imageType, $entity->id, 512, 512, true);
             $entity->cover()->associate($image);
             $entity->save();
         }
 
         if ($removeImage) {
-            $this->imageRepo->destroyImage($entity->cover);
+            $this->imageRepo->destroyImage($entity->cover()->first());
             $entity->image_id = 0;
             $entity->save();
         }
index 0ad424de2d0797aca7e5d10db5afe0976d7c58a0..f45bdfcc1b98fabcdb2d4dc774c30752a7582ef1 100644 (file)
@@ -181,7 +181,7 @@ class BookContents
             $model->changeBook($newBook->id);
         }
 
-        if ($chapterChanged) {
+        if ($model instanceof Page && $chapterChanged) {
             $model->chapter_id = $newChapter->id ?? 0;
         }
 
@@ -235,7 +235,7 @@ class BookContents
             }
 
             $hasPageEditPermission = userCan('page-update', $model);
-            $newParentInRightLocation = ($newParent instanceof Book || $newParent->book_id === $newBook->id);
+            $newParentInRightLocation = ($newParent instanceof Book || ($newParent instanceof Chapter && $newParent->book_id === $newBook->id));
             $newParentPermission = ($newParent instanceof Chapter) ? 'chapter-update' : 'book-update';
             $hasNewParentPermission = userCan($newParentPermission, $newParent);
 
index 52a8f4cf0f2760f45d546803a0e8215d61e6de0f..5594c33a605bcfc1069cdd5de60d31010d6d379c 100644 (file)
@@ -7,6 +7,7 @@ use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\HasCoverImage;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Repos\ChapterRepo;
@@ -109,9 +110,11 @@ class Cloner
         $inputData['tags'] = $this->entityTagsToInputArray($entity);
 
         // Add a cover to the data if existing on the original entity
-        if ($entity->cover instanceof Image) {
-            $uploadedFile = $this->imageToUploadedFile($entity->cover);
-            $inputData['image'] = $uploadedFile;
+        if ($entity instanceof HasCoverImage) {
+            $cover = $entity->cover()->first();
+            if ($cover) {
+                $inputData['image'] = $this->imageToUploadedFile($cover);
+            }
         }
 
         return $inputData;
index ec3f3697534fcabe6e74ddf4759e610160bcca75..da8009d78cb18b9665f84031b69da4e4e5d1fa12 100644 (file)
@@ -3,6 +3,8 @@
 namespace BookStack\Http\Controllers;
 
 use BookStack\Actions\Activity;
+use BookStack\Actions\ActivityType;
+use BookStack\Util\SimpleListOptions;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\DB;
 
@@ -13,10 +15,15 @@ class AuditLogController extends Controller
         $this->checkPermission('settings-manage');
         $this->checkPermission('users-manage');
 
-        $listDetails = [
-            'order'     => $request->get('order', 'desc'),
+        $sort = $request->get('sort', 'activity_date');
+        $order = $request->get('order', 'desc');
+        $listOptions = (new SimpleListOptions('', $sort, $order))->withSortOptions([
+            'created_at' => trans('settings.audit_table_date'),
+            'type' => trans('settings.audit_table_event'),
+        ]);
+
+        $filters = [
             'event'     => $request->get('event', ''),
-            'sort'      => $request->get('sort', 'created_at'),
             'date_from' => $request->get('date_from', ''),
             'date_to'   => $request->get('date_to', ''),
             'user'      => $request->get('user', ''),
@@ -25,39 +32,38 @@ class AuditLogController extends Controller
 
         $query = Activity::query()
             ->with([
-                'entity' => function ($query) {
-                    $query->withTrashed();
-                },
+                'entity' => fn ($query) => $query->withTrashed(),
                 'user',
             ])
-            ->orderBy($listDetails['sort'], $listDetails['order']);
+            ->orderBy($listOptions->getSort(), $listOptions->getOrder());
 
-        if ($listDetails['event']) {
-            $query->where('type', '=', $listDetails['event']);
+        if ($filters['event']) {
+            $query->where('type', '=', $filters['event']);
         }
-        if ($listDetails['user']) {
-            $query->where('user_id', '=', $listDetails['user']);
+        if ($filters['user']) {
+            $query->where('user_id', '=', $filters['user']);
         }
 
-        if ($listDetails['date_from']) {
-            $query->where('created_at', '>=', $listDetails['date_from']);
+        if ($filters['date_from']) {
+            $query->where('created_at', '>=', $filters['date_from']);
         }
-        if ($listDetails['date_to']) {
-            $query->where('created_at', '<=', $listDetails['date_to']);
+        if ($filters['date_to']) {
+            $query->where('created_at', '<=', $filters['date_to']);
         }
-        if ($listDetails['ip']) {
-            $query->where('ip', 'like', $listDetails['ip'] . '%');
+        if ($filters['ip']) {
+            $query->where('ip', 'like', $filters['ip'] . '%');
         }
 
         $activities = $query->paginate(100);
-        $activities->appends($listDetails);
+        $activities->appends($request->all());
 
-        $types = DB::table('activities')->select('type')->distinct()->pluck('type');
+        $types = ActivityType::all();
         $this->setPageTitle(trans('settings.audit'));
 
         return view('settings.audit', [
             'activities'    => $activities,
-            'listDetails'   => $listDetails,
+            'filters'       => $filters,
+            'listOptions'   => $listOptions,
             'activityTypes' => $types,
         ]);
     }
index ea633ff3ab8cb9bc039cd41ea84dc16450c58a04..b282d0601f31eaab351f971be08d0f0ad6540150 100644 (file)
@@ -51,14 +51,28 @@ class ConfirmEmailController extends Controller
         return view('auth.user-unconfirmed', ['user' => $user]);
     }
 
+    /**
+     * Show the form for a user to provide their positive confirmation of their email.
+     */
+    public function showAcceptForm(string $token)
+    {
+        return view('auth.register-confirm-accept', ['token' => $token]);
+    }
+
     /**
      * Confirms an email via a token and logs the user into the system.
      *
      * @throws ConfirmationEmailException
      * @throws Exception
      */
-    public function confirm(string $token)
+    public function confirm(Request $request)
     {
+        $validated = $this->validate($request, [
+            'token' => ['required', 'string']
+        ]);
+
+        $token = $validated['token'];
+
         try {
             $userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
         } catch (UserTokenNotFoundException $exception) {
index b323ae496e42586a1702c9dafea528a0810837e5..14c3af1cc5cf8f0a8c92d70de7c24d8a1d608c96 100644 (file)
@@ -15,6 +15,7 @@ use BookStack\Exceptions\ImageUploadException;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Facades\Activity;
 use BookStack\References\ReferenceFetcher;
+use BookStack\Util\SimpleListOptions;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
 use Throwable;
@@ -35,13 +36,16 @@ class BookController extends Controller
     /**
      * Display a listing of the book.
      */
-    public function index()
+    public function index(Request $request)
     {
         $view = setting()->getForCurrentUser('books_view_type');
-        $sort = setting()->getForCurrentUser('books_sort', 'name');
-        $order = setting()->getForCurrentUser('books_sort_order', 'asc');
+        $listOptions = SimpleListOptions::fromRequest($request, 'books')->withSortOptions([
+            'name' => trans('common.sort_name'),
+            'created_at' => trans('common.sort_created_at'),
+            'updated_at' => trans('common.sort_updated_at'),
+        ]);
 
-        $books = $this->bookRepo->getAllPaginated(18, $sort, $order);
+        $books = $this->bookRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
         $recents = $this->isSignedIn() ? $this->bookRepo->getRecentlyViewed(4) : false;
         $popular = $this->bookRepo->getPopular(4);
         $new = $this->bookRepo->getRecentlyCreated(4);
@@ -56,8 +60,7 @@ class BookController extends Controller
             'popular' => $popular,
             'new'     => $new,
             'view'    => $view,
-            'sort'    => $sort,
-            'order'   => $order,
+            'listOptions' => $listOptions,
         ]);
     }
 
index 3c63be6318b92b28e1c5ed3e6584538d0bfad6df..537ea915b76130deb086c934b0c22516e070dd07 100644 (file)
@@ -10,6 +10,7 @@ use BookStack\Entities\Tools\ShelfContext;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\References\ReferenceFetcher;
+use BookStack\Util\SimpleListOptions;
 use Exception;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
@@ -30,18 +31,16 @@ class BookshelfController extends Controller
     /**
      * Display a listing of the book.
      */
-    public function index()
+    public function index(Request $request)
     {
         $view = setting()->getForCurrentUser('bookshelves_view_type');
-        $sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
-        $order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
-        $sortOptions = [
+        $listOptions = SimpleListOptions::fromRequest($request, 'bookshelves')->withSortOptions([
             'name'       => trans('common.sort_name'),
             'created_at' => trans('common.sort_created_at'),
             'updated_at' => trans('common.sort_updated_at'),
-        ];
+        ]);
 
-        $shelves = $this->shelfRepo->getAllPaginated(18, $sort, $order);
+        $shelves = $this->shelfRepo->getAllPaginated(18, $listOptions->getSort(), $listOptions->getOrder());
         $recents = $this->isSignedIn() ? $this->shelfRepo->getRecentlyViewed(4) : false;
         $popular = $this->shelfRepo->getPopular(4);
         $new = $this->shelfRepo->getRecentlyCreated(4);
@@ -55,9 +54,7 @@ class BookshelfController extends Controller
             'popular'     => $popular,
             'new'         => $new,
             'view'        => $view,
-            'sort'        => $sort,
-            'order'       => $order,
-            'sortOptions' => $sortOptions,
+            'listOptions' => $listOptions,
         ]);
     }
 
@@ -100,16 +97,21 @@ class BookshelfController extends Controller
      *
      * @throws NotFoundException
      */
-    public function show(ActivityQueries $activities, string $slug)
+    public function show(Request $request, ActivityQueries $activities, string $slug)
     {
         $shelf = $this->shelfRepo->getBySlug($slug);
         $this->checkOwnablePermission('bookshelf-view', $shelf);
 
-        $sort = setting()->getForCurrentUser('shelf_books_sort', 'default');
-        $order = setting()->getForCurrentUser('shelf_books_sort_order', 'asc');
+        $listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
+            'default' => trans('common.sort_default'),
+            'name' => trans('common.sort_name'),
+            'created_at' => trans('common.sort_created_at'),
+            'updated_at' => trans('common.sort_updated_at'),
+        ]);
 
+        $sort = $listOptions->getSort();
         $sortedVisibleShelfBooks = $shelf->visibleBooks()->get()
-            ->sortBy($sort === 'default' ? 'pivot.order' : $sort, SORT_REGULAR, $order === 'desc')
+            ->sortBy($sort === 'default' ? 'pivot.order' : $sort, SORT_REGULAR, $listOptions->getOrder() === 'desc')
             ->values()
             ->all();
 
@@ -124,8 +126,7 @@ class BookshelfController extends Controller
             'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
             'view'                    => $view,
             'activity'                => $activities->entityActivity($shelf, 20, 1),
-            'order'                   => $order,
-            'sort'                    => $sort,
+            'listOptions'             => $listOptions,
             'referenceCount'          => $this->referenceFetcher->getPageReferenceCountToEntity($shelf),
         ]);
     }
index f38bd71dfc7ea456a9e5e46f74140035ba777c91..c3c8d1066d75dd664cad9750a45f955905c651dc 100644 (file)
@@ -10,13 +10,15 @@ use BookStack\Entities\Queries\TopFavourites;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Repos\BookshelfRepo;
 use BookStack\Entities\Tools\PageContent;
+use BookStack\Util\SimpleListOptions;
+use Illuminate\Http\Request;
 
 class HomeController extends Controller
 {
     /**
      * Display the homepage.
      */
-    public function index(ActivityQueries $activities)
+    public function index(Request $request, ActivityQueries $activities)
     {
         $activity = $activities->latest(10);
         $draftPages = [];
@@ -61,33 +63,27 @@ class HomeController extends Controller
         if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
             $key = $homepageOption;
             $view = setting()->getForCurrentUser($key . '_view_type');
-            $sort = setting()->getForCurrentUser($key . '_sort', 'name');
-            $order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
-
-            $sortOptions = [
-                'name'       => trans('common.sort_name'),
+            $listOptions = SimpleListOptions::fromRequest($request, $key)->withSortOptions([
+                'name' => trans('common.sort_name'),
                 'created_at' => trans('common.sort_created_at'),
                 'updated_at' => trans('common.sort_updated_at'),
-            ];
+            ]);
 
             $commonData = array_merge($commonData, [
                 'view'        => $view,
-                'sort'        => $sort,
-                'order'       => $order,
-                'sortOptions' => $sortOptions,
+                'listOptions' => $listOptions,
             ]);
         }
 
         if ($homepageOption === 'bookshelves') {
-            $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
+            $shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder());
             $data = array_merge($commonData, ['shelves' => $shelves]);
 
             return view('home.shelves', $data);
         }
 
         if ($homepageOption === 'books') {
-            $bookRepo = app(BookRepo::class);
-            $books = $bookRepo->getAllPaginated(18, $commonData['sort'], $commonData['order']);
+            $books = app(BookRepo::class)->getAllPaginated(18, $commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder());
             $data = array_merge($commonData, ['books' => $books]);
 
             return view('home.books', $data);
index 89775a60213e0a4e8931be951061f89e6f1a7331..3da5e7c2dd07894c6ed67389d30cc5746b43fe86 100644 (file)
@@ -3,10 +3,13 @@
 namespace BookStack\Http\Controllers;
 
 use BookStack\Actions\ActivityType;
+use BookStack\Entities\Models\PageRevision;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Entities\Tools\PageContent;
 use BookStack\Exceptions\NotFoundException;
 use BookStack\Facades\Activity;
+use BookStack\Util\SimpleListOptions;
+use Illuminate\Http\Request;
 use Ssddanbrown\HtmlDiff\Diff;
 
 class PageRevisionController extends Controller
@@ -23,22 +26,29 @@ class PageRevisionController extends Controller
      *
      * @throws NotFoundException
      */
-    public function index(string $bookSlug, string $pageSlug)
+    public function index(Request $request, string $bookSlug, string $pageSlug)
     {
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        $listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([
+            'id' => trans('entities.pages_revisions_sort_number')
+        ]);
+
         $revisions = $page->revisions()->select([
-            'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',
-            'type', 'revision_number', 'summary',
-        ])
+                'id', 'page_id', 'name', 'created_at', 'created_by', 'updated_at',
+                'type', 'revision_number', 'summary',
+            ])
             ->selectRaw("IF(markdown = '', false, true) as is_markdown")
             ->with(['page.book', 'createdBy'])
-            ->get();
+            ->reorder('id', $listOptions->getOrder())
+            ->reorder('created_at', $listOptions->getOrder())
+            ->paginate(50);
 
         $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()]));
 
         return view('pages.revisions', [
-            'revisions' => $revisions,
-            'page'      => $page,
+            'revisions'   => $revisions,
+            'page'        => $page,
+            'listOptions' => $listOptions,
         ]);
     }
 
@@ -50,6 +60,7 @@ class PageRevisionController extends Controller
     public function show(string $bookSlug, string $pageSlug, int $revisionId)
     {
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        /** @var ?PageRevision $revision */
         $revision = $page->revisions()->where('id', '=', $revisionId)->first();
         if ($revision === null) {
             throw new NotFoundException();
@@ -78,6 +89,7 @@ class PageRevisionController extends Controller
     public function changes(string $bookSlug, string $pageSlug, int $revisionId)
     {
         $page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
+        /** @var ?PageRevision $revision */
         $revision = $page->revisions()->where('id', '=', $revisionId)->first();
         if ($revision === null) {
             throw new NotFoundException();
index fee31ffbfe297a8806a9b9d12a8c310c3325418a..a9be19e0cc7276de07e3c682638caea80db18fc1 100644 (file)
@@ -3,19 +3,18 @@
 namespace BookStack\Http\Controllers;
 
 use BookStack\Auth\Permissions\PermissionsRepo;
+use BookStack\Auth\Queries\RolesAllPaginatedAndSorted;
 use BookStack\Auth\Role;
 use BookStack\Exceptions\PermissionsException;
+use BookStack\Util\SimpleListOptions;
 use Exception;
 use Illuminate\Http\Request;
 use Illuminate\Validation\ValidationException;
 
 class RoleController extends Controller
 {
-    protected $permissionsRepo;
+    protected PermissionsRepo $permissionsRepo;
 
-    /**
-     * PermissionController constructor.
-     */
     public function __construct(PermissionsRepo $permissionsRepo)
     {
         $this->permissionsRepo = $permissionsRepo;
@@ -24,14 +23,27 @@ class RoleController extends Controller
     /**
      * Show a listing of the roles in the system.
      */
-    public function index()
+    public function index(Request $request)
     {
         $this->checkPermission('user-roles-manage');
-        $roles = $this->permissionsRepo->getAllRoles();
+
+        $listOptions = SimpleListOptions::fromRequest($request, 'roles')->withSortOptions([
+            'display_name' => trans('common.sort_name'),
+            'users_count' => trans('settings.roles_assigned_users'),
+            'permissions_count' => trans('settings.roles_permissions_provided'),
+            'created_at' => trans('common.sort_created_at'),
+            'updated_at' => trans('common.sort_updated_at'),
+        ]);
+
+        $roles = (new RolesAllPaginatedAndSorted())->run(20, $listOptions);
+        $roles->appends($listOptions->getPaginationAppends());
 
         $this->setPageTitle(trans('settings.roles'));
 
-        return view('settings.roles.index', ['roles' => $roles]);
+        return view('settings.roles.index', [
+            'roles'       => $roles,
+            'listOptions' => $listOptions,
+        ]);
     }
 
     /**
@@ -75,16 +87,11 @@ class RoleController extends Controller
 
     /**
      * Show the form for editing a user role.
-     *
-     * @throws PermissionsException
      */
     public function edit(string $id)
     {
         $this->checkPermission('user-roles-manage');
         $role = $this->permissionsRepo->getRoleById($id);
-        if ($role->hidden) {
-            throw new PermissionsException(trans('errors.role_cannot_be_edited'));
-        }
 
         $this->setPageTitle(trans('settings.role_edit'));
 
index 699733e377008f508634d9dbbda6755e103c6394..8df5cfafb805514ec9daf728c8832255129d42a7 100644 (file)
@@ -11,7 +11,7 @@ use Illuminate\Http\Request;
 
 class SearchController extends Controller
 {
-    protected $searchRunner;
+    protected SearchRunner $searchRunner;
 
     public function __construct(SearchRunner $searchRunner)
     {
@@ -69,7 +69,7 @@ class SearchController extends Controller
      * Search for a list of entities and return a partial HTML response of matching entities.
      * Returns the most popular entities if no search is provided.
      */
-    public function searchEntitiesAjax(Request $request)
+    public function searchForSelector(Request $request)
     {
         $entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
         $searchTerm = $request->get('term', false);
@@ -83,7 +83,25 @@ class SearchController extends Controller
             $entities = (new Popular())->run(20, 0, $entityTypes);
         }
 
-        return view('search.parts.entity-ajax-list', ['entities' => $entities, 'permission' => $permission]);
+        return view('search.parts.entity-selector-list', ['entities' => $entities, 'permission' => $permission]);
+    }
+
+    /**
+     * Search for a list of entities and return a partial HTML response of matching entities
+     * to be used as a result preview suggestion list for global system searches.
+     */
+    public function searchSuggestions(Request $request)
+    {
+        $searchTerm = $request->get('term', '');
+        $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 5)['results'];
+
+        foreach ($entities as $entity) {
+            $entity->setAttribute('preview_content', '');
+        }
+
+        return view('search.parts.entity-suggestion-list', [
+            'entities' => $entities->slice(0, 5)
+        ]);
     }
 
     /**
index 056cc9902d564f4204899a08e0b4b7af3bd8725c..6c2876043c4fa78e24c959bf1ca00442183d5b58 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Http\Controllers;
 
 use BookStack\Actions\TagRepo;
+use BookStack\Util\SimpleListOptions;
 use Illuminate\Http\Request;
 
 class TagController extends Controller
@@ -19,22 +20,25 @@ class TagController extends Controller
      */
     public function index(Request $request)
     {
-        $search = $request->get('search', '');
+        $listOptions = SimpleListOptions::fromRequest($request, 'tags')->withSortOptions([
+            'name' => trans('common.sort_name'),
+            'usages' => trans('entities.tags_usages'),
+        ]);
+
         $nameFilter = $request->get('name', '');
         $tags = $this->tagRepo
-            ->queryWithTotals($search, $nameFilter)
+            ->queryWithTotals($listOptions, $nameFilter)
             ->paginate(50)
-            ->appends(array_filter([
-                'search' => $search,
+            ->appends(array_filter(array_merge($listOptions->getPaginationAppends(), [
                 'name'   => $nameFilter,
-            ]));
+            ])));
 
         $this->setPageTitle(trans('entities.tags'));
 
         return view('tags.index', [
-            'tags'       => $tags,
-            'search'     => $search,
-            'nameFilter' => $nameFilter,
+            'tags'        => $tags,
+            'nameFilter'  => $nameFilter,
+            'listOptions' => $listOptions,
         ]);
     }
 
index 895481d02405305c80198541883613941a69a13a..f69f00cf79e602217e4971e4db0992721b87760b 100644 (file)
@@ -3,13 +3,13 @@
 namespace BookStack\Http\Controllers;
 
 use BookStack\Auth\Access\SocialAuthService;
-use BookStack\Auth\Queries\AllUsersPaginatedAndSorted;
+use BookStack\Auth\Queries\UsersAllPaginatedAndSorted;
 use BookStack\Auth\Role;
-use BookStack\Auth\User;
 use BookStack\Auth\UserRepo;
 use BookStack\Exceptions\ImageUploadException;
 use BookStack\Exceptions\UserUpdateException;
 use BookStack\Uploads\ImageRepo;
+use BookStack\Util\SimpleListOptions;
 use Exception;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\DB;
@@ -21,9 +21,6 @@ class UserController extends Controller
     protected UserRepo $userRepo;
     protected ImageRepo $imageRepo;
 
-    /**
-     * UserController constructor.
-     */
     public function __construct(UserRepo $userRepo, ImageRepo $imageRepo)
     {
         $this->userRepo = $userRepo;
@@ -36,20 +33,23 @@ class UserController extends Controller
     public function index(Request $request)
     {
         $this->checkPermission('users-manage');
-        $listDetails = [
-            'order'  => $request->get('order', 'asc'),
-            'search' => $request->get('search', ''),
-            'sort'   => $request->get('sort', 'name'),
-        ];
 
-        $users = (new AllUsersPaginatedAndSorted())->run(20, $listDetails);
+        $listOptions = SimpleListOptions::fromRequest($request, 'users')->withSortOptions([
+            'name' => trans('common.sort_name'),
+            'email' => trans('auth.email'),
+            'created_at' => trans('common.sort_created_at'),
+            'updated_at' => trans('common.sort_updated_at'),
+            'last_activity_at' => trans('settings.users_latest_activity'),
+        ]);
+
+        $users = (new UsersAllPaginatedAndSorted())->run(20, $listOptions);
 
         $this->setPageTitle(trans('settings.users'));
-        $users->appends($listDetails);
+        $users->appends($listOptions->getPaginationAppends());
 
         return view('users.index', [
             'users'       => $users,
-            'listDetails' => $listDetails,
+            'listOptions' => $listOptions,
         ]);
     }
 
@@ -107,9 +107,8 @@ class UserController extends Controller
     {
         $this->checkPermissionOrCurrentUser('users-manage', $id);
 
-        /** @var User $user */
-        $user = User::query()->with(['apiTokens', 'mfaValues'])->findOrFail($id);
-
+        $user = $this->userRepo->getById($id);
+        $user->load(['apiTokens', 'mfaValues']);
         $authMethod = ($user->system_name) ? 'system' : config('auth.method');
 
         $activeSocialDrivers = $socialAuthService->getActiveDrivers();
@@ -202,137 +201,4 @@ class UserController extends Controller
 
         return redirect('/settings/users');
     }
-
-    /**
-     * Update the user's preferred book-list display setting.
-     */
-    public function switchBooksView(Request $request, int $id)
-    {
-        return $this->switchViewType($id, $request, 'books');
-    }
-
-    /**
-     * Update the user's preferred shelf-list display setting.
-     */
-    public function switchShelvesView(Request $request, int $id)
-    {
-        return $this->switchViewType($id, $request, 'bookshelves');
-    }
-
-    /**
-     * Update the user's preferred shelf-view book list display setting.
-     */
-    public function switchShelfView(Request $request, int $id)
-    {
-        return $this->switchViewType($id, $request, 'bookshelf');
-    }
-
-    /**
-     * For a type of list, switch with stored view type for a user.
-     */
-    protected function switchViewType(int $userId, Request $request, string $listName)
-    {
-        $this->checkPermissionOrCurrentUser('users-manage', $userId);
-
-        $viewType = $request->get('view_type');
-        if (!in_array($viewType, ['grid', 'list'])) {
-            $viewType = 'list';
-        }
-
-        $user = $this->userRepo->getById($userId);
-        $key = $listName . '_view_type';
-        setting()->putUser($user, $key, $viewType);
-
-        return redirect()->back(302, [], "/settings/users/$userId");
-    }
-
-    /**
-     * Change the stored sort type for a particular view.
-     */
-    public function changeSort(Request $request, string $id, string $type)
-    {
-        $validSortTypes = ['books', 'bookshelves', 'shelf_books'];
-        if (!in_array($type, $validSortTypes)) {
-            return redirect()->back(500);
-        }
-
-        return $this->changeListSort($id, $request, $type);
-    }
-
-    /**
-     * Toggle dark mode for the current user.
-     */
-    public function toggleDarkMode()
-    {
-        $enabled = setting()->getForCurrentUser('dark-mode-enabled', false);
-        setting()->putUser(user(), 'dark-mode-enabled', $enabled ? 'false' : 'true');
-
-        return redirect()->back();
-    }
-
-    /**
-     * Update the stored section expansion preference for the given user.
-     */
-    public function updateExpansionPreference(Request $request, string $id, string $key)
-    {
-        $this->checkPermissionOrCurrentUser('users-manage', $id);
-        $keyWhitelist = ['home-details'];
-        if (!in_array($key, $keyWhitelist)) {
-            return response('Invalid key', 500);
-        }
-
-        $newState = $request->get('expand', 'false');
-
-        $user = $this->userRepo->getById($id);
-        setting()->putUser($user, 'section_expansion#' . $key, $newState);
-
-        return response('', 204);
-    }
-
-    public function updateCodeLanguageFavourite(Request $request)
-    {
-        $validated = $this->validate($request, [
-            'language' => ['required', 'string', 'max:20'],
-            'active'   => ['required', 'bool'],
-        ]);
-
-        $currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');
-        $currentFavorites = array_filter(explode(',', $currentFavoritesStr));
-
-        $isFav = in_array($validated['language'], $currentFavorites);
-        if (!$isFav && $validated['active']) {
-            $currentFavorites[] = $validated['language'];
-        } elseif ($isFav && !$validated['active']) {
-            $index = array_search($validated['language'], $currentFavorites);
-            array_splice($currentFavorites, $index, 1);
-        }
-
-        setting()->putUser(user(), 'code-language-favourites', implode(',', $currentFavorites));
-    }
-
-    /**
-     * Changed the stored preference for a list sort order.
-     */
-    protected function changeListSort(int $userId, Request $request, string $listName)
-    {
-        $this->checkPermissionOrCurrentUser('users-manage', $userId);
-
-        $sort = $request->get('sort');
-        if (!in_array($sort, ['name', 'created_at', 'updated_at', 'default'])) {
-            $sort = 'name';
-        }
-
-        $order = $request->get('order');
-        if (!in_array($order, ['asc', 'desc'])) {
-            $order = 'asc';
-        }
-
-        $user = $this->userRepo->getById($userId);
-        $sortKey = $listName . '_sort';
-        $orderKey = $listName . '_sort_order';
-        setting()->putUser($user, $sortKey, $sort);
-        setting()->putUser($user, $orderKey, $order);
-
-        return redirect()->back(302, [], "/settings/users/$userId");
-    }
 }
diff --git a/app/Http/Controllers/UserPreferencesController.php b/app/Http/Controllers/UserPreferencesController.php
new file mode 100644 (file)
index 0000000..560dd16
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Auth\UserRepo;
+use BookStack\Settings\UserShortcutMap;
+use Illuminate\Http\Request;
+
+class UserPreferencesController extends Controller
+{
+    protected UserRepo $userRepo;
+
+    public function __construct(UserRepo $userRepo)
+    {
+        $this->userRepo = $userRepo;
+    }
+
+    /**
+     * Show the user-specific interface shortcuts.
+     */
+    public function showShortcuts()
+    {
+        $shortcuts = UserShortcutMap::fromUserPreferences();
+        $enabled = setting()->getForCurrentUser('ui-shortcuts-enabled', false);
+
+        return view('users.preferences.shortcuts', [
+            'shortcuts' => $shortcuts,
+            'enabled' => $enabled,
+        ]);
+    }
+
+    /**
+     * Update the user-specific interface shortcuts.
+     */
+    public function updateShortcuts(Request $request)
+    {
+        $enabled = $request->get('enabled') === 'true';
+        $providedShortcuts = $request->get('shortcut', []);
+        $shortcuts = new UserShortcutMap($providedShortcuts);
+
+        setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson());
+        setting()->putForCurrentUser('ui-shortcuts-enabled', $enabled);
+
+        $this->showSuccessNotification(trans('preferences.shortcuts_update_success'));
+
+        return redirect('/preferences/shortcuts');
+    }
+
+    /**
+     * Update the preferred view format for a list view of the given type.
+     */
+    public function changeView(Request $request, string $type)
+    {
+        $valueViewTypes = ['books', 'bookshelves', 'bookshelf'];
+        if (!in_array($type, $valueViewTypes)) {
+            return redirect()->back(500);
+        }
+
+        $view = $request->get('view');
+        if (!in_array($view, ['grid', 'list'])) {
+            $view = 'list';
+        }
+
+        $key = $type . '_view_type';
+        setting()->putForCurrentUser($key, $view);
+
+        return redirect()->back(302, [], "/");
+    }
+
+    /**
+     * Change the stored sort type for a particular view.
+     */
+    public function changeSort(Request $request, string $type)
+    {
+        $validSortTypes = ['books', 'bookshelves', 'shelf_books', 'users', 'roles', 'webhooks', 'tags', 'page_revisions'];
+        if (!in_array($type, $validSortTypes)) {
+            return redirect()->back(500);
+        }
+
+        $sort = substr($request->get('sort') ?: 'name', 0, 50);
+        $order = $request->get('order') === 'desc' ? 'desc' : 'asc';
+
+        $sortKey = $type . '_sort';
+        $orderKey = $type . '_sort_order';
+        setting()->putForCurrentUser($sortKey, $sort);
+        setting()->putForCurrentUser($orderKey, $order);
+
+        return redirect()->back(302, [], "/");
+    }
+
+    /**
+     * Toggle dark mode for the current user.
+     */
+    public function toggleDarkMode()
+    {
+        $enabled = setting()->getForCurrentUser('dark-mode-enabled', false);
+        setting()->putForCurrentUser('dark-mode-enabled', $enabled ? 'false' : 'true');
+
+        return redirect()->back();
+    }
+
+    /**
+     * Update the stored section expansion preference for the given user.
+     */
+    public function changeExpansion(Request $request, string $type)
+    {
+        $typeWhitelist = ['home-details'];
+        if (!in_array($type, $typeWhitelist)) {
+            return response('Invalid key', 500);
+        }
+
+        $newState = $request->get('expand', 'false');
+        setting()->putForCurrentUser('section_expansion#' . $type, $newState);
+
+        return response('', 204);
+    }
+
+    /**
+     * Update the favorite status for a code language.
+     */
+    public function updateCodeLanguageFavourite(Request $request)
+    {
+        $validated = $this->validate($request, [
+            'language' => ['required', 'string', 'max:20'],
+            'active'   => ['required', 'bool'],
+        ]);
+
+        $currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');
+        $currentFavorites = array_filter(explode(',', $currentFavoritesStr));
+
+        $isFav = in_array($validated['language'], $currentFavorites);
+        if (!$isFav && $validated['active']) {
+            $currentFavorites[] = $validated['language'];
+        } elseif ($isFav && !$validated['active']) {
+            $index = array_search($validated['language'], $currentFavorites);
+            array_splice($currentFavorites, $index, 1);
+        }
+
+        setting()->putForCurrentUser('code-language-favourites', implode(',', $currentFavorites));
+        return response('', 204);
+    }
+}
index 264921dfc39ed7743f1f58f27afe5d400196a2a9..c72dcc51066d4af80bef78c6b2cb9340d1960962 100644 (file)
@@ -3,7 +3,9 @@
 namespace BookStack\Http\Controllers;
 
 use BookStack\Actions\ActivityType;
+use BookStack\Actions\Queries\WebhooksAllPaginatedAndSorted;
 use BookStack\Actions\Webhook;
+use BookStack\Util\SimpleListOptions;
 use Illuminate\Http\Request;
 
 class WebhookController extends Controller
@@ -18,16 +20,25 @@ class WebhookController extends Controller
     /**
      * Show all webhooks configured in the system.
      */
-    public function index()
+    public function index(Request $request)
     {
-        $webhooks = Webhook::query()
-            ->orderBy('name', 'desc')
-            ->with('trackedEvents')
-            ->get();
+        $listOptions = SimpleListOptions::fromRequest($request, 'webhooks')->withSortOptions([
+            'name' => trans('common.sort_name'),
+            'endpoint'  => trans('settings.webhooks_endpoint'),
+            'created_at' => trans('common.sort_created_at'),
+            'updated_at' => trans('common.sort_updated_at'),
+            'active'     => trans('common.status'),
+        ]);
+
+        $webhooks = (new WebhooksAllPaginatedAndSorted())->run(20, $listOptions);
+        $webhooks->appends($listOptions->getPaginationAppends());
 
         $this->setPageTitle(trans('settings.webhooks'));
 
-        return view('settings.webhooks.index', ['webhooks' => $webhooks]);
+        return view('settings.webhooks.index', [
+            'webhooks'    => $webhooks,
+            'listOptions' => $listOptions,
+        ]);
     }
 
     /**
index ac3307f2ded53c4799d8e8d1dcef465bd008da37..415ec66269c692e8ae5d4de0c8ae3c391a85cbbc 100644 (file)
@@ -19,14 +19,6 @@ class RouteServiceProvider extends ServiceProvider
      */
     public const HOME = '/';
 
-    /**
-     * This namespace is applied to the controller routes in your routes file.
-     *
-     * In addition, it is set as the URL generator's root namespace.
-     *
-     * @var string
-     */
-
     /**
      * Define your route model bindings, pattern filters, etc.
      *
index cc44e6125035608469d2d285329490f3d3bb146d..013f7b380b82239e8ce475b63d8efa52d856db8f 100644 (file)
@@ -50,7 +50,7 @@ class SearchRunner
      * The provided count is for each entity to search,
      * Total returned could be larger and not guaranteed.
      *
-     * @return array{total: int, count: int, has_more: bool, results: Entity[]}
+     * @return array{total: int, count: int, has_more: bool, results: Collection<Entity>}
      */
     public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20): array
     {
index f2c4c8305c47c2db227a79456e880422171dc500..9f0a41ea2fceb228bde62b8ad687ac502eb7a9b4 100644 (file)
@@ -194,6 +194,8 @@ class SettingService
 
     /**
      * Put a user-specific setting into the database.
+     * Can only take string value types since this may use
+     * the session which is less flexible to data types.
      */
     public function putUser(User $user, string $key, string $value): bool
     {
@@ -206,6 +208,16 @@ class SettingService
         return $this->put($this->userKey($user->id, $key), $value);
     }
 
+    /**
+     * Put a user-specific setting into the database for the current access user.
+     * Can only take string value types since this may use
+     * the session which is less flexible to data types.
+     */
+    public function putForCurrentUser(string $key, string $value)
+    {
+        return $this->putUser(user(), $key, $value);
+    }
+
     /**
      * Convert a setting key into a user-specific key.
      */
diff --git a/app/Settings/UserShortcutMap.php b/app/Settings/UserShortcutMap.php
new file mode 100644 (file)
index 0000000..da2ea3c
--- /dev/null
@@ -0,0 +1,82 @@
+<?php
+
+namespace BookStack\Settings;
+
+class UserShortcutMap
+{
+    protected const DEFAULTS = [
+        // Header actions
+        "home_view" => "1",
+        "shelves_view" => "2",
+        "books_view" => "3",
+        "settings_view" => "4",
+        "favourites_view" => "5",
+        "profile_view" => "6",
+        "global_search" => "/",
+        "logout" => "0",
+
+        // Common actions
+        "edit" => "e",
+        "new" => "n",
+        "copy" => "c",
+        "delete" => "d",
+        "favourite" => "f",
+        "export" => "x",
+        "sort" => "s",
+        "permissions" => "p",
+        "move" => "m",
+        "revisions" => "r",
+
+        // Navigation
+        "next" => "ArrowRight",
+        "previous" => "ArrowLeft",
+    ];
+
+    /**
+     * @var array<string, string>
+     */
+    protected array $mapping;
+
+    public function __construct(array $map)
+    {
+        $this->mapping = static::DEFAULTS;
+        $this->merge($map);
+    }
+
+    /**
+     * Merge the given map into the current shortcut mapping.
+     */
+    protected function merge(array $map): void
+    {
+        foreach ($map as $key => $value) {
+            if (is_string($value) && isset($this->mapping[$key])) {
+                $this->mapping[$key] = $value;
+            }
+        }
+    }
+
+    /**
+     * Get the shortcut defined for the given ID.
+     */
+    public function getShortcut(string $id): string
+    {
+        return $this->mapping[$id] ?? '';
+    }
+
+    /**
+     * Convert this mapping to JSON.
+     */
+    public function toJson(): string
+    {
+        return json_encode($this->mapping);
+    }
+
+    /**
+     * Create a new instance from the current user's preferences.
+     */
+    public static function fromUserPreferences(): self
+    {
+        $userKeyMap = setting()->getForCurrentUser('ui-shortcuts');
+        return new self(json_decode($userKeyMap, true) ?: []);
+    }
+}
index 0199c207ea18927a93d17ed5be5ff75a5f73e7a3..55c327e7a552be241748833206e40f8468d6e913 100644 (file)
@@ -88,16 +88,17 @@ class ImageService
     protected function getStorageDiskName(string $imageType): string
     {
         $storageType = config('filesystems.images');
+        $localSecureInUse = ($storageType === 'local_secure' || $storageType === 'local_secure_restricted');
 
         // Ensure system images (App logo) are uploaded to a public space
-        if ($imageType === 'system' && $storageType === 'local_secure') {
-            $storageType = 'local';
+        if ($imageType === 'system' && $localSecureInUse) {
+            return 'local';
         }
 
         // Rename local_secure options to get our image specific storage driver which
         // is scoped to the relevant image directories.
-        if ($storageType === 'local_secure' || $storageType === 'local_secure_restricted') {
-            $storageType = 'local_secure_images';
+        if ($localSecureInUse) {
+            return 'local_secure_images';
         }
 
         return $storageType;
diff --git a/app/Util/SimpleListOptions.php b/app/Util/SimpleListOptions.php
new file mode 100644 (file)
index 0000000..81d8a58
--- /dev/null
@@ -0,0 +1,104 @@
+<?php
+
+namespace BookStack\Util;
+
+use Illuminate\Http\Request;
+
+/**
+ * Handled options commonly used for item lists within the system, providing a standard
+ * model for handling and validating sort, order and search options.
+ */
+class SimpleListOptions
+{
+    protected string $typeKey;
+    protected string $sort;
+    protected string $order;
+    protected string $search;
+    protected array $sortOptions = [];
+
+    public function __construct(string $typeKey, string $sort, string $order, string $search = '')
+    {
+        $this->typeKey = $typeKey;
+        $this->sort = $sort;
+        $this->order = $order;
+        $this->search = $search;
+    }
+
+    /**
+     * Create a new instance from the given request.
+     * Takes the item type (plural) that's used as a key for storing sort preferences.
+     */
+    public static function fromRequest(Request $request, string $typeKey, bool $sortDescDefault = false): self
+    {
+        $search = $request->get('search', '');
+        $sort = setting()->getForCurrentUser($typeKey . '_sort', '');
+        $order = setting()->getForCurrentUser($typeKey . '_sort_order', $sortDescDefault ? 'desc' : 'asc');
+
+        return new self($typeKey, $sort, $order, $search);
+    }
+
+    /**
+     * Configure the valid sort options for this set of list options.
+     * Provided sort options must be an array, keyed by search properties
+     * with values being user-visible option labels.
+     * Returns current options for easy fluent usage during creation.
+     */
+    public function withSortOptions(array $sortOptions): self
+    {
+        $this->sortOptions = array_merge($this->sortOptions, $sortOptions);
+
+        return $this;
+    }
+
+    /**
+     * Get the current order option.
+     */
+    public function getOrder(): string
+    {
+        return strtolower($this->order) === 'desc' ? 'desc' : 'asc';
+    }
+
+    /**
+     * Get the current sort option.
+     */
+    public function getSort(): string
+    {
+        $default = array_key_first($this->sortOptions) ?? 'name';
+        $sort = $this->sort ?: $default;
+
+        if (empty($this->sortOptions) || array_key_exists($sort, $this->sortOptions)) {
+            return $sort;
+        }
+
+        return $default;
+    }
+
+    /**
+     * Get the set search term.
+     */
+    public function getSearch(): string
+    {
+        return $this->search;
+    }
+
+    /**
+     * Get the data to append for pagination.
+     */
+    public function getPaginationAppends(): array
+    {
+        return ['search' => $this->search];
+    }
+
+    /**
+     * Get the data required by the sort control view.
+     */
+    public function getSortControlData(): array
+    {
+        return [
+            'options' => $this->sortOptions,
+            'order' => $this->getOrder(),
+            'sort' => $this->getSort(),
+            'type' => $this->typeKey,
+        ];
+    }
+}
index 5ed41c1da572455c9f9511d343f78b04579ce95b..c1a85651f096d9006fb0a5a28f16577c1e358579 100644 (file)
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.238.5",
+            "version": "3.247.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "2f5440652fa7a6f20a1bb15926dd8248c24c733c"
+                "reference": "337e447997148b9e5024c2d0ae69618b1cbf80d6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2f5440652fa7a6f20a1bb15926dd8248c24c733c",
-                "reference": "2f5440652fa7a6f20a1bb15926dd8248c24c733c",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/337e447997148b9e5024c2d0ae69618b1cbf80d6",
+                "reference": "337e447997148b9e5024c2d0ae69618b1cbf80d6",
                 "shasum": ""
             },
             "require": {
             "support": {
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.238.5"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.247.1"
             },
-            "time": "2022-10-14T18:15:06+00:00"
+            "time": "2022-11-22T19:23:34+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
         },
         {
             "name": "doctrine/dbal",
-            "version": "3.4.5",
+            "version": "3.5.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "a5a58773109c0abb13e658c8ccd92aeec8d07f9e"
+                "reference": "f38ee8aaca2d58ee88653cb34a6a3880c23f38a5"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/a5a58773109c0abb13e658c8ccd92aeec8d07f9e",
-                "reference": "a5a58773109c0abb13e658c8ccd92aeec8d07f9e",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/f38ee8aaca2d58ee88653cb34a6a3880c23f38a5",
+                "reference": "f38ee8aaca2d58ee88653cb34a6a3880c23f38a5",
                 "shasum": ""
             },
             "require": {
                 "composer-runtime-api": "^2",
                 "doctrine/cache": "^1.11|^2.0",
                 "doctrine/deprecations": "^0.5.3|^1",
-                "doctrine/event-manager": "^1.0",
+                "doctrine/event-manager": "^1|^2",
                 "php": "^7.4 || ^8.0",
                 "psr/cache": "^1|^2|^3",
                 "psr/log": "^1|^2|^3"
             "require-dev": {
                 "doctrine/coding-standard": "10.0.0",
                 "jetbrains/phpstorm-stubs": "2022.2",
-                "phpstan/phpstan": "1.8.3",
-                "phpstan/phpstan-strict-rules": "^1.3",
-                "phpunit/phpunit": "9.5.24",
+                "phpstan/phpstan": "1.8.10",
+                "phpstan/phpstan-strict-rules": "^1.4",
+                "phpunit/phpunit": "9.5.25",
                 "psalm/plugin-phpunit": "0.17.0",
                 "squizlabs/php_codesniffer": "3.7.1",
                 "symfony/cache": "^5.4|^6.0",
                 "symfony/console": "^4.4|^5.4|^6.0",
-                "vimeo/psalm": "4.27.0"
+                "vimeo/psalm": "4.29.0"
             },
             "suggest": {
                 "symfony/console": "For helpful console commands such as SQL execution and import of files."
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/3.4.5"
+                "source": "https://github.com/doctrine/dbal/tree/3.5.1"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-09-23T17:48:57+00:00"
+            "time": "2022-10-24T07:26:18+00:00"
         },
         {
             "name": "doctrine/deprecations",
         },
         {
             "name": "doctrine/inflector",
-            "version": "2.0.5",
+            "version": "2.0.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/inflector.git",
-                "reference": "ade2b3bbfb776f27f0558e26eed43b5d9fe1b392"
+                "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/inflector/zipball/ade2b3bbfb776f27f0558e26eed43b5d9fe1b392",
-                "reference": "ade2b3bbfb776f27f0558e26eed43b5d9fe1b392",
+                "url": "https://api.github.com/repos/doctrine/inflector/zipball/d9d313a36c872fd6ee06d9a6cbcf713eaa40f024",
+                "reference": "d9d313a36c872fd6ee06d9a6cbcf713eaa40f024",
                 "shasum": ""
             },
             "require": {
                 "php": "^7.2 || ^8.0"
             },
             "require-dev": {
-                "doctrine/coding-standard": "^9",
+                "doctrine/coding-standard": "^10",
                 "phpstan/phpstan": "^1.8",
                 "phpstan/phpstan-phpunit": "^1.1",
                 "phpstan/phpstan-strict-rules": "^1.3",
             ],
             "support": {
                 "issues": "https://github.com/doctrine/inflector/issues",
-                "source": "https://github.com/doctrine/inflector/tree/2.0.5"
+                "source": "https://github.com/doctrine/inflector/tree/2.0.6"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-09-07T09:01:28+00:00"
+            "time": "2022-10-20T09:10:12+00:00"
         },
         {
             "name": "doctrine/lexer",
         },
         {
             "name": "filp/whoops",
-            "version": "2.14.5",
+            "version": "2.14.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/filp/whoops.git",
-                "reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc"
+                "reference": "f7948baaa0330277c729714910336383286305da"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/filp/whoops/zipball/a63e5e8f26ebbebf8ed3c5c691637325512eb0dc",
-                "reference": "a63e5e8f26ebbebf8ed3c5c691637325512eb0dc",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/f7948baaa0330277c729714910336383286305da",
+                "reference": "f7948baaa0330277c729714910336383286305da",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/filp/whoops/issues",
-                "source": "https://github.com/filp/whoops/tree/2.14.5"
+                "source": "https://github.com/filp/whoops/tree/2.14.6"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2022-01-07T12:00:00+00:00"
+            "time": "2022-11-02T16:23:29+00:00"
         },
         {
             "name": "graham-campbell/result-type",
         },
         {
             "name": "guzzlehttp/psr7",
-            "version": "2.4.1",
+            "version": "2.4.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/psr7.git",
-                "reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379"
+                "reference": "67c26b443f348a51926030c83481b85718457d3d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/69568e4293f4fa993f3b0e51c9723e1e17c41379",
-                "reference": "69568e4293f4fa993f3b0e51c9723e1e17c41379",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/67c26b443f348a51926030c83481b85718457d3d",
+                "reference": "67c26b443f348a51926030c83481b85718457d3d",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/guzzle/psr7/issues",
-                "source": "https://github.com/guzzle/psr7/tree/2.4.1"
+                "source": "https://github.com/guzzle/psr7/tree/2.4.3"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-08-28T14:45:39+00:00"
+            "time": "2022-10-26T14:07:24+00:00"
         },
         {
             "name": "intervention/image",
         },
         {
             "name": "laravel/framework",
-            "version": "v8.83.25",
+            "version": "v8.83.26",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "b77b908a9426efa41d6286a2ef4c3adbf5398ca1"
+                "reference": "7411d9fa71c1b0fd73a33e225f14512b74e6c81e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/b77b908a9426efa41d6286a2ef4c3adbf5398ca1",
-                "reference": "b77b908a9426efa41d6286a2ef4c3adbf5398ca1",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/7411d9fa71c1b0fd73a33e225f14512b74e6c81e",
+                "reference": "7411d9fa71c1b0fd73a33e225f14512b74e6c81e",
                 "shasum": ""
             },
             "require": {
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2022-09-30T13:00:40+00:00"
+            "time": "2022-11-01T14:48:50+00:00"
         },
         {
             "name": "laravel/serializable-closure",
         },
         {
             "name": "laravel/socialite",
-            "version": "v5.5.5",
+            "version": "v5.5.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/socialite.git",
-                "reference": "ce8b2f967eead5a6bae74449e207be6f8046edc3"
+                "reference": "1cd1682b709b8808a5b5dbb68179a58d1342aa7b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/socialite/zipball/ce8b2f967eead5a6bae74449e207be6f8046edc3",
-                "reference": "ce8b2f967eead5a6bae74449e207be6f8046edc3",
+                "url": "https://api.github.com/repos/laravel/socialite/zipball/1cd1682b709b8808a5b5dbb68179a58d1342aa7b",
+                "reference": "1cd1682b709b8808a5b5dbb68179a58d1342aa7b",
                 "shasum": ""
             },
             "require": {
                 "issues": "https://github.com/laravel/socialite/issues",
                 "source": "https://github.com/laravel/socialite"
             },
-            "time": "2022-08-20T21:32:07+00:00"
+            "time": "2022-11-08T15:07:05+00:00"
         },
         {
             "name": "laravel/tinker",
-            "version": "v2.7.2",
+            "version": "v2.7.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/tinker.git",
-                "reference": "dff39b661e827dae6e092412f976658df82dbac5"
+                "reference": "5062061b4924af3392225dd482ca7b4d85d8b8ef"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/tinker/zipball/dff39b661e827dae6e092412f976658df82dbac5",
-                "reference": "dff39b661e827dae6e092412f976658df82dbac5",
+                "url": "https://api.github.com/repos/laravel/tinker/zipball/5062061b4924af3392225dd482ca7b4d85d8b8ef",
+                "reference": "5062061b4924af3392225dd482ca7b4d85d8b8ef",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/laravel/tinker/issues",
-                "source": "https://github.com/laravel/tinker/tree/v2.7.2"
+                "source": "https://github.com/laravel/tinker/tree/v2.7.3"
             },
-            "time": "2022-03-23T12:38:24+00:00"
+            "time": "2022-11-09T15:11:38+00:00"
         },
         {
             "name": "league/commonmark",
         },
         {
             "name": "nesbot/carbon",
-            "version": "2.62.1",
+            "version": "2.63.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "01bc4cdefe98ef58d1f9cb31bdbbddddf2a88f7a"
+                "reference": "ad35dd71a6a212b98e4b87e97389b6fa85f0e347"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/01bc4cdefe98ef58d1f9cb31bdbbddddf2a88f7a",
-                "reference": "01bc4cdefe98ef58d1f9cb31bdbbddddf2a88f7a",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/ad35dd71a6a212b98e4b87e97389b6fa85f0e347",
+                "reference": "ad35dd71a6a212b98e4b87e97389b6fa85f0e347",
                 "shasum": ""
             },
             "require": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-09-02T07:48:13+00:00"
+            "time": "2022-10-30T18:34:28+00:00"
         },
         {
             "name": "nikic/php-parser",
-            "version": "v4.15.1",
+            "version": "v4.15.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900"
+                "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
-                "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
+                "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1"
+                "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2"
             },
-            "time": "2022-09-04T07:30:47+00:00"
+            "time": "2022-11-12T15:38:23+00:00"
         },
         {
             "name": "onelogin/php-saml",
         },
         {
             "name": "phpseclib/phpseclib",
-            "version": "3.0.16",
+            "version": "3.0.17",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpseclib/phpseclib.git",
-                "reference": "7181378909ed8890be4db53d289faac5b77f8b05"
+                "reference": "dbc2307d5c69aeb22db136c52e91130d7f2ca761"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/7181378909ed8890be4db53d289faac5b77f8b05",
-                "reference": "7181378909ed8890be4db53d289faac5b77f8b05",
+                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/dbc2307d5c69aeb22db136c52e91130d7f2ca761",
+                "reference": "dbc2307d5c69aeb22db136c52e91130d7f2ca761",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/phpseclib/phpseclib/issues",
-                "source": "https://github.com/phpseclib/phpseclib/tree/3.0.16"
+                "source": "https://github.com/phpseclib/phpseclib/tree/3.0.17"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-09-05T18:03:08+00:00"
+            "time": "2022-10-24T10:51:50+00:00"
         },
         {
             "name": "pragmarx/google2fa",
         },
         {
             "name": "psy/psysh",
-            "version": "v0.11.8",
+            "version": "v0.11.9",
             "source": {
                 "type": "git",
                 "url": "https://github.com/bobthecow/psysh.git",
-                "reference": "f455acf3645262ae389b10e9beba0c358aa6994e"
+                "reference": "1acec99d6684a54ff92f8b548a4e41b566963778"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/f455acf3645262ae389b10e9beba0c358aa6994e",
-                "reference": "f455acf3645262ae389b10e9beba0c358aa6994e",
+                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1acec99d6684a54ff92f8b548a4e41b566963778",
+                "reference": "1acec99d6684a54ff92f8b548a4e41b566963778",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/bobthecow/psysh/issues",
-                "source": "https://github.com/bobthecow/psysh/tree/v0.11.8"
+                "source": "https://github.com/bobthecow/psysh/tree/v0.11.9"
             },
-            "time": "2022-07-28T14:25:11+00:00"
+            "time": "2022-11-06T15:29:46+00:00"
         },
         {
             "name": "ralouphie/getallheaders",
         },
         {
             "name": "symfony/console",
-            "version": "v5.4.14",
+            "version": "v5.4.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/console.git",
-                "reference": "984ea2c0f45f42dfed01d2f3987b187467c4b16d"
+                "reference": "ea59bb0edfaf9f28d18d8791410ee0355f317669"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/console/zipball/984ea2c0f45f42dfed01d2f3987b187467c4b16d",
-                "reference": "984ea2c0f45f42dfed01d2f3987b187467c4b16d",
+                "url": "https://api.github.com/repos/symfony/console/zipball/ea59bb0edfaf9f28d18d8791410ee0355f317669",
+                "reference": "ea59bb0edfaf9f28d18d8791410ee0355f317669",
                 "shasum": ""
             },
             "require": {
                 "terminal"
             ],
             "support": {
-                "source": "https://github.com/symfony/console/tree/v5.4.14"
+                "source": "https://github.com/symfony/console/tree/v5.4.15"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-10-07T08:01:20+00:00"
+            "time": "2022-10-26T21:41:52+00:00"
         },
         {
             "name": "symfony/css-selector",
         },
         {
             "name": "symfony/error-handler",
-            "version": "v5.4.14",
+            "version": "v5.4.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/error-handler.git",
-                "reference": "5fe6d42ffeb68b094df8fdbf3acf23f391cc6be0"
+                "reference": "539cf1428b8442303c6e876ad7bf5a7babd91091"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/error-handler/zipball/5fe6d42ffeb68b094df8fdbf3acf23f391cc6be0",
-                "reference": "5fe6d42ffeb68b094df8fdbf3acf23f391cc6be0",
+                "url": "https://api.github.com/repos/symfony/error-handler/zipball/539cf1428b8442303c6e876ad7bf5a7babd91091",
+                "reference": "539cf1428b8442303c6e876ad7bf5a7babd91091",
                 "shasum": ""
             },
             "require": {
             "description": "Provides tools to manage errors and ease debugging PHP code",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/error-handler/tree/v5.4.14"
+                "source": "https://github.com/symfony/error-handler/tree/v5.4.15"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-10-03T15:15:50+00:00"
+            "time": "2022-10-27T06:32:25+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
         },
         {
             "name": "symfony/http-foundation",
-            "version": "v5.4.14",
+            "version": "v5.4.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-                "reference": "e7c7b395c3a61d746919c21e915f51f0039c3f75"
+                "reference": "75bd663ff2db90141bfb733682459d5bbe9e29c3"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e7c7b395c3a61d746919c21e915f51f0039c3f75",
-                "reference": "e7c7b395c3a61d746919c21e915f51f0039c3f75",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/75bd663ff2db90141bfb733682459d5bbe9e29c3",
+                "reference": "75bd663ff2db90141bfb733682459d5bbe9e29c3",
                 "shasum": ""
             },
             "require": {
             "description": "Defines an object-oriented layer for the HTTP specification",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-foundation/tree/v5.4.14"
+                "source": "https://github.com/symfony/http-foundation/tree/v5.4.15"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-10-01T21:59:28+00:00"
+            "time": "2022-10-12T09:43:19+00:00"
         },
         {
             "name": "symfony/http-kernel",
-            "version": "v5.4.14",
+            "version": "v5.4.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-                "reference": "6f77fabc1a37c2dceecc6f78cca44772705dc52f"
+                "reference": "fc63c8c3e1036d424820cc993a4ea163778dc5c7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6f77fabc1a37c2dceecc6f78cca44772705dc52f",
-                "reference": "6f77fabc1a37c2dceecc6f78cca44772705dc52f",
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/fc63c8c3e1036d424820cc993a4ea163778dc5c7",
+                "reference": "fc63c8c3e1036d424820cc993a4ea163778dc5c7",
                 "shasum": ""
             },
             "require": {
             "description": "Provides a structured process for converting a Request into a Response",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/http-kernel/tree/v5.4.14"
+                "source": "https://github.com/symfony/http-kernel/tree/v5.4.15"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-10-12T07:12:21+00:00"
+            "time": "2022-10-28T17:52:18+00:00"
         },
         {
             "name": "symfony/mime",
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.26.0",
+            "version": "v1.27.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
+                "reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
-                "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
+                "reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.26-dev"
+                    "dev-main": "1.27-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "portable"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-24T11:49:31+00:00"
+            "time": "2022-11-03T14:55:06+00:00"
         },
         {
             "name": "symfony/polyfill-iconv",
-            "version": "v1.26.0",
+            "version": "v1.27.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-iconv.git",
-                "reference": "143f1881e655bebca1312722af8068de235ae5dc"
+                "reference": "927013f3aac555983a5059aada98e1907d842695"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/143f1881e655bebca1312722af8068de235ae5dc",
-                "reference": "143f1881e655bebca1312722af8068de235ae5dc",
+                "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/927013f3aac555983a5059aada98e1907d842695",
+                "reference": "927013f3aac555983a5059aada98e1907d842695",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.26-dev"
+                    "dev-main": "1.27-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-iconv/tree/v1.26.0"
+                "source": "https://github.com/symfony/polyfill-iconv/tree/v1.27.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-24T11:49:31+00:00"
+            "time": "2022-11-03T14:55:06+00:00"
         },
         {
             "name": "symfony/polyfill-intl-grapheme",
-            "version": "v1.26.0",
+            "version": "v1.27.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
-                "reference": "433d05519ce6990bf3530fba6957499d327395c2"
+                "reference": "511a08c03c1960e08a883f4cffcacd219b758354"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2",
-                "reference": "433d05519ce6990bf3530fba6957499d327395c2",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354",
+                "reference": "511a08c03c1960e08a883f4cffcacd219b758354",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.26-dev"
+                    "dev-main": "1.27-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0"
+                "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.27.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-24T11:49:31+00:00"
+            "time": "2022-11-03T14:55:06+00:00"
         },
         {
             "name": "symfony/polyfill-intl-idn",
-            "version": "v1.26.0",
+            "version": "v1.27.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-idn.git",
-                "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8"
+                "reference": "639084e360537a19f9ee352433b84ce831f3d2da"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8",
-                "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da",
+                "reference": "639084e360537a19f9ee352433b84ce831f3d2da",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.26-dev"
+                    "dev-main": "1.27-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0"
+                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-24T11:49:31+00:00"
+            "time": "2022-11-03T14:55:06+00:00"
         },
         {
             "name": "symfony/polyfill-intl-normalizer",
-            "version": "v1.26.0",
+            "version": "v1.27.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
-                "reference": "219aa369ceff116e673852dce47c3a41794c14bd"
+                "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd",
-                "reference": "219aa369ceff116e673852dce47c3a41794c14bd",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6",
+                "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.26-dev"
+                    "dev-main": "1.27-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0"
+                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-24T11:49:31+00:00"
+            "time": "2022-11-03T14:55:06+00:00"
         },
         {
             "name": "symfony/polyfill-mbstring",
-            "version": "v1.26.0",
+            "version": "v1.27.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-mbstring.git",
-                "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
+                "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
-                "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
+                "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.26-dev"
+                    "dev-main": "1.27-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-24T11:49:31+00:00"
+            "time": "2022-11-03T14:55:06+00:00"
         },
         {
             "name": "symfony/polyfill-php72",
-            "version": "v1.26.0",
+            "version": "v1.27.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php72.git",
-                "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2"
+                "reference": "869329b1e9894268a8a61dabb69153029b7a8c97"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/bf44a9fd41feaac72b074de600314a93e2ae78e2",
-                "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2",
+                "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97",
+                "reference": "869329b1e9894268a8a61dabb69153029b7a8c97",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.26-dev"
+                    "dev-main": "1.27-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php72/tree/v1.26.0"
+                "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-24T11:49:31+00:00"
+            "time": "2022-11-03T14:55:06+00:00"
         },
         {
             "name": "symfony/polyfill-php73",
-            "version": "v1.26.0",
+            "version": "v1.27.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php73.git",
-                "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85"
+                "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/e440d35fa0286f77fb45b79a03fedbeda9307e85",
-                "reference": "e440d35fa0286f77fb45b79a03fedbeda9307e85",
+                "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9",
+                "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.26-dev"
+                    "dev-main": "1.27-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php73/tree/v1.26.0"
+                "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-24T11:49:31+00:00"
+            "time": "2022-11-03T14:55:06+00:00"
         },
         {
             "name": "symfony/polyfill-php80",
-            "version": "v1.26.0",
+            "version": "v1.27.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php80.git",
-                "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
+                "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
-                "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
+                "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.26-dev"
+                    "dev-main": "1.27-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-10T07:21:04+00:00"
+            "time": "2022-11-03T14:55:06+00:00"
         },
         {
             "name": "symfony/polyfill-php81",
-            "version": "v1.26.0",
+            "version": "v1.27.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-php81.git",
-                "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1"
+                "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1",
-                "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1",
+                "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/707403074c8ea6e2edaf8794b0157a0bfa52157a",
+                "reference": "707403074c8ea6e2edaf8794b0157a0bfa52157a",
                 "shasum": ""
             },
             "require": {
             "type": "library",
             "extra": {
                 "branch-alias": {
-                    "dev-main": "1.26-dev"
+                    "dev-main": "1.27-dev"
                 },
                 "thanks": {
                     "name": "symfony/polyfill",
                 "shim"
             ],
             "support": {
-                "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0"
+                "source": "https://github.com/symfony/polyfill-php81/tree/v1.27.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-05-24T11:49:31+00:00"
+            "time": "2022-11-03T14:55:06+00:00"
         },
         {
             "name": "symfony/process",
         },
         {
             "name": "symfony/routing",
-            "version": "v5.4.11",
+            "version": "v5.4.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/routing.git",
-                "reference": "3e01ccd9b2a3a4167ba2b3c53612762300300226"
+                "reference": "5c9b129efe9abce9470e384bf65d8a7e262eee69"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/routing/zipball/3e01ccd9b2a3a4167ba2b3c53612762300300226",
-                "reference": "3e01ccd9b2a3a4167ba2b3c53612762300300226",
+                "url": "https://api.github.com/repos/symfony/routing/zipball/5c9b129efe9abce9470e384bf65d8a7e262eee69",
+                "reference": "5c9b129efe9abce9470e384bf65d8a7e262eee69",
                 "shasum": ""
             },
             "require": {
                 "url"
             ],
             "support": {
-                "source": "https://github.com/symfony/routing/tree/v5.4.11"
+                "source": "https://github.com/symfony/routing/tree/v5.4.15"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-07-20T13:00:38+00:00"
+            "time": "2022-10-13T14:10:41+00:00"
         },
         {
             "name": "symfony/service-contracts",
         },
         {
             "name": "symfony/string",
-            "version": "v5.4.14",
+            "version": "v5.4.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/string.git",
-                "reference": "089e7237497fae7a9c404d0c3aeb8db3254733e4"
+                "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/string/zipball/089e7237497fae7a9c404d0c3aeb8db3254733e4",
-                "reference": "089e7237497fae7a9c404d0c3aeb8db3254733e4",
+                "url": "https://api.github.com/repos/symfony/string/zipball/571334ce9f687e3e6af72db4d3b2a9431e4fd9ed",
+                "reference": "571334ce9f687e3e6af72db4d3b2a9431e4fd9ed",
                 "shasum": ""
             },
             "require": {
                 "utf8"
             ],
             "support": {
-                "source": "https://github.com/symfony/string/tree/v5.4.14"
+                "source": "https://github.com/symfony/string/tree/v5.4.15"
             },
             "funding": [
                 {
     "packages-dev": [
         {
             "name": "brianium/paratest",
-            "version": "v6.6.4",
+            "version": "v6.6.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/paratestphp/paratest.git",
-                "reference": "4ce800dc32fd0292a4f05c00f347142dce1ecdda"
+                "reference": "31fd5d69b41725f383c9a083831eefcc7ecd9061"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/paratestphp/paratest/zipball/4ce800dc32fd0292a4f05c00f347142dce1ecdda",
-                "reference": "4ce800dc32fd0292a4f05c00f347142dce1ecdda",
+                "url": "https://api.github.com/repos/paratestphp/paratest/zipball/31fd5d69b41725f383c9a083831eefcc7ecd9061",
+                "reference": "31fd5d69b41725f383c9a083831eefcc7ecd9061",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/paratestphp/paratest/issues",
-                "source": "https://github.com/paratestphp/paratest/tree/v6.6.4"
+                "source": "https://github.com/paratestphp/paratest/tree/v6.6.5"
             },
             "funding": [
                 {
                     "type": "paypal"
                 }
             ],
-            "time": "2022-09-13T10:47:01+00:00"
+            "time": "2022-10-28T12:22:26+00:00"
         },
         {
             "name": "composer/ca-bundle",
         },
         {
             "name": "composer/composer",
-            "version": "2.4.3",
+            "version": "2.4.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/composer.git",
-                "reference": "b34c0e9a93f2cd688c62ce4dfcc69e13b6ce7aa4"
+                "reference": "e8d9087229bcdbc5867594d3098091412f1130cf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/composer/zipball/b34c0e9a93f2cd688c62ce4dfcc69e13b6ce7aa4",
-                "reference": "b34c0e9a93f2cd688c62ce4dfcc69e13b6ce7aa4",
+                "url": "https://api.github.com/repos/composer/composer/zipball/e8d9087229bcdbc5867594d3098091412f1130cf",
+                "reference": "e8d9087229bcdbc5867594d3098091412f1130cf",
                 "shasum": ""
             },
             "require": {
             "support": {
                 "irc": "ircs://irc.libera.chat:6697/composer",
                 "issues": "https://github.com/composer/composer/issues",
-                "source": "https://github.com/composer/composer/tree/2.4.3"
+                "source": "https://github.com/composer/composer/tree/2.4.4"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-10-14T14:56:41+00:00"
+            "time": "2022-10-27T12:39:29+00:00"
         },
         {
             "name": "composer/metadata-minifier",
         },
         {
             "name": "composer/pcre",
-            "version": "3.0.0",
+            "version": "3.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/composer/pcre.git",
-                "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd"
+                "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/composer/pcre/zipball/e300eb6c535192decd27a85bc72a9290f0d6b3bd",
-                "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd",
+                "url": "https://api.github.com/repos/composer/pcre/zipball/4bff79ddd77851fe3cdd11616ed3f92841ba5bd2",
+                "reference": "4bff79ddd77851fe3cdd11616ed3f92841ba5bd2",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/composer/pcre/issues",
-                "source": "https://github.com/composer/pcre/tree/3.0.0"
+                "source": "https://github.com/composer/pcre/tree/3.1.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-02-25T20:21:48+00:00"
+            "time": "2022-11-17T09:50:14+00:00"
         },
         {
             "name": "composer/semver",
         },
         {
             "name": "itsgoingd/clockwork",
-            "version": "v5.1.8",
+            "version": "v5.1.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/itsgoingd/clockwork.git",
-                "reference": "74ee05a61296aa7298164ef5346f0a568aa6106e"
+                "reference": "a790200347f0c6d07e2fca252ccb446df87520c6"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/74ee05a61296aa7298164ef5346f0a568aa6106e",
-                "reference": "74ee05a61296aa7298164ef5346f0a568aa6106e",
+                "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/a790200347f0c6d07e2fca252ccb446df87520c6",
+                "reference": "a790200347f0c6d07e2fca252ccb446df87520c6",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/itsgoingd/clockwork/issues",
-                "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.8"
+                "source": "https://github.com/itsgoingd/clockwork/tree/v5.1.11"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2022-09-25T20:21:14+00:00"
+            "time": "2022-11-02T21:11:04+00:00"
         },
         {
             "name": "jean85/pretty-package-versions",
         },
         {
             "name": "nunomaduro/larastan",
-            "version": "1.0.3",
+            "version": "1.0.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nunomaduro/larastan.git",
-                "reference": "f5ce15319da184a5e461ef12c60489c15a9cf65b"
+                "reference": "769bc6346a6cce3b823c30eaace33d9c3a0dd40e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/f5ce15319da184a5e461ef12c60489c15a9cf65b",
-                "reference": "f5ce15319da184a5e461ef12c60489c15a9cf65b",
+                "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/769bc6346a6cce3b823c30eaace33d9c3a0dd40e",
+                "reference": "769bc6346a6cce3b823c30eaace33d9c3a0dd40e",
                 "shasum": ""
             },
             "require": {
                 "illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0",
                 "mockery/mockery": "^0.9 || ^1.0",
                 "php": "^7.2 || ^8.0",
-                "phpstan/phpstan": "^1.0",
+                "phpstan/phpstan": "^1.0 <1.9",
                 "symfony/process": "^4.3 || ^5.0 || ^6.0"
             },
             "require-dev": {
             ],
             "support": {
                 "issues": "https://github.com/nunomaduro/larastan/issues",
-                "source": "https://github.com/nunomaduro/larastan/tree/1.0.3"
+                "source": "https://github.com/nunomaduro/larastan/tree/1.0.4"
             },
             "funding": [
                 {
                     "type": "patreon"
                 }
             ],
-            "time": "2022-01-20T19:33:48+00:00"
+            "time": "2022-11-09T09:09:31+00:00"
         },
         {
             "name": "phar-io/manifest",
         },
         {
             "name": "phpstan/phpstan",
-            "version": "1.8.9",
+            "version": "1.8.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "3a72d9d9f2528fbd50c2d8fcf155fd9f74ade3f2"
+                "reference": "46e223dd68a620da18855c23046ddb00940b4014"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3a72d9d9f2528fbd50c2d8fcf155fd9f74ade3f2",
-                "reference": "3a72d9d9f2528fbd50c2d8fcf155fd9f74ade3f2",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46e223dd68a620da18855c23046ddb00940b4014",
+                "reference": "46e223dd68a620da18855c23046ddb00940b4014",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/phpstan/phpstan/issues",
-                "source": "https://github.com/phpstan/phpstan/tree/1.8.9"
+                "source": "https://github.com/phpstan/phpstan/tree/1.8.11"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-10-13T13:40:18+00:00"
+            "time": "2022-10-24T15:45:13+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "9.2.17",
+            "version": "9.2.19",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8"
+                "reference": "c77b56b63e3d2031bd8997fcec43c1925ae46559"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa94dc41e8661fe90c7316849907cba3007b10d8",
-                "reference": "aa94dc41e8661fe90c7316849907cba3007b10d8",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c77b56b63e3d2031bd8997fcec43c1925ae46559",
+                "reference": "c77b56b63e3d2031bd8997fcec43c1925ae46559",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.17"
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.19"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2022-08-30T12:24:04+00:00"
+            "time": "2022-11-18T07:47:47+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.5.25",
+            "version": "9.5.26",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d"
+                "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d",
-                "reference": "3e6f90ca7e3d02025b1d147bd8d4a89fd4ca8a1d",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/851867efcbb6a1b992ec515c71cdcf20d895e9d2",
+                "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.25"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.26"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-09-25T03:44:45+00:00"
+            "time": "2022-10-28T06:00:21+00:00"
         },
         {
             "name": "react/promise",
         },
         {
             "name": "symfony/dom-crawler",
-            "version": "v5.4.12",
+            "version": "v5.4.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/dom-crawler.git",
-                "reference": "291c1e92281a09152dda089f782e23dedd34bd4f"
+                "reference": "b8fd0ff9a0f00d944f1534f6d21e84f92eda7258"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/291c1e92281a09152dda089f782e23dedd34bd4f",
-                "reference": "291c1e92281a09152dda089f782e23dedd34bd4f",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b8fd0ff9a0f00d944f1534f6d21e84f92eda7258",
+                "reference": "b8fd0ff9a0f00d944f1534f6d21e84f92eda7258",
                 "shasum": ""
             },
             "require": {
             "description": "Eases DOM navigation for HTML and XML documents",
             "homepage": "https://symfony.com",
             "support": {
-                "source": "https://github.com/symfony/dom-crawler/tree/v5.4.12"
+                "source": "https://github.com/symfony/dom-crawler/tree/v5.4.15"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-08-03T13:09:21+00:00"
+            "time": "2022-10-27T08:04:35+00:00"
         },
         {
             "name": "symfony/filesystem",
index 3d47a1ad8a240e6fe9c1fc48826809018807856a..a1092ce92569ffad21aec73471fbde7b8b621b93 100644 (file)
@@ -24,7 +24,7 @@ class Dropdown {
 
 All usage of $refs, $manyRefs and $opts should be done at the top of the `setup` function so any requirements can be easily seen.
 
-Once defined, the component has to be registered for use. This is done in the `resources/js/components/index.js` file. You'll need to import the component class then add it to `componentMapping` object, following the pattern of other components. 
+Once defined, the component has to be registered for use. This is done in the `resources/js/components/index.js` file by defining an additional export, following the pattern of other components. 
 
 ### Using a Component in HTML
 
@@ -80,9 +80,9 @@ Will result with `this.$opts` being:
 }
 ```
 
-#### Component Properties
+#### Component Properties & Methods
 
-A component has the below shown properties available for use. As mentioned above, most of these should be used within the `setup()` function to make the requirements/dependencies of the component clear.
+A component has the below shown properties & methods available for use. As mentioned above, most of these should be used within the `setup()` function to make the requirements/dependencies of the component clear.
 
 ```javascript
 // The root element that the compontent has been applied to.
@@ -98,6 +98,15 @@ this.$manyRefs
 
 // Options defined for the compontent.
 this.$opts
+
+// The registered name of the component, usually kebab-case.
+this.$name
+
+// Emit a custom event from this component.
+// Will be bubbled up from the dom element this is registered on, 
+// as a custom event with the name `<elementName>-<eventName>`,
+// with the provided data in the event detail.
+this.$emit(eventName, data = {})
 ```
 
 ## Global JavaScript Helpers
@@ -132,7 +141,16 @@ window.trans_plural(translationString, count, replacements);
 
 // Component System
 // Parse and initialise any components from the given root el down.
-window.components.init(rootEl);
-// Get the first active component of the given name
-window.components.first(name);
+window.$components.init(rootEl);
+// Register component models to be used by the component system.
+// Takes a mapping of classes/constructors keyed by component names.
+// Names will be converted to kebab-case.
+window.$components.register(mapping);
+// Get the first active component of the given name.
+window.$components.first(name);
+// Get all the active components of the given name. 
+window.$components.get(name);
+// Get the first active component of the given name that's been
+// created on the given element.
+window.$components.firstOnElement(element, name);
 ```
\ No newline at end of file
index 1448d592fa0f72b3f40b39fc4b86d033fb402928..dbe0f90d20595a491fc8b5eea662d809717a4e07 100644 (file)
       },
       "devDependencies": {
         "chokidar-cli": "^3.0",
-        "esbuild": "0.14.42",
+        "esbuild": "^0.15.12",
         "livereload": "^0.9.3",
         "npm-run-all": "^4.1.5",
         "punycode": "^2.1.1",
-        "sass": "^1.52.1"
+        "sass": "^1.55.0"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.12.tgz",
+      "integrity": "sha512-IC7TqIqiyE0MmvAhWkl/8AEzpOtbhRNDo7aph47We1NbE5w2bt/Q+giAhe0YYeVpYnIhGMcuZY92qDK6dQauvA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.12.tgz",
+      "integrity": "sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
       }
     },
     "node_modules/ansi-regex": {
       }
     },
     "node_modules/chokidar": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
-      "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
-      "dev": true,
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://paulmillr.com/funding/"
+        }
+      ],
       "dependencies": {
         "anymatch": "~3.1.2",
         "braces": "~3.0.2",
       }
     },
     "node_modules/codemirror": {
-      "version": "5.65.5",
-      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.5.tgz",
-      "integrity": "sha512-HNyhvGLnYz5c+kIsB9QKVitiZUevha3ovbIYaQiGzKo7ECSL/elWD9RXt3JgNr0NdnyqE9/Rc/7uLfkJQL638w=="
+      "version": "5.65.9",
+      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz",
+      "integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw=="
     },
     "node_modules/color-convert": {
       "version": "1.9.3",
     "node_modules/color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
       "dev": true
     },
     "node_modules/concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
       "dev": true
     },
     "node_modules/cross-spawn": {
     "node_modules/decamelize": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
       "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "node_modules/define-properties": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
-      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
+      "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
       "dev": true,
       "dependencies": {
-        "object-keys": "^1.0.12"
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
       },
       "engines": {
         "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/delegate": {
       }
     },
     "node_modules/es-abstract": {
-      "version": "1.19.1",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
-      "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
+      "version": "1.20.4",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz",
+      "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
         "es-to-primitive": "^1.2.1",
         "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.1.1",
+        "function.prototype.name": "^1.1.5",
+        "get-intrinsic": "^1.1.3",
         "get-symbol-description": "^1.0.0",
         "has": "^1.0.3",
-        "has-symbols": "^1.0.2",
+        "has-property-descriptors": "^1.0.0",
+        "has-symbols": "^1.0.3",
         "internal-slot": "^1.0.3",
-        "is-callable": "^1.2.4",
-        "is-negative-zero": "^2.0.1",
+        "is-callable": "^1.2.7",
+        "is-negative-zero": "^2.0.2",
         "is-regex": "^1.1.4",
-        "is-shared-array-buffer": "^1.0.1",
+        "is-shared-array-buffer": "^1.0.2",
         "is-string": "^1.0.7",
-        "is-weakref": "^1.0.1",
-        "object-inspect": "^1.11.0",
+        "is-weakref": "^1.0.2",
+        "object-inspect": "^1.12.2",
         "object-keys": "^1.1.1",
-        "object.assign": "^4.1.2",
-        "string.prototype.trimend": "^1.0.4",
-        "string.prototype.trimstart": "^1.0.4",
-        "unbox-primitive": "^1.0.1"
+        "object.assign": "^4.1.4",
+        "regexp.prototype.flags": "^1.4.3",
+        "safe-regex-test": "^1.0.0",
+        "string.prototype.trimend": "^1.0.5",
+        "string.prototype.trimstart": "^1.0.5",
+        "unbox-primitive": "^1.0.2"
       },
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/esbuild": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz",
-      "integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.12.tgz",
+      "integrity": "sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==",
       "dev": true,
       "hasInstallScript": true,
       "bin": {
         "node": ">=12"
       },
       "optionalDependencies": {
-        "esbuild-android-64": "0.14.42",
-        "esbuild-android-arm64": "0.14.42",
-        "esbuild-darwin-64": "0.14.42",
-        "esbuild-darwin-arm64": "0.14.42",
-        "esbuild-freebsd-64": "0.14.42",
-        "esbuild-freebsd-arm64": "0.14.42",
-        "esbuild-linux-32": "0.14.42",
-        "esbuild-linux-64": "0.14.42",
-        "esbuild-linux-arm": "0.14.42",
-        "esbuild-linux-arm64": "0.14.42",
-        "esbuild-linux-mips64le": "0.14.42",
-        "esbuild-linux-ppc64le": "0.14.42",
-        "esbuild-linux-riscv64": "0.14.42",
-        "esbuild-linux-s390x": "0.14.42",
-        "esbuild-netbsd-64": "0.14.42",
-        "esbuild-openbsd-64": "0.14.42",
-        "esbuild-sunos-64": "0.14.42",
-        "esbuild-windows-32": "0.14.42",
-        "esbuild-windows-64": "0.14.42",
-        "esbuild-windows-arm64": "0.14.42"
+        "@esbuild/android-arm": "0.15.12",
+        "@esbuild/linux-loong64": "0.15.12",
+        "esbuild-android-64": "0.15.12",
+        "esbuild-android-arm64": "0.15.12",
+        "esbuild-darwin-64": "0.15.12",
+        "esbuild-darwin-arm64": "0.15.12",
+        "esbuild-freebsd-64": "0.15.12",
+        "esbuild-freebsd-arm64": "0.15.12",
+        "esbuild-linux-32": "0.15.12",
+        "esbuild-linux-64": "0.15.12",
+        "esbuild-linux-arm": "0.15.12",
+        "esbuild-linux-arm64": "0.15.12",
+        "esbuild-linux-mips64le": "0.15.12",
+        "esbuild-linux-ppc64le": "0.15.12",
+        "esbuild-linux-riscv64": "0.15.12",
+        "esbuild-linux-s390x": "0.15.12",
+        "esbuild-netbsd-64": "0.15.12",
+        "esbuild-openbsd-64": "0.15.12",
+        "esbuild-sunos-64": "0.15.12",
+        "esbuild-windows-32": "0.15.12",
+        "esbuild-windows-64": "0.15.12",
+        "esbuild-windows-arm64": "0.15.12"
       }
     },
     "node_modules/esbuild-android-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz",
-      "integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.12.tgz",
+      "integrity": "sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/esbuild-android-arm64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz",
-      "integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.12.tgz",
+      "integrity": "sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/esbuild-darwin-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz",
-      "integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.12.tgz",
+      "integrity": "sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/esbuild-darwin-arm64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz",
-      "integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.12.tgz",
+      "integrity": "sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/esbuild-freebsd-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz",
-      "integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.12.tgz",
+      "integrity": "sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/esbuild-freebsd-arm64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz",
-      "integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.12.tgz",
+      "integrity": "sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/esbuild-linux-32": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz",
-      "integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.12.tgz",
+      "integrity": "sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==",
       "cpu": [
         "ia32"
       ],
       }
     },
     "node_modules/esbuild-linux-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz",
-      "integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.12.tgz",
+      "integrity": "sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/esbuild-linux-arm": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz",
-      "integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.12.tgz",
+      "integrity": "sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==",
       "cpu": [
         "arm"
       ],
       }
     },
     "node_modules/esbuild-linux-arm64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz",
-      "integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.12.tgz",
+      "integrity": "sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/esbuild-linux-mips64le": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz",
-      "integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.12.tgz",
+      "integrity": "sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==",
       "cpu": [
         "mips64el"
       ],
       }
     },
     "node_modules/esbuild-linux-ppc64le": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz",
-      "integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.12.tgz",
+      "integrity": "sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==",
       "cpu": [
         "ppc64"
       ],
       }
     },
     "node_modules/esbuild-linux-riscv64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz",
-      "integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.12.tgz",
+      "integrity": "sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==",
       "cpu": [
         "riscv64"
       ],
       }
     },
     "node_modules/esbuild-linux-s390x": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz",
-      "integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.12.tgz",
+      "integrity": "sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==",
       "cpu": [
         "s390x"
       ],
       }
     },
     "node_modules/esbuild-netbsd-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz",
-      "integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.12.tgz",
+      "integrity": "sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/esbuild-openbsd-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz",
-      "integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.12.tgz",
+      "integrity": "sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/esbuild-sunos-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz",
-      "integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.12.tgz",
+      "integrity": "sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/esbuild-windows-32": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz",
-      "integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.12.tgz",
+      "integrity": "sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==",
       "cpu": [
         "ia32"
       ],
       }
     },
     "node_modules/esbuild-windows-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz",
-      "integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.12.tgz",
+      "integrity": "sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/esbuild-windows-arm64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz",
-      "integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.12.tgz",
+      "integrity": "sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==",
       "cpu": [
         "arm64"
       ],
     "node_modules/escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
       "dev": true,
       "engines": {
         "node": ">=0.8.0"
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
       "dev": true
     },
+    "node_modules/function.prototype.name": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+      "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.19.0",
+        "functions-have-names": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/functions-have-names": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/get-caller-file": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
-      "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
+      "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
       "dev": true,
       "dependencies": {
         "function-bind": "^1.1.1",
         "has": "^1.0.3",
-        "has-symbols": "^1.0.1"
+        "has-symbols": "^1.0.3"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
     "node_modules/good-listener": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
-      "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
       "dependencies": {
         "delegate": "^3.1.2"
       }
     },
     "node_modules/graceful-fs": {
-      "version": "4.2.8",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
-      "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
+      "version": "4.2.10",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
       "dev": true
     },
     "node_modules/has": {
       }
     },
     "node_modules/has-bigints": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
-      "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+      "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
       "dev": true,
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
     "node_modules/has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
       "dev": true,
       "engines": {
         "node": ">=4"
       }
     },
+    "node_modules/has-property-descriptors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+      "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+      "dev": true,
+      "dependencies": {
+        "get-intrinsic": "^1.1.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/has-symbols": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
-      "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
       "dev": true,
       "engines": {
         "node": ">= 0.4"
       "dev": true
     },
     "node_modules/immutable": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
-      "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
+      "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
       "dev": true
     },
     "node_modules/internal-slot": {
     "node_modules/is-arrayish": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
       "dev": true
     },
     "node_modules/is-bigint": {
       }
     },
     "node_modules/is-callable": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
-      "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
       "dev": true,
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/is-core-module": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
-      "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+      "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
       "dev": true,
       "dependencies": {
         "has": "^1.0.3"
     "node_modules/is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
       "dev": true,
       "engines": {
         "node": ">=0.10.0"
     "node_modules/is-fullwidth-code-point": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+      "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
       "dev": true,
       "engines": {
         "node": ">=4"
       }
     },
     "node_modules/is-negative-zero": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
-      "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+      "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
       "dev": true,
       "engines": {
         "node": ">= 0.4"
       }
     },
     "node_modules/is-number-object": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
-      "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+      "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
       "dev": true,
       "dependencies": {
         "has-tostringtag": "^1.0.0"
       }
     },
     "node_modules/is-shared-array-buffer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
-      "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+      "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
       "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2"
+      },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
       }
     },
     "node_modules/is-weakref": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz",
-      "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+      "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.0"
+        "call-bind": "^1.0.2"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
     "node_modules/isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
       "dev": true
     },
     "node_modules/json-parse-better-errors": {
       }
     },
     "node_modules/livereload-js": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.2.tgz",
-      "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==",
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.4.1.tgz",
+      "integrity": "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==",
       "dev": true
     },
     "node_modules/load-json-file": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
-      "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=",
+      "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
       "dev": true,
       "dependencies": {
         "graceful-fs": "^4.1.2",
     "node_modules/lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
-      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
+      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
       "dev": true
     },
     "node_modules/lodash.throttle": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
-      "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=",
+      "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
       "dev": true
     },
     "node_modules/markdown-it": {
     "node_modules/mdurl": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
-      "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
+      "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
     },
     "node_modules/memorystream": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
-      "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=",
+      "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
       "dev": true,
       "engines": {
         "node": ">= 0.10.0"
       }
     },
     "node_modules/minimatch": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
       "dev": true,
       "dependencies": {
         "brace-expansion": "^1.1.7"
       }
     },
     "node_modules/object-inspect": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz",
-      "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==",
+      "version": "1.12.2",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
+      "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
       "dev": true,
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/object.assign": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
-      "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+      "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
       "dev": true,
       "dependencies": {
-        "call-bind": "^1.0.0",
-        "define-properties": "^1.1.3",
-        "has-symbols": "^1.0.1",
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.4",
+        "has-symbols": "^1.0.3",
         "object-keys": "^1.1.1"
       },
       "engines": {
     "node_modules/parse-json": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
-      "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+      "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
       "dev": true,
       "dependencies": {
         "error-ex": "^1.3.1",
     "node_modules/path-exists": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
-      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+      "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
       "dev": true,
       "engines": {
         "node": ">=4"
     "node_modules/path-key": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
-      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+      "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
       "dev": true,
       "engines": {
         "node": ">=4"
       }
     },
     "node_modules/picomatch": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
-      "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
       "dev": true,
       "engines": {
         "node": ">=8.6"
     "node_modules/pify": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
-      "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+      "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
       "dev": true,
       "engines": {
         "node": ">=4"
     "node_modules/read-pkg": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
-      "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=",
+      "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==",
       "dev": true,
       "dependencies": {
         "load-json-file": "^4.0.0",
         "node": ">=8.10.0"
       }
     },
+    "node_modules/regexp.prototype.flags": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
+      "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.3",
+        "functions-have-names": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
       "dev": true,
       "engines": {
         "node": ">=0.10.0"
       "dev": true
     },
     "node_modules/resolve": {
-      "version": "1.20.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
-      "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+      "version": "1.22.1",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+      "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
       "dev": true,
       "dependencies": {
-        "is-core-module": "^2.2.0",
-        "path-parse": "^1.0.6"
+        "is-core-module": "^2.9.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/safe-regex-test": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+      "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "get-intrinsic": "^1.1.3",
+        "is-regex": "^1.1.4"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/sass": {
-      "version": "1.52.1",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.52.1.tgz",
-      "integrity": "sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q==",
+      "version": "1.55.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz",
+      "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==",
       "dev": true,
       "dependencies": {
         "chokidar": ">=3.0.0 <4.0.0",
     "node_modules/select": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
-      "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
     },
     "node_modules/semver": {
       "version": "5.7.1",
     "node_modules/set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
       "dev": true
     },
     "node_modules/shebang-command": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
-      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
       "dev": true,
       "dependencies": {
         "shebang-regex": "^1.0.0"
     "node_modules/shebang-regex": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
-      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
       "dev": true,
       "engines": {
         "node": ">=0.10.0"
       }
     },
     "node_modules/shell-quote": {
-      "version": "1.7.3",
-      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
-      "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
-      "dev": true
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz",
+      "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==",
+      "dev": true,
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
     },
     "node_modules/side-channel": {
       "version": "1.0.4",
       }
     },
     "node_modules/snabbdom": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.0.tgz",
-      "integrity": "sha512-Ff5BKG18KrrPuskHJlA9aujPHqEabItaDl96l7ZZndF4zt5AYSczz7ZjjgQAX5IBd5cd25lw9NfgX21yVUJ+9g==",
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz",
+      "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==",
       "engines": {
         "node": ">=8.3.0"
       }
       }
     },
     "node_modules/spdx-license-ids": {
-      "version": "3.0.10",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz",
-      "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
+      "version": "3.0.12",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz",
+      "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==",
       "dev": true
     },
     "node_modules/string-width": {
       }
     },
     "node_modules/string.prototype.trimend": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
-      "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
+      "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
-        "define-properties": "^1.1.3"
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.19.5"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/string.prototype.trimstart": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
-      "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
+      "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
       "dev": true,
       "dependencies": {
         "call-bind": "^1.0.2",
-        "define-properties": "^1.1.3"
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.19.5"
       },
       "funding": {
         "url": "https://github.com/sponsors/ljharb"
     "node_modules/strip-bom": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
-      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
       "dev": true,
       "engines": {
         "node": ">=4"
         "node": ">=4"
       }
     },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/tiny-emitter": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
       "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
     },
     "node_modules/unbox-primitive": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
-      "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+      "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
       "dev": true,
       "dependencies": {
-        "function-bind": "^1.1.1",
-        "has-bigints": "^1.0.1",
-        "has-symbols": "^1.0.2",
+        "call-bind": "^1.0.2",
+        "has-bigints": "^1.0.2",
+        "has-symbols": "^1.0.3",
         "which-boxed-primitive": "^1.0.2"
       },
       "funding": {
     "node_modules/which-module": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
-      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+      "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==",
       "dev": true
     },
     "node_modules/wrap-ansi": {
       }
     },
     "node_modules/ws": {
-      "version": "7.5.5",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
-      "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
+      "version": "7.5.9",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
+      "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
       "dev": true,
       "engines": {
         "node": ">=8.3.0"
     }
   },
   "dependencies": {
+    "@esbuild/android-arm": {
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.12.tgz",
+      "integrity": "sha512-IC7TqIqiyE0MmvAhWkl/8AEzpOtbhRNDo7aph47We1NbE5w2bt/Q+giAhe0YYeVpYnIhGMcuZY92qDK6dQauvA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-loong64": {
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.12.tgz",
+      "integrity": "sha512-tZEowDjvU7O7I04GYvWQOS4yyP9E/7YlsB0jjw1Ycukgr2ycEzKyIk5tms5WnLBymaewc6VmRKnn5IJWgK4eFw==",
+      "dev": true,
+      "optional": true
+    },
     "ansi-regex": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
       }
     },
     "chokidar": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
-      "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
       "dev": true,
       "requires": {
         "anymatch": "~3.1.2",
       }
     },
     "codemirror": {
-      "version": "5.65.5",
-      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.5.tgz",
-      "integrity": "sha512-HNyhvGLnYz5c+kIsB9QKVitiZUevha3ovbIYaQiGzKo7ECSL/elWD9RXt3JgNr0NdnyqE9/Rc/7uLfkJQL638w=="
+      "version": "5.65.9",
+      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.9.tgz",
+      "integrity": "sha512-19Jox5sAKpusTDgqgKB5dawPpQcY+ipQK7xoEI+MVucEF9qqFaXpeqY1KaoyGBso/wHQoDa4HMMxMjdsS3Zzzw=="
     },
     "color-convert": {
       "version": "1.9.3",
     "color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+      "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
       "dev": true
     },
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
       "dev": true
     },
     "cross-spawn": {
     "decamelize": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
       "dev": true
     },
     "define-properties": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
-      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz",
+      "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==",
       "dev": true,
       "requires": {
-        "object-keys": "^1.0.12"
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
       }
     },
     "delegate": {
       }
     },
     "es-abstract": {
-      "version": "1.19.1",
-      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz",
-      "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==",
+      "version": "1.20.4",
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz",
+      "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==",
       "dev": true,
       "requires": {
         "call-bind": "^1.0.2",
         "es-to-primitive": "^1.2.1",
         "function-bind": "^1.1.1",
-        "get-intrinsic": "^1.1.1",
+        "function.prototype.name": "^1.1.5",
+        "get-intrinsic": "^1.1.3",
         "get-symbol-description": "^1.0.0",
         "has": "^1.0.3",
-        "has-symbols": "^1.0.2",
+        "has-property-descriptors": "^1.0.0",
+        "has-symbols": "^1.0.3",
         "internal-slot": "^1.0.3",
-        "is-callable": "^1.2.4",
-        "is-negative-zero": "^2.0.1",
+        "is-callable": "^1.2.7",
+        "is-negative-zero": "^2.0.2",
         "is-regex": "^1.1.4",
-        "is-shared-array-buffer": "^1.0.1",
+        "is-shared-array-buffer": "^1.0.2",
         "is-string": "^1.0.7",
-        "is-weakref": "^1.0.1",
-        "object-inspect": "^1.11.0",
+        "is-weakref": "^1.0.2",
+        "object-inspect": "^1.12.2",
         "object-keys": "^1.1.1",
-        "object.assign": "^4.1.2",
-        "string.prototype.trimend": "^1.0.4",
-        "string.prototype.trimstart": "^1.0.4",
-        "unbox-primitive": "^1.0.1"
+        "object.assign": "^4.1.4",
+        "regexp.prototype.flags": "^1.4.3",
+        "safe-regex-test": "^1.0.0",
+        "string.prototype.trimend": "^1.0.5",
+        "string.prototype.trimstart": "^1.0.5",
+        "unbox-primitive": "^1.0.2"
       }
     },
     "es-to-primitive": {
       }
     },
     "esbuild": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.42.tgz",
-      "integrity": "sha512-V0uPZotCEHokJdNqyozH6qsaQXqmZEOiZWrXnds/zaH/0SyrIayRXWRB98CENO73MIZ9T3HBIOsmds5twWtmgw==",
-      "dev": true,
-      "requires": {
-        "esbuild-android-64": "0.14.42",
-        "esbuild-android-arm64": "0.14.42",
-        "esbuild-darwin-64": "0.14.42",
-        "esbuild-darwin-arm64": "0.14.42",
-        "esbuild-freebsd-64": "0.14.42",
-        "esbuild-freebsd-arm64": "0.14.42",
-        "esbuild-linux-32": "0.14.42",
-        "esbuild-linux-64": "0.14.42",
-        "esbuild-linux-arm": "0.14.42",
-        "esbuild-linux-arm64": "0.14.42",
-        "esbuild-linux-mips64le": "0.14.42",
-        "esbuild-linux-ppc64le": "0.14.42",
-        "esbuild-linux-riscv64": "0.14.42",
-        "esbuild-linux-s390x": "0.14.42",
-        "esbuild-netbsd-64": "0.14.42",
-        "esbuild-openbsd-64": "0.14.42",
-        "esbuild-sunos-64": "0.14.42",
-        "esbuild-windows-32": "0.14.42",
-        "esbuild-windows-64": "0.14.42",
-        "esbuild-windows-arm64": "0.14.42"
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.12.tgz",
+      "integrity": "sha512-PcT+/wyDqJQsRVhaE9uX/Oq4XLrFh0ce/bs2TJh4CSaw9xuvI+xFrH2nAYOADbhQjUgAhNWC5LKoUsakm4dxng==",
+      "dev": true,
+      "requires": {
+        "@esbuild/android-arm": "0.15.12",
+        "@esbuild/linux-loong64": "0.15.12",
+        "esbuild-android-64": "0.15.12",
+        "esbuild-android-arm64": "0.15.12",
+        "esbuild-darwin-64": "0.15.12",
+        "esbuild-darwin-arm64": "0.15.12",
+        "esbuild-freebsd-64": "0.15.12",
+        "esbuild-freebsd-arm64": "0.15.12",
+        "esbuild-linux-32": "0.15.12",
+        "esbuild-linux-64": "0.15.12",
+        "esbuild-linux-arm": "0.15.12",
+        "esbuild-linux-arm64": "0.15.12",
+        "esbuild-linux-mips64le": "0.15.12",
+        "esbuild-linux-ppc64le": "0.15.12",
+        "esbuild-linux-riscv64": "0.15.12",
+        "esbuild-linux-s390x": "0.15.12",
+        "esbuild-netbsd-64": "0.15.12",
+        "esbuild-openbsd-64": "0.15.12",
+        "esbuild-sunos-64": "0.15.12",
+        "esbuild-windows-32": "0.15.12",
+        "esbuild-windows-64": "0.15.12",
+        "esbuild-windows-arm64": "0.15.12"
       }
     },
     "esbuild-android-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.42.tgz",
-      "integrity": "sha512-P4Y36VUtRhK/zivqGVMqhptSrFILAGlYp0Z8r9UQqHJ3iWztRCNWnlBzD9HRx0DbueXikzOiwyOri+ojAFfW6A==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.12.tgz",
+      "integrity": "sha512-MJKXwvPY9g0rGps0+U65HlTsM1wUs9lbjt5CU19RESqycGFDRijMDQsh68MtbzkqWSRdEtiKS1mtPzKneaAI0Q==",
       "dev": true,
       "optional": true
     },
     "esbuild-android-arm64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.42.tgz",
-      "integrity": "sha512-0cOqCubq+RWScPqvtQdjXG3Czb3AWI2CaKw3HeXry2eoA2rrPr85HF7IpdU26UWdBXgPYtlTN1LUiuXbboROhg==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.12.tgz",
+      "integrity": "sha512-Hc9SEcZbIMhhLcvhr1DH+lrrec9SFTiRzfJ7EGSBZiiw994gfkVV6vG0sLWqQQ6DD7V4+OggB+Hn0IRUdDUqvA==",
       "dev": true,
       "optional": true
     },
     "esbuild-darwin-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.42.tgz",
-      "integrity": "sha512-ipiBdCA3ZjYgRfRLdQwP82rTiv/YVMtW36hTvAN5ZKAIfxBOyPXY7Cejp3bMXWgzKD8B6O+zoMzh01GZsCuEIA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.12.tgz",
+      "integrity": "sha512-qkmqrTVYPFiePt5qFjP8w/S+GIUMbt6k8qmiPraECUWfPptaPJUGkCKrWEfYFRWB7bY23FV95rhvPyh/KARP8Q==",
       "dev": true,
       "optional": true
     },
     "esbuild-darwin-arm64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.42.tgz",
-      "integrity": "sha512-bU2tHRqTPOaoH/4m0zYHbFWpiYDmaA0gt90/3BMEFaM0PqVK/a6MA2V/ypV5PO0v8QxN6gH5hBPY4YJ2lopXgA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.12.tgz",
+      "integrity": "sha512-z4zPX02tQ41kcXMyN3c/GfZpIjKoI/BzHrdKUwhC/Ki5BAhWv59A9M8H+iqaRbwpzYrYidTybBwiZAIWCLJAkw==",
       "dev": true,
       "optional": true
     },
     "esbuild-freebsd-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.42.tgz",
-      "integrity": "sha512-75h1+22Ivy07+QvxHyhVqOdekupiTZVLN1PMwCDonAqyXd8TVNJfIRFrdL8QmSJrOJJ5h8H1I9ETyl2L8LQDaw==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.12.tgz",
+      "integrity": "sha512-XFL7gKMCKXLDiAiBjhLG0XECliXaRLTZh6hsyzqUqPUf/PY4C6EJDTKIeqqPKXaVJ8+fzNek88285krSz1QECw==",
       "dev": true,
       "optional": true
     },
     "esbuild-freebsd-arm64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.42.tgz",
-      "integrity": "sha512-W6Jebeu5TTDQMJUJVarEzRU9LlKpNkPBbjqSu+GUPTHDCly5zZEQq9uHkmHHl7OKm+mQ2zFySN83nmfCeZCyNA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.12.tgz",
+      "integrity": "sha512-jwEIu5UCUk6TjiG1X+KQnCGISI+ILnXzIzt9yDVrhjug2fkYzlLbl0K43q96Q3KB66v6N1UFF0r5Ks4Xo7i72g==",
       "dev": true,
       "optional": true
     },
     "esbuild-linux-32": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.42.tgz",
-      "integrity": "sha512-Ooy/Bj+mJ1z4jlWcK5Dl6SlPlCgQB9zg1UrTCeY8XagvuWZ4qGPyYEWGkT94HUsRi2hKsXvcs6ThTOjBaJSMfg==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.12.tgz",
+      "integrity": "sha512-uSQuSEyF1kVzGzuIr4XM+v7TPKxHjBnLcwv2yPyCz8riV8VUCnO/C4BF3w5dHiVpCd5Z1cebBtZJNlC4anWpwA==",
       "dev": true,
       "optional": true
     },
     "esbuild-linux-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.42.tgz",
-      "integrity": "sha512-2L0HbzQfbTuemUWfVqNIjOfaTRt9zsvjnme6lnr7/MO9toz/MJ5tZhjqrG6uDWDxhsaHI2/nsDgrv8uEEN2eoA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.12.tgz",
+      "integrity": "sha512-QcgCKb7zfJxqT9o5z9ZUeGH1k8N6iX1Y7VNsEi5F9+HzN1OIx7ESxtQXDN9jbeUSPiRH1n9cw6gFT3H4qbdvcA==",
       "dev": true,
       "optional": true
     },
     "esbuild-linux-arm": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.42.tgz",
-      "integrity": "sha512-STq69yzCMhdRaWnh29UYrLSr/qaWMm/KqwaRF1pMEK7kDiagaXhSL1zQGXbYv94GuGY/zAwzK98+6idCMUOOCg==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.12.tgz",
+      "integrity": "sha512-Wf7T0aNylGcLu7hBnzMvsTfEXdEdJY/hY3u36Vla21aY66xR0MS5I1Hw8nVquXjTN0A6fk/vnr32tkC/C2lb0A==",
       "dev": true,
       "optional": true
     },
     "esbuild-linux-arm64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.42.tgz",
-      "integrity": "sha512-c3Ug3e9JpVr8jAcfbhirtpBauLxzYPpycjWulD71CF6ZSY26tvzmXMJYooQ2YKqDY4e/fPu5K8bm7MiXMnyxuA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.12.tgz",
+      "integrity": "sha512-HtNq5xm8fUpZKwWKS2/YGwSfTF+339L4aIA8yphNKYJckd5hVdhfdl6GM2P3HwLSCORS++++7++//ApEwXEuAQ==",
       "dev": true,
       "optional": true
     },
     "esbuild-linux-mips64le": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.42.tgz",
-      "integrity": "sha512-QuvpHGbYlkyXWf2cGm51LBCHx6eUakjaSrRpUqhPwjh/uvNUYvLmz2LgPTTPwCqaKt0iwL+OGVL0tXA5aDbAbg==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.12.tgz",
+      "integrity": "sha512-Qol3+AvivngUZkTVFgLpb0H6DT+N5/zM3V1YgTkryPYFeUvuT5JFNDR3ZiS6LxhyF8EE+fiNtzwlPqMDqVcc6A==",
       "dev": true,
       "optional": true
     },
     "esbuild-linux-ppc64le": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.42.tgz",
-      "integrity": "sha512-8ohIVIWDbDT+i7lCx44YCyIRrOW1MYlks9fxTo0ME2LS/fxxdoJBwHWzaDYhjvf8kNpA+MInZvyOEAGoVDrMHg==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.12.tgz",
+      "integrity": "sha512-4D8qUCo+CFKaR0cGXtGyVsOI7w7k93Qxb3KFXWr75An0DHamYzq8lt7TNZKoOq/Gh8c40/aKaxvcZnTgQ0TJNg==",
       "dev": true,
       "optional": true
     },
     "esbuild-linux-riscv64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.42.tgz",
-      "integrity": "sha512-DzDqK3TuoXktPyG1Lwx7vhaF49Onv3eR61KwQyxYo4y5UKTpL3NmuarHSIaSVlTFDDpcIajCDwz5/uwKLLgKiQ==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.12.tgz",
+      "integrity": "sha512-G9w6NcuuCI6TUUxe6ka0enjZHDnSVK8bO+1qDhMOCtl7Tr78CcZilJj8SGLN00zO5iIlwNRZKHjdMpfFgNn1VA==",
       "dev": true,
       "optional": true
     },
     "esbuild-linux-s390x": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.42.tgz",
-      "integrity": "sha512-YFRhPCxl8nb//Wn6SiS5pmtplBi4z9yC2gLrYoYI/tvwuB1jldir9r7JwAGy1Ck4D7sE7wBN9GFtUUX/DLdcEQ==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.12.tgz",
+      "integrity": "sha512-Lt6BDnuXbXeqSlVuuUM5z18GkJAZf3ERskGZbAWjrQoi9xbEIsj/hEzVnSAFLtkfLuy2DE4RwTcX02tZFunXww==",
       "dev": true,
       "optional": true
     },
     "esbuild-netbsd-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.42.tgz",
-      "integrity": "sha512-QYSD2k+oT9dqB/4eEM9c+7KyNYsIPgzYOSrmfNGDIyJrbT1d+CFVKvnKahDKNJLfOYj8N4MgyFaU9/Ytc6w5Vw==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.12.tgz",
+      "integrity": "sha512-jlUxCiHO1dsqoURZDQts+HK100o0hXfi4t54MNRMCAqKGAV33JCVvMplLAa2FwviSojT/5ZG5HUfG3gstwAG8w==",
       "dev": true,
       "optional": true
     },
     "esbuild-openbsd-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.42.tgz",
-      "integrity": "sha512-M2meNVIKWsm2HMY7+TU9AxM7ZVwI9havdsw6m/6EzdXysyCFFSoaTQ/Jg03izjCsK17FsVRHqRe26Llj6x0MNA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.12.tgz",
+      "integrity": "sha512-1o1uAfRTMIWNOmpf8v7iudND0L6zRBYSH45sofCZywrcf7NcZA+c7aFsS1YryU+yN7aRppTqdUK1PgbZVaB1Dw==",
       "dev": true,
       "optional": true
     },
     "esbuild-sunos-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.42.tgz",
-      "integrity": "sha512-uXV8TAZEw36DkgW8Ak3MpSJs1ofBb3Smkc/6pZ29sCAN1KzCAQzsje4sUwugf+FVicrHvlamCOlFZIXgct+iqQ==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.12.tgz",
+      "integrity": "sha512-nkl251DpoWoBO9Eq9aFdoIt2yYmp4I3kvQjba3jFKlMXuqQ9A4q+JaqdkCouG3DHgAGnzshzaGu6xofGcXyPXg==",
       "dev": true,
       "optional": true
     },
     "esbuild-windows-32": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.42.tgz",
-      "integrity": "sha512-4iw/8qWmRICWi9ZOnJJf9sYt6wmtp3hsN4TdI5NqgjfOkBVMxNdM9Vt3626G1Rda9ya2Q0hjQRD9W1o+m6Lz6g==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.12.tgz",
+      "integrity": "sha512-WlGeBZHgPC00O08luIp5B2SP4cNCp/PcS+3Pcg31kdcJPopHxLkdCXtadLU9J82LCfw4TVls21A6lilQ9mzHrw==",
       "dev": true,
       "optional": true
     },
     "esbuild-windows-64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.42.tgz",
-      "integrity": "sha512-j3cdK+Y3+a5H0wHKmLGTJcq0+/2mMBHPWkItR3vytp/aUGD/ua/t2BLdfBIzbNN9nLCRL9sywCRpOpFMx3CxzA==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.12.tgz",
+      "integrity": "sha512-VActO3WnWZSN//xjSfbiGOSyC+wkZtI8I4KlgrTo5oHJM6z3MZZBCuFaZHd8hzf/W9KPhF0lY8OqlmWC9HO5AA==",
       "dev": true,
       "optional": true
     },
     "esbuild-windows-arm64": {
-      "version": "0.14.42",
-      "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.42.tgz",
-      "integrity": "sha512-+lRAARnF+hf8J0mN27ujO+VbhPbDqJ8rCcJKye4y7YZLV6C4n3pTRThAb388k/zqF5uM0lS5O201u0OqoWSicw==",
+      "version": "0.15.12",
+      "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.12.tgz",
+      "integrity": "sha512-Of3MIacva1OK/m4zCNIvBfz8VVROBmQT+gRX6pFTLPngFYcj6TFH/12VveAqq1k9VB2l28EoVMNMUCcmsfwyuA==",
       "dev": true,
       "optional": true
     },
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
       "dev": true
     },
     "fill-range": {
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
       "dev": true
     },
+    "function.prototype.name": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz",
+      "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==",
+      "dev": true,
+      "requires": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.19.0",
+        "functions-have-names": "^1.2.2"
+      }
+    },
+    "functions-have-names": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+      "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+      "dev": true
+    },
     "get-caller-file": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
       "dev": true
     },
     "get-intrinsic": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
-      "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
+      "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
       "dev": true,
       "requires": {
         "function-bind": "^1.1.1",
         "has": "^1.0.3",
-        "has-symbols": "^1.0.1"
+        "has-symbols": "^1.0.3"
       }
     },
     "get-symbol-description": {
     "good-listener": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
-      "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
       "requires": {
         "delegate": "^3.1.2"
       }
     },
     "graceful-fs": {
-      "version": "4.2.8",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
-      "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
+      "version": "4.2.10",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+      "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
       "dev": true
     },
     "has": {
       }
     },
     "has-bigints": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz",
-      "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+      "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
       "dev": true
     },
     "has-flag": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+      "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
       "dev": true
     },
+    "has-property-descriptors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz",
+      "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==",
+      "dev": true,
+      "requires": {
+        "get-intrinsic": "^1.1.1"
+      }
+    },
     "has-symbols": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
-      "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+      "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
       "dev": true
     },
     "has-tostringtag": {
       "dev": true
     },
     "immutable": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
-      "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
+      "integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
       "dev": true
     },
     "internal-slot": {
     "is-arrayish": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+      "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
       "dev": true
     },
     "is-bigint": {
       }
     },
     "is-callable": {
-      "version": "1.2.4",
-      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz",
-      "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==",
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+      "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
       "dev": true
     },
     "is-core-module": {
-      "version": "2.8.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
-      "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
+      "version": "2.11.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
+      "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
       "dev": true,
       "requires": {
         "has": "^1.0.3"
     "is-extglob": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-      "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
       "dev": true
     },
     "is-fullwidth-code-point": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
-      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+      "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
       "dev": true
     },
     "is-glob": {
       }
     },
     "is-negative-zero": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz",
-      "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
+      "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
       "dev": true
     },
     "is-number": {
       "dev": true
     },
     "is-number-object": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz",
-      "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+      "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
       "dev": true,
       "requires": {
         "has-tostringtag": "^1.0.0"
       }
     },
     "is-shared-array-buffer": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz",
-      "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==",
-      "dev": true
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
+      "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
+      "dev": true,
+      "requires": {
+        "call-bind": "^1.0.2"
+      }
     },
     "is-string": {
       "version": "1.0.7",
       }
     },
     "is-weakref": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz",
-      "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+      "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.0"
+        "call-bind": "^1.0.2"
       }
     },
     "isexe": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
-      "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
       "dev": true
     },
     "json-parse-better-errors": {
       }
     },
     "livereload-js": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.2.tgz",
-      "integrity": "sha512-w677WnINxFkuixAoUEXOStewzLYGI76XVag+0JWMMEyjJQKs0ibWZMxkTlB96Lm3EjZ7IeOxVziBEbtxVQqQZA==",
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.4.1.tgz",
+      "integrity": "sha512-5MP0uUeVCec89ZbNOT/i97Mc+q3SxXmiUGhRFOTmhrGPn//uWVQdCvcLJDy64MSBR5MidFdOR7B9viumoavy6g==",
       "dev": true
     },
     "load-json-file": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
-      "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=",
+      "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
       "dev": true,
       "requires": {
         "graceful-fs": "^4.1.2",
     "lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
-      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
+      "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
       "dev": true
     },
     "lodash.throttle": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
-      "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=",
+      "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
       "dev": true
     },
     "markdown-it": {
     "mdurl": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
-      "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
+      "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
     },
     "memorystream": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
-      "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=",
+      "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
       "dev": true
     },
     "minimatch": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
       "dev": true,
       "requires": {
         "brace-expansion": "^1.1.7"
       }
     },
     "object-inspect": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz",
-      "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==",
+      "version": "1.12.2",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz",
+      "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==",
       "dev": true
     },
     "object-keys": {
       "dev": true
     },
     "object.assign": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
-      "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
+      "version": "4.1.4",
+      "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz",
+      "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==",
       "dev": true,
       "requires": {
-        "call-bind": "^1.0.0",
-        "define-properties": "^1.1.3",
-        "has-symbols": "^1.0.1",
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.4",
+        "has-symbols": "^1.0.3",
         "object-keys": "^1.1.1"
       }
     },
     "parse-json": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
-      "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=",
+      "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
       "dev": true,
       "requires": {
         "error-ex": "^1.3.1",
     "path-exists": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
-      "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
+      "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
       "dev": true
     },
     "path-key": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
-      "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
+      "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
       "dev": true
     },
     "path-parse": {
       }
     },
     "picomatch": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
-      "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
       "dev": true
     },
     "pidtree": {
     "pify": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
-      "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
+      "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
       "dev": true
     },
     "punycode": {
     "read-pkg": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
-      "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=",
+      "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==",
       "dev": true,
       "requires": {
         "load-json-file": "^4.0.0",
         "picomatch": "^2.2.1"
       }
     },
+    "regexp.prototype.flags": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
+      "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
+      "dev": true,
+      "requires": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.3",
+        "functions-have-names": "^1.2.2"
+      }
+    },
     "require-directory": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+      "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
       "dev": true
     },
     "require-main-filename": {
       "dev": true
     },
     "resolve": {
-      "version": "1.20.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
-      "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+      "version": "1.22.1",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
+      "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
+      "dev": true,
+      "requires": {
+        "is-core-module": "^2.9.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      }
+    },
+    "safe-regex-test": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
+      "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==",
       "dev": true,
       "requires": {
-        "is-core-module": "^2.2.0",
-        "path-parse": "^1.0.6"
+        "call-bind": "^1.0.2",
+        "get-intrinsic": "^1.1.3",
+        "is-regex": "^1.1.4"
       }
     },
     "sass": {
-      "version": "1.52.1",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.52.1.tgz",
-      "integrity": "sha512-fSzYTbr7z8oQnVJ3Acp9hV80dM1fkMN7mSD/25mpcct9F7FPBMOI8krEYALgU1aZoqGhQNhTPsuSmxjnIvAm4Q==",
+      "version": "1.55.0",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.55.0.tgz",
+      "integrity": "sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==",
       "dev": true,
       "requires": {
         "chokidar": ">=3.0.0 <4.0.0",
     "select": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
-      "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
     },
     "semver": {
       "version": "5.7.1",
     "set-blocking": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
-      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
       "dev": true
     },
     "shebang-command": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
-      "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+      "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
       "dev": true,
       "requires": {
         "shebang-regex": "^1.0.0"
     "shebang-regex": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
-      "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
+      "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
       "dev": true
     },
     "shell-quote": {
-      "version": "1.7.3",
-      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
-      "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz",
+      "integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==",
       "dev": true
     },
     "side-channel": {
       }
     },
     "snabbdom": {
-      "version": "3.5.0",
-      "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.0.tgz",
-      "integrity": "sha512-Ff5BKG18KrrPuskHJlA9aujPHqEabItaDl96l7ZZndF4zt5AYSczz7ZjjgQAX5IBd5cd25lw9NfgX21yVUJ+9g=="
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/snabbdom/-/snabbdom-3.5.1.tgz",
+      "integrity": "sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA=="
     },
     "sortablejs": {
       "version": "1.15.0",
       }
     },
     "spdx-license-ids": {
-      "version": "3.0.10",
-      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz",
-      "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
+      "version": "3.0.12",
+      "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz",
+      "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==",
       "dev": true
     },
     "string-width": {
       }
     },
     "string.prototype.trimend": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz",
-      "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz",
+      "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==",
       "dev": true,
       "requires": {
         "call-bind": "^1.0.2",
-        "define-properties": "^1.1.3"
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.19.5"
       }
     },
     "string.prototype.trimstart": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz",
-      "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==",
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz",
+      "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==",
       "dev": true,
       "requires": {
         "call-bind": "^1.0.2",
-        "define-properties": "^1.1.3"
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.19.5"
       }
     },
     "strip-ansi": {
     "strip-bom": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
-      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+      "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
       "dev": true
     },
     "supports-color": {
         "has-flag": "^3.0.0"
       }
     },
+    "supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true
+    },
     "tiny-emitter": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
       "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
     },
     "unbox-primitive": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
-      "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==",
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+      "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
       "dev": true,
       "requires": {
-        "function-bind": "^1.1.1",
-        "has-bigints": "^1.0.1",
-        "has-symbols": "^1.0.2",
+        "call-bind": "^1.0.2",
+        "has-bigints": "^1.0.2",
+        "has-symbols": "^1.0.3",
         "which-boxed-primitive": "^1.0.2"
       }
     },
     "which-module": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
-      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+      "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==",
       "dev": true
     },
     "wrap-ansi": {
       }
     },
     "ws": {
-      "version": "7.5.5",
-      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
-      "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
+      "version": "7.5.9",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
+      "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
       "dev": true,
       "requires": {}
     },
index 9a2f664482a08958a2ae1d3e72fcce593e40c6bd..9abdcc3464075fade61c8d8b244bbffeb2a58c61 100644 (file)
   },
   "devDependencies": {
     "chokidar-cli": "^3.0",
-    "esbuild": "0.14.42",
+    "esbuild": "^0.15.12",
     "livereload": "^0.9.3",
     "npm-run-all": "^4.1.5",
     "punycode": "^2.1.1",
-    "sass": "^1.52.1"
+    "sass": "^1.55.0"
   },
   "dependencies": {
     "clipboard": "^2.0.11",
index 12ab73870cc64ccedb88711cc9569f5872e840d2..fcb5e8e8540d7c4dbe372dceb26f6ebc1359be36 100644 (file)
@@ -1,4 +1 @@
-<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
-    <path d="M0 0h24v24H0z" fill="none"/>
-    <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
-</svg>
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.86 8.86l-3 3.87L9 13.14 6 17h12l-3.86-5.14z"/></svg>
diff --git a/resources/icons/shortcuts.svg b/resources/icons/shortcuts.svg
new file mode 100644 (file)
index 0000000..8d23aac
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 5H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm8 7H9c-.55 0-1-.45-1-1s.45-1 1-1h6c.55 0 1 .45 1 1s-.45 1-1 1zm1-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z"/></svg>
\ No newline at end of file
index 82748b75e223c2f21612a2662bc2fad22c23b6c0..e49bf5e955bd040d1b68114fb17e8b307a3a024e 100644 (file)
@@ -27,5 +27,8 @@ window.trans_choice = translator.getPlural.bind(translator);
 window.trans_plural = translator.parsePlural.bind(translator);
 
 // Load Components
-import components from "./components"
-components();
\ No newline at end of file
+import * as components from "./services/components"
+import * as componentMap from "./components";
+components.register(componentMap);
+window.$components = components;
+components.init();
index 5881e25129be5f874e422c8135dd71cbcbc7f22f..ad282f2be10b2192ba1c497b5ce2c5aeb67a0a70 100644 (file)
@@ -4,6 +4,7 @@ import Clipboard from "clipboard/dist/clipboard.min";
 // Modes
 import 'codemirror/mode/css/css';
 import 'codemirror/mode/clike/clike';
+import 'codemirror/mode/dart/dart';
 import 'codemirror/mode/diff/diff';
 import 'codemirror/mode/fortran/fortran';
 import 'codemirror/mode/go/go';
@@ -27,6 +28,7 @@ import 'codemirror/mode/rust/rust';
 import 'codemirror/mode/shell/shell';
 import 'codemirror/mode/sql/sql';
 import 'codemirror/mode/stex/stex';
+import 'codemirror/mode/swift/swift';
 import 'codemirror/mode/toml/toml';
 import 'codemirror/mode/vb/vb';
 import 'codemirror/mode/vbscript/vbscript';
@@ -49,6 +51,7 @@ const modeMap = {
     'c++': 'text/x-c++src',
     'c#': 'text/x-csharp',
     csharp: 'text/x-csharp',
+    dart: 'application/dart',
     diff: 'diff',
     for: 'fortran',
     fortran: 'fortran',
@@ -91,11 +94,12 @@ const modeMap = {
     rs: 'rust',
     shell: 'shell',
     sh: 'shell',
+    sql: 'text/x-sql',
     stext: 'text/x-stex',
+    swift: 'text/x-swift',
     toml: 'toml',
     ts: 'text/typescript',
     typescript: 'text/typescript',
-    sql: 'text/x-sql',
     vbs: 'vbscript',
     vbscript: 'vbscript',
     'vb.net': 'text/x-vb',
index 9a5f019c501e66c79e8855601eefd4e087e2d1bd..19d2249fb28ece1607f00367e7e443b75ea8f8d5 100644 (file)
@@ -1,13 +1,13 @@
 import {onChildEvent} from "../services/dom";
 import {uniqueId} from "../services/util";
+import {Component} from "./component";
 
 /**
  * AddRemoveRows
  * Allows easy row add/remove controls onto a table.
  * Needs a model row to use when adding a new row.
- * @extends {Component}
  */
-class AddRemoveRows {
+export class AddRemoveRows extends Component {
     setup() {
         this.modelRow = this.$refs.model;
         this.addButton = this.$refs.add;
@@ -31,7 +31,7 @@ class AddRemoveRows {
         clone.classList.remove('hidden');
         this.setClonedInputNames(clone);
         this.modelRow.parentNode.insertBefore(clone, this.modelRow);
-        window.components.init(clone);
+        window.$components.init(clone);
     }
 
     /**
@@ -49,6 +49,4 @@ class AddRemoveRows {
             elem.name = elem.name.split('randrowid').join(rowId);
         }
     }
-}
-
-export default AddRemoveRows;
\ No newline at end of file
+}
\ No newline at end of file
index 2feb3d5ac72bcaf9e10ec252af187ad633efe4c2..f1af7f6cb10e97d2e65cfb1c4e49d1d5a4e7580e 100644 (file)
@@ -1,10 +1,7 @@
-/**
- * AjaxDelete
- * @extends {Component}
- */
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
-class AjaxDeleteRow {
+export class AjaxDeleteRow extends Component {
     setup() {
         this.row = this.$el;
         this.url = this.$opts.url;
@@ -27,6 +24,4 @@ class AjaxDeleteRow {
             this.row.style.pointerEvents = null;
         });
     }
-}
-
-export default AjaxDeleteRow;
\ No newline at end of file
+}
\ No newline at end of file
index 91029d04221247cbc4b079fd3d807a4eb25cb93f..6f4e5af08c8bfbe20483ea0805cf4a44261c3861 100644 (file)
@@ -1,4 +1,5 @@
 import {onEnterPress, onSelect} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * Ajax Form
@@ -8,10 +9,8 @@ import {onEnterPress, onSelect} from "../services/dom";
  *
  * Will handle a real form if that's what the component is added to
  * otherwise will act as a fake form element.
- *
- * @extends {Component}
  */
-class AjaxForm {
+export class AjaxForm extends Component {
     setup() {
         this.container = this.$el;
         this.responseContainer = this.container;
@@ -72,11 +71,9 @@ class AjaxForm {
             this.responseContainer.innerHTML = err.data;
         }
 
-        window.components.init(this.responseContainer);
+        window.$components.init(this.responseContainer);
         this.responseContainer.style.opacity = null;
         this.responseContainer.style.pointerEvents = null;
     }
 
-}
-
-export default AjaxForm;
\ No newline at end of file
+}
\ No newline at end of file
index 34979c2e7a8207d376aa12534b0d3745e3a34d7d..dfefd9b7f84afb24c1cdb77dd8dbe22281dc1533 100644 (file)
@@ -1,10 +1,11 @@
+import {Component} from "./component";
+
 /**
  * Attachments List
  * Adds '?open=true' query to file attachment links
  * when ctrl/cmd is pressed down.
- * @extends {Component}
  */
-class AttachmentsList {
+export class AttachmentsList extends Component {
 
     setup() {
         this.container = this.$el;
@@ -42,6 +43,4 @@ class AttachmentsList {
             link.removeAttribute('target');
         }
     }
-}
-
-export default AttachmentsList;
\ No newline at end of file
+}
\ No newline at end of file
index 6dcfe9f128cf39a8e7a2d0586b32b9320613c5f8..b4e400aeb716b53047b0d7c80492e2a9dbdba7b0 100644 (file)
@@ -1,10 +1,7 @@
-/**
- * Attachments
- * @extends {Component}
- */
 import {showLoading} from "../services/dom";
+import {Component} from "./component";
 
-class Attachments {
+export class Attachments extends Component {
 
     setup() {
         this.container = this.$el;
@@ -46,10 +43,12 @@ class Attachments {
 
     reloadList() {
         this.stopEdit();
-        this.mainTabs.components.tabs.show('items');
+        /** @var {Tabs} */
+        const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs');
+        tabs.show('items');
         window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
             this.list.innerHTML = resp.data;
-            window.components.init(this.list);
+            window.$components.init(this.list);
         });
     }
 
@@ -66,7 +65,7 @@ class Attachments {
         showLoading(this.editContainer);
         const resp = await window.$http.get(`/attachments/edit/${id}`);
         this.editContainer.innerHTML = resp.data;
-        window.components.init(this.editContainer);
+        window.$components.init(this.editContainer);
     }
 
     stopEdit() {
@@ -74,6 +73,4 @@ class Attachments {
         this.listContainer.classList.remove('hidden');
     }
 
-}
-
-export default Attachments;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/resources/js/components/auto-submit.js b/resources/js/components/auto-submit.js
new file mode 100644 (file)
index 0000000..c8726ca
--- /dev/null
@@ -0,0 +1,11 @@
+import {Component} from "./component";
+
+export class AutoSubmit extends Component {
+
+    setup() {
+        this.form = this.$el;
+
+        this.form.submit();
+    }
+
+}
\ No newline at end of file
index 80857cbe5c5cb20d121bfbcb6d40dd17ff786285..b4e6c5957879b759c7e9a358fcd5b528c7673b22 100644 (file)
@@ -1,13 +1,13 @@
 import {escapeHtml} from "../services/util";
 import {onChildEvent} from "../services/dom";
+import {Component} from "./component";
 
 const ajaxCache = {};
 
 /**
  * AutoSuggest
- * @extends {Component}
  */
-class AutoSuggest {
+export class AutoSuggest extends Component {
     setup() {
         this.parent = this.$el.parentElement;
         this.container = this.$el;
@@ -148,6 +148,4 @@ class AutoSuggest {
             this.hideSuggestions();
         }
     }
-}
-
-export default AutoSuggest;
\ No newline at end of file
+}
\ No newline at end of file
index a1d87f22eb3c3732213a2be6e06e8adfdb7180ae..4f0a46f009b19822332bd680f766bc205acc43b9 100644 (file)
@@ -1,34 +1,35 @@
+import {Component} from "./component";
 
-class BackToTop {
+export class BackToTop extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
+    setup() {
+        this.button = this.$el;
         this.targetElem = document.getElementById('header');
         this.showing = false;
         this.breakPoint = 1200;
 
         if (document.body.classList.contains('flexbox')) {
-            this.elem.style.display = 'none';
+            this.button.style.display = 'none';
             return;
         }
 
-        this.elem.addEventListener('click', this.scrollToTop.bind(this));
+        this.button.addEventListener('click', this.scrollToTop.bind(this));
         window.addEventListener('scroll', this.onPageScroll.bind(this));
     }
 
     onPageScroll() {
         let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0;
         if (!this.showing && scrollTopPos > this.breakPoint) {
-            this.elem.style.display = 'block';
+            this.button.style.display = 'block';
             this.showing = true;
             setTimeout(() => {
-                this.elem.style.opacity = 0.4;
+                this.button.style.opacity = 0.4;
             }, 1);
         } else if (this.showing && scrollTopPos < this.breakPoint) {
-            this.elem.style.opacity = 0;
+            this.button.style.opacity = 0;
             this.showing = false;
             setTimeout(() => {
-                this.elem.style.display = 'none';
+                this.button.style.display = 'none';
             }, 500);
         }
     }
@@ -54,6 +55,4 @@ class BackToTop {
         requestAnimationFrame(setPos.bind(this));
     }
 
-}
-
-export default BackToTop;
\ No newline at end of file
+}
\ No newline at end of file
index 2b94ca4a7a19a68ff82b31345efbc52fc28dc56e..3ffadf99150d3813b06faea5d706e789257a3e8d 100644 (file)
@@ -1,4 +1,6 @@
 import Sortable from "sortablejs";
+import {Component} from "./component";
+import {htmlToDom} from "../services/dom";
 
 // Auto sort control
 const sortOperations = {
@@ -35,14 +37,14 @@ const sortOperations = {
     },
 };
 
-class BookSort {
+export class BookSort extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.sortContainer = elem.querySelector('[book-sort-boxes]');
-        this.input = elem.querySelector('[book-sort-input]');
+    setup() {
+        this.container = this.$el;
+        this.sortContainer = this.$refs.sortContainer;
+        this.input = this.$refs.input;
 
-        const initialSortBox = elem.querySelector('.sort-box');
+        const initialSortBox = this.container.querySelector('.sort-box');
         this.setupBookSortable(initialSortBox);
         this.setupSortPresets();
 
@@ -90,14 +92,12 @@ class BookSort {
      * @param {Object} entityInfo
      */
     bookSelect(entityInfo) {
-        const alreadyAdded = this.elem.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
+        const alreadyAdded = this.container.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
         if (alreadyAdded) return;
 
         const entitySortItemUrl = entityInfo.link + '/sort-item';
         window.$http.get(entitySortItemUrl).then(resp => {
-            const wrap = document.createElement('div');
-            wrap.innerHTML = resp.data;
-            const newBookContainer = wrap.children[0];
+            const newBookContainer = htmlToDom(resp.data);
             this.sortContainer.append(newBookContainer);
             this.setupBookSortable(newBookContainer);
         });
@@ -155,7 +155,7 @@ class BookSort {
      */
     buildEntityMap() {
         const entityMap = [];
-        const lists = this.elem.querySelectorAll('.sort-list');
+        const lists = this.container.querySelectorAll('.sort-list');
 
         for (let list of lists) {
             const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
@@ -202,6 +202,4 @@ class BookSort {
         }
     }
 
-}
-
-export default BookSort;
\ No newline at end of file
+}
\ No newline at end of file
index c824d0f78b0b7fad13fcb16f84453746be6f1c7c..37df213e3c98e1ce12fa2de347e9397c3213238f 100644 (file)
@@ -1,9 +1,7 @@
 import {slideUp, slideDown} from "../services/animations";
+import {Component} from "./component";
 
-/**
- * @extends {Component}
- */
-class ChapterContents {
+export class ChapterContents extends Component {
 
     setup() {
         this.list = this.$refs.list;
@@ -31,7 +29,4 @@ class ChapterContents {
         event.preventDefault();
         this.isOpen ?  this.close() : this.open();
     }
-
 }
-
-export default ChapterContents;
index 2d8031205f4d3fa600fef01f27f0be5f4e2dbb3d..205cbd8fdbc21efec49301430a7eee97fa67a991 100644 (file)
@@ -1,10 +1,8 @@
 import {onChildEvent, onEnterPress, onSelect} from "../services/dom";
+import {Component} from "./component";
 
-/**
- * Code Editor
- * @extends {Component}
- */
-class CodeEditor {
+
+export class CodeEditor extends Component {
 
     setup() {
         this.container = this.$refs.container;
@@ -73,7 +71,7 @@ class CodeEditor {
             isFavorite ? this.favourites.add(language) : this.favourites.delete(language);
             button.setAttribute('data-favourite', isFavorite ? 'true' : 'false');
 
-            window.$http.patch('/settings/users/update-code-language-favourite', {
+            window.$http.patch('/preferences/update-code-language-favourite', {
                 language: language,
                 active: isFavorite
             });
@@ -128,7 +126,7 @@ class CodeEditor {
         }
 
         this.loadHistory();
-        this.popup.components.popup.show(() => {
+        this.getPopup().show(() => {
             Code.updateLayout(this.editor);
             this.editor.focus();
         }, () => {
@@ -137,10 +135,17 @@ class CodeEditor {
     }
 
     hide() {
-        this.popup.components.popup.hide();
+        this.getPopup().hide();
         this.addHistory();
     }
 
+    /**
+     * @returns {Popup}
+     */
+    getPopup() {
+        return window.$components.firstOnElement(this.popup, 'popup');
+    }
+
     async updateEditorMode(language) {
         const Code = await window.importVersioned('code');
         Code.setMode(this.editor, language, this.editor.getValue());
@@ -184,6 +189,4 @@ class CodeEditor {
         window.sessionStorage.setItem(this.historyKey, historyString);
     }
 
-}
-
-export default CodeEditor;
\ No newline at end of file
+}
\ No newline at end of file
index 5ffab377525d875f34e02f7be6d65e68dee1c4c3..14bfc97f04ed5358b2758112e9cf3f06d47ae423 100644 (file)
@@ -1,14 +1,16 @@
-class CodeHighlighter {
+import {Component} from "./component";
 
-    constructor(elem) {
-        const codeBlocks = elem.querySelectorAll('pre');
+export class CodeHighlighter extends Component{
+
+    setup() {
+        const container = this.$el;
+
+        const codeBlocks = container.querySelectorAll('pre');
         if (codeBlocks.length > 0) {
             window.importVersioned('code').then(Code => {
-               Code.highlightWithin(elem);
+               Code.highlightWithin(container);
             });
         }
     }
 
-}
-
-export default CodeHighlighter;
\ No newline at end of file
+}
\ No newline at end of file
index 988e51f199f040f57846869a029e9d6dfabc712d..0e49aec1755693c8615fa1447e37b1fe549b3545 100644 (file)
@@ -1,9 +1,10 @@
 /**
  * A simple component to render a code editor within the textarea
  * this exists upon.
- * @extends {Component}
  */
-class CodeTextarea {
+import {Component} from "./component";
+
+export class CodeTextarea extends Component {
 
     async setup() {
         const mode = this.$opts.mode;
@@ -11,6 +12,4 @@ class CodeTextarea {
         Code.inlineEditor(this.$el, mode);
     }
 
-}
-
-export default CodeTextarea;
\ No newline at end of file
+}
\ No newline at end of file
index 544f91008c7d13eaeec88a9f119ebfb47e839a85..bb8ed477ffe9f600769dace8ba6d7f2af32d5fb7 100644 (file)
@@ -1,35 +1,37 @@
 import {slideDown, slideUp} from "../services/animations";
+import {Component} from "./component";
 
 /**
  * Collapsible
  * Provides some simple logic to allow collapsible sections.
  */
-class Collapsible {
+export class Collapsible extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.trigger = elem.querySelector('[collapsible-trigger]');
-        this.content = elem.querySelector('[collapsible-content]');
+    setup() {
+        this.container = this.$el;
+        this.trigger = this.$refs.trigger;
+        this.content = this.$refs.content;
 
-        if (!this.trigger) return;
-        this.trigger.addEventListener('click', this.toggle.bind(this));
-        this.openIfContainsError();
+        if (this.trigger) {
+            this.trigger.addEventListener('click', this.toggle.bind(this));
+            this.openIfContainsError();
+        }
     }
 
     open() {
-        this.elem.classList.add('open');
+        this.container.classList.add('open');
         this.trigger.setAttribute('aria-expanded', 'true');
         slideDown(this.content, 300);
     }
 
     close() {
-        this.elem.classList.remove('open');
+        this.container.classList.remove('open');
         this.trigger.setAttribute('aria-expanded', 'false');
         slideUp(this.content, 300);
     }
 
     toggle() {
-        if (this.elem.classList.contains('open')) {
+        if (this.container.classList.contains('open')) {
             this.close();
         } else {
             this.open();
@@ -43,6 +45,4 @@ class Collapsible {
         }
     }
 
-}
-
-export default Collapsible;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/resources/js/components/component.js b/resources/js/components/component.js
new file mode 100644 (file)
index 0000000..292bbb6
--- /dev/null
@@ -0,0 +1,58 @@
+export class Component {
+
+    /**
+     * The registered name of the component.
+     * @type {string}
+     */
+    $name = '';
+
+    /**
+     * The element that the component is registered upon.
+     * @type {Element}
+     */
+    $el = null;
+
+    /**
+     * Mapping of referenced elements within the component.
+     * @type {Object<string, Element>}
+     */
+    $refs = {};
+
+    /**
+     * Mapping of arrays of referenced elements within the component so multiple
+     * references, sharing the same name, can be fetched.
+     * @type {Object<string, Element[]>}
+     */
+    $manyRefs = {};
+
+    /**
+     * Options passed into this component.
+     * @type {Object<String, String>}
+     */
+    $opts = {};
+
+    /**
+     * Component-specific setup methods.
+     * Use this to assign local variables and run any initial setup or actions.
+     */
+    setup() {
+        //
+    }
+
+    /**
+     * Emit an event from this component.
+     * Will be bubbled up from the dom element this is registered on, as a custom event
+     * with the name `<elementName>-<eventName>`, with the provided data in the event detail.
+     * @param {String} eventName
+     * @param {Object} data
+     */
+    $emit(eventName, data = {}) {
+        data.from = this;
+        const componentName = this.$name;
+        const event = new CustomEvent(`${componentName}-${eventName}`, {
+            bubbles: true,
+            detail: data
+        });
+        this.$el.dispatchEvent(event);
+    }
+}
\ No newline at end of file
index 858be1b852be7eb23636113720294a8b878dca27..572945d5aba4ae7099a30dc29a28fe58f62d4542 100644 (file)
@@ -1,12 +1,12 @@
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * Custom equivalent of window.confirm() using our popup component.
  * Is promise based so can be used like so:
  * `const result = await dialog.show()`
- * @extends {Component}
  */
-class ConfirmDialog {
+export class ConfirmDialog extends Component {
 
     setup() {
         this.container = this.$el;
@@ -34,7 +34,7 @@ class ConfirmDialog {
      * @returns {Popup}
      */
     getPopup() {
-        return this.container.components.popup;
+        return window.$components.firstOnElement(this.container, 'popup');
     }
 
     /**
@@ -47,6 +47,4 @@ class ConfirmDialog {
         }
     }
 
-}
-
-export default ConfirmDialog;
\ No newline at end of file
+}
\ No newline at end of file
index 65ce8c194d641db09d37ae437742611e909a1ebe..99804c4bcea010165ad4298f405860e904b9689d 100644 (file)
@@ -1,18 +1,19 @@
+import {Component} from "./component";
 
-class CustomCheckbox {
+export class CustomCheckbox extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.checkbox = elem.querySelector('input[type=checkbox]');
-        this.display = elem.querySelector('[role="checkbox"]');
+    setup() {
+        this.container = this.$el;
+        this.checkbox = this.container.querySelector('input[type=checkbox]');
+        this.display = this.container.querySelector('[role="checkbox"]');
 
         this.checkbox.addEventListener('change', this.stateChange.bind(this));
-        this.elem.addEventListener('keydown', this.onKeyDown.bind(this));
+        this.container.addEventListener('keydown', this.onKeyDown.bind(this));
     }
 
     onKeyDown(event) {
-        const isEnterOrPress = event.keyCode === 32 || event.keyCode === 13;
-        if (isEnterOrPress) {
+        const isEnterOrSpace = event.key === ' ' || event.key === 'Enter';
+        if (isEnterOrSpace) {
             event.preventDefault();
             this.toggle();
         }
@@ -29,6 +30,4 @@ class CustomCheckbox {
         this.display.setAttribute('aria-checked', checked);
     }
 
-}
-
-export default CustomCheckbox;
\ No newline at end of file
+}
\ No newline at end of file
index 1f3b66c674c2c882b7f1d71b4fcb79867c06dcfe..6466fb584882b0af36280f06dc2420f7599305e8 100644 (file)
@@ -1,21 +1,22 @@
-class DetailsHighlighter {
+import {Component} from "./component";
 
-    constructor(elem) {
-        this.elem = elem;
+export class DetailsHighlighter extends Component {
+
+    setup() {
+        this.container = this.$el;
         this.dealtWith = false;
-        elem.addEventListener('toggle', this.onToggle.bind(this));
+
+        this.container.addEventListener('toggle', this.onToggle.bind(this));
     }
 
     onToggle() {
         if (this.dealtWith) return;
 
-        if (this.elem.querySelector('pre')) {
+        if (this.container.querySelector('pre')) {
             window.importVersioned('code').then(Code => {
-                Code.highlightWithin(this.elem);
+                Code.highlightWithin(this.container);
             });
         }
         this.dealtWith = true;
     }
-}
-
-export default DetailsHighlighter;
\ No newline at end of file
+}
\ No newline at end of file
index 81fa940c24ca7cf4a38ba66fe7b55e300d746100..30a2aadc1467ddd97a1a3ccb30efea591383f07b 100644 (file)
@@ -1,7 +1,8 @@
 import {debounce} from "../services/util";
 import {transitionHeight} from "../services/animations";
+import {Component} from "./component";
 
-class DropdownSearch {
+export class DropdownSearch extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -78,6 +79,4 @@ class DropdownSearch {
         this.loadingElem.style.display = show ? 'block' : 'none';
     }
 
-}
-
-export default DropdownSearch;
\ No newline at end of file
+}
\ No newline at end of file
index 781f90860f7aa539595d56723a95d8347c1c4060..ed69088b29acae227a9f57557406299a74e71459 100644 (file)
@@ -1,11 +1,12 @@
 import {onSelect} from "../services/dom";
+import {KeyboardNavigationHandler} from "../services/keyboard-navigation";
+import {Component} from "./component";
 
 /**
  * Dropdown
  * Provides some simple logic to create simple dropdown menus.
- * @extends {Component}
  */
-class DropDown {
+export class Dropdown extends Component {
 
     setup() {
         this.container = this.$el;
@@ -17,8 +18,9 @@ class DropDown {
         this.direction = (document.dir === 'rtl') ? 'right' : 'left';
         this.body = document.body;
         this.showing = false;
-        this.setupListeners();
+
         this.hide = this.hide.bind(this);
+        this.setupListeners();
     }
 
     show(event = null) {
@@ -52,7 +54,7 @@ class DropDown {
         }
 
         // Set listener to hide on mouse leave or window click
-        this.menu.addEventListener('mouseleave', this.hide.bind(this));
+        this.menu.addEventListener('mouseleave', this.hide);
         window.addEventListener('click', event => {
             if (!this.menu.contains(event.target)) {
                 this.hide();
@@ -74,7 +76,7 @@ class DropDown {
     }
 
     hideAll() {
-        for (let dropdown of window.components.dropdown) {
+        for (let dropdown of window.$components.get('dropdown')) {
             dropdown.hide();
         }
     }
@@ -97,33 +99,25 @@ class DropDown {
         this.showing = false;
     }
 
-    getFocusable() {
-        return Array.from(this.menu.querySelectorAll('[tabindex]:not([tabindex="-1"]),[href],button,input:not([type=hidden])'));
-    }
-
-    focusNext() {
-        const focusable = this.getFocusable();
-        const currentIndex = focusable.indexOf(document.activeElement);
-        let newIndex = currentIndex + 1;
-        if (newIndex >= focusable.length) {
-            newIndex = 0;
-        }
-
-        focusable[newIndex].focus();
-    }
+    setupListeners() {
+        const keyboardNavHandler = new KeyboardNavigationHandler(this.container, (event) => {
+            this.hide();
+            this.toggle.focus();
+            if (!this.bubbleEscapes) {
+                event.stopPropagation();
+            }
+        }, (event) => {
+            if (event.target.nodeName === 'INPUT') {
+                event.preventDefault();
+                event.stopPropagation();
+            }
+            this.hide();
+        });
 
-    focusPrevious() {
-        const focusable = this.getFocusable();
-        const currentIndex = focusable.indexOf(document.activeElement);
-        let newIndex = currentIndex - 1;
-        if (newIndex < 0) {
-            newIndex = focusable.length - 1;
+        if (this.moveMenu) {
+            keyboardNavHandler.shareHandlingToEl(this.menu);
         }
 
-        focusable[newIndex].focus();
-    }
-
-    setupListeners() {
         // Hide menu on option click
         this.container.addEventListener('click', event => {
              const possibleChildren = Array.from(this.menu.querySelectorAll('a'));
@@ -136,41 +130,9 @@ class DropDown {
             event.stopPropagation();
             this.show(event);
             if (event instanceof KeyboardEvent) {
-                this.focusNext();
-            }
-        });
-
-        // Keyboard navigation
-        const keyboardNavigation = event => {
-            if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
-                this.focusNext();
-                event.preventDefault();
-            } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
-                this.focusPrevious();
-                event.preventDefault();
-            } else if (event.key === 'Escape') {
-                this.hide();
-                this.toggle.focus();
-                if (!this.bubbleEscapes) {
-                    event.stopPropagation();
-                }
-            }
-        };
-        this.container.addEventListener('keydown', keyboardNavigation);
-        if (this.moveMenu) {
-            this.menu.addEventListener('keydown', keyboardNavigation);
-        }
-
-        // Hide menu on enter press or escape
-        this.menu.addEventListener('keydown ', event => {
-            if (event.key === 'Enter') {
-                event.preventDefault();
-                event.stopPropagation();
-                this.hide();
+                keyboardNavHandler.focusNext();
             }
         });
     }
 
 }
-
-export default DropDown;
\ No newline at end of file
index 44fdf2d0d3fba800e31bf68364088751186428a0..911a033c776b81ca757db2ad1e85875d599c0877 100644 (file)
@@ -1,11 +1,8 @@
 import DropZoneLib from "dropzone";
 import {fadeOut} from "../services/animations";
+import {Component} from "./component";
 
-/**
- * Dropzone
- * @extends {Component}
- */
-class Dropzone {
+export class Dropzone extends Component {
     setup() {
         this.container = this.$el;
         this.url = this.$opts.url;
@@ -73,6 +70,4 @@ class Dropzone {
     removeAll() {
         this.dz.removeAllFiles(true);
     }
-}
-
-export default Dropzone;
\ No newline at end of file
+}
\ No newline at end of file
index 3a1442d75150f043b821795d21e709f4578fcccc..a581ae7b4609727200ad3251b6f0e5e59dd1213e 100644 (file)
@@ -1,51 +1,58 @@
-class EditorToolbox {
+import {Component} from "./component";
 
-    constructor(elem) {
+export class EditorToolbox extends Component {
+
+    setup() {
         // Elements
-        this.elem = elem;
-        this.buttons = elem.querySelectorAll('[toolbox-tab-button]');
-        this.contentElements = elem.querySelectorAll('[toolbox-tab-content]');
-        this.toggleButton = elem.querySelector('[toolbox-toggle]');
+        this.container = this.$el;
+        this.buttons = this.$manyRefs.tabButton;
+        this.contentElements = this.$manyRefs.tabContent;
+        this.toggleButton = this.$refs.toggle;
+
+        this.setupListeners();
+
+        // Set the first tab as active on load
+        this.setActiveTab(this.contentElements[0].dataset.tabContent);
+    }
 
+    setupListeners() {
         // Toolbox toggle button click
-        this.toggleButton.addEventListener('click', this.toggle.bind(this));
+        this.toggleButton.addEventListener('click', () => this.toggle());
         // Tab button click
-        this.elem.addEventListener('click', event => {
-            let button = event.target.closest('[toolbox-tab-button]');
-            if (button === null) return;
-            let name = button.getAttribute('toolbox-tab-button');
-            this.setActiveTab(name, true);
+        this.container.addEventListener('click', event => {
+            const button = event.target.closest('button');
+            if (this.buttons.includes(button)) {
+                const name = button.dataset.tab;
+                this.setActiveTab(name, true);
+            }
         });
-
-        // Set the first tab as active on load
-        this.setActiveTab(this.contentElements[0].getAttribute('toolbox-tab-content'));
     }
 
     toggle() {
-        this.elem.classList.toggle('open');
-        const expanded = this.elem.classList.contains('open') ? 'true' : 'false';
+        this.container.classList.toggle('open');
+        const expanded = this.container.classList.contains('open') ? 'true' : 'false';
         this.toggleButton.setAttribute('aria-expanded', expanded);
     }
 
     setActiveTab(tabName, openToolbox = false) {
+
         // Set button visibility
-        for (let i = 0, len = this.buttons.length; i < len; i++) {
-            this.buttons[i].classList.remove('active');
-            let bName =  this.buttons[i].getAttribute('toolbox-tab-button');
-            if (bName === tabName) this.buttons[i].classList.add('active');
+        for (const button of this.buttons) {
+            button.classList.remove('active');
+            const bName =  button.dataset.tab;
+            if (bName === tabName) button.classList.add('active');
         }
+
         // Set content visibility
-        for (let i = 0, len = this.contentElements.length; i < len; i++) {
-            this.contentElements[i].style.display = 'none';
-            let cName = this.contentElements[i].getAttribute('toolbox-tab-content');
-            if (cName === tabName) this.contentElements[i].style.display = 'block';
+        for (const contentEl of this.contentElements) {
+            contentEl.style.display = 'none';
+            const cName = contentEl.dataset.tabContent;
+            if (cName === tabName) contentEl.style.display = 'block';
         }
 
-        if (openToolbox && !this.elem.classList.contains('open')) {
+        if (openToolbox && !this.container.classList.contains('open')) {
             this.toggle();
         }
     }
 
-}
-
-export default EditorToolbox;
\ No newline at end of file
+}
\ No newline at end of file
index c67c85f19a699ef43fad99c7d300b6f4b8851b58..d4a616ff1d5765e2f278028016e8fa95bf1d8cbc 100644 (file)
@@ -1,9 +1,7 @@
-/**
- * @extends {Component}
- */
 import {htmlToDom} from "../services/dom";
+import {Component} from "./component";
 
-class EntityPermissions {
+export class EntityPermissions extends Component {
 
     setup() {
         this.container = this.$el;
@@ -62,7 +60,7 @@ class EntityPermissions {
     }
 
     removeRowOnButtonClick(button) {
-        const row = button.closest('.content-permissions-row');
+        const row = button.closest('.item-list-row');
         const roleId = button.dataset.roleId;
         const roleName = button.dataset.roleName;
 
@@ -74,6 +72,4 @@ class EntityPermissions {
         row.remove();
     }
 
-}
-
-export default EntityPermissions;
\ No newline at end of file
+}
\ No newline at end of file
index 8b1861ecf6afec1eb66e9d46843e34561471efc5..b0e42401d51b1520807cfb97d295afaf27ecd826 100644 (file)
@@ -1,10 +1,7 @@
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
-/**
- * Class EntitySearch
- * @extends {Component}
- */
-class EntitySearch {
+export class EntitySearch extends Component {
     setup() {
         this.entityId = this.$opts.entityId;
         this.entityType = this.$opts.entityType;
@@ -54,6 +51,4 @@ class EntitySearch {
         this.loadingBlock.classList.add('hidden');
         this.searchInput.value = '';
     }
-}
-
-export default EntitySearch;
\ No newline at end of file
+}
\ No newline at end of file
index e7cb60b1f6c4c05c751033ebec4424b8de7fea1d..d455f7ee7d5286f3bbfb9979ec0651ea6dfac98c 100644 (file)
@@ -1,14 +1,10 @@
-/**
- * Entity Selector Popup
- * @extends {Component}
- */
-class EntitySelectorPopup {
+import {Component} from "./component";
+
+export class EntitySelectorPopup extends Component {
 
     setup() {
-        this.elem = this.$el;
+        this.container = this.$el;
         this.selectButton = this.$refs.select;
-
-        window.EntitySelectorPopup = this;
         this.selectorEl = this.$refs.selector;
 
         this.callback = null;
@@ -21,16 +17,26 @@ class EntitySelectorPopup {
 
     show(callback) {
         this.callback = callback;
-        this.elem.components.popup.show();
+        this.getPopup().show();
         this.getSelector().focusSearch();
     }
 
     hide() {
-        this.elem.components.popup.hide();
+        this.getPopup().hide();
     }
 
+    /**
+     * @returns {Popup}
+     */
+    getPopup() {
+        return window.$components.firstOnElement(this.container, 'popup');
+    }
+
+    /**
+     * @returns {EntitySelector}
+     */
     getSelector() {
-        return this.selectorEl.components['entity-selector'];
+        return window.$components.firstOnElement(this.selectorEl, 'entity-selector');
     }
 
     onSelectButtonClick() {
@@ -51,6 +57,4 @@ class EntitySelectorPopup {
         this.getSelector().reset();
         if (this.callback && entity) this.callback(entity);
     }
-}
-
-export default EntitySelectorPopup;
\ No newline at end of file
+}
\ No newline at end of file
index e2596998aedea01a105b40496f1909c0d6b60f20..1384b33a9660c086882fd768054bf1c4eed73e21 100644 (file)
@@ -1,10 +1,10 @@
 import {onChildEvent} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * Entity Selector
- * @extends {Component}
  */
-class EntitySelector {
+export class EntitySelector extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -115,7 +115,7 @@ class EntitySelector {
     }
 
     searchUrl() {
-        return `/ajax/search/entities?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
+        return `/search/entity-selector?types=${encodeURIComponent(this.entityTypes)}&permission=${encodeURIComponent(this.entityPermission)}`;
     }
 
     searchEntities(searchTerm) {
@@ -185,6 +185,4 @@ class EntitySelector {
         this.selectedItemData = null;
     }
 
-}
-
-export default EntitySelector;
\ No newline at end of file
+}
\ No newline at end of file
index cf0215850d07c6f4acb2c98c2899cf373e87dba0..2e6fd5fdbac008566f7fc88701000327853e7f0d 100644 (file)
@@ -1,4 +1,5 @@
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * EventEmitSelect
@@ -10,10 +11,8 @@ import {onSelect} from "../services/dom";
  *
  * All options will be set as the "detail" of the event with
  * their values included.
- *
- * @extends {Component}
  */
-class EventEmitSelect {
+export class EventEmitSelect extends Component{
     setup() {
         this.container = this.$el;
         this.name = this.$opts.name;
@@ -24,6 +23,4 @@ class EventEmitSelect {
         });
     }
 
-}
-
-export default EventEmitSelect;
\ No newline at end of file
+}
\ No newline at end of file
index cce1b215c9378f30659befc189424f76145f4c6c..ab4d38ab1df2224270a27aab2f1fca6ae3786e10 100644 (file)
@@ -1,17 +1,15 @@
 import {slideUp, slideDown} from "../services/animations";
+import {Component} from "./component";
 
-class ExpandToggle {
+export class ExpandToggle extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-
-        // Component state
-        this.isOpen = elem.getAttribute('expand-toggle-is-open') === 'yes';
-        this.updateEndpoint = elem.getAttribute('expand-toggle-update-endpoint');
-        this.selector = elem.getAttribute('expand-toggle');
+    setup(elem) {
+        this.targetSelector = this.$opts.targetSelector;
+        this.isOpen = this.$opts.isOpen === 'true';
+        this.updateEndpoint = this.$opts.updateEndpoint;
 
         // Listener setup
-        elem.addEventListener('click', this.click.bind(this));
+        this.$el.addEventListener('click', this.click.bind(this));
     }
 
     open(elemToToggle) {
@@ -25,7 +23,7 @@ class ExpandToggle {
     click(event) {
         event.preventDefault();
 
-        const matchingElems = document.querySelectorAll(this.selector);
+        const matchingElems = document.querySelectorAll(this.targetSelector);
         for (let match of matchingElems) {
             this.isOpen ?  this.close(match) : this.open(match);
         }
@@ -40,6 +38,4 @@ class ExpandToggle {
         });
     }
 
-}
-
-export default ExpandToggle;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/resources/js/components/global-search.js b/resources/js/components/global-search.js
new file mode 100644 (file)
index 0000000..7bc8a1d
--- /dev/null
@@ -0,0 +1,82 @@
+import {htmlToDom} from "../services/dom";
+import {debounce} from "../services/util";
+import {KeyboardNavigationHandler} from "../services/keyboard-navigation";
+import {Component} from "./component";
+
+/**
+ * Global (header) search box handling.
+ * Mainly to show live results preview.
+ */
+export class GlobalSearch extends Component {
+
+    setup() {
+        this.container = this.$el;
+        this.input = this.$refs.input;
+        this.suggestions = this.$refs.suggestions;
+        this.suggestionResultsWrap = this.$refs.suggestionResults;
+        this.loadingWrap = this.$refs.loading;
+        this.button = this.$refs.button;
+
+        this.setupListeners();
+    }
+
+    setupListeners() {
+        const updateSuggestionsDebounced = debounce(this.updateSuggestions.bind(this), 200, false);
+
+        // Handle search input changes
+        this.input.addEventListener('input', () => {
+            const value = this.input.value;
+            if (value.length > 0) {
+                this.loadingWrap.style.display = 'block';
+                this.suggestionResultsWrap.style.opacity = '0.5';
+                updateSuggestionsDebounced(value);
+            }  else {
+                this.hideSuggestions();
+            }
+        });
+
+        // Allow double click to show auto-click suggestions
+        this.input.addEventListener('dblclick', () => {
+            this.input.setAttribute('autocomplete', 'on');
+            this.button.focus();
+            this.input.focus();
+        });
+
+        new KeyboardNavigationHandler(this.container, () => {
+            this.hideSuggestions();
+        });
+    }
+
+    /**
+     * @param {String} search
+     */
+    async updateSuggestions(search) {
+        const {data: results} = await window.$http.get('/search/suggest', {term: search});
+        if (!this.input.value) {
+            return;
+        }
+        
+        const resultDom = htmlToDom(results);
+
+        this.suggestionResultsWrap.innerHTML = '';
+        this.suggestionResultsWrap.style.opacity = '1';
+        this.loadingWrap.style.display = 'none';
+        this.suggestionResultsWrap.append(resultDom);
+        if (!this.container.classList.contains('search-active')) {
+            this.showSuggestions();
+        }
+    }
+
+    showSuggestions() {
+        this.container.classList.add('search-active');
+        window.requestAnimationFrame(() => {
+            this.suggestions.classList.add('search-suggestions-animation');
+        })
+    }
+
+    hideSuggestions() {
+        this.container.classList.remove('search-active');
+        this.suggestions.classList.remove('search-suggestions-animation');
+        this.suggestionResultsWrap.innerHTML = '';
+    }
+}
\ No newline at end of file
index 99737bfb8b0fb741564a5c854d566fd3f774067a..11b23cca6cc06f833d8d3146ceb6c0bb0ddf6bdf 100644 (file)
@@ -1,5 +1,6 @@
+import {Component} from "./component";
 
-class HeaderMobileToggle {
+export class HeaderMobileToggle extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -36,6 +37,4 @@ class HeaderMobileToggle {
         this.onToggle(event);
     }
 
-}
-
-export default HeaderMobileToggle;
\ No newline at end of file
+}
\ No newline at end of file
index 23a6c4cbb9bc1ffb29120997af3291dd36cb2d31..a44fffc1b437776af3d723e445eef617db7757b9 100644 (file)
@@ -1,13 +1,9 @@
 import {onChildEvent, onSelect, removeLoading, showLoading} from "../services/dom";
+import {Component} from "./component";
 
-/**
- * ImageManager
- * @extends {Component}
- */
-class ImageManager {
+export class ImageManager extends Component {
 
     setup() {
-
         // Options
         this.uploadedTo = this.$opts.uploadedTo;
 
@@ -36,8 +32,6 @@ class ImageManager {
         this.resetState();
 
         this.setupListeners();
-
-        window.ImageManager = this;
     }
 
     setupListeners() {
@@ -100,7 +94,7 @@ class ImageManager {
 
         this.callback = callback;
         this.type = type;
-        this.popupEl.components.popup.show();
+        this.getPopup().show();
         this.dropzoneContainer.classList.toggle('hidden', type !== 'gallery');
 
         if (!this.hasData) {
@@ -110,7 +104,14 @@ class ImageManager {
     }
 
     hide() {
-        this.popupEl.components.popup.hide();
+        this.getPopup().hide();
+    }
+
+    /**
+     * @returns {Popup}
+     */
+    getPopup() {
+        return window.$components.firstOnElement(this.popupEl, 'popup');
     }
 
     async loadGallery() {
@@ -132,7 +133,7 @@ class ImageManager {
     addReturnedHtmlElementsToList(html) {
         const el = document.createElement('div');
         el.innerHTML = html;
-        window.components.init(el);
+        window.$components.init(el);
         for (const child of [...el.children]) {
             this.listContainer.appendChild(child);
         }
@@ -207,9 +208,7 @@ class ImageManager {
         const params = requestDelete ? {delete: true} : {};
         const {data: formHtml} = await window.$http.get(`/images/edit/${imageId}`, params);
         this.formContainer.innerHTML = formHtml;
-        window.components.init(this.formContainer);
+        window.$components.init(this.formContainer);
     }
 
-}
-
-export default ImageManager;
\ No newline at end of file
+}
\ No newline at end of file
index 7455fa622379a798ae102dd65dc7c72aed28417e..03d9567d22e331c82a04c75bc1840e207c5d1eec 100644 (file)
@@ -1,21 +1,25 @@
+import {Component} from "./component";
 
-class ImagePicker {
+export class ImagePicker extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.imageElem = elem.querySelector('img');
-        this.imageInput = elem.querySelector('input[type=file]');
-        this.resetInput = elem.querySelector('input[data-reset-input]');
-        this.removeInput = elem.querySelector('input[data-remove-input]');
+    setup() {
+        this.imageElem = this.$refs.image;
+        this.imageInput = this.$refs.imageInput;
+        this.resetInput = this.$refs.resetInput;
+        this.removeInput = this.$refs.removeInput;
+        this.resetButton = this.$refs.resetButton;
+        this.removeButton = this.$refs.removeButton || null;
 
-        this.defaultImage = elem.getAttribute('data-default-image');
+        this.defaultImage = this.$opts.defaultImage;
 
-        const resetButton = elem.querySelector('button[data-action="reset-image"]');
-        resetButton.addEventListener('click', this.reset.bind(this));
+        this.setupListeners();
+    }
+
+    setupListeners() {
+        this.resetButton.addEventListener('click', this.reset.bind(this));
 
-        const removeButton = elem.querySelector('button[data-action="remove-image"]');
-        if (removeButton) {
-            removeButton.addEventListener('click', this.removeImage.bind(this));
+        if (this.removeButton) {
+            this.removeButton.addEventListener('click', this.removeImage.bind(this));
         }
 
         this.imageInput.addEventListener('change', this.fileInputChange.bind(this));
@@ -50,6 +54,4 @@ class ImagePicker {
         this.resetInput.setAttribute('disabled', 'disabled');
     }
 
-}
-
-export default ImagePicker;
\ No newline at end of file
+}
\ No newline at end of file
index 5b84edba035308aea25638927f35594dbbe5a5d3..27bce48dbb3d0354ac0dfde6961cd9e7c410974e 100644 (file)
-import addRemoveRows from "./add-remove-rows.js"
-import ajaxDeleteRow from "./ajax-delete-row.js"
-import ajaxForm from "./ajax-form.js"
-import attachments from "./attachments.js"
-import attachmentsList from "./attachments-list.js"
-import autoSuggest from "./auto-suggest.js"
-import backToTop from "./back-to-top.js"
-import bookSort from "./book-sort.js"
-import chapterContents from "./chapter-contents.js"
-import codeEditor from "./code-editor.js"
-import codeHighlighter from "./code-highlighter.js"
-import codeTextarea from "./code-textarea.js"
-import collapsible from "./collapsible.js"
-import confirmDialog from "./confirm-dialog"
-import customCheckbox from "./custom-checkbox.js"
-import detailsHighlighter from "./details-highlighter.js"
-import dropdown from "./dropdown.js"
-import dropdownSearch from "./dropdown-search.js"
-import dropzone from "./dropzone.js"
-import editorToolbox from "./editor-toolbox.js"
-import entityPermissions from "./entity-permissions";
-import entitySearch from "./entity-search.js"
-import entitySelector from "./entity-selector.js"
-import entitySelectorPopup from "./entity-selector-popup.js"
-import eventEmitSelect from "./event-emit-select.js"
-import expandToggle from "./expand-toggle.js"
-import headerMobileToggle from "./header-mobile-toggle.js"
-import homepageControl from "./homepage-control.js"
-import imageManager from "./image-manager.js"
-import imagePicker from "./image-picker.js"
-import listSortControl from "./list-sort-control.js"
-import markdownEditor from "./markdown-editor.js"
-import newUserPassword from "./new-user-password.js"
-import notification from "./notification.js"
-import optionalInput from "./optional-input.js"
-import pageComments from "./page-comments.js"
-import pageDisplay from "./page-display.js"
-import pageEditor from "./page-editor.js"
-import pagePicker from "./page-picker.js"
-import permissionsTable from "./permissions-table.js"
-import pointer from "./pointer.js";
-import popup from "./popup.js"
-import settingAppColorPicker from "./setting-app-color-picker.js"
-import settingColorPicker from "./setting-color-picker.js"
-import shelfSort from "./shelf-sort.js"
-import sidebar from "./sidebar.js"
-import sortableList from "./sortable-list.js"
-import submitOnChange from "./submit-on-change.js"
-import tabs from "./tabs.js"
-import tagManager from "./tag-manager.js"
-import templateManager from "./template-manager.js"
-import toggleSwitch from "./toggle-switch.js"
-import triLayout from "./tri-layout.js"
-import userSelect from "./user-select.js"
-import webhookEvents from "./webhook-events";
-import wysiwygEditor from "./wysiwyg-editor.js"
-
-const componentMapping = {
-    "add-remove-rows": addRemoveRows,
-    "ajax-delete-row": ajaxDeleteRow,
-    "ajax-form": ajaxForm,
-    "attachments": attachments,
-    "attachments-list": attachmentsList,
-    "auto-suggest": autoSuggest,
-    "back-to-top": backToTop,
-    "book-sort": bookSort,
-    "chapter-contents": chapterContents,
-    "code-editor": codeEditor,
-    "code-highlighter": codeHighlighter,
-    "code-textarea": codeTextarea,
-    "collapsible": collapsible,
-    "confirm-dialog": confirmDialog,
-    "custom-checkbox": customCheckbox,
-    "details-highlighter": detailsHighlighter,
-    "dropdown": dropdown,
-    "dropdown-search": dropdownSearch,
-    "dropzone": dropzone,
-    "editor-toolbox": editorToolbox,
-    "entity-permissions": entityPermissions,
-    "entity-search": entitySearch,
-    "entity-selector": entitySelector,
-    "entity-selector-popup": entitySelectorPopup,
-    "event-emit-select": eventEmitSelect,
-    "expand-toggle": expandToggle,
-    "header-mobile-toggle": headerMobileToggle,
-    "homepage-control": homepageControl,
-    "image-manager": imageManager,
-    "image-picker": imagePicker,
-    "list-sort-control": listSortControl,
-    "markdown-editor": markdownEditor,
-    "new-user-password": newUserPassword,
-    "notification": notification,
-    "optional-input": optionalInput,
-    "page-comments": pageComments,
-    "page-display": pageDisplay,
-    "page-editor": pageEditor,
-    "page-picker": pagePicker,
-    "permissions-table": permissionsTable,
-    "pointer": pointer,
-    "popup": popup,
-    "setting-app-color-picker": settingAppColorPicker,
-    "setting-color-picker": settingColorPicker,
-    "shelf-sort": shelfSort,
-    "sidebar": sidebar,
-    "sortable-list": sortableList,
-    "submit-on-change": submitOnChange,
-    "tabs": tabs,
-    "tag-manager": tagManager,
-    "template-manager": templateManager,
-    "toggle-switch": toggleSwitch,
-    "tri-layout": triLayout,
-    "user-select": userSelect,
-    "webhook-events": webhookEvents,
-    "wysiwyg-editor": wysiwygEditor,
-};
-
-window.components = {};
-
-/**
- * Initialize components of the given name within the given element.
- * @param {String} componentName
- * @param {HTMLElement|Document} parentElement
- */
-function searchForComponentInParent(componentName, parentElement) {
-    const elems = parentElement.querySelectorAll(`[${componentName}]`);
-    for (let j = 0, jLen = elems.length; j < jLen; j++) {
-        initComponent(componentName, elems[j]);
-    }
-}
-
-/**
- * Initialize a component instance on the given dom element.
- * @param {String} name
- * @param {Element} element
- */
-function initComponent(name, element) {
-    const componentModel = componentMapping[name];
-    if (componentModel === undefined) return;
-
-    // Create our component instance
-    let instance;
-    try {
-        instance = new componentModel(element);
-        instance.$el = element;
-        const allRefs = parseRefs(name, element);
-        instance.$refs = allRefs.refs;
-        instance.$manyRefs = allRefs.manyRefs;
-        instance.$opts = parseOpts(name, element);
-        instance.$emit = (eventName, data = {}) => {
-            data.from = instance;
-            const event = new CustomEvent(`${name}-${eventName}`, {
-                bubbles: true,
-                detail: data
-            });
-            instance.$el.dispatchEvent(event);
-        };
-        if (typeof instance.setup === 'function') {
-            instance.setup();
-        }
-    } catch (e) {
-        console.error('Failed to create component', e, name, element);
-    }
-
-
-    // Add to global listing
-    if (typeof window.components[name] === "undefined") {
-        window.components[name] = [];
-    }
-    window.components[name].push(instance);
-
-    // Add to element listing
-    if (typeof element.components === 'undefined') {
-        element.components = {};
-    }
-    element.components[name] = instance;
-}
-
-/**
- * Parse out the element references within the given element
- * for the given component name.
- * @param {String} name
- * @param {Element} element
- */
-function parseRefs(name, element) {
-    const refs = {};
-    const manyRefs = {};
-
-    const prefix = `${name}@`
-    const selector = `[refs*="${prefix}"]`;
-    const refElems = [...element.querySelectorAll(selector)];
-    if (element.matches(selector)) {
-        refElems.push(element);
-    }
-
-    for (const el of refElems) {
-        const refNames = el.getAttribute('refs')
-            .split(' ')
-            .filter(str => str.startsWith(prefix))
-            .map(str => str.replace(prefix, ''))
-            .map(kebabToCamel);
-        for (const ref of refNames) {
-            refs[ref] = el;
-            if (typeof manyRefs[ref] === 'undefined') {
-                manyRefs[ref] = [];
-            }
-            manyRefs[ref].push(el);
-        }
-    }
-    return {refs, manyRefs};
-}
-
-/**
- * Parse out the element component options.
- * @param {String} name
- * @param {Element} element
- * @return {Object<String, String>}
- */
-function parseOpts(name, element) {
-    const opts = {};
-    const prefix = `option:${name}:`;
-    for (const {name, value} of element.attributes) {
-        if (name.startsWith(prefix)) {
-            const optName = name.replace(prefix, '');
-            opts[kebabToCamel(optName)] = value || '';
-        }
-    }
-    return opts;
-}
-
-/**
- * Convert a kebab-case string to camelCase
- * @param {String} kebab
- * @returns {string}
- */
-function kebabToCamel(kebab) {
-    const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
-    const words = kebab.split('-');
-    return words[0] + words.slice(1).map(ucFirst).join('');
-}
-
-/**
- * Initialize all components found within the given element.
- * @param parentElement
- */
-function initAll(parentElement) {
-    if (typeof parentElement === 'undefined') parentElement = document;
-
-    // Old attribute system
-    for (const componentName of Object.keys(componentMapping)) {
-        searchForComponentInParent(componentName, parentElement);
-    }
-
-    // New component system
-    const componentElems = parentElement.querySelectorAll(`[component],[components]`);
-
-    for (const el of componentElems) {
-        const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
-        for (const name of componentNames) {
-            initComponent(name, el);
-        }
-    }
-}
-
-window.components.init = initAll;
-window.components.first = (name) => (window.components[name] || [null])[0];
-
-export default initAll;
-
-/**
- * @typedef Component
- * @property {HTMLElement} $el
- * @property {Object<String, HTMLElement>} $refs
- * @property {Object<String, HTMLElement[]>} $manyRefs
- * @property {Object<String, String>} $opts
- * @property {function(string, Object)} $emit
- */
\ No newline at end of file
+export {AddRemoveRows} from "./add-remove-rows.js"
+export {AjaxDeleteRow} from "./ajax-delete-row.js"
+export {AjaxForm} from "./ajax-form.js"
+export {Attachments} from "./attachments.js"
+export {AttachmentsList} from "./attachments-list.js"
+export {AutoSuggest} from "./auto-suggest.js"
+export {AutoSubmit} from "./auto-submit.js"
+export {BackToTop} from "./back-to-top.js"
+export {BookSort} from "./book-sort.js"
+export {ChapterContents} from "./chapter-contents.js"
+export {CodeEditor} from "./code-editor.js"
+export {CodeHighlighter} from "./code-highlighter.js"
+export {CodeTextarea} from "./code-textarea.js"
+export {Collapsible} from "./collapsible.js"
+export {ConfirmDialog} from "./confirm-dialog"
+export {CustomCheckbox} from "./custom-checkbox.js"
+export {DetailsHighlighter} from "./details-highlighter.js"
+export {Dropdown} from "./dropdown.js"
+export {DropdownSearch} from "./dropdown-search.js"
+export {Dropzone} from "./dropzone.js"
+export {EditorToolbox} from "./editor-toolbox.js"
+export {EntityPermissions} from "./entity-permissions"
+export {EntitySearch} from "./entity-search.js"
+export {EntitySelector} from "./entity-selector.js"
+export {EntitySelectorPopup} from "./entity-selector-popup.js"
+export {EventEmitSelect} from "./event-emit-select.js"
+export {ExpandToggle} from "./expand-toggle.js"
+export {GlobalSearch} from "./global-search.js"
+export {HeaderMobileToggle} from "./header-mobile-toggle.js"
+export {ImageManager} from "./image-manager.js"
+export {ImagePicker} from "./image-picker.js"
+export {ListSortControl} from "./list-sort-control.js"
+export {MarkdownEditor} from "./markdown-editor.js"
+export {NewUserPassword} from "./new-user-password.js"
+export {Notification} from "./notification.js"
+export {OptionalInput} from "./optional-input.js"
+export {PageComments} from "./page-comments.js"
+export {PageDisplay} from "./page-display.js"
+export {PageEditor} from "./page-editor.js"
+export {PagePicker} from "./page-picker.js"
+export {PermissionsTable} from "./permissions-table.js"
+export {Pointer} from "./pointer.js"
+export {Popup} from "./popup.js"
+export {SettingAppColorPicker} from "./setting-app-color-picker.js"
+export {SettingColorPicker} from "./setting-color-picker.js"
+export {SettingHomepageControl} from "./setting-homepage-control.js"
+export {ShelfSort} from "./shelf-sort.js"
+export {Shortcuts} from "./shortcuts"
+export {ShortcutInput} from "./shortcut-input"
+export {SortableList} from "./sortable-list.js"
+export {SubmitOnChange} from "./submit-on-change.js"
+export {Tabs} from "./tabs.js"
+export {TagManager} from "./tag-manager.js"
+export {TemplateManager} from "./template-manager.js"
+export {ToggleSwitch} from "./toggle-switch.js"
+export {TriLayout} from "./tri-layout.js"
+export {UserSelect} from "./user-select.js"
+export {WebhookEvents} from "./webhook-events"
+export {WysiwygEditor} from "./wysiwyg-editor.js"
index 23fc64ae6a47cde19602f4af610c75926582d275..b8d4de73a0e7b8cd3326ca9e30679366043af7be 100644 (file)
@@ -2,16 +2,22 @@
  * ListSortControl
  * Manages the logic for the control which provides list sorting options.
  */
-class ListSortControl {
+import {Component} from "./component";
 
-    constructor(elem) {
-        this.elem = elem;
-        this.menu = elem.querySelector('ul');
+export class ListSortControl extends Component {
 
-        this.sortInput = elem.querySelector('[name="sort"]');
-        this.orderInput = elem.querySelector('[name="order"]');
-        this.form = elem.querySelector('form');
+    setup() {
+        this.elem = this.$el;
+        this.menu = this.$refs.menu;
 
+        this.sortInput = this.$refs.sort;
+        this.orderInput = this.$refs.order;
+        this.form = this.$refs.form;
+
+        this.setupListeners();
+    }
+
+    setupListeners() {
         this.menu.addEventListener('click', event => {
             if (event.target.closest('[data-sort-value]') !== null) {
                 this.sortOptionClick(event);
@@ -34,12 +40,9 @@ class ListSortControl {
 
     sortDirectionClick(event) {
         const currentDir = this.orderInput.value;
-        const newDir = (currentDir === 'asc') ? 'desc' : 'asc';
-        this.orderInput.value = newDir;
+        this.orderInput.value = (currentDir === 'asc') ? 'desc' : 'asc';
         event.preventDefault();
         this.form.submit();
     }
 
-}
-
-export default ListSortControl;
\ No newline at end of file
+}
\ No newline at end of file
index 3290fc3001ac59786af0f4f140f301171926da5a..4c3de91f68cafff30c947edb4085a4898109cf9d 100644 (file)
@@ -1,11 +1,8 @@
-import MarkdownIt from "markdown-it";
-import mdTasksLists from 'markdown-it-task-lists';
-import Clipboard from "../services/clipboard";
 import {debounce} from "../services/util";
-import {patchDomFromHtmlString} from "../services/vdom";
-import DrawIO from "../services/drawio";
+import {Component} from "./component";
+import {init as initEditor} from "../markdown/editor";
 
-class MarkdownEditor {
+export class MarkdownEditor extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -15,80 +12,59 @@ class MarkdownEditor {
         this.imageUploadErrorText = this.$opts.imageUploadErrorText;
         this.serverUploadLimitText = this.$opts.serverUploadLimitText;
 
-        this.markdown = new MarkdownIt({html: true});
-        this.markdown.use(mdTasksLists, {label: true});
+        this.display = this.$refs.display;
+        this.input = this.$refs.input;
+        this.divider = this.$refs.divider;
+        this.displayWrap = this.$refs.displayWrap;
 
-        this.display = this.elem.querySelector('.markdown-display');
-
-        this.displayStylesLoaded = false;
-        this.input = this.elem.querySelector('textarea');
-
-        this.cm = null;
-        this.Code = null;
-        const cmLoadPromise = window.importVersioned('code').then(Code => {
-            this.cm = Code.markdownEditor(this.input);
-            this.Code = Code;
-            return this.cm;
-        });
-
-        this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
-
-        const displayLoad = () => {
-            this.displayDoc = this.display.contentDocument;
-            this.init(cmLoadPromise);
-        };
-
-        if (this.display.contentDocument.readyState === 'complete') {
-            displayLoad();
-        } else {
-            this.display.addEventListener('load', displayLoad.bind(this));
-        }
+        const settingContainer = this.$refs.settingContainer;
+        const settingInputs = settingContainer.querySelectorAll('input[type="checkbox"]');
 
+        this.editor = null;
+        initEditor({
+            pageId: this.pageId,
+            container: this.elem,
+            displayEl: this.display,
+            inputEl: this.input,
+            drawioUrl: this.getDrawioUrl(),
+            settingInputs: Array.from(settingInputs),
+            text: {
+                serverUploadLimit: this.serverUploadLimitText,
+                imageUploadError: this.imageUploadErrorText,
+            },
+        }).then(editor => {
+            this.editor = editor;
+            this.setupListeners();
+            this.emitEditorEvents();
+            this.scrollToTextIfNeeded();
+            this.editor.actions.updateAndRender();
+        });
+    }
+
+    emitEditorEvents() {
         window.$events.emitPublic(this.elem, 'editor-markdown::setup', {
-            markdownIt: this.markdown,
+            markdownIt: this.editor.markdown.getRenderer(),
             displayEl: this.display,
-            codeMirrorInstance: this.cm,
+            codeMirrorInstance: this.editor.cm,
         });
     }
 
-    init(cmLoadPromise) {
-
-        let lastClick = 0;
-
-        // Prevent markdown display link click redirect
-        this.displayDoc.addEventListener('click', event => {
-            let isDblClick = Date.now() - lastClick < 300;
-
-            let link = event.target.closest('a');
-            if (link !== null) {
-                event.preventDefault();
-                window.open(link.getAttribute('href'));
-                return;
-            }
-
-            let drawing = event.target.closest('[drawio-diagram]');
-            if (drawing !== null && isDblClick) {
-                this.actionEditDrawing(drawing);
-                return;
-            }
-
-            lastClick = Date.now();
-        });
+    setupListeners() {
 
         // Button actions
         this.elem.addEventListener('click', event => {
             let button = event.target.closest('button[data-action]');
             if (button === null) return;
 
-            let action = button.getAttribute('data-action');
-            if (action === 'insertImage') this.actionInsertImage();
-            if (action === 'insertLink') this.actionShowLinkSelector();
+            const action = button.getAttribute('data-action');
+            if (action === 'insertImage') this.editor.actions.insertImage();
+            if (action === 'insertLink') this.editor.actions.showLinkSelector();
             if (action === 'insertDrawing' && (event.ctrlKey || event.metaKey)) {
-                this.actionShowImageManager();
+                this.editor.actions.showImageManager();
                 return;
             }
-            if (action === 'insertDrawing') this.actionStartDrawing();
-            if (action === 'fullscreen') this.actionFullScreen();
+            if (action === 'insertDrawing') this.editor.actions.startDrawing();
+            if (action === 'fullscreen') this.editor.actions.fullScreen();
         });
 
         // Mobile section toggling
@@ -97,527 +73,68 @@ class MarkdownEditor {
             if (!toolbarLabel) return;
 
             const currentActiveSections = this.elem.querySelectorAll('.markdown-editor-wrap');
-            for (let activeElem of currentActiveSections) {
+            for (const activeElem of currentActiveSections) {
                 activeElem.classList.remove('active');
             }
 
             toolbarLabel.closest('.markdown-editor-wrap').classList.add('active');
         });
 
-        cmLoadPromise.then(cm => {
-            this.codeMirrorSetup(cm);
-
-            // Refresh CodeMirror on container resize
-            const resizeDebounced = debounce(() => this.Code.updateLayout(cm), 100, false);
-            const observer = new ResizeObserver(resizeDebounced);
-            observer.observe(this.elem);
-        });
-
-        this.listenForBookStackEditorEvents();
-
-        // Scroll to text if needed.
-        const queryParams = (new URL(window.location)).searchParams;
-        const scrollText = queryParams.get('content-text');
-        if (scrollText) {
-            this.scrollToText(scrollText);
-        }
-    }
-
-    // Update the input content and render the display.
-    updateAndRender() {
-        const content = this.cm.getValue();
-        this.input.value = content;
-
-        const html = this.markdown.render(content);
-        window.$events.emit('editor-html-change', html);
-        window.$events.emit('editor-markdown-change', content);
-
-        // Set body content
-        const target = this.getDisplayTarget();
-        this.displayDoc.body.className = 'page-content';
-        patchDomFromHtmlString(target, html);
-
-        // Copy styles from page head and set custom styles for editor
-        this.loadStylesIntoDisplay();
-    }
-
-    getDisplayTarget() {
-        const body = this.displayDoc.body;
-
-        if (body.children.length === 0) {
-            const wrap = document.createElement('div');
-            this.displayDoc.body.append(wrap);
-        }
-
-        return body.children[0];
-    }
-
-    loadStylesIntoDisplay() {
-        if (this.displayStylesLoaded) return;
-        this.displayDoc.documentElement.classList.add('markdown-editor-display');
-        // Set display to be dark mode if parent is
-
-        if (document.documentElement.classList.contains('dark-mode')) {
-            this.displayDoc.documentElement.style.backgroundColor = '#222';
-            this.displayDoc.documentElement.classList.add('dark-mode');
-        }
-
-        this.displayDoc.head.innerHTML = '';
-        const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
-        for (let style of styles) {
-            const copy = style.cloneNode(true);
-            this.displayDoc.head.appendChild(copy);
-        }
-
-        this.displayStylesLoaded = true;
-    }
-
-    onMarkdownScroll(lineCount) {
-        const elems = this.displayDoc.body.children;
-        if (elems.length <= lineCount) return;
-
-        const topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount];
-        topElem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth'});
-    }
-
-    codeMirrorSetup(cm) {
-        const context = this;
-
-        // Text direction
-        // cm.setOption('direction', this.textDirection);
-        cm.setOption('direction', 'ltr'); // Will force to remain as ltr for now due to issues when HTML is in editor.
-        // Custom key commands
-        let metaKey = this.Code.getMetaKey();
-        const extraKeys = {};
-        // Insert Image shortcut
-        extraKeys[`${metaKey}-Alt-I`] = function(cm) {
-            let selectedText = cm.getSelection();
-            let newText = `![${selectedText}](http://)`;
-            let cursorPos = cm.getCursor('from');
-            cm.replaceSelection(newText);
-            cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1);
-        };
-        // Save draft
-        extraKeys[`${metaKey}-S`] = cm => {window.$events.emit('editor-save-draft')};
-        // Save page
-        extraKeys[`${metaKey}-Enter`] = cm => {window.$events.emit('editor-save-page')};
-        // Show link selector
-        extraKeys[`Shift-${metaKey}-K`] = cm => {this.actionShowLinkSelector()};
-        // Insert Link
-        extraKeys[`${metaKey}-K`] = cm => {insertLink()};
-        // FormatShortcuts
-        extraKeys[`${metaKey}-1`] = cm => {replaceLineStart('##');};
-        extraKeys[`${metaKey}-2`] = cm => {replaceLineStart('###');};
-        extraKeys[`${metaKey}-3`] = cm => {replaceLineStart('####');};
-        extraKeys[`${metaKey}-4`] = cm => {replaceLineStart('#####');};
-        extraKeys[`${metaKey}-5`] = cm => {replaceLineStart('');};
-        extraKeys[`${metaKey}-D`] = cm => {replaceLineStart('');};
-        extraKeys[`${metaKey}-6`] = cm => {replaceLineStart('>');};
-        extraKeys[`${metaKey}-Q`] = cm => {replaceLineStart('>');};
-        extraKeys[`${metaKey}-7`] = cm => {wrapSelection('\n```\n', '\n```');};
-        extraKeys[`${metaKey}-8`] = cm => {wrapSelection('`', '`');};
-        extraKeys[`Shift-${metaKey}-E`] = cm => {wrapSelection('`', '`');};
-        extraKeys[`${metaKey}-9`] = cm => {wrapSelection('<p class="callout info">', '</p>');};
-        extraKeys[`${metaKey}-P`] = cm => {replaceLineStart('-')}
-        extraKeys[`${metaKey}-O`] = cm => {replaceLineStartForOrderedList()}
-        cm.setOption('extraKeys', extraKeys);
-
-        // Update data on content change
-        cm.on('change', (instance, changeObj) => {
-            this.updateAndRender();
-        });
-
-        const onScrollDebounced = debounce((instance) => {
-            // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
-            let scroll = instance.getScrollInfo();
-            let atEnd = scroll.top + scroll.clientHeight === scroll.height;
-            if (atEnd) {
-                this.onMarkdownScroll(-1);
-                return;
-            }
-
-            let lineNum = instance.lineAtHeight(scroll.top, 'local');
-            let range = instance.getRange({line: 0, ch: null}, {line: lineNum, ch: null});
-            let parser = new DOMParser();
-            let doc = parser.parseFromString(this.markdown.render(range), 'text/html');
-            let totalLines = doc.documentElement.querySelectorAll('body > *');
-            this.onMarkdownScroll(totalLines.length);
-        }, 100);
-
-        // Handle scroll to sync display view
-        cm.on('scroll', instance => {
-            onScrollDebounced(instance);
-        });
-
-        // Handle image paste
-        cm.on('paste', (cm, event) => {
-            const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);
-
-            // Don't handle the event ourselves if no items exist of contains table-looking data
-            if (!clipboard.hasItems() || clipboard.containsTabularData()) {
-                return;
-            }
-
-            const images = clipboard.getImages();
-            for (const image of images) {
-                uploadImage(image);
-            }
-        });
-
-        // Handle image & content drag n drop
-        cm.on('drop', (cm, event) => {
-
-            const templateId = event.dataTransfer.getData('bookstack/template');
-            if (templateId) {
-                const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
-                cm.setCursor(cursorPos);
-                event.preventDefault();
-                window.$http.get(`/templates/${templateId}`).then(resp => {
-                    const content = resp.data.markdown || resp.data.html;
-                    cm.replaceSelection(content);
-                });
-            }
-
-            const clipboard = new Clipboard(event.dataTransfer);
-            if (clipboard.hasItems() && clipboard.getImages().length > 0) {
-                const cursorPos = cm.coordsChar({left: event.pageX, top: event.pageY});
-                cm.setCursor(cursorPos);
-                event.stopPropagation();
-                event.preventDefault();
-                const images = clipboard.getImages();
-                for (const image of images) {
-                    uploadImage(image);
-                }
-            }
-
-        });
-
-        // Helper to replace editor content
-        function replaceContent(search, replace) {
-            let text = cm.getValue();
-            let cursor = cm.listSelections();
-            cm.setValue(text.replace(search, replace));
-            cm.setSelections(cursor);
-        }
-
-        // Helper to replace the start of the line
-        function replaceLineStart(newStart) {
-            let cursor = cm.getCursor();
-            let lineContent = cm.getLine(cursor.line);
-            let lineLen = lineContent.length;
-            let lineStart = lineContent.split(' ')[0];
-
-            // Remove symbol if already set
-            if (lineStart === newStart) {
-                lineContent = lineContent.replace(`${newStart} `, '');
-                cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
-                cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)});
-                return;
-            }
-
-            let alreadySymbol = /^[#>`]/.test(lineStart);
-            let posDif = 0;
-            if (alreadySymbol) {
-                posDif = newStart.length - lineStart.length;
-                lineContent = lineContent.replace(lineStart, newStart).trim();
-            } else if (newStart !== '') {
-                posDif = newStart.length + 1;
-                lineContent = newStart + ' ' + lineContent;
-            }
-            cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
-            cm.setCursor({line: cursor.line, ch: cursor.ch + posDif});
-        }
-
-        function wrapLine(start, end) {
-            let cursor = cm.getCursor();
-            let lineContent = cm.getLine(cursor.line);
-            let lineLen = lineContent.length;
-            let newLineContent = lineContent;
-
-            if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
-                newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
-            } else {
-                newLineContent = `${start}${lineContent}${end}`;
-            }
-
-            cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
-            cm.setCursor({line: cursor.line, ch: cursor.ch + start.length});
-        }
-
-        function wrapSelection(start, end) {
-            let selection = cm.getSelection();
-            if (selection === '') return wrapLine(start, end);
-
-            let newSelection = selection;
-            let frontDiff = 0;
-            let endDiff = 0;
-
-            if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
-                newSelection = selection.slice(start.length, selection.length - end.length);
-                endDiff = -(end.length + start.length);
-            } else {
-                newSelection = `${start}${selection}${end}`;
-                endDiff = start.length + end.length;
-            }
-
-            let selections = cm.listSelections()[0];
-            cm.replaceSelection(newSelection);
-            let headFirst = selections.head.ch <= selections.anchor.ch;
-            selections.head.ch += headFirst ? frontDiff : endDiff;
-            selections.anchor.ch += headFirst ? endDiff : frontDiff;
-            cm.setSelections([selections]);
-        }
-
-        function replaceLineStartForOrderedList() {
-            const cursor = cm.getCursor();
-            const prevLineContent = cm.getLine(cursor.line - 1) || '';
-            const listMatch = prevLineContent.match(/^(\s*)(\d)([).])\s/) || [];
-
-            const number = (Number(listMatch[2]) || 0) + 1;
-            const whiteSpace = listMatch[1] || '';
-            const listMark = listMatch[3] || '.'
-
-            const prefix = `${whiteSpace}${number}${listMark}`;
-            return replaceLineStart(prefix);
-        }
-
-        // Handle image upload and add image into markdown content
-        function uploadImage(file) {
-            if (file === null || file.type.indexOf('image') !== 0) return;
-            let ext = 'png';
-
-            if (file.name) {
-                let fileNameMatches = file.name.match(/\.(.+)$/);
-                if (fileNameMatches.length > 1) ext = fileNameMatches[1];
-            }
-
-            // Insert image into markdown
-            const id = "image-" + Math.random().toString(16).slice(2);
-            const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
-            const selectedText = cm.getSelection();
-            const placeHolderText = `![${selectedText}](${placeholderImage})`;
-            const cursor = cm.getCursor();
-            cm.replaceSelection(placeHolderText);
-            cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3});
-
-            const remoteFilename = "image-" + Date.now() + "." + ext;
-            const formData = new FormData();
-            formData.append('file', file, remoteFilename);
-            formData.append('uploaded_to', context.pageId);
+        // Refresh CodeMirror on container resize
+        const resizeDebounced = debounce(() => this.editor.cm.refresh(), 100, false);
+        const observer = new ResizeObserver(resizeDebounced);
+        observer.observe(this.elem);
 
-            window.$http.post('/images/gallery', formData).then(resp => {
-                const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`;
-                replaceContent(placeHolderText, newContent);
-            }).catch(err => {
-                window.$events.emit('error', context.imageUploadErrorText);
-                replaceContent(placeHolderText, selectedText);
-                console.log(err);
-            });
-        }
-
-        function insertLink() {
-            let cursorPos = cm.getCursor('from');
-            let selectedText = cm.getSelection() || '';
-            let newText = `[${selectedText}]()`;
-            cm.focus();
-            cm.replaceSelection(newText);
-            let cursorPosDiff = (selectedText === '') ? -3 : -1;
-            cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff);
-        }
-
-       this.updateAndRender();
-    }
-
-    actionInsertImage() {
-        const cursorPos = this.cm.getCursor('from');
-        window.ImageManager.show(image => {
-            const imageUrl = image.thumbs.display || image.url;
-            let selectedText = this.cm.getSelection();
-            let newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")";
-            this.cm.focus();
-            this.cm.replaceSelection(newText);
-            this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
-        }, 'gallery');
-    }
-
-    actionShowImageManager() {
-        const cursorPos = this.cm.getCursor('from');
-        window.ImageManager.show(image => {
-            this.insertDrawing(image, cursorPos);
-        }, 'drawio');
-    }
-
-    // Show the popup link selector and insert a link when finished
-    actionShowLinkSelector() {
-        const cursorPos = this.cm.getCursor('from');
-        window.EntitySelectorPopup.show(entity => {
-            let selectedText = this.cm.getSelection() || entity.name;
-            let newText = `[${selectedText}](${entity.link})`;
-            this.cm.focus();
-            this.cm.replaceSelection(newText);
-            this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
-        });
-    }
-
-    getDrawioUrl() {
-        const drawioUrlElem = document.querySelector('[drawio-url]');
-        return drawioUrlElem ? drawioUrlElem.getAttribute('drawio-url') : false;
+        this.handleDividerDrag();
     }
 
-    // Show draw.io if enabled and handle save.
-    actionStartDrawing() {
-        const url = this.getDrawioUrl();
-        if (!url) return;
-
-        const cursorPos = this.cm.getCursor('from');
-
-        DrawIO.show(url,() => {
-            return Promise.resolve('');
-        }, (pngData) => {
-
-            const data = {
-                image: pngData,
-                uploaded_to: Number(this.pageId),
+    handleDividerDrag() {
+        this.divider.addEventListener('pointerdown', event => {
+            const wrapRect = this.elem.getBoundingClientRect();
+            const moveListener = (event) => {
+                const xRel = event.pageX - wrapRect.left;
+                const xPct = Math.min(Math.max(20, Math.floor((xRel / wrapRect.width) * 100)), 80);
+                this.displayWrap.style.flexBasis = `${100-xPct}%`;
+                this.editor.settings.set('editorWidth', xPct);
             };
-
-            window.$http.post("/images/drawio", data).then(resp => {
-                this.insertDrawing(resp.data, cursorPos);
-                DrawIO.close();
-            }).catch(err => {
-                this.handleDrawingUploadError(err);
-            });
-        });
-    }
-
-    insertDrawing(image, originalCursor) {
-        const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
-        this.cm.focus();
-        this.cm.replaceSelection(newText);
-        this.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
-    }
-
-    // Show draw.io if enabled and handle save.
-    actionEditDrawing(imgContainer) {
-        const drawioUrl = this.getDrawioUrl();
-        if (!drawioUrl) {
-            return;
-        }
-
-        const cursorPos = this.cm.getCursor('from');
-        const drawingId = imgContainer.getAttribute('drawio-diagram');
-
-        DrawIO.show(drawioUrl, () => {
-            return DrawIO.load(drawingId);
-        }, (pngData) => {
-
-            let data = {
-                image: pngData,
-                uploaded_to: Number(this.pageId),
+            const upListener = (event) => {
+                window.removeEventListener('pointermove', moveListener);
+                window.removeEventListener('pointerup', upListener);
+                this.display.style.pointerEvents = null;
+                document.body.style.userSelect = null;
+                this.editor.cm.refresh();
             };
 
-            window.$http.post("/images/drawio", data).then(resp => {
-                let newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
-                let newContent = this.cm.getValue().split('\n').map(line => {
-                    if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
-                        return newText;
-                    }
-                    return line;
-                }).join('\n');
-                this.cm.setValue(newContent);
-                this.cm.setCursor(cursorPos);
-                this.cm.focus();
-                DrawIO.close();
-            }).catch(err => {
-                this.handleDrawingUploadError(err);
-            });
+            this.display.style.pointerEvents = 'none';
+            document.body.style.userSelect = 'none';
+            window.addEventListener('pointermove', moveListener);
+            window.addEventListener('pointerup', upListener);
         });
-    }
-
-    handleDrawingUploadError(error) {
-        if (error.status === 413) {
-            window.$events.emit('error', this.serverUploadLimitText);
-        } else {
-            window.$events.emit('error', this.imageUploadErrorText);
+        const widthSetting = this.editor.settings.get('editorWidth');
+        if (widthSetting) {
+            this.displayWrap.style.flexBasis = `${100-widthSetting}%`;
         }
-        console.log(error);
     }
 
-    // Make the editor full screen
-    actionFullScreen() {
-        const alreadyFullscreen = this.elem.classList.contains('fullscreen');
-        this.elem.classList.toggle('fullscreen', !alreadyFullscreen);
-        document.body.classList.toggle('markdown-fullscreen', !alreadyFullscreen);
-    }
-
-    // Scroll to a specified text
-    scrollToText(searchText) {
-        if (!searchText) {
-            return;
-        }
-
-        const content = this.cm.getValue();
-        const lines = content.split(/\r?\n/);
-        let lineNumber = lines.findIndex(line => {
-            return line && line.indexOf(searchText) !== -1;
-        });
-
-        if (lineNumber === -1) {
-            return;
+    scrollToTextIfNeeded() {
+        const queryParams = (new URL(window.location)).searchParams;
+        const scrollText = queryParams.get('content-text');
+        if (scrollText) {
+            this.editor.actions.scrollToText(scrollText);
         }
-
-        this.cm.scrollIntoView({
-            line: lineNumber,
-        }, 200);
-        this.cm.focus();
-        // set the cursor location.
-        this.cm.setCursor({
-            line: lineNumber,
-            char: lines[lineNumber].length
-        })
     }
 
-    listenForBookStackEditorEvents() {
-
-        function getContentToInsert({html, markdown}) {
-            return markdown || html;
+    /**
+     * Get the URL for the configured drawio instance.
+     * @returns {String}
+     */
+    getDrawioUrl() {
+        const drawioAttrEl = document.querySelector('[drawio-url]');
+        if (!drawioAttrEl) {
+            return '';
         }
 
-        // Replace editor content
-        window.$events.listen('editor::replace', (eventContent) => {
-            const markdown = getContentToInsert(eventContent);
-            this.cm.setValue(markdown);
-        });
-
-        // Append editor content
-        window.$events.listen('editor::append', (eventContent) => {
-            const cursorPos = this.cm.getCursor('from');
-            const markdown = getContentToInsert(eventContent);
-            const content = this.cm.getValue() + '\n' + markdown;
-            this.cm.setValue(content);
-            this.cm.setCursor(cursorPos.line, cursorPos.ch);
-        });
-
-        // Prepend editor content
-        window.$events.listen('editor::prepend', (eventContent) => {
-            const cursorPos = this.cm.getCursor('from');
-            const markdown = getContentToInsert(eventContent);
-            const content = markdown + '\n' + this.cm.getValue();
-            this.cm.setValue(content);
-            const prependLineCount = markdown.split('\n').length;
-            this.cm.setCursor(cursorPos.line + prependLineCount, cursorPos.ch);
-        });
-
-        // Insert editor content at the current location
-        window.$events.listen('editor::insert', (eventContent) => {
-            const markdown = getContentToInsert(eventContent);
-            this.cm.replaceSelection(markdown);
-        });
-
-        // Focus on editor
-        window.$events.listen('editor::focus', () => {
-            this.cm.focus();
-        });
+        return drawioAttrEl.getAttribute('drawio-url') || '';
     }
-}
 
-export default MarkdownEditor ;
+}
index 9c4c21c14cc0bae368f8bf9be2fd5abd546b8482..a4ed4d15b300c5e255e86ef43eee9f8c7b31d014 100644 (file)
@@ -1,9 +1,11 @@
+import {Component} from "./component";
 
-class NewUserPassword {
+export class NewUserPassword extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.inviteOption = elem.querySelector('input[name=send_invite]');
+    setup() {
+        this.container = this.$el;
+        this.inputContainer = this.$refs.inputContainer;
+        this.inviteOption = this.container.querySelector('input[name=send_invite]');
 
         if (this.inviteOption) {
             this.inviteOption.addEventListener('change', this.inviteOptionChange.bind(this));
@@ -13,16 +15,12 @@ class NewUserPassword {
 
     inviteOptionChange() {
         const inviting = (this.inviteOption.value === 'true');
-        const passwordBoxes = this.elem.querySelectorAll('input[type=password]');
+        const passwordBoxes = this.container.querySelectorAll('input[type=password]');
         for (const input of passwordBoxes) {
             input.disabled = inviting;
         }
-        const container = this.elem.querySelector('#password-input-container');
-        if (container) {
-            container.style.display = inviting ? 'none' : 'block';
-        }
-    }
 
-}
+        this.inputContainer.style.display = inviting ? 'none' : 'block';
+    }
 
-export default NewUserPassword;
\ No newline at end of file
+}
\ No newline at end of file
index 35bab4ea656b1c052e875fac2bcdf0e4ef27d268..8a0876241fe15232b82264e93556357691816614 100644 (file)
@@ -1,19 +1,21 @@
+import {Component} from "./component";
 
-class Notification {
+export class Notification  extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.type = elem.getAttribute('notification');
-        this.textElem = elem.querySelector('span');
-        this.autohide = this.elem.hasAttribute('data-autohide');
-        this.elem.style.display = 'grid';
+    setup() {
+        this.container = this.$el;
+        this.type = this.$opts.type;
+        this.textElem = this.container.querySelector('span');
+        this.autoHide = this.$opts.autoHide === 'true';
+        this.initialShow = this.$opts.show === 'true'
+        this.container.style.display = 'grid';
 
         window.$events.listen(this.type, text => {
             this.show(text);
         });
-        elem.addEventListener('click', this.hide.bind(this));
+        this.container.addEventListener('click', this.hide.bind(this));
 
-        if (elem.hasAttribute('data-show')) {
+        if (this.initialShow) {
             setTimeout(() => this.show(this.textElem.textContent), 100);
         }
 
@@ -21,14 +23,14 @@ class Notification {
     }
 
     show(textToShow = '') {
-        this.elem.removeEventListener('transitionend', this.hideCleanup);
+        this.container.removeEventListener('transitionend', this.hideCleanup);
         this.textElem.textContent = textToShow;
-        this.elem.style.display = 'grid';
+        this.container.style.display = 'grid';
         setTimeout(() => {
-            this.elem.classList.add('showing');
+            this.container.classList.add('showing');
         }, 1);
 
-        if (this.autohide) {
+        if (this.autoHide) {
             const words = textToShow.split(' ').length;
             const timeToShow = Math.max(2000, 1000 + (250 * words));
             setTimeout(this.hide.bind(this), timeToShow);
@@ -36,15 +38,13 @@ class Notification {
     }
 
     hide() {
-        this.elem.classList.remove('showing');
-        this.elem.addEventListener('transitionend', this.hideCleanup);
+        this.container.classList.remove('showing');
+        this.container.addEventListener('transitionend', this.hideCleanup);
     }
 
     hideCleanup() {
-        this.elem.style.display = 'none';
-        this.elem.removeEventListener('transitionend', this.hideCleanup);
+        this.container.style.display = 'none';
+        this.container.removeEventListener('transitionend', this.hideCleanup);
     }
 
-}
-
-export default Notification;
\ No newline at end of file
+}
\ No newline at end of file
index eab58e42a584a53c18a1727a3fe497789c23c381..cc429c991522bcd9b51a5fc811127e7775a4a066 100644 (file)
@@ -1,6 +1,7 @@
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
-class OptionalInput {
+export class OptionalInput extends Component {
     setup() {
         this.removeButton = this.$refs.remove;
         this.showButton = this.$refs.show;
@@ -23,6 +24,4 @@ class OptionalInput {
         });
     }
 
-}
-
-export default OptionalInput;
\ No newline at end of file
+}
\ No newline at end of file
index c86eead1b865bd8bdaa8184f44bd4ab55a961d7b..726531e951f86f4a8447b41844803eb1d01dd083 100644 (file)
@@ -1,9 +1,8 @@
 import {scrollAndHighlightElement} from "../services/util";
+import {Component} from "./component";
+import {htmlToDom} from "../services/dom";
 
-/**
- * @extends {Component}
- */
-class PageComments {
+export class PageComments extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -90,7 +89,7 @@ class PageComments {
             newComment.innerHTML = resp.data;
             this.editingComment.innerHTML = newComment.children[0].innerHTML;
             window.$events.success(this.updatedText);
-            window.components.init(this.editingComment);
+            window.$components.init(this.editingComment);
             this.closeUpdateForm();
             this.editingComment = null;
         }).catch(window.$events.showValidationErrors).then(() => {
@@ -119,11 +118,9 @@ class PageComments {
         };
         this.showLoading(this.form);
         window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
-            let newComment = document.createElement('div');
-            newComment.innerHTML = resp.data;
-            let newElem = newComment.children[0];
+            const newElem = htmlToDom(resp.data);
             this.container.appendChild(newElem);
-            window.components.init(newElem);
+            window.$components.init(newElem);
             window.$events.success(this.createdText);
             this.resetForm();
             this.updateCount();
@@ -199,6 +196,4 @@ class PageComments {
         formElem.querySelector('.form-group.loading').style.display = 'none';
     }
 
-}
-
-export default PageComments;
\ No newline at end of file
+}
\ No newline at end of file
index b4f1cca4fbc5682fc124d0342ee3189c198a0c3a..c06c3310dee2c44c221b331c8a78b8d3eee9d3f3 100644 (file)
@@ -1,11 +1,12 @@
 import * as DOM from "../services/dom";
 import {scrollAndHighlightElement} from "../services/util";
+import {Component} from "./component";
 
-class PageDisplay {
+export class PageDisplay extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.pageId = elem.getAttribute('page-display');
+    setup() {
+        this.container = this.$el;
+        this.pageId = this.$opts.pageId;
 
         window.importVersioned('code').then(Code => Code.highlight());
         this.setupNavHighlighting();
@@ -13,7 +14,7 @@ class PageDisplay {
 
         // Check the hash on load
         if (window.location.hash) {
-            let text = window.location.hash.replace(/\%20/g, ' ').substr(1);
+            const text = window.location.hash.replace(/%20/g, ' ').substring(1);
             this.goToText(text);
         }
 
@@ -22,7 +23,7 @@ class PageDisplay {
         if (sidebarPageNav) {
             DOM.onChildEvent(sidebarPageNav, 'a', 'click', (event, child) => {
                 event.preventDefault();
-                window.components['tri-layout'][0].showContent();
+                window.$components.first('tri-layout').showContent();
                 const contentId = child.getAttribute('href').substr(1);
                 this.goToText(contentId);
                 window.history.pushState(null, null, '#' + contentId);
@@ -49,17 +50,10 @@ class PageDisplay {
     }
 
     setupNavHighlighting() {
-        // Check if support is present for IntersectionObserver
-        if (!('IntersectionObserver' in window) ||
-            !('IntersectionObserverEntry' in window) ||
-            !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) {
-            return;
-        }
-
-        let pageNav = document.querySelector('.sidebar-page-nav');
+        const pageNav = document.querySelector('.sidebar-page-nav');
 
         // fetch all the headings.
-        let headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
+        const headings = document.querySelector('.page-content').querySelectorAll('h1, h2, h3, h4, h5, h6');
         // if headings are present, add observers.
         if (headings.length > 0 && pageNav !== null) {
             addNavObserver(headings);
@@ -67,21 +61,21 @@ class PageDisplay {
 
         function addNavObserver(headings) {
             // Setup the intersection observer.
-            let intersectOpts = {
+            const intersectOpts = {
                 rootMargin: '0px 0px 0px 0px',
                 threshold: 1.0
             };
-            let pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
+            const pageNavObserver = new IntersectionObserver(headingVisibilityChange, intersectOpts);
 
             // observe each heading
-            for (let heading of headings) {
+            for (const heading of headings) {
                 pageNavObserver.observe(heading);
             }
         }
 
         function headingVisibilityChange(entries, observer) {
-            for (let entry of entries) {
-                let isVisible = (entry.intersectionRatio === 1);
+            for (const entry of entries) {
+                const isVisible = (entry.intersectionRatio === 1);
                 toggleAnchorHighlighting(entry.target.id, isVisible);
             }
         }
@@ -99,9 +93,7 @@ class PageDisplay {
             codeMirrors.forEach(cm => cm.CodeMirror && cm.CodeMirror.refresh());
         };
 
-        const details = [...this.elem.querySelectorAll('details')];
+        const details = [...this.container.querySelectorAll('details')];
         details.forEach(detail => detail.addEventListener('toggle', onToggle));
     }
-}
-
-export default PageDisplay;
+}
\ No newline at end of file
index ce123e987b055db94769f7326a8fdf63f841342b..950a5a3b3308ee928039e82a96eac859f20beb53 100644 (file)
@@ -1,11 +1,9 @@
 import * as Dates from "../services/dates";
 import {onSelect} from "../services/dom";
+import {debounce} from "../services/util";
+import {Component} from "./component";
 
-/**
- * Page Editor
- * @extends {Component}
- */
-class PageEditor {
+export class PageEditor extends Component {
     setup() {
         // Options
         this.draftsEnabled = this.$opts.draftsEnabled === 'true';
@@ -69,7 +67,8 @@ class PageEditor {
         });
 
         // Changelog controls
-        this.changelogInput.addEventListener('change', this.updateChangelogDisplay.bind(this));
+        const updateChangelogDebounced = debounce(this.updateChangelogDisplay.bind(this), 300, false);
+        this.changelogInput.addEventListener('input', updateChangelogDebounced);
 
         // Draft Controls
         onSelect(this.saveDraftButton, this.saveDraft.bind(this));
@@ -199,7 +198,8 @@ class PageEditor {
         event.preventDefault();
 
         const link = event.target.closest('a').href;
-        const dialog = this.switchDialogContainer.components['confirm-dialog'];
+        /** @var {ConfirmDialog} **/
+        const dialog = window.$components.firstOnElement(this.switchDialogContainer, 'confirm-dialog');
         const [saved, confirmed] = await Promise.all([this.saveDraft(), dialog.show()]);
 
         if (saved && confirmed) {
@@ -208,5 +208,3 @@ class PageEditor {
     }
 
 }
-
-export default PageEditor;
\ No newline at end of file
index 577e9f6db7c8a19491c37860a20a6bd6711d3445..fba0a0a43f779a8802c0a7130789fb948f2e0226 100644 (file)
@@ -1,14 +1,14 @@
+import {Component} from "./component";
 
-class PagePicker {
+export class PagePicker extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.input = elem.querySelector('input');
-        this.resetButton = elem.querySelector('[page-picker-reset]');
-        this.selectButton = elem.querySelector('[page-picker-select]');
-        this.display = elem.querySelector('[page-picker-display]');
-        this.defaultDisplay = elem.querySelector('[page-picker-default]');
-        this.buttonSep = elem.querySelector('span.sep');
+    setup() {
+        this.input = this.$refs.input;
+        this.resetButton = this.$refs.resetButton;
+        this.selectButton = this.$refs.selectButton;
+        this.display = this.$refs.display;
+        this.defaultDisplay = this.$refs.defaultDisplay;
+        this.buttonSep = this.$refs.buttonSeperator;
 
         this.value = this.input.value;
         this.setupListeners();
@@ -24,7 +24,9 @@ class PagePicker {
     }
 
     showPopup() {
-        window.EntitySelectorPopup.show(entity => {
+        /** @type {EntitySelectorPopup} **/
+        const selectorPopup = window.$components.first('entity-selector-popup');
+        selectorPopup.show(entity => {
             this.setValue(entity.id, entity.name);
         });
     }
@@ -36,7 +38,7 @@ class PagePicker {
     }
 
     controlView(name) {
-        let hasValue = this.value && this.value !== 0;
+        const hasValue = this.value && this.value !== 0;
         toggleElem(this.resetButton, hasValue);
         toggleElem(this.buttonSep, hasValue);
         toggleElem(this.defaultDisplay, !hasValue);
@@ -55,8 +57,5 @@ class PagePicker {
 }
 
 function toggleElem(elem, show) {
-    let display = (elem.tagName === 'BUTTON' || elem.tagName === 'SPAN') ? 'inline-block' : 'block';
-    elem.style.display = show ? display : 'none';
-}
-
-export default PagePicker;
\ No newline at end of file
+    elem.style.display = show ? null : 'none';
+}
\ No newline at end of file
index df3c055cafa037f59a551b7887a3fb0f355646f4..58ead1d57620b58a798ccdc0dfc3708121c8a1f9 100644 (file)
@@ -1,8 +1,11 @@
+import {Component} from "./component";
 
-class PermissionsTable {
+export class PermissionsTable extends Component {
 
     setup() {
         this.container = this.$el;
+        this.cellSelector = this.$opts.cellSelector || 'td,th';
+        this.rowSelector = this.$opts.rowSelector || 'tr';
 
         // Handle toggle all event
         for (const toggleAllElem of (this.$manyRefs.toggleAll || [])) {
@@ -27,15 +30,15 @@ class PermissionsTable {
 
     toggleRowClick(event) {
         event.preventDefault();
-        this.toggleAllInElement(event.target.closest('tr'));
+        this.toggleAllInElement(event.target.closest(this.rowSelector));
     }
 
     toggleColumnClick(event) {
         event.preventDefault();
 
-        const tableCell = event.target.closest('th,td');
+        const tableCell = event.target.closest(this.cellSelector);
         const colIndex = Array.from(tableCell.parentElement.children).indexOf(tableCell);
-        const tableRows = tableCell.closest('table').querySelectorAll('tr');
+        const tableRows = this.container.querySelectorAll(this.rowSelector);
         const inputsToToggle = [];
 
         for (let row of tableRows) {
@@ -60,6 +63,4 @@ class PermissionsTable {
         }
     }
 
-}
-
-export default PermissionsTable;
\ No newline at end of file
+}
\ No newline at end of file
index d1967acd01c7821e81291ac22e4b1474568ac529..d884dc7214ce3ac71673ac0bddf71f39c2755fee 100644 (file)
@@ -1,10 +1,9 @@
 import * as DOM from "../services/dom";
 import Clipboard from "clipboard/dist/clipboard.min";
+import {Component} from "./component";
 
-/**
- * @extends Component
- */
-class Pointer {
+
+export class Pointer extends Component {
 
     setup() {
         this.container = this.$el;
@@ -126,6 +125,4 @@ class Pointer {
             editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
         }
     }
-}
-
-export default Pointer;
\ No newline at end of file
+}
\ No newline at end of file
index ec111963f51e65358c51d5d3333bd5d47fb8e55d..4c20876f854dbfd8441b7cd4d3d8ffb38bc6c28d 100644 (file)
@@ -1,13 +1,13 @@
 import {fadeIn, fadeOut} from "../services/animations";
 import {onSelect} from "../services/dom";
+import {Component} from "./component";
 
 /**
  * Popup window that will contain other content.
  * This component provides the show/hide functionality
  * with the ability for popup@hide child references to close this.
- * @extends {Component}
  */
-class Popup {
+export class Popup extends Component {
 
     setup() {
         this.container = this.$el;
@@ -56,6 +56,4 @@ class Popup {
         this.onHide = onHide;
     }
 
-}
-
-export default Popup;
\ No newline at end of file
+}
\ No newline at end of file
index ee894c9325c5c5fc0bef05f684a2e6e8032f3661..68e5abce5a2d8fddb670a9b8a5ee2c482490a3d8 100644 (file)
@@ -1,23 +1,13 @@
+import {Component} from "./component";
 
-class SettingAppColorPicker {
+export class SettingAppColorPicker extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.colorInput = elem.querySelector('input[type=color]');
-        this.lightColorInput = elem.querySelector('input[name="setting-app-color-light"]');
-        this.resetButton = elem.querySelector('[setting-app-color-picker-reset]');
-        this.defaultButton = elem.querySelector('[setting-app-color-picker-default]');
+    setup() {
+        this.colorInput = this.$refs.input;
+        this.lightColorInput = this.$refs.lightInput;
 
         this.colorInput.addEventListener('change', this.updateColor.bind(this));
         this.colorInput.addEventListener('input', this.updateColor.bind(this));
-        this.resetButton.addEventListener('click', event => {
-            this.colorInput.value = this.colorInput.dataset.current;
-            this.updateColor();
-        });
-        this.defaultButton.addEventListener('click', event => {
-            this.colorInput.value = this.colorInput.dataset.default;
-            this.updateColor();
-        });
     }
 
     /**
@@ -44,8 +34,8 @@ class SettingAppColorPicker {
     /**
      * Covert a hex color code to rgb components.
      * @attribution https://stackoverflow.com/a/5624139
-     * @param hex
-     * @returns {*}
+     * @param {String} hex
+     * @returns {{r: Number, g: Number, b: Number}}
      */
     hexToRgb(hex) {
         const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
@@ -57,5 +47,3 @@ class SettingAppColorPicker {
     }
 
 }
-
-export default SettingAppColorPicker;
index 4d8ce0f933ced670923e375902712cc82b7dd0dd..876e14f20c276a2fc7c4401316f0b80e1fa6a0a8 100644 (file)
@@ -1,18 +1,20 @@
+import {Component} from "./component";
 
-class SettingColorPicker {
+export class SettingColorPicker extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.colorInput = elem.querySelector('input[type=color]');
-        this.resetButton = elem.querySelector('[setting-color-picker-reset]');
-        this.defaultButton = elem.querySelector('[setting-color-picker-default]');
-        this.resetButton.addEventListener('click', event => {
-            this.colorInput.value = this.colorInput.dataset.current;
-        });
-        this.defaultButton.addEventListener('click', event => {
-          this.colorInput.value = this.colorInput.dataset.default;
-        });
+    setup() {
+        this.colorInput = this.$refs.input;
+        this.resetButton = this.$refs.resetButton;
+        this.defaultButton = this.$refs.defaultButton;
+        this.currentColor = this.$opts.current;
+        this.defaultColor = this.$opts.default;
+
+        this.resetButton.addEventListener('click', () => this.setValue(this.currentColor));
+        this.defaultButton.addEventListener('click', () => this.setValue(this.defaultColor));
     }
-}
 
-export default SettingColorPicker;
+    setValue(value) {
+        this.colorInput.value = value;
+        this.colorInput.dispatchEvent(new Event('change'));
+    }
+}
\ No newline at end of file
similarity index 55%
rename from resources/js/components/homepage-control.js
rename to resources/js/components/setting-homepage-control.js
index 9db9e17b88ee58419256b6576a8ff76a7b5a59a1..992be9f826dad07c36b74abcdbc647ff53dc0ce2 100644 (file)
@@ -1,10 +1,10 @@
+import {Component} from "./component";
 
-class HomepageControl {
+export class SettingHomepageControl extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.typeControl = elem.querySelector('[name="setting-app-homepage-type"]');
-        this.pagePickerContainer = elem.querySelector('[page-picker-container]');
+    setup() {
+        this.typeControl = this.$refs.typeControl;
+        this.pagePickerContainer = this.$refs.pagePickerContainer;
 
         this.typeControl.addEventListener('change', this.controlPagePickerVisibility.bind(this));
         this.controlPagePickerVisibility();
@@ -14,9 +14,4 @@ class HomepageControl {
         const showPagePicker = this.typeControl.value === 'page';
         this.pagePickerContainer.style.display = (showPagePicker ? 'block' : 'none');
     }
-
-
-
-}
-
-export default HomepageControl;
\ No newline at end of file
+}
\ No newline at end of file
index 30eda5a21f7cf920fc99f254ac051b3e36b79408..d10470bd79a4ea7e19774bcb5eedc3c6e533ffad 100644 (file)
@@ -1,6 +1,7 @@
 import Sortable from "sortablejs";
+import {Component} from "./component";
 
-class ShelfSort {
+export class ShelfSort extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -15,7 +16,7 @@ class ShelfSort {
 
     initSortable() {
         const scrollBoxes = this.elem.querySelectorAll('.scroll-box');
-        for (let scrollBox of scrollBoxes) {
+        for (const scrollBox of scrollBoxes) {
             new Sortable(scrollBox, {
                 group: 'shelf-books',
                 ghostClass: 'primary-background-light',
@@ -78,6 +79,4 @@ class ShelfSort {
         this.input.value = shelfBookElems.map(elem => elem.getAttribute('data-id')).join(',');
     }
 
-}
-
-export default ShelfSort;
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/resources/js/components/shortcut-input.js b/resources/js/components/shortcut-input.js
new file mode 100644 (file)
index 0000000..2a2aaa2
--- /dev/null
@@ -0,0 +1,54 @@
+import {Component} from "./component";
+
+/**
+ * Keys to ignore when recording shortcuts.
+ * @type {string[]}
+ */
+const ignoreKeys = ['Control', 'Alt', 'Shift', 'Meta', 'Super', ' ', '+', 'Tab', 'Escape'];
+
+export class ShortcutInput extends Component {
+
+    setup() {
+        this.input = this.$el;
+
+        this.setupListeners();
+    }
+
+    setupListeners() {
+        this.listenerRecordKey = this.listenerRecordKey.bind(this);
+
+        this.input.addEventListener('focus', () => {
+             this.startListeningForInput();
+        });
+
+        this.input.addEventListener('blur', () => {
+            this.stopListeningForInput();
+        })
+    }
+
+    startListeningForInput() {
+        this.input.addEventListener('keydown', this.listenerRecordKey)
+    }
+
+    /**
+     * @param {KeyboardEvent} event
+     */
+    listenerRecordKey(event) {
+        if (ignoreKeys.includes(event.key)) {
+            return;
+        }
+
+        const keys = [
+            event.ctrlKey ? 'Ctrl' : '',
+            event.metaKey ? 'Cmd' : '',
+            event.key,
+        ];
+
+        this.input.value = keys.filter(s => Boolean(s)).join(' + ');
+    }
+
+    stopListeningForInput() {
+        this.input.removeEventListener('keydown', this.listenerRecordKey);
+    }
+
+}
\ No newline at end of file
diff --git a/resources/js/components/shortcuts.js b/resources/js/components/shortcuts.js
new file mode 100644 (file)
index 0000000..a87213b
--- /dev/null
@@ -0,0 +1,162 @@
+import {Component} from "./component";
+
+function reverseMap(map) {
+    const reversed = {};
+    for (const [key, value] of Object.entries(map)) {
+        reversed[value] = key;
+    }
+    return reversed;
+}
+
+
+export class Shortcuts extends Component {
+
+    setup() {
+        this.container = this.$el;
+        this.mapById = JSON.parse(this.$opts.keyMap);
+        this.mapByShortcut = reverseMap(this.mapById);
+
+        this.hintsShowing = false;
+
+        this.hideHints = this.hideHints.bind(this);
+
+        this.setupListeners();
+    }
+
+    setupListeners() {
+        window.addEventListener('keydown', event => {
+
+            if (event.target.closest('input, select, textarea')) {
+                return;
+            }
+
+            this.handleShortcutPress(event);
+        });
+
+        window.addEventListener('keydown', event => {
+            if (event.key === '?') {
+                this.hintsShowing ? this.hideHints() : this.showHints();
+            }
+        });
+    }
+
+    /**
+     * @param {KeyboardEvent} event
+     */
+    handleShortcutPress(event) {
+
+        const keys = [
+            event.ctrlKey ? 'Ctrl' : '',
+            event.metaKey ? 'Cmd' : '',
+            event.key,
+        ];
+
+        const combo = keys.filter(s => Boolean(s)).join(' + ');
+
+        const shortcutId = this.mapByShortcut[combo];
+        if (shortcutId) {
+            const wasHandled = this.runShortcut(shortcutId);
+            if (wasHandled) {
+                event.preventDefault();
+            }
+        }
+    }
+
+    /**
+     * Run the given shortcut, and return a boolean to indicate if the event
+     * was successfully handled by a shortcut action.
+     * @param {String} id
+     * @return {boolean}
+     */
+    runShortcut(id) {
+        const el = this.container.querySelector(`[data-shortcut="${id}"]`);
+        if (!el) {
+            return false;
+        }
+
+        if (el.matches('input, textarea, select')) {
+            el.focus();
+            return true;
+        }
+
+        if (el.matches('a, button')) {
+            el.click();
+            return true;
+        }
+
+        if (el.matches('div[tabindex]')) {
+            el.click();
+            el.focus();
+            return true;
+        }
+
+        console.error(`Shortcut attempted to be ran for element type that does not have handling setup`, el);
+
+        return false;
+    }
+
+    showHints() {
+        const wrapper = document.createElement('div');
+        wrapper.classList.add('shortcut-container');
+        this.container.append(wrapper);
+
+        const shortcutEls = this.container.querySelectorAll('[data-shortcut]');
+        const displayedIds = new Set();
+        for (const shortcutEl of shortcutEls) {
+            const id = shortcutEl.getAttribute('data-shortcut');
+            if (displayedIds.has(id)) {
+                continue;
+            }
+
+            const key = this.mapById[id];
+            this.showHintLabel(shortcutEl, key, wrapper);
+            displayedIds.add(id);
+        }
+
+        window.addEventListener('scroll', this.hideHints);
+        window.addEventListener('focus', this.hideHints);
+        window.addEventListener('blur', this.hideHints);
+        window.addEventListener('click', this.hideHints);
+
+        this.hintsShowing = true;
+    }
+
+    /**
+     * @param {Element} targetEl
+     * @param {String} key
+     * @param {Element} wrapper
+     */
+    showHintLabel(targetEl, key, wrapper) {
+        const targetBounds = targetEl.getBoundingClientRect();
+
+        const label = document.createElement('div');
+        label.classList.add('shortcut-hint');
+        label.textContent = key;
+
+        const linkage = document.createElement('div');
+        linkage.classList.add('shortcut-linkage');
+        linkage.style.left = targetBounds.x + 'px';
+        linkage.style.top = targetBounds.y + 'px';
+        linkage.style.width = targetBounds.width + 'px';
+        linkage.style.height = targetBounds.height + 'px';
+
+        wrapper.append(label, linkage);
+
+        const labelBounds = label.getBoundingClientRect();
+
+        label.style.insetInlineStart = `${((targetBounds.x + targetBounds.width) - (labelBounds.width + 6))}px`;
+        label.style.insetBlockStart = `${(targetBounds.y + (targetBounds.height - labelBounds.height) / 2)}px`;
+    }
+
+    hideHints() {
+        const wrapper = this.container.querySelector('.shortcut-container');
+        wrapper.remove();
+
+        window.removeEventListener('scroll', this.hideHints);
+        window.removeEventListener('focus', this.hideHints);
+        window.removeEventListener('blur', this.hideHints);
+        window.removeEventListener('click', this.hideHints);
+
+        this.hintsShowing = false;
+    }
+}
\ No newline at end of file
diff --git a/resources/js/components/sidebar.js b/resources/js/components/sidebar.js
deleted file mode 100644 (file)
index 0fecc5e..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-
-class Sidebar {
-
-    constructor(elem) {
-        this.elem = elem;
-        this.toggleElem = elem.querySelector('.sidebar-toggle');
-        this.toggleElem.addEventListener('click', this.toggle.bind(this));
-    }
-
-    toggle(show = true) {
-        this.elem.classList.toggle('open');
-    }
-
-}
-
-export default Sidebar;
\ No newline at end of file
index 0af0e11c901a5b58d98f4ef7687004c117334e3d..bbbd92088ab9f3191e516cecae9833c2058b05a7 100644 (file)
@@ -1,4 +1,5 @@
 import Sortable from "sortablejs";
+import {Component} from "./component";
 
 /**
  * SortableList
@@ -6,10 +7,8 @@ import Sortable from "sortablejs";
  * Can have data set on the dragged items by setting a 'data-drag-content' attribute.
  * This attribute must contain JSON where the keys are content types and the values are
  * the data to set on the data-transfer.
- *
- * @extends {Component}
  */
-class SortableList {
+export class SortableList extends Component {
     setup() {
         this.container = this.$el;
         this.handleSelector = this.$opts.handleSelector;
@@ -34,6 +33,4 @@ class SortableList {
             dragoverBubble: false,
         });
     }
-}
-
-export default SortableList;
\ No newline at end of file
+}
\ No newline at end of file
index aeacae23213bd96b94290ed9ab6a4e9e131a4b78..da4ac699608f21a11a5392dfd272a3492ae178a9 100644 (file)
@@ -1,9 +1,10 @@
+import {Component} from "./component";
+
 /**
  * Submit on change
  * Simply submits a parent form when this input is changed.
- * @extends {Component}
  */
-class SubmitOnChange {
+export class SubmitOnChange extends Component {
 
     setup() {
         this.filter = this.$opts.filter;
@@ -21,6 +22,4 @@ class SubmitOnChange {
         });
     }
 
-}
-
-export default SubmitOnChange;
\ No newline at end of file
+}
\ No newline at end of file
index 7121d70448c728530231eed0cbd74dc5647326f3..46063d240e3e3704910fe5a0782b6df4bfd73d27 100644 (file)
@@ -1,11 +1,11 @@
+import {onSelect} from "../services/dom";
+import {Component} from "./component";
+
 /**
  * Tabs
  * Works by matching 'tabToggle<Key>' with 'tabContent<Key>' sections.
- * @extends {Component}
  */
-import {onSelect} from "../services/dom";
-
-class Tabs {
+export class Tabs extends Component {
 
     setup() {
         this.tabContentsByName = {};
@@ -46,6 +46,4 @@ class Tabs {
         }
     }
 
-}
-
-export default Tabs;
\ No newline at end of file
+}
\ No newline at end of file
index 99302b6c05176e057ffd161edc9fbd3b9b8ca46f..cfbc514a07250070563798cb04c1b1f330e92c1f 100644 (file)
@@ -1,8 +1,6 @@
-/**
- * TagManager
- * @extends {Component}
- */
-class TagManager {
+import {Component} from "./component";
+
+export class TagManager extends Component {
     setup() {
         this.addRemoveComponentEl = this.$refs.addRemove;
         this.container = this.$el;
@@ -13,7 +11,8 @@ class TagManager {
 
     setupListeners() {
         this.container.addEventListener('change', event => {
-            const addRemoveComponent = this.addRemoveComponentEl.components['add-remove-rows'];
+            /** @var {AddRemoveRows} **/
+            const addRemoveComponent = window.$components.firstOnElement(this.addRemoveComponentEl, 'add-remove-rows');
             if (!this.hasEmptyRows()) {
                 addRemoveComponent.add();
             }
@@ -27,6 +26,4 @@ class TagManager {
         });
         return firstEmpty !== undefined;
     }
-}
-
-export default TagManager;
\ No newline at end of file
+}
\ No newline at end of file
index f8b19a40c86c5baf51d4239e887c55beba97c48b..774336471f00c954ec74b6625566d886d2a1ac7b 100644 (file)
@@ -1,25 +1,48 @@
 import * as DOM from "../services/dom";
+import {Component} from "./component";
 
-class TemplateManager {
+export class TemplateManager extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.list = elem.querySelector('[template-manager-list]');
-        this.searching = false;
+    setup() {
+        this.container = this.$el;
+        this.list = this.$refs.list;
 
+        this.searchInput = this.$refs.searchInput;
+        this.searchButton = this.$refs.searchButton;
+        this.searchCancel = this.$refs.searchCancel;
+
+        this.setupListeners();
+    }
+
+    setupListeners() {
         // Template insert action buttons
-        DOM.onChildEvent(this.elem, '[template-action]', 'click', this.handleTemplateActionClick.bind(this));
+        DOM.onChildEvent(this.container, '[template-action]', 'click', this.handleTemplateActionClick.bind(this));
 
         // Template list pagination click
-        DOM.onChildEvent(this.elem, '.pagination a', 'click', this.handlePaginationClick.bind(this));
+        DOM.onChildEvent(this.container, '.pagination a', 'click', this.handlePaginationClick.bind(this));
 
         // Template list item content click
-        DOM.onChildEvent(this.elem, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this));
+        DOM.onChildEvent(this.container, '.template-item-content', 'click', this.handleTemplateItemClick.bind(this));
 
         // Template list item drag start
-        DOM.onChildEvent(this.elem, '.template-item', 'dragstart', this.handleTemplateItemDragStart.bind(this));
+        DOM.onChildEvent(this.container, '.template-item', 'dragstart', this.handleTemplateItemDragStart.bind(this));
 
-        this.setupSearchBox();
+        // Search box enter press
+        this.searchInput.addEventListener('keypress', event => {
+            if (event.key === 'Enter') {
+                event.preventDefault();
+                this.performSearch();
+            }
+        });
+
+        // Search submit button press
+        this.searchButton.addEventListener('click', event => this.performSearch());
+
+        // Search cancel button press
+        this.searchCancel.addEventListener('click', event => {
+            this.searchInput.value = '';
+            this.performSearch();
+        });
     }
 
     handleTemplateItemClick(event, templateItem) {
@@ -54,45 +77,12 @@ class TemplateManager {
         this.list.innerHTML = resp.data;
     }
 
-    setupSearchBox() {
-        const searchBox = this.elem.querySelector('.search-box');
-
-        // Search box may not exist if there are no existing templates in the system.
-        if (!searchBox) return;
-
-        const input = searchBox.querySelector('input');
-        const submitButton = searchBox.querySelector('button');
-        const cancelButton = searchBox.querySelector('button.search-box-cancel');
-
-        async function performSearch() {
-            const searchTerm = input.value;
-            const resp = await window.$http.get(`/templates`, {
-                search: searchTerm
-            });
-            cancelButton.style.display = searchTerm ? 'block' : 'none';
-            this.list.innerHTML = resp.data;
-        }
-        performSearch = performSearch.bind(this);
-
-        // Search box enter press
-        searchBox.addEventListener('keypress', event => {
-            if (event.key === 'Enter') {
-                event.preventDefault();
-                performSearch();
-            }
-        });
-
-        // Submit button press
-        submitButton.addEventListener('click', event => {
-            performSearch();
-        });
-
-        // Cancel button press
-        cancelButton.addEventListener('click', event => {
-            input.value = '';
-            performSearch();
+    async performSearch() {
+        const searchTerm = this.searchInput.value;
+        const resp = await window.$http.get(`/templates`, {
+            search: searchTerm
         });
+        this.searchCancel.style.display = searchTerm ? 'block' : 'none';
+        this.list.innerHTML = resp.data;
     }
-}
-
-export default TemplateManager;
\ No newline at end of file
+}
\ No newline at end of file
index b9b96afc5d07728d992e9cd49eab9e29a6df1f2a..b749eb54132492efa06b53a05a151fe35c282c3c 100644 (file)
@@ -1,10 +1,10 @@
+import {Component} from "./component";
 
-class ToggleSwitch {
+export class ToggleSwitch extends Component {
 
-    constructor(elem) {
-        this.elem = elem;
-        this.input = elem.querySelector('input[type=hidden]');
-        this.checkbox = elem.querySelector('input[type=checkbox]');
+    setup() {
+        this.input = this.$el.querySelector('input[type=hidden]');
+        this.checkbox = this.$el.querySelector('input[type=checkbox]');
 
         this.checkbox.addEventListener('change', this.stateChange.bind(this));
     }
@@ -18,6 +18,4 @@ class ToggleSwitch {
         this.input.dispatchEvent(changeEvent);
     }
 
-}
-
-export default ToggleSwitch;
\ No newline at end of file
+}
\ No newline at end of file
index f801e52a193715fdea427fa4eac03c2372ca9abf..ead2ac3d0dae85d4f865cd191d6ff768307eaa76 100644 (file)
@@ -1,5 +1,6 @@
+import {Component} from "./component";
 
-class TriLayout {
+export class TriLayout extends Component {
 
     setup() {
         this.container = this.$refs.container;
@@ -108,6 +109,4 @@ class TriLayout {
         this.lastTabShown = tabName;
     }
 
-}
-
-export default TriLayout;
\ No newline at end of file
+}
\ No newline at end of file
index aba43e0a920a5c66b0cdb8097061cec9c039d211..d4d88a633c115ab06a9bd718e32ca41090efc109 100644 (file)
@@ -1,25 +1,28 @@
 import {onChildEvent} from "../services/dom";
+import {Component} from "./component";
 
-class UserSelect {
+export class UserSelect extends Component {
 
     setup() {
+        this.container = this.$el;
         this.input = this.$refs.input;
         this.userInfoContainer = this.$refs.userInfo;
 
-        this.hide = this.$el.components.dropdown.hide;
-
-        onChildEvent(this.$el, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
+        onChildEvent(this.container, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
     }
 
     selectUser(event, userEl) {
         event.preventDefault();
-        const id = userEl.getAttribute('data-id');
-        this.input.value = id;
+        this.input.value = userEl.getAttribute('data-id');
         this.userInfoContainer.innerHTML = userEl.innerHTML;
         this.input.dispatchEvent(new Event('change', {bubbles: true}));
         this.hide();
     }
 
-}
+    hide() {
+        /** @var {Dropdown} **/
+        const dropdown = window.$components.firstOnElement(this.container, 'dropdown');
+        dropdown.hide();
+    }
 
-export default UserSelect;
\ No newline at end of file
+}
\ No newline at end of file
index aa50aa9d883f1541cbf08eed3d9865b65c252789..ad8c59ac2abd7c73a1857602f1f465b754f016e9 100644 (file)
@@ -1,10 +1,10 @@
-
 /**
  * Webhook Events
  * Manages dynamic selection control in the webhook form interface.
- * @extends {Component}
  */
-class WebhookEvents {
+import {Component} from "./component";
+
+export class WebhookEvents extends Component {
 
     setup() {
         this.checkboxes = this.$el.querySelectorAll('input[type="checkbox"]');
@@ -27,6 +27,4 @@ class WebhookEvents {
         }
     }
 
-}
-
-export default WebhookEvents;
\ No newline at end of file
+}
\ No newline at end of file
index 446f2ca4938696e6538774fa8357419f727b72c0..976dba68f6eefc654f83495dc2d245d8eeb164bb 100644 (file)
@@ -1,6 +1,7 @@
 import {build as buildEditorConfig} from "../wysiwyg/config";
+import {Component} from "./component";
 
-class WysiwygEditor {
+export class WysiwygEditor extends Component {
 
     setup() {
         this.elem = this.$el;
@@ -35,6 +36,4 @@ class WysiwygEditor {
         return '';
     }
 
-}
-
-export default WysiwygEditor;
+}
\ No newline at end of file
diff --git a/resources/js/markdown/actions.js b/resources/js/markdown/actions.js
new file mode 100644 (file)
index 0000000..c2c3409
--- /dev/null
@@ -0,0 +1,448 @@
+import DrawIO from "../services/drawio";
+
+export class Actions {
+    /**
+     * @param {MarkdownEditor} editor
+     */
+    constructor(editor) {
+        this.editor = editor;
+    }
+
+    updateAndRender() {
+        const content = this.editor.cm.getValue();
+        this.editor.config.inputEl.value = content;
+
+        const html = this.editor.markdown.render(content);
+        window.$events.emit('editor-html-change', html);
+        window.$events.emit('editor-markdown-change', content);
+        this.editor.display.patchWithHtml(html);
+    }
+
+    insertImage() {
+        const cursorPos = this.editor.cm.getCursor('from');
+        /** @type {ImageManager} **/
+        const imageManager = window.$components.first('image-manager');
+        imageManager.show(image => {
+            const imageUrl = image.thumbs.display || image.url;
+            let selectedText = this.editor.cm.getSelection();
+            let newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")";
+            this.editor.cm.focus();
+            this.editor.cm.replaceSelection(newText);
+            this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
+        }, 'gallery');
+    }
+
+    insertLink() {
+        const cursorPos = this.editor.cm.getCursor('from');
+        const selectedText = this.editor.cm.getSelection() || '';
+        const newText = `[${selectedText}]()`;
+        this.editor.cm.focus();
+        this.editor.cm.replaceSelection(newText);
+        const cursorPosDiff = (selectedText === '') ? -3 : -1;
+        this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff);
+    }
+
+    showImageManager() {
+        const cursorPos = this.editor.cm.getCursor('from');
+        /** @type {ImageManager} **/
+        const imageManager = window.$components.first('image-manager');
+        imageManager.show(image => {
+            this.insertDrawing(image, cursorPos);
+        }, 'drawio');
+    }
+
+    // Show the popup link selector and insert a link when finished
+    showLinkSelector() {
+        const cursorPos = this.editor.cm.getCursor('from');
+        /** @type {EntitySelectorPopup} **/
+        const selector = window.$components.first('entity-selector-popup');
+        selector.show(entity => {
+            let selectedText = this.editor.cm.getSelection() || entity.name;
+            let newText = `[${selectedText}](${entity.link})`;
+            this.editor.cm.focus();
+            this.editor.cm.replaceSelection(newText);
+            this.editor.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
+        });
+    }
+
+    // Show draw.io if enabled and handle save.
+    startDrawing() {
+        const url = this.editor.config.drawioUrl;
+        if (!url) return;
+
+        const cursorPos = this.editor.cm.getCursor('from');
+
+        DrawIO.show(url,() => {
+            return Promise.resolve('');
+        }, (pngData) => {
+
+            const data = {
+                image: pngData,
+                uploaded_to: Number(this.editor.config.pageId),
+            };
+
+            window.$http.post("/images/drawio", data).then(resp => {
+                this.insertDrawing(resp.data, cursorPos);
+                DrawIO.close();
+            }).catch(err => {
+                this.handleDrawingUploadError(err);
+            });
+        });
+    }
+
+    insertDrawing(image, originalCursor) {
+        const newText = `<div drawio-diagram="${image.id}"><img src="${image.url}"></div>`;
+        this.editor.cm.focus();
+        this.editor.cm.replaceSelection(newText);
+        this.editor.cm.setCursor(originalCursor.line, originalCursor.ch + newText.length);
+    }
+
+    // Show draw.io if enabled and handle save.
+    editDrawing(imgContainer) {
+        const drawioUrl = this.editor.config.drawioUrl;
+        if (!drawioUrl) {
+            return;
+        }
+
+        const cursorPos = this.editor.cm.getCursor('from');
+        const drawingId = imgContainer.getAttribute('drawio-diagram');
+
+        DrawIO.show(drawioUrl, () => {
+            return DrawIO.load(drawingId);
+        }, (pngData) => {
+
+            const data = {
+                image: pngData,
+                uploaded_to: Number(this.editor.config.pageId),
+            };
+
+            window.$http.post("/images/drawio", data).then(resp => {
+                const newText = `<div drawio-diagram="${resp.data.id}"><img src="${resp.data.url}"></div>`;
+                const newContent = this.editor.cm.getValue().split('\n').map(line => {
+                    if (line.indexOf(`drawio-diagram="${drawingId}"`) !== -1) {
+                        return newText;
+                    }
+                    return line;
+                }).join('\n');
+                this.editor.cm.setValue(newContent);
+                this.editor.cm.setCursor(cursorPos);
+                this.editor.cm.focus();
+                DrawIO.close();
+            }).catch(err => {
+                this.handleDrawingUploadError(err);
+            });
+        });
+    }
+
+    handleDrawingUploadError(error) {
+        if (error.status === 413) {
+            window.$events.emit('error', this.editor.config.text.serverUploadLimit);
+        } else {
+            window.$events.emit('error', this.editor.config.text.imageUploadError);
+        }
+        console.log(error);
+    }
+
+    // Make the editor full screen
+    fullScreen() {
+        const container = this.editor.config.container;
+        const alreadyFullscreen = container.classList.contains('fullscreen');
+        container.classList.toggle('fullscreen', !alreadyFullscreen);
+        document.body.classList.toggle('markdown-fullscreen', !alreadyFullscreen);
+    }
+
+    // Scroll to a specified text
+    scrollToText(searchText) {
+        if (!searchText) {
+            return;
+        }
+
+        const content = this.editor.cm.getValue();
+        const lines = content.split(/\r?\n/);
+        let lineNumber = lines.findIndex(line => {
+            return line && line.indexOf(searchText) !== -1;
+        });
+
+        if (lineNumber === -1) {
+            return;
+        }
+
+        this.editor.cm.scrollIntoView({
+            line: lineNumber,
+        }, 200);
+        this.editor.cm.focus();
+        // set the cursor location.
+        this.editor.cm.setCursor({
+            line: lineNumber,
+            char: lines[lineNumber].length
+        })
+    }
+
+    focus() {
+        this.editor.cm.focus();
+    }
+
+    /**
+     * Insert content into the editor.
+     * @param {String} content
+     */
+    insertContent(content) {
+        this.editor.cm.replaceSelection(content);
+    }
+
+    /**
+     * Prepend content to the editor.
+     * @param {String} content
+     */
+    prependContent(content) {
+        const cursorPos = this.editor.cm.getCursor('from');
+        const newContent = content + '\n' + this.editor.cm.getValue();
+        this.editor.cm.setValue(newContent);
+        const prependLineCount = content.split('\n').length;
+        this.editor.cm.setCursor(cursorPos.line + prependLineCount, cursorPos.ch);
+    }
+
+    /**
+     * Append content to the editor.
+     * @param {String} content
+     */
+    appendContent(content) {
+        const cursorPos = this.editor.cm.getCursor('from');
+        const newContent = this.editor.cm.getValue() + '\n' + content;
+        this.editor.cm.setValue(newContent);
+        this.editor.cm.setCursor(cursorPos.line, cursorPos.ch);
+    }
+
+    /**
+     * Replace the editor's contents
+     * @param {String} content
+     */
+    replaceContent(content) {
+        this.editor.cm.setValue(content);
+    }
+
+    /**
+     * @param {String|RegExp} search
+     * @param {String} replace
+     */
+    findAndReplaceContent(search, replace) {
+        const text = this.editor.cm.getValue();
+        const cursor = this.editor.cm.listSelections();
+        this.editor.cm.setValue(text.replace(search, replace));
+        this.editor.cm.setSelections(cursor);
+    }
+
+    /**
+     * Replace the start of the line
+     * @param {String} newStart
+     */
+    replaceLineStart(newStart) {
+        const cursor = this.editor.cm.getCursor();
+        let lineContent = this.editor.cm.getLine(cursor.line);
+        const lineLen = lineContent.length;
+        const lineStart = lineContent.split(' ')[0];
+
+        // Remove symbol if already set
+        if (lineStart === newStart) {
+            lineContent = lineContent.replace(`${newStart} `, '');
+            this.editor.cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
+            this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)});
+            return;
+        }
+
+        const alreadySymbol = /^[#>`]/.test(lineStart);
+        let posDif = 0;
+        if (alreadySymbol) {
+            posDif = newStart.length - lineStart.length;
+            lineContent = lineContent.replace(lineStart, newStart).trim();
+        } else if (newStart !== '') {
+            posDif = newStart.length + 1;
+            lineContent = newStart + ' ' + lineContent;
+        }
+        this.editor.cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
+        this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + posDif});
+    }
+
+    /**
+     * Wrap the line in the given start and end contents.
+     * @param {String} start
+     * @param {String} end
+     */
+    wrapLine(start, end) {
+        const cursor = this.editor.cm.getCursor();
+        const lineContent = this.editor.cm.getLine(cursor.line);
+        const lineLen = lineContent.length;
+        let newLineContent = lineContent;
+
+        if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
+            newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
+        } else {
+            newLineContent = `${start}${lineContent}${end}`;
+        }
+
+        this.editor.cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
+        this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + start.length});
+    }
+
+    /**
+     * Wrap the selection in the given contents start and end contents.
+     * @param {String} start
+     * @param {String} end
+     */
+    wrapSelection(start, end) {
+        const selection = this.editor.cm.getSelection();
+        if (selection === '') return this.wrapLine(start, end);
+
+        let newSelection = selection;
+        const frontDiff = 0;
+        let endDiff;
+
+        if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
+            newSelection = selection.slice(start.length, selection.length - end.length);
+            endDiff = -(end.length + start.length);
+        } else {
+            newSelection = `${start}${selection}${end}`;
+            endDiff = start.length + end.length;
+        }
+
+        const selections = this.editor.cm.listSelections()[0];
+        this.editor.cm.replaceSelection(newSelection);
+        const headFirst = selections.head.ch <= selections.anchor.ch;
+        selections.head.ch += headFirst ? frontDiff : endDiff;
+        selections.anchor.ch += headFirst ? endDiff : frontDiff;
+        this.editor.cm.setSelections([selections]);
+    }
+
+    replaceLineStartForOrderedList() {
+        const cursor = this.editor.cm.getCursor();
+        const prevLineContent = this.editor.cm.getLine(cursor.line - 1) || '';
+        const listMatch = prevLineContent.match(/^(\s*)(\d)([).])\s/) || [];
+
+        const number = (Number(listMatch[2]) || 0) + 1;
+        const whiteSpace = listMatch[1] || '';
+        const listMark = listMatch[3] || '.'
+
+        const prefix = `${whiteSpace}${number}${listMark}`;
+        return this.replaceLineStart(prefix);
+    }
+
+    /**
+     * Cycles through the type of callout block within the selection.
+     * Creates a callout block if none existing, and removes it if cycling past the danger type.
+     */
+    cycleCalloutTypeAtSelection() {
+        const selectionRange = this.editor.cm.listSelections()[0];
+        const lineContent = this.editor.cm.getLine(selectionRange.anchor.line);
+        const lineLength = lineContent.length;
+        const contentRange = {
+            anchor: {line: selectionRange.anchor.line, ch: 0},
+            head: {line: selectionRange.anchor.line, ch: lineLength},
+        };
+
+        const formats = ['info', 'success', 'warning', 'danger'];
+        const joint = formats.join('|');
+        const regex = new RegExp(`class="((${joint})\\s+callout|callout\\s+(${joint}))"`, 'i');
+        const matches = regex.exec(lineContent);
+        const format = (matches ? (matches[2] || matches[3]) : '').toLowerCase();
+
+        if (format === formats[formats.length - 1]) {
+            this.wrapLine(`<p class="callout ${formats[formats.length - 1]}">`, '</p>');
+        } else if (format === '') {
+            this.wrapLine('<p class="callout info">', '</p>');
+        } else {
+            const newFormatIndex = formats.indexOf(format) + 1;
+            const newFormat = formats[newFormatIndex];
+            const newContent = lineContent.replace(matches[0], matches[0].replace(format, newFormat));
+            this.editor.cm.replaceRange(newContent, contentRange.anchor, contentRange.head);
+
+            const chDiff = newContent.length - lineContent.length;
+            selectionRange.anchor.ch += chDiff;
+            if (selectionRange.anchor !== selectionRange.head) {
+                selectionRange.head.ch += chDiff;
+            }
+            this.editor.cm.setSelection(selectionRange.anchor, selectionRange.head);
+        }
+    }
+
+    /**
+     * Handle image upload and add image into markdown content
+     * @param {File} file
+     */
+    uploadImage(file) {
+        if (file === null || file.type.indexOf('image') !== 0) return;
+        let ext = 'png';
+
+        if (file.name) {
+            let fileNameMatches = file.name.match(/\.(.+)$/);
+            if (fileNameMatches.length > 1) ext = fileNameMatches[1];
+        }
+
+        // Insert image into markdown
+        const id = "image-" + Math.random().toString(16).slice(2);
+        const placeholderImage = window.baseUrl(`/loading.gif#upload${id}`);
+        const selectedText = this.editor.cm.getSelection();
+        const placeHolderText = `![${selectedText}](${placeholderImage})`;
+        const cursor = this.editor.cm.getCursor();
+        this.editor.cm.replaceSelection(placeHolderText);
+        this.editor.cm.setCursor({line: cursor.line, ch: cursor.ch + selectedText.length + 3});
+
+        const remoteFilename = "image-" + Date.now() + "." + ext;
+        const formData = new FormData();
+        formData.append('file', file, remoteFilename);
+        formData.append('uploaded_to', this.editor.config.pageId);
+
+        window.$http.post('/images/gallery', formData).then(resp => {
+            const newContent = `[![${selectedText}](${resp.data.thumbs.display})](${resp.data.url})`;
+            this.findAndReplaceContent(placeHolderText, newContent);
+        }).catch(err => {
+            window.$events.emit('error', this.editor.config.text.imageUploadError);
+            this.findAndReplaceContent(placeHolderText, selectedText);
+            console.log(err);
+        });
+    }
+
+    syncDisplayPosition() {
+        // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html
+        const scroll = this.editor.cm.getScrollInfo();
+        const atEnd = scroll.top + scroll.clientHeight === scroll.height;
+        if (atEnd) {
+            this.editor.display.scrollToIndex(-1);
+            return;
+        }
+
+        const lineNum = this.editor.cm.lineAtHeight(scroll.top, 'local');
+        const range = this.editor.cm.getRange({line: 0, ch: null}, {line: lineNum, ch: null});
+        const parser = new DOMParser();
+        const doc = parser.parseFromString(this.editor.markdown.render(range), 'text/html');
+        const totalLines = doc.documentElement.querySelectorAll('body > *');
+        this.editor.display.scrollToIndex(totalLines.length);
+    }
+
+    /**
+     * Fetch and insert the template of the given ID.
+     * The page-relative position provided can be used to determine insert location if possible.
+     * @param {String} templateId
+     * @param {Number} posX
+     * @param {Number} posY
+     */
+    insertTemplate(templateId, posX, posY) {
+        const cursorPos = this.editor.cm.coordsChar({left: posX, top: posY});
+        this.editor.cm.setCursor(cursorPos);
+        window.$http.get(`/templates/${templateId}`).then(resp => {
+            const content = resp.data.markdown || resp.data.html;
+            this.editor.cm.replaceSelection(content);
+        });
+    }
+
+    /**
+     * Insert multiple images from the clipboard.
+     * @param {File[]} images
+     */
+    insertClipboardImages(images) {
+        const cursorPos = this.editor.cm.coordsChar({left: event.pageX, top: event.pageY});
+        this.editor.cm.setCursor(cursorPos);
+        for (const image of images) {
+            this.uploadImage(image);
+        }
+    }
+}
\ No newline at end of file
diff --git a/resources/js/markdown/codemirror.js b/resources/js/markdown/codemirror.js
new file mode 100644 (file)
index 0000000..8724a23
--- /dev/null
@@ -0,0 +1,70 @@
+import {provide as provideShortcuts} from "./shortcuts";
+import {debounce} from "../services/util";
+import Clipboard from "../services/clipboard";
+
+/**
+ * Initiate the codemirror instance for the markdown editor.
+ * @param {MarkdownEditor} editor
+ * @returns {Promise<void>}
+ */
+export async function init(editor) {
+    const Code = await window.importVersioned('code');
+    const cm = Code.markdownEditor(editor.config.inputEl);
+
+    // Will force to remain as ltr for now due to issues when HTML is in editor.
+    cm.setOption('direction', 'ltr');
+    // Register shortcuts
+    cm.setOption('extraKeys', provideShortcuts(editor, Code.getMetaKey()));
+
+
+    // Register codemirror events
+
+    // Update data on content change
+    cm.on('change', (instance, changeObj) => editor.actions.updateAndRender());
+
+    // Handle scroll to sync display view
+    const onScrollDebounced = debounce(editor.actions.syncDisplayPosition.bind(editor.actions), 100, false);
+    let syncActive = editor.settings.get('scrollSync');
+    editor.settings.onChange('scrollSync', val => syncActive = val);
+    cm.on('scroll', instance => {
+        if (syncActive) {
+            onScrollDebounced(instance);
+        }
+    });
+
+    // Handle image paste
+    cm.on('paste', (cm, event) => {
+        const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);
+
+        // Don't handle the event ourselves if no items exist of contains table-looking data
+        if (!clipboard.hasItems() || clipboard.containsTabularData()) {
+            return;
+        }
+
+        const images = clipboard.getImages();
+        for (const image of images) {
+            editor.actions.uploadImage(image);
+        }
+    });
+
+    // Handle image & content drag n drop
+    cm.on('drop', (cm, event) => {
+
+        const templateId = event.dataTransfer.getData('bookstack/template');
+        if (templateId) {
+            event.preventDefault();
+            editor.actions.insertTemplate(templateId, event.pageX, event.pageY);
+        }
+
+        const clipboard = new Clipboard(event.dataTransfer);
+        const clipboardImages = clipboard.getImages();
+        if (clipboardImages.length > 0) {
+            event.stopPropagation();
+            event.preventDefault();
+            editor.actions.insertClipboardImages(clipboardImages);
+        }
+
+    });
+
+    return cm;
+}
\ No newline at end of file
diff --git a/resources/js/markdown/common-events.js b/resources/js/markdown/common-events.js
new file mode 100644 (file)
index 0000000..3afd03d
--- /dev/null
@@ -0,0 +1,33 @@
+function getContentToInsert({html, markdown}) {
+    return markdown || html;
+}
+
+/**
+ * @param {MarkdownEditor} editor
+ */
+export function listen(editor) {
+
+    window.$events.listen('editor::replace', (eventContent) => {
+        const markdown = getContentToInsert(eventContent);
+        editor.actions.replaceContent(markdown);
+    });
+
+    window.$events.listen('editor::append', (eventContent) => {
+        const markdown = getContentToInsert(eventContent);
+        editor.actions.appendContent(markdown);
+    });
+
+    window.$events.listen('editor::prepend', (eventContent) => {
+        const markdown = getContentToInsert(eventContent);
+        editor.actions.prependContent(markdown);
+    });
+
+    window.$events.listen('editor::insert', (eventContent) => {
+        const markdown = getContentToInsert(eventContent);
+        editor.actions.insertContent(markdown);
+    });
+
+    window.$events.listen('editor::focus', () => {
+        editor.actions.focus();
+    });
+}
\ No newline at end of file
diff --git a/resources/js/markdown/display.js b/resources/js/markdown/display.js
new file mode 100644 (file)
index 0000000..2c78da1
--- /dev/null
@@ -0,0 +1,109 @@
+import {patchDomFromHtmlString} from "../services/vdom";
+
+export class Display {
+
+    /**
+     * @param {MarkdownEditor} editor
+     */
+    constructor(editor) {
+        this.editor = editor;
+        this.container = editor.config.displayEl;
+
+        this.doc = null;
+        this.lastDisplayClick = 0;
+
+        if (this.container.contentDocument.readyState === 'complete') {
+            this.onLoad();
+        } else {
+            this.container.addEventListener('load', this.onLoad.bind(this));
+        }
+
+        this.updateVisibility(editor.settings.get('showPreview'));
+        editor.settings.onChange('showPreview', show => this.updateVisibility(show));
+    }
+
+    updateVisibility(show) {
+        const wrap = this.container.closest('.markdown-editor-wrap');
+        wrap.style.display = show ? null : 'none';
+    }
+
+    onLoad() {
+        this.doc = this.container.contentDocument;
+
+        this.loadStylesIntoDisplay();
+        this.doc.body.className = 'page-content';
+
+        // Prevent markdown display link click redirect
+        this.doc.addEventListener('click', this.onDisplayClick.bind(this));
+    }
+
+    /**
+     * @param {MouseEvent} event
+     */
+    onDisplayClick(event) {
+        const isDblClick = Date.now() - this.lastDisplayClick < 300;
+
+        const link = event.target.closest('a');
+        if (link !== null) {
+            event.preventDefault();
+            window.open(link.getAttribute('href'));
+            return;
+        }
+
+        const drawing = event.target.closest('[drawio-diagram]');
+        if (drawing !== null && isDblClick) {
+            this.editor.actions.editDrawing(drawing);
+            return;
+        }
+
+        this.lastDisplayClick = Date.now();
+    }
+
+    loadStylesIntoDisplay() {
+        this.doc.documentElement.classList.add('markdown-editor-display');
+
+        // Set display to be dark mode if parent is
+        if (document.documentElement.classList.contains('dark-mode')) {
+            this.doc.documentElement.style.backgroundColor = '#222';
+            this.doc.documentElement.classList.add('dark-mode');
+        }
+
+        this.doc.head.innerHTML = '';
+        const styles = document.head.querySelectorAll('style,link[rel=stylesheet]');
+        for (const style of styles) {
+            const copy = style.cloneNode(true);
+            this.doc.head.appendChild(copy);
+        }
+    }
+
+    /**
+     * Patch the display DOM with the given HTML content.
+     * @param {String} html
+     */
+    patchWithHtml(html) {
+        const body = this.doc.body;
+
+        if (body.children.length === 0) {
+            const wrap = document.createElement('div');
+            this.doc.body.append(wrap);
+        }
+
+        const target = body.children[0];
+
+        patchDomFromHtmlString(target, html);
+    }
+
+    /**
+     * Scroll to the given block index within the display content.
+     * Will scroll to the end if the index is -1.
+     * @param {Number} index
+     */
+    scrollToIndex(index) {
+        const elems = this.doc.body?.children[0]?.children;
+        if (elems && elems.length <= index) return;
+
+        const topElem = (index === -1) ? elems[elems.length-1] : elems[index];
+        topElem.scrollIntoView({ block: 'start', inline: 'nearest', behavior: 'smooth'});
+    }
+
+}
\ No newline at end of file
diff --git a/resources/js/markdown/editor.js b/resources/js/markdown/editor.js
new file mode 100644 (file)
index 0000000..1cf4cef
--- /dev/null
@@ -0,0 +1,54 @@
+import {Markdown} from "./markdown";
+import {Display} from "./display";
+import {Actions} from "./actions";
+import {Settings} from "./settings";
+import {listen} from "./common-events";
+import {init as initCodemirror} from "./codemirror";
+
+
+/**
+ * Initiate a new markdown editor instance.
+ * @param {MarkdownEditorConfig} config
+ * @returns {Promise<MarkdownEditor>}
+ */
+export async function init(config) {
+
+    /**
+     * @type {MarkdownEditor}
+     */
+    const editor = {
+        config,
+        markdown: new Markdown(),
+        settings: new Settings(config.settingInputs),
+    };
+
+    editor.actions = new Actions(editor);
+    editor.display = new Display(editor);
+    editor.cm = await initCodemirror(editor);
+
+    listen(editor);
+
+    return editor;
+}
+
+
+/**
+ * @typedef MarkdownEditorConfig
+ * @property {String} pageId
+ * @property {Element} container
+ * @property {Element} displayEl
+ * @property {HTMLTextAreaElement} inputEl
+ * @property {String} drawioUrl
+ * @property {HTMLInputElement[]} settingInputs
+ * @property {Object<String, String>} text
+ */
+
+/**
+ * @typedef MarkdownEditor
+ * @property {MarkdownEditorConfig} config
+ * @property {Display} display
+ * @property {Markdown} markdown
+ * @property {Actions} actions
+ * @property {CodeMirror} cm
+ * @property {Settings} settings
+ */
\ No newline at end of file
diff --git a/resources/js/markdown/markdown.js b/resources/js/markdown/markdown.js
new file mode 100644 (file)
index 0000000..ef3a028
--- /dev/null
@@ -0,0 +1,30 @@
+import MarkdownIt from "markdown-it";
+import mdTasksLists from 'markdown-it-task-lists';
+
+export class Markdown {
+
+    constructor() {
+        this.renderer = new MarkdownIt({html: true});
+        this.renderer.use(mdTasksLists, {label: true});
+    }
+
+    /**
+     * Get the front-end render used to convert markdown to HTML.
+     * @returns {MarkdownIt}
+     */
+    getRenderer() {
+        return this.renderer;
+    }
+
+    /**
+     * Convert the given Markdown to HTML.
+     * @param {String} markdown
+     * @returns {String}
+     */
+    render(markdown) {
+        return this.renderer.render(markdown);
+    }
+}
+
+
+
diff --git a/resources/js/markdown/settings.js b/resources/js/markdown/settings.js
new file mode 100644 (file)
index 0000000..62aab82
--- /dev/null
@@ -0,0 +1,62 @@
+export class Settings {
+
+    constructor(settingInputs) {
+        this.settingMap = {
+            scrollSync: true,
+            showPreview: true,
+            editorWidth: 50,
+        };
+        this.changeListeners = {};
+        this.loadFromLocalStorage();
+        this.applyToInputs(settingInputs);
+        this.listenToInputChanges(settingInputs);
+    }
+
+    applyToInputs(inputs) {
+        for (const input of inputs) {
+            const name = input.getAttribute('name').replace('md-', '');
+            input.checked = this.settingMap[name];
+        }
+    }
+
+    listenToInputChanges(inputs) {
+        for (const input of inputs) {
+            input.addEventListener('change', event => {
+                const name = input.getAttribute('name').replace('md-', '');
+                this.set(name, input.checked);
+            });
+        }
+    }
+
+    loadFromLocalStorage() {
+        const lsValString = window.localStorage.getItem('md-editor-settings');
+        if (!lsValString) {
+            return;
+        }
+
+        const lsVals = JSON.parse(lsValString);
+        for (const [key, value] of Object.entries(lsVals)) {
+            if (value !== null && this.settingMap[key] !== undefined) {
+                this.settingMap[key] = value;
+            }
+        }
+    }
+
+    set(key, value) {
+        this.settingMap[key] = value;
+        window.localStorage.setItem('md-editor-settings', JSON.stringify(this.settingMap));
+        for (const listener of (this.changeListeners[key] || [])) {
+            listener(value);
+        }
+    }
+
+    get(key) {
+        return this.settingMap[key] || null;
+    }
+
+    onChange(key, callback) {
+        const listeners = this.changeListeners[key] || [];
+        listeners.push(callback);
+        this.changeListeners[key] = listeners;
+    }
+}
\ No newline at end of file
diff --git a/resources/js/markdown/shortcuts.js b/resources/js/markdown/shortcuts.js
new file mode 100644 (file)
index 0000000..17ffe2f
--- /dev/null
@@ -0,0 +1,48 @@
+/**
+ * Provide shortcuts for the given codemirror instance.
+ * @param {MarkdownEditor} editor
+ * @param {String} metaKey
+ * @returns {Object<String, Function>}
+ */
+export function provide(editor, metaKey) {
+    const shortcuts = {};
+
+    // Insert Image shortcut
+    shortcuts[`${metaKey}-Alt-I`] = function(cm) {
+        const selectedText = cm.getSelection();
+        const newText = `![${selectedText}](http://)`;
+        const cursorPos = cm.getCursor('from');
+        cm.replaceSelection(newText);
+        cm.setCursor(cursorPos.line, cursorPos.ch + newText.length -1);
+    };
+
+    // Save draft
+    shortcuts[`${metaKey}-S`] = cm => window.$events.emit('editor-save-draft');
+
+    // Save page
+    shortcuts[`${metaKey}-Enter`] = cm => window.$events.emit('editor-save-page');
+
+    // Show link selector
+    shortcuts[`Shift-${metaKey}-K`] = cm => editor.actions.showLinkSelector();
+
+    // Insert Link
+    shortcuts[`${metaKey}-K`] = cm => editor.actions.insertLink();
+
+    // FormatShortcuts
+    shortcuts[`${metaKey}-1`] = cm => editor.actions.replaceLineStart('##');
+    shortcuts[`${metaKey}-2`] = cm => editor.actions.replaceLineStart('###');
+    shortcuts[`${metaKey}-3`] = cm => editor.actions.replaceLineStart('####');
+    shortcuts[`${metaKey}-4`] = cm => editor.actions.replaceLineStart('#####');
+    shortcuts[`${metaKey}-5`] = cm => editor.actions.replaceLineStart('');
+    shortcuts[`${metaKey}-D`] = cm => editor.actions.replaceLineStart('');
+    shortcuts[`${metaKey}-6`] = cm => editor.actions.replaceLineStart('>');
+    shortcuts[`${metaKey}-Q`] = cm => editor.actions.replaceLineStart('>');
+    shortcuts[`${metaKey}-7`] = cm => editor.actions.wrapSelection('\n```\n', '\n```');
+    shortcuts[`${metaKey}-8`] = cm => editor.actions.wrapSelection('`', '`');
+    shortcuts[`Shift-${metaKey}-E`] = cm => editor.actions.wrapSelection('`', '`');
+    shortcuts[`${metaKey}-9`] = cm => editor.actions.cycleCalloutTypeAtSelection();
+    shortcuts[`${metaKey}-P`] = cm => editor.actions.replaceLineStart('-')
+    shortcuts[`${metaKey}-O`] = cm => editor.actions.replaceLineStartForOrderedList()
+
+    return shortcuts;
+}
\ No newline at end of file
diff --git a/resources/js/services/components.js b/resources/js/services/components.js
new file mode 100644 (file)
index 0000000..d1503db
--- /dev/null
@@ -0,0 +1,165 @@
+import {kebabToCamel, camelToKebab} from "./text";
+
+/**
+ * A mapping of active components keyed by name, with values being arrays of component
+ * instances since there can be multiple components of the same type.
+ * @type {Object<String, Component[]>}
+ */
+const components = {};
+
+/**
+ * A mapping of component class models, keyed by name.
+ * @type {Object<String, Constructor<Component>>}
+ */
+const componentModelMap = {};
+
+/**
+ * A mapping of active component maps, keyed by the element components are assigned to.
+ * @type {WeakMap<Element, Object<String, Component>>}
+ */
+const elementComponentMap = new WeakMap();
+
+/**
+ * Initialize a component instance on the given dom element.
+ * @param {String} name
+ * @param {Element} element
+ */
+function initComponent(name, element) {
+    /** @type {Function<Component>|undefined} **/
+    const componentModel = componentModelMap[name];
+    if (componentModel === undefined) return;
+
+    // Create our component instance
+    /** @type {Component} **/
+    let instance;
+    try {
+        instance = new componentModel();
+        instance.$name = name;
+        instance.$el = element;
+        const allRefs = parseRefs(name, element);
+        instance.$refs = allRefs.refs;
+        instance.$manyRefs = allRefs.manyRefs;
+        instance.$opts = parseOpts(name, element);
+        instance.setup();
+    } catch (e) {
+        console.error('Failed to create component', e, name, element);
+    }
+
+    // Add to global listing
+    if (typeof components[name] === "undefined") {
+        components[name] = [];
+    }
+    components[name].push(instance);
+
+    // Add to element mapping
+    const elComponents = elementComponentMap.get(element) || {};
+    elComponents[name] = instance;
+    elementComponentMap.set(element, elComponents);
+}
+
+/**
+ * Parse out the element references within the given element
+ * for the given component name.
+ * @param {String} name
+ * @param {Element} element
+ */
+function parseRefs(name, element) {
+    const refs = {};
+    const manyRefs = {};
+
+    const prefix = `${name}@`
+    const selector = `[refs*="${prefix}"]`;
+    const refElems = [...element.querySelectorAll(selector)];
+    if (element.matches(selector)) {
+        refElems.push(element);
+    }
+
+    for (const el of refElems) {
+        const refNames = el.getAttribute('refs')
+            .split(' ')
+            .filter(str => str.startsWith(prefix))
+            .map(str => str.replace(prefix, ''))
+            .map(kebabToCamel);
+        for (const ref of refNames) {
+            refs[ref] = el;
+            if (typeof manyRefs[ref] === 'undefined') {
+                manyRefs[ref] = [];
+            }
+            manyRefs[ref].push(el);
+        }
+    }
+    return {refs, manyRefs};
+}
+
+/**
+ * Parse out the element component options.
+ * @param {String} name
+ * @param {Element} element
+ * @return {Object<String, String>}
+ */
+function parseOpts(name, element) {
+    const opts = {};
+    const prefix = `option:${name}:`;
+    for (const {name, value} of element.attributes) {
+        if (name.startsWith(prefix)) {
+            const optName = name.replace(prefix, '');
+            opts[kebabToCamel(optName)] = value || '';
+        }
+    }
+    return opts;
+}
+
+/**
+ * Initialize all components found within the given element.
+ * @param {Element|Document} parentElement
+ */
+export function init(parentElement = document) {
+    const componentElems = parentElement.querySelectorAll(`[component],[components]`);
+
+    for (const el of componentElems) {
+        const componentNames = `${el.getAttribute('component') || ''} ${(el.getAttribute('components'))}`.toLowerCase().split(' ').filter(Boolean);
+        for (const name of componentNames) {
+            initComponent(name, el);
+        }
+    }
+}
+
+/**
+ * Register the given component mapping into the component system.
+ * @param {Object<String, ObjectConstructor<Component>>} mapping
+ */
+export function register(mapping) {
+    const keys = Object.keys(mapping);
+    for (const key of keys) {
+        componentModelMap[camelToKebab(key)] = mapping[key];
+    }
+}
+
+/**
+ * Get the first component of the given name.
+ * @param {String} name
+ * @returns {Component|null}
+ */
+export function first(name) {
+    return (components[name] || [null])[0];
+}
+
+/**
+ * Get all the components of the given name.
+ * @param {String} name
+ * @returns {Component[]}
+ */
+export function get(name) {
+    return components[name] || [];
+}
+
+/**
+ * Get the first component, of the given name, that's assigned to the given element.
+ * @param {Element} element
+ * @param {String} name
+ * @returns {Component|null}
+ */
+export function firstOnElement(element, name) {
+    const elComponents = elementComponentMap.get(element) || {};
+    return elComponents[name] || null;
+}
\ No newline at end of file
index eb5f6a8530d512c6a4a965baa57d2b24a14c7d51..882d5228d7593abf80e4cb98c2e7c5133611d47b 100644 (file)
@@ -128,6 +128,6 @@ export function removeLoading(element) {
 export function htmlToDom(html) {
     const wrap = document.createElement('div');
     wrap.innerHTML = html;
-    window.components.init(wrap);
+    window.$components.init(wrap);
     return wrap.children[0];
 }
\ No newline at end of file
diff --git a/resources/js/services/keyboard-navigation.js b/resources/js/services/keyboard-navigation.js
new file mode 100644 (file)
index 0000000..9e05ef5
--- /dev/null
@@ -0,0 +1,89 @@
+/**
+ * Handle common keyboard navigation events within a given container.
+ */
+export class KeyboardNavigationHandler {
+
+    /**
+     * @param {Element} container
+     * @param {Function|null} onEscape
+     * @param {Function|null} onEnter
+     */
+    constructor(container, onEscape = null, onEnter = null) {
+        this.containers = [container];
+        this.onEscape = onEscape;
+        this.onEnter = onEnter;
+        container.addEventListener('keydown', this.#keydownHandler.bind(this));
+    }
+
+    /**
+     * Also share the keyboard event handling to the given element.
+     * Only elements within the original container are considered focusable though.
+     * @param {Element} element
+     */
+    shareHandlingToEl(element) {
+        this.containers.push(element);
+        element.addEventListener('keydown', this.#keydownHandler.bind(this));
+    }
+
+    /**
+     * Focus on the next focusable element within the current containers.
+     */
+    focusNext() {
+        const focusable = this.#getFocusable();
+        const currentIndex = focusable.indexOf(document.activeElement);
+        let newIndex = currentIndex + 1;
+        if (newIndex >= focusable.length) {
+            newIndex = 0;
+        }
+
+        focusable[newIndex].focus();
+    }
+
+    /**
+     * Focus on the previous existing focusable element within the current containers.
+     */
+    focusPrevious() {
+        const focusable = this.#getFocusable();
+        const currentIndex = focusable.indexOf(document.activeElement);
+        let newIndex = currentIndex - 1;
+        if (newIndex < 0) {
+            newIndex = focusable.length - 1;
+        }
+
+        focusable[newIndex].focus();
+    }
+
+    /**
+     * @param {KeyboardEvent} event
+     */
+    #keydownHandler(event) {
+        if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
+            this.focusNext();
+            event.preventDefault();
+        } else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
+            this.focusPrevious();
+            event.preventDefault();
+        } else if (event.key === 'Escape') {
+            if (this.onEscape) {
+                this.onEscape(event);
+            } else if  (document.activeElement) {
+                document.activeElement.blur();
+            }
+        } else if (event.key === 'Enter' && this.onEnter) {
+            this.onEnter(event);
+        }
+    }
+
+    /**
+     * Get an array of focusable elements within the current containers.
+     * @returns {Element[]}
+     */
+    #getFocusable() {
+        const focusable = [];
+        const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"]),input:not([type=hidden])';
+        for (const container of this.containers) {
+            focusable.push(...container.querySelectorAll(selector))
+        }
+        return focusable;
+    }
+}
\ No newline at end of file
diff --git a/resources/js/services/text.js b/resources/js/services/text.js
new file mode 100644 (file)
index 0000000..ea82f99
--- /dev/null
@@ -0,0 +1,19 @@
+/**
+ * Convert a kebab-case string to camelCase
+ * @param {String} kebab
+ * @returns {string}
+ */
+export function kebabToCamel(kebab) {
+    const ucFirst = (word) => word.slice(0,1).toUpperCase() + word.slice(1);
+    const words = kebab.split('-');
+    return words[0] + words.slice(1).map(ucFirst).join('');
+}
+
+/**
+ * Convert a camelCase string to a kebab-case string.
+ * @param {String} camelStr
+ * @returns {String}
+ */
+export function camelToKebab(camelStr) {
+    return camelStr.replace(/[A-Z]/g, (str, offset) =>  (offset > 0 ? '-' : '') + str.toLowerCase());
+}
\ No newline at end of file
index de2ca20c13eb934b00a475b817489e9fa29d9670..1a56ebf6ce8595a29cab4f48fbd9422f9a9649a2 100644 (file)
@@ -6,9 +6,9 @@
  * N milliseconds. If `immediate` is passed, trigger the function on the
  * leading edge, instead of the trailing.
  * @attribution https://davidwalsh.name/javascript-debounce-function
- * @param func
- * @param wait
- * @param immediate
+ * @param {Function} func
+ * @param {Number} wait
+ * @param {Boolean} immediate
  * @returns {Function}
  */
 export function debounce(func, wait, immediate) {
index acf5e1d52530c9b8478e4eeb10455a7b664d08fa..d5ec20e2631356ba2d09ed7e4f0b7cfd2521e7ca 100644 (file)
@@ -73,7 +73,9 @@ function file_picker_callback(callback, value, meta) {
 
     // field_name, url, type, win
     if (meta.filetype === 'file') {
-        window.EntitySelectorPopup.show(entity => {
+        /** @type {EntitySelectorPopup} **/
+        const selector = window.$components.first('entity-selector-popup');
+        selector.show(entity => {
             callback(entity.link, {
                 text: entity.name,
                 title: entity.name,
@@ -83,7 +85,9 @@ function file_picker_callback(callback, value, meta) {
 
     if (meta.filetype === 'image') {
         // Show image manager
-        window.ImageManager.show(function (image) {
+        /** @type {ImageManager} **/
+        const imageManager = window.$components.first('image-manager');
+        imageManager.show(function (image) {
             callback(image.url, {alt: image.name});
         }, 'gallery');
     }
index 66441c87e9ee8387bf32653fa6ca3bd41b468218..cd0078b1d914da39ccdf3df993527f44f7ce4626 100644 (file)
@@ -9,7 +9,7 @@ function elemIsCodeBlock(elem) {
  * @param {function(string, string)} callback (Receives (code: string,language: string)
  */
 function showPopup(editor, code, language, callback) {
-    window.components.first('code-editor').open(code, language, (newCode, newLang) => {
+    window.$components.first('code-editor').open(code, language, (newCode, newLang) => {
         callback(newCode, newLang)
         editor.focus()
     });
index 64ef1fffbcfc26cd63f2c06c90b4f4ed762dbdc8..ad7e09f95fabbcf6f79a0b0f19a08e2d353df77a 100644 (file)
@@ -15,8 +15,10 @@ function isDrawing(node) {
 function showDrawingManager(mceEditor, selectedNode = null) {
     pageEditor = mceEditor;
     currentNode = selectedNode;
-    // Show image manager
-    window.ImageManager.show(function (image) {
+
+    /** @type {ImageManager} **/
+    const imageManager = window.$components.first('image-manager');
+    imageManager.show(function (image) {
         if (selectedNode) {
             const imgElem = selectedNode.querySelector('img');
             pageEditor.undoManager.transact(function () {
index d484284cad24afb14b926ad3e7e9aa2e1e4e3a9b..df1984d4ee998ae4e61f4f6b95721fee1f5919fb 100644 (file)
@@ -10,7 +10,7 @@ function register(editor, url) {
         parentNode.insertBefore(hrElem, cNode);
     });
 
-    editor.ui.registry.addButton('hr', {
+    editor.ui.registry.addButton('customhr', {
         icon: 'horizontal-rule',
         tooltip: 'Insert horizontal line',
         onAction() {
index 225f271fdb9c2c7aef16d56e0aaf578685e222f8..6969a50e22264bb77e96c626dc93d04329edeb3e 100644 (file)
@@ -3,14 +3,15 @@
  * @param {String} url
  */
 function register(editor, url) {
-
     // Custom Image picker button
     editor.ui.registry.addButton('imagemanager-insert', {
         title: 'Insert image',
         icon: 'image',
         tooltip: 'Insert image',
         onAction() {
-            window.ImageManager.show(function (image) {
+            /** @type {ImageManager} **/
+            const imageManager = window.$components.first('image-manager');
+            imageManager.show(function (image) {
                 const imageUrl = image.thumbs.display || image.url;
                 let html = `<a href="${image.url}" target="_blank">`;
                 html += `<img src="${imageUrl}" alt="${image.name}">`;
index b42851a46588b7274b9feec28fee13db704fea4d..ef364ddadab16b1e95105e7218c4b742cb91a6e4 100644 (file)
@@ -44,7 +44,9 @@ export function register(editor) {
 
     // Link selector shortcut
     editor.shortcuts.add('meta+shift+K', '', function() {
-        window.EntitySelectorPopup.show(function(entity) {
+        /** @var {EntitySelectorPopup} **/
+        const selectorPopup = window.$components.first('entity-selector-popup');
+        selectorPopup.show(function(entity) {
 
             if (editor.selection.isCollapsed()) {
                 editor.insertContent(editor.dom.createHTML('a', {href: entity.link}, editor.dom.encode(entity.name)));
index 208b3c508dc8e2f8b2671ed1c8ad3b43199da685..9debb08b5c4a5d9049ee37638764f5dd8dc1f90a 100644 (file)
@@ -36,7 +36,7 @@ function registerPrimaryToolbarGroups(editor) {
     editor.ui.registry.addGroupToolbarButton('insertoverflow', {
         icon: 'more-drawer',
         tooltip: 'More',
-        items: 'hr codeeditor drawio media details'
+        items: 'customhr codeeditor drawio media details'
     });
 }
 
index 1f7e29bf17b3e32b1ff11812a1ee31bd0c9d4561..55c9aae4132866977a7d231a12e1fdef3cd6ce26 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'تأكيد البريد الإلكتروني مطلوب ولكن النظام لم يستطع إرسال الرسالة. تواصل مع مشرف النظام للتأكد من إعدادات البريد.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'تمت إعادة إرسال رسالة التأكيد. الرجاء مراجعة صندوق الوارد',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'لم يتم تأكيد البريد الإلكتروني',
     'email_not_confirmed_text' => 'لم يتم بعد تأكيد عنوان البريد الإلكتروني.',
index 6d34d3ee8b885ee579535a1c60b6742190da5604..8dcb1dde0adbd38ec3d625133cb836f4d5ef5629 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'إجراءات',
     'view' => 'عرض',
     'view_all' => 'عرض الكل',
+    'new' => 'New',
     'create' => 'إنشاء',
     'update' => 'تحديث',
     'edit' => 'تعديل',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'عرض القائمة',
     'profile_menu' => 'قائمة ملف التعريف',
     'view_profile' => 'عرض الملف الشخصي',
     'edit_profile' => 'تعديل الملف الشخصي',
     'dark_mode' => 'الوضع المظلم',
     'light_mode' => 'الوضع المضيء',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'معلومات',
index 01bad5302eca999dec753a340f95ee4905600d50..d951f62a12524460a2331768b8de03e2bae4d638 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index e19f5bb7465f890d927eaee9344dc8a103b2804a..49471178c47f0a675018bd818a85bcf950dd3912 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'نتائج البحث',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'إدخال صورة',
     'pages_md_insert_link' => 'إدراج ارتباط الكيان',
     'pages_md_insert_drawing' => 'إدخال رسمة',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'صفحة ليست في فصل',
     'pages_move' => 'نقل الصفحة',
     'pages_move_success' => 'تم نقل الصفحة إلى ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'تم تحديث أذونات الصفحة',
     'pages_revision' => 'مراجعة',
     'pages_revisions' => 'مراجعات الصفحة',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'مراجعات صفحة :pageName',
     'pages_revision_named' => 'مراجعة صفحة :pageName',
     'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'أنشئ بواسطة',
     'pages_revisions_date' => 'تاريخ المراجعة',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'مراجعة #:id',
     'pages_revisions_numbered_changes' => 'مراجعة #: رقم تعريفي التغييرات',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'علامات الرف',
     'tag' => 'وسم',
     'tags' =>  'وسوم',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'اسم العلامة',
     'tag_value' => 'قيمة الوسم (اختياري)',
     'tags_explain' => "إضافة الوسوم تساعد بترتيب وتقسيم المحتوى. \n من الممكن وضع قيمة لكل وسم لترتيب أفضل وأدق.",
diff --git a/resources/lang/ar/preferences.php b/resources/lang/ar/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 4e636c6f80c28e7dd44707efa2fffb82d0eb44e1..1cf0e19983f18d1cb6895b6eb74336791d14c303 100755 (executable)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'الأدوار',
     'role_user_roles' => 'أدوار المستخدمين',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'إنشاء دور جديد',
     'role_create_success' => 'تم إنشاء الدور بنجاح',
     'role_delete' => 'حذف الدور',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'المستخدمون',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'ملف المستخدم',
     'users_add_new' => 'إضافة مستخدم جديد',
     'users_search' => 'بحث عن مستخدم',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 97476e8be1652ac62f9304d6a592675ecd215238..dd3c1566acf3a0e17654ca6167b88fc892d4a910 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Нужно ви е потвърждение чрез емейл, но системата не успя да го изпрати. Моля свържете се с администратора, за да проверите дали вашият емейл адрес е конфигуриран правилно.',
     'email_confirm_success' => 'Имейлът ти е потвърден! Вече би трябвало да можеш да се впишеш с този имейл адрес.',
     'email_confirm_resent' => 'Беше изпратен имейл с потвърждение, Моля, проверете кутията си.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Имейл адресът не е потвърден',
     'email_not_confirmed_text' => 'Вашият емейл адрес все още не е потвърден.',
index 0d070c84c3b2fe6515b5096442059aaded8da642..0b065ecc921a1f7a3a89990fc079736444f55cec 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Действия',
     'view' => 'Преглед',
     'view_all' => 'Преглед на всички',
+    'new' => 'New',
     'create' => 'Създай',
     'update' => 'Обновяване',
     'edit' => 'Редактиране',
@@ -80,12 +81,14 @@ return [
     'none' => 'Няма',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Разшири заглавното меню',
     'profile_menu' => 'Профил меню',
     'view_profile' => 'Разглеждане на профил',
     'edit_profile' => 'Редактиране на профила',
     'dark_mode' => 'Тъмен режим',
     'light_mode' => 'Светъл режим',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Информация',
index c7526f1e9c55e14f62306eab3bcce387f7db80e7..e47f629d44d431ebcbab7c8a92f1c68862a6fa9f 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Текст за показване',
     'title' => 'Заглавие',
-    'open_link' => 'Отваряне не връзката в...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Текущ прозорец',
     'open_link_new' => 'Нов прозорец',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Вмъкни сгъваем блок',
     'collapsible_unwrap' => 'Разгъни',
     'edit_label' => 'Редактирай етикета',
index 4d9c926f699e55d1cbef4229903860c7c8515663..32ed94c5f3b3d8a83bd276d9dddbb53c084a6f20 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Резултати от търсенето',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Добавяна на изображение',
     'pages_md_insert_link' => 'Добави линк към обекта',
     'pages_md_insert_drawing' => 'Вмъкни рисунка',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Страницата не принадлежи в никоя глава',
     'pages_move' => 'Премести страницата',
     'pages_move_success' => 'Страницата беше преместена в ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Настройките за достъп до страницата бяха обновени',
     'pages_revision' => 'Ревизия',
     'pages_revisions' => 'Ревизии на страницата',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Ревизии на страницата :pageName',
     'pages_revision_named' => 'Ревизия на страницата :pageName',
     'pages_revision_restored_from' => 'Възстановено от #:id; :summary',
     'pages_revisions_created_by' => 'Създадено от',
     'pages_revisions_date' => 'Дата на ревизията',
     'pages_revisions_number' => '№',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Ревизия №:id',
     'pages_revisions_numbered_changes' => 'Ревизия №:id Промени',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Тагове на рафта',
     'tag' => 'Таг',
     'tags' =>  'Тагове',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Име на таг',
     'tag_value' => 'Съдържание на тага (Опционално)',
     'tags_explain' => "Добавете няколко тага за да категоризирате по добре вашето съдържание. \n Може да добавите съдържание на таговете за по-подробна организация.",
diff --git a/resources/lang/bg/preferences.php b/resources/lang/bg/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index c7fc93c81e959832d6ec48024241944381fff6f7..13c148ecc76e84698ff6abf43c96b174c9348c43 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Роли',
     'role_user_roles' => 'Потребителски роли',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Създай нова роля',
     'role_create_success' => 'Ролята беше успешно създадена',
     'role_delete' => 'Изтрий роля',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Потребители',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Потребителски профил',
     'users_add_new' => 'Добави нов потребител',
     'users_search' => 'Търси Потребители',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Уебкука',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Създай нова уебкука',
     'webhooks_none_created' => 'Няма създадени уебкуки.',
     'webhooks_edit' => 'Редактирай уебкука',
index b9a72e2cfe6d15e038bbb8a827e52867e11e0cdb..96ab0efea1932c8bf2c5043e621934190153eb24 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Potvrda e-maila je obavezna ali sistem nije mogao poslati e-mail. Kontaktirajte administratora da biste bili sigurni da je e-mail postavljen ispravno.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'E-mail za potvrdu je ponovno poslan. Provjerite vaš e-mail.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'E-mail adresa nije potvrđena',
     'email_not_confirmed_text' => 'Vaša e-mail adresa nije još potvrđena.',
index 0008cbcc8c700194adf340dff62184fb311774a4..a1236e2309ef6f3fbe323f724cc8240bc1c39fd2 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Akcije',
     'view' => 'Prikaz',
     'view_all' => 'Prikaži sve',
+    'new' => 'New',
     'create' => 'Kreiraj',
     'update' => 'Ažuriraj',
     'edit' => 'Uredi',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Otvori meni u zaglavlju',
     'profile_menu' => 'Meni profila',
     'view_profile' => 'Pogledaj profil',
     'edit_profile' => 'Izmjeni profil',
     'dark_mode' => 'Tamni način rada',
     'light_mode' => 'Svijetli način rada',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Informacije',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index 3672b148deb9b654b93abfd5139f7f8ec1041c9f..ce002249f3ce9bcb5dbd6a10accab923ed1d022b 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Rezultati pretrage',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Insert Image',
     'pages_md_insert_link' => 'Insert Entity Link',
     'pages_md_insert_drawing' => 'Insert Drawing',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Page is not in a chapter',
     'pages_move' => 'Move Page',
     'pages_move_success' => 'Page moved to ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Page permissions updated',
     'pages_revision' => 'Revision',
     'pages_revisions' => 'Page Revisions',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Page Revisions for :pageName',
     'pages_revision_named' => 'Page Revision for :pageName',
     'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Created By',
     'pages_revisions_date' => 'Revision Date',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revision #:id',
     'pages_revisions_numbered_changes' => 'Revision #:id Changes',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Oznake police',
     'tag' => 'Oznaka',
     'tags' =>  'Oznake',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Naziv oznake',
     'tag_value' => 'Vrijednost oznake (nije obavezno)',
     'tags_explain' => "Dodaj nekoliko oznaka da bi sadržaj bio bolje kategorisan. \n Možeš dodati vrijednost oznaci za dublju organizaciju.",
diff --git a/resources/lang/bs/preferences.php b/resources/lang/bs/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 1ad271e7c9ebd736f1d283bc2884b756294ea56d..f4204dd68bf05858a70205983d8aa35204366efa 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roles',
     'role_user_roles' => 'User Roles',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Create New Role',
     'role_create_success' => 'Role successfully created',
     'role_delete' => 'Delete Role',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Users',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'User Profile',
     'users_add_new' => 'Add New User',
     'users_search' => 'Search Users',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index b95ae1e6dcdf62d9849d153987c3e4a8a778699f..a156ecee3193221d6774e9bf9119aba246dcf1ec 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Cal confirmar l\'adreça electrònica, però el sistema no ha pogut enviar el correu electrònic. Poseu-vos en contacte amb l\'administrador perquè s\'asseguri que el correu electrònic està ben configurat.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'S\'ha tornat a enviar el correu electrònic de confirmació. Reviseu la vostra safata d\'entrada.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Adreça electrònica no confirmada',
     'email_not_confirmed_text' => 'La vostra adreça electrònica encara no està confirmada.',
index 7c8ec04a33896d92e4376ad83c4c4ef1d242542d..72f0fac58d7feca5a7847822d53750e7f91ddf7d 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Accions',
     'view' => 'Visualitza',
     'view_all' => 'Visualitza-ho tot',
+    'new' => 'New',
     'create' => 'Crea',
     'update' => 'Actualitza',
     'edit' => 'Edita',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Expand Header Menu',
     'profile_menu' => 'Menú del perfil',
     'view_profile' => 'Mostra el perfil',
     'edit_profile' => 'Edita el perfil',
     'dark_mode' => 'Mode fosc',
     'light_mode' => 'Mode clar',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Informació',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index 0736e527a7dac11f47dde75360f6f447420d4778..8e3d13e37b1df08fdc8e69b310b9a59eb8bd8e31 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Resultats de la cerca',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Insereix una imatge',
     'pages_md_insert_link' => 'Insereix un enllaç a una entitat',
     'pages_md_insert_drawing' => 'Insereix un diagrama',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'La pàgina no pertany a cap capítol',
     'pages_move' => 'Mou la pàgina',
     'pages_move_success' => 'S\'ha mogut la pàgina a ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'S\'han actualitzat els permisos de la pàgina',
     'pages_revision' => 'Revisió',
     'pages_revisions' => 'Revisions de la pàgina',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Revisions de la pàgina :pageName',
     'pages_revision_named' => 'Revisió de la pàgina :pageName',
     'pages_revision_restored_from' => 'Restaurada de núm. :id; :summary',
     'pages_revisions_created_by' => 'Creada per',
     'pages_revisions_date' => 'Data de la revisió',
     'pages_revisions_number' => 'Núm. ',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revisió núm. :id',
     'pages_revisions_numbered_changes' => 'Canvis de la revisió núm. :id',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Etiquetes del prestatge',
     'tag' => 'Etiqueta',
     'tags' =>  'Etiquetes',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Nom de l\'etiqueta',
     'tag_value' => 'Valor de l\'etiqueta (opcional)',
     'tags_explain' => "Afegiu etiquetes per a categoritzar millor el contingut. \n Podeu assignar un valor a cada etiqueta per a una organització més detallada.",
diff --git a/resources/lang/ca/preferences.php b/resources/lang/ca/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 7d5bdcafbabe62393cda7455821e9e27d496cb26..c6c3b03dbf4919e725da10a3b4018ef5489e738b 100755 (executable)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Rols',
     'role_user_roles' => 'Rols d\'usuari',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Crea un rol nou',
     'role_create_success' => 'Rol creat correctament',
     'role_delete' => 'Suprimeix el rol',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Usuaris',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Perfil de l\'usuari',
     'users_add_new' => 'Afegeix un usuari nou',
     'users_search' => 'Cerca usuaris',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index e6a990f93ce75eb7fa0ad872504d70e01dff7a2e..2afc33aa6d0ad389cec930559e44b1f831488447 100644 (file)
@@ -28,7 +28,7 @@ return [
     // Books
     'book_create'                 => 'vytvořil/a knihu',
     'book_create_notification'    => 'Kniha byla úspěšně vytvořena',
-    'book_create_from_chapter'              => 'converted chapter to book',
+    'book_create_from_chapter'              => 'převést kapitolu na knihu',
     'book_create_from_chapter_notification' => 'Kapitola byla úspěšně převedena na knihu',
     'book_update'                 => 'aktualizoval/a knihu',
     'book_update_notification'    => 'Kniha byla úspěšně aktualizována',
@@ -38,14 +38,14 @@ return [
     'book_sort_notification'      => 'Kniha byla úspěšně seřazena',
 
     // Bookshelves
-    'bookshelf_create'            => 'created shelf',
-    'bookshelf_create_notification'    => 'Shelf successfully created',
-    'bookshelf_create_from_book'    => 'converted book to shelf',
-    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',
-    'bookshelf_update'                 => 'updated shelf',
-    'bookshelf_update_notification'    => 'Shelf successfully updated',
-    'bookshelf_delete'                 => 'deleted shelf',
-    'bookshelf_delete_notification'    => 'Shelf successfully deleted',
+    'bookshelf_create'            => 'vytvořit knihovnu',
+    'bookshelf_create_notification'    => 'Knihovna byla úspěšně vytvořena',
+    'bookshelf_create_from_book'    => 'převést knihu na knihovnu',
+    'bookshelf_create_from_book_notification'    => 'Kniha byla úspěšně převedena na knihovnu',
+    'bookshelf_update'                 => 'aktualizovat knihovnu',
+    'bookshelf_update_notification'    => 'Knihovna byla úspěšně aktualizována',
+    'bookshelf_delete'                 => 'odstranit knihovnu',
+    'bookshelf_delete_notification'    => 'Knihovna byla úspěšně smazána',
 
     // Favourites
     'favourite_add_notification' => '":name" byla přidána do Vašich oblíbených',
@@ -64,8 +64,8 @@ return [
     'webhook_delete_notification' => 'Webhook byl úspěšně odstraněn',
 
     // Users
-    'user_update_notification' => 'User successfully updated',
-    'user_delete_notification' => 'User successfully removed',
+    'user_update_notification' => 'Uživatel byl úspěšně aktualizován',
+    'user_delete_notification' => 'Uživatel byl úspěšně odstraněn',
 
     // Other
     'commented_on'                => 'okomentoval/a',
index 00ed80cf86ffd049f7e4610bea8adad9e948d73a..585315ee000337f31960cb89d7c60b2fe2d02443 100644 (file)
@@ -39,9 +39,9 @@ return [
     'register_success' => 'Děkujeme za registraci! Nyní jste zaregistrováni a přihlášeni.',
 
     // Login auto-initiation
-    'auto_init_starting' => 'Attempting Login',
-    'auto_init_starting_desc' => 'We\'re contacting your authentication system to start the login process. If there\'s no progress after 5 seconds you can try clicking the link below.',
-    'auto_init_start_link' => 'Proceed with authentication',
+    'auto_init_starting' => 'Pokus o přihlášení',
+    'auto_init_starting_desc' => 'Kontaktujeme váš ověřovací systém pro zahájení procesu přihlášení. Pokud po 5 sekundách nedojde k žádnému pokroku, můžete zkusit kliknout na odkaz níže.',
+    'auto_init_start_link' => 'Pokračovat s ověřováním',
 
     // Password Reset
     'reset_password' => 'Obnovit heslo',
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Potvrzení e-mailu je vyžadováno, ale systém nemohl odeslat e-mail. Obraťte se na správce, abyste se ujistili, že je e-mail správně nastaven.',
     'email_confirm_success' => 'Váš email byl ověřen! Nyní byste měli být schopni se touto emailovou adresou přihlásit.',
     'email_confirm_resent' => 'E-mail s potvrzením byl znovu odeslán. Zkontrolujte svou příchozí poštu.',
+    'email_confirm_thanks' => 'Děkujeme za potvrzení!',
+    'email_confirm_thanks_desc' => 'Počkejte prosím chvíli, než se vaše potvrzení vyřizuje. Pokud nebudete po 3 sekundách přesměrováni, klikněte na odkaz "Pokračovat" níže pro pokračování.',
 
     'email_not_confirmed' => 'E-mailová adresa nebyla potvrzena',
     'email_not_confirmed_text' => 'Vaše e-mailová adresa nebyla dosud potvrzena.',
@@ -97,19 +99,19 @@ return [
     'mfa_gen_backup_codes_usage_warning' => 'Každý kód může být použit pouze jednou',
     'mfa_gen_totp_title' => 'Nastavení mobilní aplikace',
     'mfa_gen_totp_desc' => 'Pro použití vícefaktorového ověření budete potřebovat mobilní aplikaci, která podporuje TOTP jako např. Google Authenticator, Authy nebo Microsoft Authenticator.',
-    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_scan' => 'Naskenujte QR kód níže pomocí vaší preferované ověřovací aplikace.',
     'mfa_gen_totp_verify_setup' => 'Ověřit nastavení',
-    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_verify_setup_desc' => 'Ověřte, že vše funguje zadáním kódu, generovaného v ověřovací aplikaci, do níže uvedeného vstupního pole:',
     'mfa_gen_totp_provide_code_here' => 'Zde zadejte kód vygenerovaný vaší aplikací',
     'mfa_verify_access' => 'Ověřit přístup',
-    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_access_desc' => 'Váš uživatelský účet vyžaduje, abyste před udělením přístupu potvrdili svou totožnost prostřednictvím další úrovně ověření. Ověřte pomocí jedné z vašich nakonfigurovaných metod, abyste mohli pokračovat.',
     'mfa_verify_no_methods' => 'Nejsou nastaveny žádné metody',
-    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_no_methods_desc' => 'Pro váš účet nebyly nalezeny žádné vícefázové metody ověřování. Před získáním přístupu budete muset nastavit alespoň jednu metodu.',
     'mfa_verify_use_totp' => 'Ověřit pomocí mobilní aplikace',
     'mfa_verify_use_backup_codes' => 'Ověřit pomocí záložního kódu',
     'mfa_verify_backup_code' => 'Záložní kód',
     'mfa_verify_backup_code_desc' => 'Níže zadejte jeden z vašich zbývajících záložních kódů:',
     'mfa_verify_backup_code_enter_here' => 'Zde zadejte záložní kód',
     'mfa_verify_totp_desc' => 'Níže zadejte kód, který jste si vygenerovali pomocí mobilní aplikace:',
-    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
+    'mfa_setup_login_notification' => 'Vícefázová metoda nastavena, nyní se prosím znovu přihlaste pomocí konfigurované metody.',
 ];
index bbf271fe4813f61ecf71ae1e19725de5b8f6d497..637db2446f2b63262cda43cb7223479c5d2cf196 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Akce',
     'view' => 'Zobrazit',
     'view_all' => 'Zobrazit vše',
+    'new' => 'Nový',
     'create' => 'Vytvořit',
     'update' => 'Aktualizovat',
     'edit' => 'Upravit',
@@ -47,8 +48,8 @@ return [
     'previous' => 'Předchozí',
     'filter_active' => 'Aktivní filtr:',
     'filter_clear' => 'Zrušit filtr',
-    'download' => 'Download',
-    'open_in_tab' => 'Open in Tab',
+    'download' => 'Stáhnout',
+    'open_in_tab' => 'Otevřít v nové záložce',
 
     // Sort Options
     'sort_options' => 'Možnosti řazení',
@@ -77,15 +78,17 @@ return [
     'status_active' => 'Aktivní',
     'status_inactive' => 'Neaktivní',
     'never' => 'Nikdy',
-    'none' => 'None',
+    'none' => 'Žádná',
 
     // Header
+    'homepage' => 'Domovská stránka',
     'header_menu_expand' => 'Rozbalit menu v záhlaví',
     'profile_menu' => 'Nabídka profilu',
     'view_profile' => 'Zobrazit profil',
     'edit_profile' => 'Upravit profil',
     'dark_mode' => 'Tmavý režim',
     'light_mode' => 'Světlý režim',
+    'global_search' => 'Globální vyhledávání',
 
     // Layout tabs
     'tab_info' => 'Informace',
index faf351da2e878e929722ac15ff93edf17464632e..5e16ad947c039bdc5627849f54141211bef60c94 100644 (file)
  */
 return [
     // General editor terms
-    'general' => 'General',
-    'advanced' => 'Advanced',
-    'none' => 'None',
-    'cancel' => 'Cancel',
-    'save' => 'Save',
-    'close' => 'Close',
-    'undo' => 'Undo',
-    'redo' => 'Redo',
-    'left' => 'Left',
-    'center' => 'Center',
-    'right' => 'Right',
-    'top' => 'Top',
-    'middle' => 'Middle',
-    'bottom' => 'Bottom',
-    'width' => 'Width',
-    'height' => 'Height',
-    'More' => 'More',
-    'select' => 'Select...',
+    'general' => 'Základní nastavení',
+    'advanced' => 'Pokročilé',
+    'none' => 'Nic',
+    'cancel' => 'Zrušit',
+    'save' => 'Uložit',
+    'close' => 'Zavřít‏',
+    'undo' => 'Zpět',
+    'redo' => 'Znovu',
+    'left' => 'Vlevo',
+    'center' => 'Na střed',
+    'right' => 'Vpravo',
+    'top' => 'Nahoru',
+    'middle' => 'Uprostřed',
+    'bottom' => 'Odspodu',
+    'width' => 'Šířka',
+    'height' => 'výška',
+    'More' => 'Více',
+    'select' => 'Vybrat...',
 
     // Toolbar
-    'formats' => 'Formats',
-    'header_large' => 'Large Header',
-    'header_medium' => 'Medium Header',
-    'header_small' => 'Small Header',
-    'header_tiny' => 'Tiny Header',
-    'paragraph' => 'Paragraph',
-    'blockquote' => 'Blockquote',
-    'inline_code' => 'Inline code',
-    'callouts' => 'Callouts',
-    'callout_information' => 'Information',
-    'callout_success' => 'Success',
-    'callout_warning' => 'Warning',
-    'callout_danger' => 'Danger',
-    'bold' => 'Bold',
-    'italic' => 'Italic',
-    'underline' => 'Underline',
-    'strikethrough' => 'Strikethrough',
-    'superscript' => 'Superscript',
-    'subscript' => 'Subscript',
-    'text_color' => 'Text color',
-    'custom_color' => 'Custom color',
-    'remove_color' => 'Remove color',
-    'background_color' => 'Background color',
-    'align_left' => 'Align left',
-    'align_center' => 'Align center',
-    'align_right' => 'Align right',
-    'align_justify' => 'Justify',
-    'list_bullet' => 'Bullet list',
-    'list_numbered' => 'Numbered list',
-    'list_task' => 'Task list',
-    'indent_increase' => 'Increase indent',
-    'indent_decrease' => 'Decrease indent',
-    'table' => 'Table',
-    'insert_image' => 'Insert image',
-    'insert_image_title' => 'Insert/Edit Image',
-    'insert_link' => 'Insert/edit link',
-    'insert_link_title' => 'Insert/Edit Link',
-    'insert_horizontal_line' => 'Insert horizontal line',
-    'insert_code_block' => 'Insert code block',
-    'edit_code_block' => 'Edit code block',
-    'insert_drawing' => 'Insert/edit drawing',
-    'drawing_manager' => 'Drawing manager',
-    'insert_media' => 'Insert/edit media',
-    'insert_media_title' => 'Insert/Edit Media',
-    'clear_formatting' => 'Clear formatting',
-    'source_code' => 'Source code',
-    'source_code_title' => 'Source Code',
-    'fullscreen' => 'Fullscreen',
-    'image_options' => 'Image options',
+    'formats' => 'Formáty',
+    'header_large' => 'Velké záhlaví',
+    'header_medium' => 'Střední záhlaví',
+    'header_small' => 'Malé záhlaví',
+    'header_tiny' => 'Malá hlavička',
+    'paragraph' => 'Odstavec',
+    'blockquote' => 'Citát do bloku',
+    'inline_code' => 'Vložený kód',
+    'callouts' => 'Poznámka',
+    'callout_information' => 'Informace',
+    'callout_success' => 'Úspěšně dokončeno',
+    'callout_warning' => 'Upozornění',
+    'callout_danger' => 'Nebezpečí',
+    'bold' => 'Tučně',
+    'italic' => 'Kurzíva',
+    'underline' => 'Podtržené',
+    'strikethrough' => 'Proškrtnuté',
+    'superscript' => 'horní index',
+    'subscript' => 'Dolní index',
+    'text_color' => 'Barva textu:',
+    'custom_color' => 'Vlastní barva',
+    'remove_color' => 'Odstranit barvu',
+    'background_color' => 'Barva pozadí',
+    'align_left' => 'Zarovnat vlevo',
+    'align_center' => 'Zarovnat na střed',
+    'align_right' => 'Zarovnat doprava',
+    'align_justify' => 'Zarovnat do bloku',
+    'list_bullet' => 'Bodový seznam',
+    'list_numbered' => 'Číslovaný seznam',
+    'list_task' => 'Seznam úkolů',
+    'indent_increase' => 'Zvýšit odsazení',
+    'indent_decrease' => 'Zmenšit odsazení',
+    'table' => 'Tabulka',
+    'insert_image' => 'Vložit obrázek',
+    'insert_image_title' => 'Vložit/upravit obrázek',
+    'insert_link' => 'Vložit/upravit odkaz',
+    'insert_link_title' => 'Vložit/upravit odkaz',
+    'insert_horizontal_line' => 'Vložit vodorovnou čáru',
+    'insert_code_block' => 'Vložit blok s kódem',
+    'edit_code_block' => 'Upravit blok kódu',
+    'insert_drawing' => 'Vložit/upravit kreslení',
+    'drawing_manager' => 'Správce kreslení',
+    'insert_media' => 'Vložit/upravit média',
+    'insert_media_title' => 'Vložit/upravit média',
+    'clear_formatting' => 'Vymazat formátování',
+    'source_code' => 'Zdrojový kód',
+    'source_code_title' => 'Zdrojový kód',
+    'fullscreen' => 'Celá obrazovka',
+    'image_options' => 'Možnosti obrázku',
 
     // Tables
-    'table_properties' => 'Table properties',
-    'table_properties_title' => 'Table Properties',
-    'delete_table' => 'Delete table',
-    'insert_row_before' => 'Insert row before',
-    'insert_row_after' => 'Insert row after',
-    'delete_row' => 'Delete row',
-    'insert_column_before' => 'Insert column before',
-    'insert_column_after' => 'Insert column after',
-    'delete_column' => 'Delete column',
-    'table_cell' => 'Cell',
-    'table_row' => 'Row',
-    'table_column' => 'Column',
-    'cell_properties' => 'Cell properties',
-    'cell_properties_title' => 'Cell Properties',
-    'cell_type' => 'Cell type',
-    'cell_type_cell' => 'Cell',
-    'cell_scope' => 'Scope',
-    'cell_type_header' => 'Header cell',
-    'merge_cells' => 'Merge cells',
-    'split_cell' => 'Split cell',
-    'table_row_group' => 'Row Group',
-    'table_column_group' => 'Column Group',
-    'horizontal_align' => 'Horizontal align',
-    'vertical_align' => 'Vertical align',
-    'border_width' => 'Border width',
-    'border_style' => 'Border style',
-    'border_color' => 'Border color',
-    'row_properties' => 'Row properties',
-    'row_properties_title' => 'Row Properties',
-    'cut_row' => 'Cut row',
-    'copy_row' => 'Copy row',
-    'paste_row_before' => 'Paste row before',
-    'paste_row_after' => 'Paste row after',
-    'row_type' => 'Row type',
-    'row_type_header' => 'Header',
-    'row_type_body' => 'Body',
-    'row_type_footer' => 'Footer',
-    'alignment' => 'Alignment',
-    'cut_column' => 'Cut column',
-    'copy_column' => 'Copy column',
-    'paste_column_before' => 'Paste column before',
-    'paste_column_after' => 'Paste column after',
-    'cell_padding' => 'Cell padding',
-    'cell_spacing' => 'Cell spacing',
-    'caption' => 'Caption',
-    'show_caption' => 'Show caption',
-    'constrain' => 'Constrain proportions',
-    'cell_border_solid' => 'Solid',
-    'cell_border_dotted' => 'Dotted',
-    'cell_border_dashed' => 'Dashed',
-    'cell_border_double' => 'Double',
-    'cell_border_groove' => 'Groove',
-    'cell_border_ridge' => 'Ridge',
-    'cell_border_inset' => 'Inset',
-    'cell_border_outset' => 'Outset',
-    'cell_border_none' => 'None',
-    'cell_border_hidden' => 'Hidden',
+    'table_properties' => 'Vlastnosti tabulky',
+    'table_properties_title' => 'Vlastnosti tabulky',
+    'delete_table' => 'Smazat tabulku',
+    'insert_row_before' => 'Vložit řádek před',
+    'insert_row_after' => 'Vložit řádek za',
+    'delete_row' => 'Smazat řádek',
+    'insert_column_before' => 'Vložit sloupec před',
+    'insert_column_after' => 'Vložit sloupec za',
+    'delete_column' => 'Odstranit sloupec',
+    'table_cell' => 'Buňka',
+    'table_row' => 'Řádek',
+    'table_column' => 'Sloupec',
+    'cell_properties' => 'Vlastnosti buňky',
+    'cell_properties_title' => 'Vlastnosti buňky',
+    'cell_type' => 'Typ buňky',
+    'cell_type_cell' => 'Buňka',
+    'cell_scope' => 'Rozsah',
+    'cell_type_header' => 'buňka záhlaví',
+    'merge_cells' => 'Sloučit buňky',
+    'split_cell' => 'Rozdělit buňku',
+    'table_row_group' => 'Skupina řádků',
+    'table_column_group' => 'Skupina sloupců',
+    'horizontal_align' => 'Vodorovné zarovnání',
+    'vertical_align' => 'Svislé vyrovnání',
+    'border_width' => 'Šířka okraje',
+    'border_style' => 'Styl okraje',
+    'border_color' => 'Barva okraje',
+    'row_properties' => 'Vlastnosti řádku',
+    'row_properties_title' => 'Vlastnosti řádku',
+    'cut_row' => 'Vyjmout řádek',
+    'copy_row' => 'Kopírovat řádek',
+    'paste_row_before' => 'Vložit řádek před',
+    'paste_row_after' => 'Vložit za',
+    'row_type' => 'Typ řádku',
+    'row_type_header' => 'Hlavička',
+    'row_type_body' => 'Tělo',
+    'row_type_footer' => 'Zápatí',
+    'alignment' => 'zarovnání',
+    'cut_column' => 'Vyjmout sloupec',
+    'copy_column' => 'Kopírovat sloupec',
+    'paste_column_before' => 'Přidat sloupec před',
+    'paste_column_after' => 'Přidat sloupec za',
+    'cell_padding' => 'Odsazení obsahu buněk',
+    'cell_spacing' => 'Mezery mezi buňkami',
+    'caption' => 'Titulek',
+    'show_caption' => 'Zobrazit titulek',
+    'constrain' => 'Vazba poměrů',
+    'cell_border_solid' => 'Nepřerušovaná čára',
+    'cell_border_dotted' => 'Tečkovaná čára',
+    'cell_border_dashed' => 'Přerušovaná čára',
+    'cell_border_double' => 'Dvojitá',
+    'cell_border_groove' => 'Drážek',
+    'cell_border_ridge' => 'hřeben',
+    'cell_border_inset' => 'Vsazený',
+    'cell_border_outset' => 'Počátek',
+    'cell_border_none' => 'Žádné',
+    'cell_border_hidden' => 'Skrytý',
 
     // Images, links, details/summary & embed
-    'source' => 'Source',
-    'alt_desc' => 'Alternative description',
-    'embed' => 'Embed',
-    'paste_embed' => 'Paste your embed code below:',
-    'url' => 'URL',
-    'text_to_display' => 'Text to display',
-    'title' => 'Title',
-    'open_link' => 'Open link in...',
-    'open_link_current' => 'Current window',
-    'open_link_new' => 'New window',
-    'insert_collapsible' => 'Insert collapsible block',
-    'collapsible_unwrap' => 'Unwrap',
-    'edit_label' => 'Edit label',
-    'toggle_open_closed' => 'Toggle open/closed',
-    'collapsible_edit' => 'Edit collapsible block',
-    'toggle_label' => 'Toggle label',
+    'source' => 'Zdroj',
+    'alt_desc' => 'Alternativní popis',
+    'embed' => 'Vložení',
+    'paste_embed' => 'Vložte svůj vložený kód níže:',
+    'url' => 'Adresa URL',
+    'text_to_display' => 'Text k zobrazení',
+    'title' => 'Titulek',
+    'open_link' => 'Otevřít odkaz',
+    'open_link_in' => 'Otevřít odkaz v...',
+    'open_link_current' => 'Aktuální okno',
+    'open_link_new' => 'Nové okno',
+    'remove_link' => 'Odstranit odkaz',
+    'insert_collapsible' => 'Vložit sbalitelný blok',
+    'collapsible_unwrap' => 'Rozbalit',
+    'edit_label' => 'Upravit štítek',
+    'toggle_open_closed' => 'Přepnout otevření/zavření',
+    'collapsible_edit' => 'Upravit sbalitelný blok',
+    'toggle_label' => 'Přepnout popisek',
 
     // About view
-    'about' => 'About the editor',
-    'about_title' => 'About the WYSIWYG Editor',
-    'editor_license' => 'Editor License & Copyright',
-    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',
-    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',
-    'save_continue' => 'Save Page & Continue',
-    'callouts_cycle' => '(Keep pressing to toggle through types)',
-    'link_selector' => 'Link to content',
-    'shortcuts' => 'Shortcuts',
-    'shortcut' => 'Shortcut',
-    'shortcuts_intro' => 'The following shortcuts are available in the editor:',
+    'about' => 'O editoru',
+    'about_title' => 'O WYSIWYG editoru',
+    'editor_license' => 'Editor licence a autorská práva',
+    'editor_tiny_license' => 'Tento editor je vytvořen pomocí :tinyLink, který je poskytován pod licencí MIT.',
+    'editor_tiny_license_link' => 'Podrobnosti o autorských právech a licenci TinyMCE naleznete zde.',
+    'save_continue' => 'Uložit stránku a pokračovat',
+    'callouts_cycle' => '(Stiskněte pro přepnutí typů)',
+    'link_selector' => 'Odkaz na obsah',
+    'shortcuts' => 'Zkratky',
+    'shortcut' => 'Zástupce',
+    'shortcuts_intro' => 'Následující zkratky jsou k dispozici v editoru:',
     'windows_linux' => '(Windows/Linux)',
     'mac' => '(Mac)',
-    'description' => 'Description',
+    'description' => 'Popis',
 ];
index 0b552346eb4854f276843c0f12489971fc8d8539..b674c6e63900e6a42bd7903d6f4da57554ff219f 100644 (file)
@@ -23,9 +23,9 @@ return [
     'meta_updated' => 'Aktualizováno :timeLength',
     'meta_updated_name' => 'Aktualizováno :timeLength uživatelem :user',
     'meta_owned_name' => 'Vlastník :user',
-    'meta_reference_page_count' => 'Referenced on 1 page|Referenced on :count pages',
+    'meta_reference_page_count' => 'Odkazováno na 1 stránce|Odkazováno na :count stránky',
     'entity_select' => 'Výběr entity',
-    'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
+    'entity_select_lack_permission' => 'Nemáte dostatečná oprávnění k výběru této položky',
     'images' => 'Obrázky',
     'my_recent_drafts' => 'Mé nedávné koncepty',
     'my_recently_viewed' => 'Mé nedávno zobrazené',
@@ -42,14 +42,15 @@ return [
 
     // Permissions and restrictions
     'permissions' => 'Oprávnění',
-    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',
-    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',
-    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',
+    'permissions_desc' => 'Nastavte oprávnění, které změní výchozích oprávnění pochazejících z uživatelské role.',
+    'permissions_book_cascade' => 'Oprávnění nastavená v knihách budou automaticky kaskádována do podřízených kapitol a stránek, pokud nemají svá vlastní oprávnění.',
+    'permissions_chapter_cascade' => 'Oprávnění nastavená v knihách budou automaticky kaskádována do podřízených kapitol a stránek, pokud nemají svá vlastní oprávnění.',
     'permissions_save' => 'Uložit oprávnění',
     'permissions_owner' => 'Vlastník',
-    'permissions_role_everyone_else' => 'Everyone Else',
-    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
-    'permissions_role_override' => 'Override permissions for role',
+    'permissions_role_everyone_else' => 'Všichni ostatní',
+    'permissions_role_everyone_else_desc' => 'Nastavte oprávnění pro všechny role, které nejsou výslovně přepsány.',
+    'permissions_role_override' => 'Přepsat oprávnění pro roli',
+    'permissions_inherit_defaults' => 'Zdědit výchozí oprávnění',
 
     // Search
     'search_results' => 'Výsledky hledání',
@@ -92,23 +93,23 @@ return [
     'shelves_save' => 'Uložit knihovnu',
     'shelves_books' => 'Knihy v této knihovně',
     'shelves_add_books' => 'Přidat knihy do knihovny',
-    'shelves_drag_books' => 'Drag books below to add them to this shelf',
+    'shelves_drag_books' => 'Přetáhněte knihy níže a přidejte je do této knihovny',
     'shelves_empty_contents' => 'Tato knihovna neobsahuje žádné knihy',
     'shelves_edit_and_assign' => 'Upravit knihovnu a přiřadit knihy',
-    'shelves_edit_named' => 'Edit Shelf :name',
-    'shelves_edit' => 'Edit Shelf',
-    'shelves_delete' => 'Delete Shelf',
-    'shelves_delete_named' => 'Delete Shelf :name',
-    'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
-    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
-    'shelves_permissions' => 'Shelf Permissions',
-    '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_edit_named' => 'Upravit knihovnu :name',
+    'shelves_edit' => 'Upravit knihovnu',
+    'shelves_delete' => 'Odstranit knihovnu',
+    'shelves_delete_named' => 'Odstranit knihovnu :name',
+    'shelves_delete_explain' => "Chystáte se smazat knihovnu ':name'. Knihy v ní obsažené zůstanou zachovány.",
+    'shelves_delete_confirmation' => 'Opravdu chcete odstranit tuto knihovnu?',
+    'shelves_permissions' => 'Oprávnění knihovny',
+    'shelves_permissions_updated' => 'Oprávnění knihovny byla aktualizována',
+    'shelves_permissions_active' => 'Oprávnění knihovny byla aktivována',
+    'shelves_permissions_cascade_warning' => 'Oprávnění v Knihovnách nejsou automaticky kaskádována do obsažených knih. To proto, že kniha může existovat ve více Knihovnách. Oprávnění však lze zkopírovat do podřízených knih pomocí níže uvedené možnosti.',
     'shelves_copy_permissions_to_books' => 'Kopírovat oprávnění na knihy',
     'shelves_copy_permissions' => 'Kopírovat oprávnění',
-    '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.',
-    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
+    'shelves_copy_permissions_explain' => 'Toto použije aktuální nastavení oprávnění knihovny na všechny knihy v ní obsažené. Před aktivací se ujistěte, že byly uloženy všechny změny oprávnění této knihovny.',
+    'shelves_copy_permission_success' => 'Oprávnění knihovny byla zkopírována na :count knih',
 
     // Books
     'book' => 'Kniha',
@@ -175,7 +176,7 @@ return [
     'chapters_permissions_active' => 'Oprávnění kapitoly byla aktivována',
     'chapters_permissions_success' => 'Oprávnění kapitoly byla aktualizována',
     'chapters_search_this' => 'Prohledat tuto kapitolu',
-    'chapter_sort_book' => 'Sort Book',
+    'chapter_sort_book' => 'Seřadit knihy',
 
     // Pages
     'page' => 'Stránka',
@@ -202,19 +203,19 @@ return [
     'pages_edit_draft_save_at' => 'Koncept uložen v ',
     'pages_edit_delete_draft' => 'Odstranit koncept',
     'pages_edit_discard_draft' => 'Zahodit koncept',
-    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
-    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
-    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
-    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
+    'pages_edit_switch_to_markdown' => 'Přepnout na Markdown Editor',
+    'pages_edit_switch_to_markdown_clean' => '(Vytvořený obsah)',
+    'pages_edit_switch_to_markdown_stable' => '(Stabilní obsah)',
+    'pages_edit_switch_to_wysiwyg' => 'Přepnout na WYSIWYG Editor',
     'pages_edit_set_changelog' => 'Nastavit protokol změn',
     'pages_edit_enter_changelog_desc' => 'Zadejte stručný popis změn, které jste provedli',
     'pages_edit_enter_changelog' => 'Zadejte protokol změn',
-    'pages_editor_switch_title' => 'Switch Editor',
-    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
-    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
-    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
-    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
-    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
+    'pages_editor_switch_title' => 'Přepnout editor',
+    'pages_editor_switch_are_you_sure' => 'Jste si jisti, že chcete změnit editor této stránky?',
+    'pages_editor_switch_consider_following' => 'Při změně editorů zvažte následující:',
+    'pages_editor_switch_consideration_a' => 'Po uložení bude nová možnost editoru použita všemi budoucími editory, včetně těch, které nemusí být schopny změnit typ editoru.',
+    'pages_editor_switch_consideration_b' => 'To může za určitých okolností vést ke ztrátě podrobností a syntaxe.',
+    'pages_editor_switch_consideration_c' => 'Změny tagu nebo seznamu změn, provedené od posledního uložení, nebudou přetrvávat po celé této změně.',
     'pages_save' => 'Uložit stránku',
     'pages_title' => 'Nadpis stránky',
     'pages_name' => 'Název stránky',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Vložit obrázek',
     'pages_md_insert_link' => 'Vložit odkaz na entitu',
     'pages_md_insert_drawing' => 'Vložit kresbu',
+    'pages_md_show_preview' => 'Zobrazit náhled',
+    'pages_md_sync_scroll' => 'Synchronizovat náhled',
     'pages_not_in_chapter' => 'Stránka není v kapitole',
     'pages_move' => 'Přesunout stránku',
     'pages_move_success' => 'Stránka přesunuta do ":parentName"',
@@ -233,15 +236,17 @@ return [
     'pages_permissions_success' => 'Oprávnění stránky byla aktualizována',
     'pages_revision' => 'Revize',
     'pages_revisions' => 'Revize stránky',
+    'pages_revisions_desc' => 'Níže uvedené jsou všechny minulé revize této stránky. Můžete se podívat zpět, porovnat a obnovit staré verze stránek, pokud to dovolí oprávnění. Úplná historie stránky nemusí být plně zohledněna, protože v závislosti na konfiguraci systému mohou být staré revize automaticky smazány.',
     'pages_revisions_named' => 'Revize stránky pro :pageName',
     'pages_revision_named' => 'Revize stránky pro :pageName',
     'pages_revision_restored_from' => 'Obnoveno z #:id; :summary',
     'pages_revisions_created_by' => 'Vytvořeno uživatelem',
     'pages_revisions_date' => 'Datum revize',
     'pages_revisions_number' => 'Č. ',
+    'pages_revisions_sort_number' => 'Číslo revize',
     'pages_revisions_numbered' => 'Revize č. :id',
     'pages_revisions_numbered_changes' => 'Změny revize č. :id',
-    'pages_revisions_editor' => 'Editor Type',
+    'pages_revisions_editor' => 'Typ editoru',
     'pages_revisions_changelog' => 'Protokol změn',
     'pages_revisions_changes' => 'Změny',
     'pages_revisions_current' => 'Aktuální verze',
@@ -252,7 +257,7 @@ return [
     'pages_edit_content_link' => 'Upravit obsah',
     'pages_permissions_active' => 'Oprávnění stránky byla aktivována',
     'pages_initial_revision' => 'První vydání',
-    'pages_references_update_revision' => 'System auto-update of internal links',
+    'pages_references_update_revision' => 'Automatická aktualizace interních odkazů',
     'pages_initial_name' => 'Nová stránka',
     'pages_editing_draft_notification' => 'Právě upravujete koncept, který byl uložen :timeDiff.',
     'pages_draft_edited_notification' => 'Tato stránka se od té doby změnila. Je doporučeno aktuální koncept zahodit.',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Štítky knihovny',
     'tag' => 'Štítek',
     'tags' =>  'Štítky',
+    'tags_index_desc' => 'Tagy mohou být použity pro obsah v rámci systému pro pružnou formu kategorizace. Tagy mohou mít klíč i hodnotu, přičemž hodnota je nepovinná. Po aplikaci může být obsah dotazován pomocí názvu a hodnoty štítku.',
     'tag_name' =>  'Název štítku',
     'tag_value' => 'Hodnota štítku (volitelné)',
     'tags_explain' => "Přidejte si štítky pro lepší kategorizaci knih. \n Štítky mohou nést i hodnotu pro detailnější klasifikaci.",
@@ -356,27 +362,27 @@ return [
     'revision_cannot_delete_latest' => 'Nelze odstranit poslední revizi.',
 
     // Copy view
-    'copy_consider' => 'Please consider the below when copying content.',
+    'copy_consider' => 'Při kopírování obsahu zvažte prosím níže.',
     'copy_consider_permissions' => 'Vlastní nastavení oprávnění nebudou zkopírovány.',
     'copy_consider_owner' => 'Stanete se vlastníkem veškerého kopírovaného obsahu.',
-    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
+    'copy_consider_images' => 'Soubory obrázků stránky nebudou duplikovány a původní obrázky si zachovají jejich vztah ke stránce, na kterou byly původně nahrány.',
     'copy_consider_attachments' => 'Přílohy stránky nebudou zkopírovány.',
     'copy_consider_access' => 'Po změně umístění, vlastníka nebo oprávnění může dojít k tomu, že obsah může být přístupný těm, kteří přístup dříve něměli.',
 
     // Conversions
-    'convert_to_shelf' => 'Convert to Shelf',
-    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',
-    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',
-    'convert_book' => 'Convert Book',
-    'convert_book_confirm' => 'Are you sure you want to convert this book?',
-    'convert_undo_warning' => 'This cannot be as easily undone.',
-    'convert_to_book' => 'Convert to Book',
-    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
-    'convert_chapter' => 'Convert Chapter',
-    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
+    'convert_to_shelf' => 'Převést na knihovnu',
+    'convert_to_shelf_contents_desc' => 'Tuto knihu můžete převést na novou knihovnu se stejným obsahem. Kapitoly obsažené v této knize budou převedeny na nové knihy. Pokud tato kniha obsahuje jakékoli stránky, které nejsou uvedeny v kapitole, Tato kniha bude přejmenována a bude obsahovat tyto stránky a tato kniha se stane součástí nové knihovny.',
+    'convert_to_shelf_permissions_desc' => 'Veškerá oprávnění nastavená v této knize budou zkopírována do nové knihovny a do všech nových podřazených knih, které nemají vlastní oprávnění. Všimněte si, že oprávnění na regálech neobsahují automatickou kaskádu na obsah, jak to dělají pro knihy.',
+    'convert_book' => 'Převést knihu',
+    'convert_book_confirm' => 'Opravdu chcete převést tuto knihu?',
+    'convert_undo_warning' => 'To nelze tak snadno vrátit zpět.',
+    'convert_to_book' => 'Převést knihu',
+    'convert_to_book_desc' => 'Tuto kapitolu můžete převést na novou knihu se stejným obsahem. Veškerá oprávnění nastavená v této kapitole budou zkopírována do nové knihy, ale všechna zděděná oprávnění, z nadřazené knihy nebudou kopírovány, což by mohlo vést ke změně kontroly přístupu.',
+    'convert_chapter' => 'Převést kapitolu',
+    'convert_chapter_confirm' => 'Jste si jisti, že chcete převést tuto kapitolu?',
 
     // References
-    '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.',
+    'references' => 'Odkazy',
+    'references_none' => 'Nebyly nalezeny žádné odkazy na tuto položku.',
+    'references_to_desc' => 'Níže jsou uvedeny všechny známé stránky systému, které odkazují na tuto položku.',
 ];
index 942894c94e068b90033c0445811b724b1fdb1190..469c7c534f9264d336eadc9e84583378874ff0c0 100644 (file)
@@ -23,10 +23,10 @@ return [
     'saml_no_email_address' => 'Nelze najít e-mailovou adresu pro tohoto uživatele v datech poskytnutých externím přihlašovacím systémem',
     'saml_invalid_response_id' => 'Požadavek z externího ověřovacího systému nebyl rozpoznám procesem, který tato aplikace spustila. Tento problém může způsobit stisknutí tlačítka Zpět po přihlášení.',
     'saml_fail_authed' => 'Přihlášení pomocí :system selhalo, systém neposkytl úspěšnou autorizaci',
-    'oidc_already_logged_in' => 'Already logged in',
-    'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
-    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
-    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
+    'oidc_already_logged_in' => 'Již jste přihlášeni',
+    'oidc_user_not_registered' => 'Uživatel :name není registrován a automatická registrace je zakázána',
+    'oidc_no_email_address' => 'Nelze najít e-mailovou adresu pro tohoto uživatele v datech poskytnutých externím přihlašovacím systémem',
+    'oidc_fail_authed' => 'Přihlášení pomocí :system selhalo, systém neposkytl úspěšnou autorizaci',
     'social_no_action_defined' => 'Nebyla zvolena žádá akce',
     'social_login_bad_response' => "Nastala chyba během přihlašování přes :socialAccount \n:error",
     'social_account_in_use' => 'Tento účet na :socialAccount se již používá. Pokuste se s ním přihlásit volbou Přihlásit přes :socialAccount.',
@@ -58,7 +58,7 @@ return [
 
     // Entities
     'entity_not_found' => 'Prvek nenalezen',
-    'bookshelf_not_found' => 'Shelf not found',
+    'bookshelf_not_found' => 'Knihovna nenalezena',
     'book_not_found' => 'Kniha nenalezena',
     'page_not_found' => 'Stránka nenalezena',
     'chapter_not_found' => 'Kapitola nenalezena',
index 40b12b6070fe64cc7fa4f623ab83abadc3b13c4f..9dc1c0fc3c084e4358c132fc354123178ae97eb5 100644 (file)
@@ -6,8 +6,8 @@
  */
 return [
 
-    'password' => 'Heslo musí mít alespoň osm znaků a musí odpovídat potvrzení hesla.',
-    'user' => "Nemůžeme nalézt uživatele s touto e-mailovou adresou.",
+    'password' => 'Heslo musí mít alespoň osm znaků a shodovat se v obou polích.',
+    'user' => "Uživatel s touto e-mailovou adresou nebyl nalezen.",
     'token' => 'Token pro obnovení hesla není platný pro tuto e-mailovou adresu.',
     'sent' => 'Poslali jsme Vám e-mail s odkazem pro obnovení hesla!',
     'reset' => 'Vaše heslo bylo obnoveno!',
diff --git a/resources/lang/cs/preferences.php b/resources/lang/cs/preferences.php
new file mode 100644 (file)
index 0000000..6727470
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Zkratky',
+    'shortcuts_interface' => 'Zobrazit klávesové zkratky',
+    'shortcuts_toggle_desc' => 'Zde můžete povolit nebo zakázat klávesové zkratky systémového rozhraní používané pro navigaci a akce.',
+    'shortcuts_customize_desc' => 'Po výběru vstupu pro zástupce si můžete přizpůsobit všechny klávesové zkratky.',
+    'shortcuts_toggle_label' => 'Klávesové zkratky povoleny',
+    'shortcuts_section_navigation' => 'Navigace',
+    'shortcuts_section_actions' => 'Společné akce',
+    'shortcuts_save' => 'Uložit zkratky',
+    'shortcuts_overlay_desc' => 'Poznámka: Když jsou povoleny zkratky, je k dispozici pomocný překryv stisknutím "? která zvýrazní dostupné zkratky pro akce viditelné na obrazovce.',
+    'shortcuts_update_success' => 'Nastavení pro zkratky bylo aktualizováno!',
+];
\ No newline at end of file
index cb4d21f14fa37075dbfbcbdc860756ccd5ed7047..5f16b3a64af4eeec333ac6f038d61d397ff0d9a3 100644 (file)
@@ -10,8 +10,8 @@ return [
     'settings' => 'Nastavení',
     'settings_save' => 'Uložit nastavení',
     'settings_save_success' => 'Nastavení uloženo',
-    'system_version' => 'System Version',
-    'categories' => 'Categories',
+    'system_version' => 'Verze systému: ',
+    'categories' => 'Kategorie',
 
     // App Settings
     'app_customization' => 'Přizpůsobení',
@@ -27,8 +27,8 @@ return [
     'app_secure_images' => 'Nahrávat obrázky neveřejně a zabezpečeně',
     'app_secure_images_toggle' => 'Zapnout bezpečnější nahrávání obrázků',
     'app_secure_images_desc' => 'Z výkonnostních důvodů jsou všechny obrázky veřejně dostupné. Tato volba přidá do adresy obrázku náhodný řetězec, aby nikdo neodhadnul adresu obrázku. Ujistěte se, že server nezobrazuje v adresáři seznam souborů, což by přístup k přístup opět otevřelo.',
-    'app_default_editor' => 'Default Page Editor',
-    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',
+    'app_default_editor' => 'Výchozí editor',
+    'app_default_editor_desc' => 'Vyberte, který editor bude použit ve výchozím nastavení při úpravách nových stránek. To může být přepsáno na úrovni stránky, kde to dovolují oprávnění.',
     'app_custom_html' => 'Vlastní obsah hlavičky HTML',
     'app_custom_html_desc' => 'Cokoliv sem napíšete bude přidáno na konec sekce <head> v každém místě této aplikace. To se hodí pro přidávání nebo změnu CSS stylů nebo přidání kódu pro analýzu používání (např.: google analytics.).',
     'app_custom_html_disabled_notice' => 'Na této stránce nastavení je zakázán vlastní obsah HTML hlavičky, aby bylo zajištěno, že bude možné vrátit případnou problematickou úpravu.',
@@ -89,10 +89,10 @@ return [
     'maint_send_test_email_mail_text' => 'Gratulujeme! Protože jste dostali tento e-mail, zdá se, že nastavení e-mailů je v pořádku.',
     'maint_recycle_bin_desc' => 'Odstraněné knihovny, knihy, kapitoly a stránky se přesouvají do Koše, aby je bylo možné obnovit nebo trvale smazat. Starší položky v koši mohou být po čase automaticky odstraněny v závislosti na konfiguraci systému.',
     'maint_recycle_bin_open' => 'Otevřít Koš',
-    'maint_regen_references' => 'Regenerate References',
-    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
-    'maint_regen_references_success' => 'Reference index has been regenerated!',
-    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
+    'maint_regen_references' => 'Přegenerovat odkazy',
+    'maint_regen_references_desc' => 'Tato akce obnoví referenční index křížových položek v rámci databáze. Toto je obvykle zpracováno automaticky, ale tato akce může být užitečná pro indexování starého obsahu nebo obsahu přidaného neoficiálními metodami.',
+    'maint_regen_references_success' => 'Referenční index byl obnoven!',
+    'maint_timeout_command_note' => 'Poznámka: Tato akce může trvat nějakou dobu, což může vést k vypršení časového limitu v některých webových prostředích. Alternativně se tato akce provádí pomocí příkazu terminálu.',
 
     // Recycle Bin
     'recycle_bin' => 'Koš',
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Role',
     'role_user_roles' => 'Uživatelské role',
+    'roles_index_desc' => 'Role se používají ke sdružování uživatelů a k poskytování systémových oprávnění jejich členům. Pokud je uživatel členem více rolí, udělená oprávnění budou uložena a uživatel zdědí všechny schopnosti.',
+    'roles_x_users_assigned' => '1 přiřazený uživatel|:count přiřazených uživatelů',
+    'roles_x_permissions_provided' => '1 oprávnění|:count oprávnění',
+    'roles_assigned_users' => 'Přiřazení uživatelé',
+    'roles_permissions_provided' => 'Poskytnutá oprávnění',
     'role_create' => 'Vytvořit novou roli',
     'role_create_success' => 'Role byla vytvořena',
     'role_delete' => 'Odstranit roli',
@@ -156,12 +161,12 @@ return [
     'role_access_api' => 'Přístup k systémovému API',
     'role_manage_settings' => 'Správa nastavení aplikace',
     'role_export_content' => 'Exportovat obsah',
-    'role_editor_change' => 'Change page editor',
+    'role_editor_change' => 'Změnit editor stránek',
     'role_asset' => 'Obsahová oprávnění',
     'roles_system_warning' => 'Berte na vědomí, že přístup k některému ze tří výše uvedených oprávnění může uživateli umožnit změnit svá vlastní oprávnění nebo oprávnění ostatních uživatelů v systému. Přiřazujte role s těmito oprávněními pouze důvěryhodným uživatelům.',
     'role_asset_desc' => 'Tato oprávnění řídí přístup k obsahu napříč systémem. Specifická oprávnění na knihách, kapitolách a stránkách převáží tato nastavení.',
     'role_asset_admins' => 'Administrátoři automaticky dostávají přístup k veškerému obsahu, ale tyto volby mohou ukázat nebo skrýt volby v uživatelském rozhraní.',
-    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
+    'role_asset_image_view_note' => 'To se týká viditelnosti ve správci obrázků. Skutečný přístup k nahraným souborům obrázků bude záviset na možnosti uložení systémových obrázků.',
     'role_all' => 'Vše',
     'role_own' => 'Vlastní',
     'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Uživatelé',
+    'users_index_desc' => 'Vytváření a správa jednotlivých uživatelských účtů v rámci systému. Uživatelské účty jsou používány pro přihlášení a přiřazování obsahu a aktivity. Přístupová práva jsou primárně založena na roli, ale vlastnictví obsahu uživatele může kromě jiných faktorů také ovlivnit oprávnění a přístup.',
     'user_profile' => 'Profil uživatele',
     'users_add_new' => 'Přidat nového uživatele',
     'users_search' => 'Vyhledávání uživatelů',
@@ -182,7 +188,7 @@ return [
     'users_role' => 'Uživatelské role',
     'users_role_desc' => 'Zvolte role, do kterých chcete uživatele zařadit. Pokud bude uživatel zařazen do více rolí, oprávnění z těchto rolí se sloučí a uživateli bude dovoleno vše, k čemu mají jednotlivé role oprávnění.',
     'users_password' => 'Heslo uživatele',
-    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
+    'users_password_desc' => 'Zadejte heslo pro přihlášení do aplikace. Heslo musí být nejméně 8 znaků dlouhé.',
     'users_send_invite_text' => 'Uživateli můžete poslat pozvánku e-mailem, která umožní uživateli, aby si zvolil sám svoje heslo do aplikace a nebo můžete zadat heslo sami.',
     'users_send_invite_option' => 'Poslat uživateli pozvánku e-mailem',
     'users_external_auth_id' => 'Přihlašovací identifikátor třetích stran',
@@ -241,30 +247,32 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooky',
+    'webhooks_index_desc' => 'Webhooks jsou způsob, jak odeslat data na externí URL, pokud se vyskytnou určité akce a události v systému, které umožňují integraci událostí s externími platformami, jako jsou systémy zasílání zpráv nebo oznámení.',
+    'webhooks_x_trigger_events' => '1 spouštěcí událost|:count spouštěcí události',
     'webhooks_create' => 'Vytvořit nový webhook',
     'webhooks_none_created' => 'Žádné webhooky nebyly doposud vytvořeny.',
     'webhooks_edit' => 'Upravit webhook',
     'webhooks_save' => 'Uložit webhook',
     'webhooks_details' => 'Podrobnosti webhooku',
-    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
+    'webhooks_details_desc' => 'Zadejte uživatelsky přívětivé jméno a koncový bod POST jako umístění pro zasílání dat webhooku.',
     'webhooks_events' => 'Události webhooku',
-    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
-    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
+    'webhooks_events_desc' => 'Vyberte všechny události, které by měly spustit tento webhook pro volání.',
+    'webhooks_events_warning' => 'Mějte na paměti, že tyto události budou spouštěny pro všechny vybrané události, i když budou použita vlastní oprávnění. Zajistěte, aby používání tohoto webového háčku nezobrazovalo důvěrné obsahy.',
     'webhooks_events_all' => 'Všechny události systému',
     'webhooks_name' => 'Název webhooku',
-    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
-    'webhooks_endpoint' => 'Webhook Endpoint',
+    'webhooks_timeout' => 'Časový limit požadavku Webhook (sekundy)',
+    'webhooks_endpoint' => 'Koncový bod webhooku',
     'webhooks_active' => 'Webhook aktivní',
     'webhook_events_table_header' => 'Události',
     'webhooks_delete' => 'Odstranit webhook',
     'webhooks_delete_warning' => 'Webhook s názvem \':webhookName\' bude úplně odstraněn ze systému.',
     'webhooks_delete_confirm' => 'Opravdu chcete odstranit tento webhook?',
     'webhooks_format_example' => 'Příklad formátu webhooku',
-    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
-    'webhooks_status' => 'Webhook Status',
-    'webhooks_last_called' => 'Last Called:',
-    'webhooks_last_errored' => 'Last Errored:',
-    'webhooks_last_error_message' => 'Last Error Message:',
+    'webhooks_format_example_desc' => 'Webový háček je odesílán jako POST požadavek na konfigurovaný koncový bod ve formátu JSON ve formátu níže. Vlastnosti "related_item" a "url" jsou volitelné a budou záviset na typu události spuštěné.',
+    'webhooks_status' => 'Stav webhooku',
+    'webhooks_last_called' => 'Poslední volání:',
+    'webhooks_last_errored' => 'Poslední chyba:',
+    'webhooks_last_error_message' => 'Poslední chybová zpráva',
 
 
     //! If editing translations files directly please ignore this in all
index 441fd30c85a7c9fc965758dc8c07c9d1debded01..83929cc887b56d21a5a383594ba32d40b92074c3 100644 (file)
@@ -32,7 +32,7 @@ return [
     'digits_between'       => ':attribute musí být dlouhé nejméně :min a nejvíce :max pozic.',
     'email'                => ':attribute není platný formát.',
     'ends_with' => ':attribute musí končit jednou z následujících hodnot: :values',
-    'file'                 => 'The :attribute must be provided as a valid file.',
+    'file'                 => ':attribute musí být zadán jako platný soubor.',
     'filled'               => ':attribute musí být vyplněno.',
     'gt'                   => [
         'numeric' => ':attribute musí být větší než :value.',
index bb008578e20bc8c47ea1bbbdf94ba5fdcb922122..6be96308076ef97973a586580beef1e31f83f48b 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Email Address Not Confirmed',
     'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',
index 703a70c7e6aa5bea2b59f7319d4739c5f7162c13..c74dcc90775219416dfe1e56cd0c2d9c577c6f4d 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Actions',
     'view' => 'View',
     'view_all' => 'View All',
+    'new' => 'New',
     'create' => 'Create',
     'update' => 'Update',
     'edit' => 'Edit',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Expand Header Menu',
     'profile_menu' => 'Profile Menu',
     'view_profile' => 'View Profile',
     'edit_profile' => 'Edit Profile',
     'dark_mode' => 'Dark Mode',
     'light_mode' => 'Light Mode',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Info',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index bf6201900fc3653724e85d715411695325b6fd0a..fa2586f8d75cab171ec753065eaf73f4efcafe5d 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Search Results',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Insert Image',
     'pages_md_insert_link' => 'Insert Entity Link',
     'pages_md_insert_drawing' => 'Insert Drawing',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Page is not in a chapter',
     'pages_move' => 'Move Page',
     'pages_move_success' => 'Page moved to ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Page permissions updated',
     'pages_revision' => 'Revision',
     'pages_revisions' => 'Page Revisions',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Page Revisions for :pageName',
     'pages_revision_named' => 'Page Revision for :pageName',
     'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Created By',
     'pages_revisions_date' => 'Revision Date',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revision #:id',
     'pages_revisions_numbered_changes' => 'Revision #:id Changes',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Shelf Tags',
     'tag' => 'Tag',
     'tags' =>  'Tags',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Tag Name',
     'tag_value' => 'Tag Value (Optional)',
     'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
diff --git a/resources/lang/cy/preferences.php b/resources/lang/cy/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 1ad271e7c9ebd736f1d283bc2884b756294ea56d..f4204dd68bf05858a70205983d8aa35204366efa 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roles',
     'role_user_roles' => 'User Roles',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Create New Role',
     'role_create_success' => 'Role successfully created',
     'role_delete' => 'Delete Role',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Users',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'User Profile',
     'users_add_new' => 'Add New User',
     'users_search' => 'Search Users',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 43a3f33785da23abd3512ecaaace18a541ee4132..2d2b7bebb8838147a93018199a3783b5171a71e6 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'E-Mail-bekræftelse kræves, men systemet kunne ikke sende E-Mailen. Kontakt administratoren for at sikre, at E-Mail er konfigureret korrekt.',
     'email_confirm_success' => 'Din email er blevet bekræftet! Du bør nu kune logge ind med denne emailadresse.',
     'email_confirm_resent' => 'Bekræftelsesmail sendt, tjek venligst din indboks.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'E-Mail adresse ikke bekræftet',
     'email_not_confirmed_text' => 'Din E-Mail adresse er endnu ikke blevet bekræftet.',
index bcd02dfc639c487972994beb21bc6189ad150b38..be8065dc871eb5525c52662e04d966fc23861693 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Handlinger',
     'view' => 'Vis',
     'view_all' => 'Vis alle',
+    'new' => 'New',
     'create' => 'Opret',
     'update' => 'Opdater',
     'edit' => 'Rediger',
@@ -80,12 +81,14 @@ return [
     'none' => 'Ingen',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Udvid header menu',
     'profile_menu' => 'Profilmenu',
     'view_profile' => 'Vis profil',
     'edit_profile' => 'Redigér Profil',
     'dark_mode' => 'Mørk tilstand',
     'light_mode' => 'Lys tilstand',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Info',
index c925391d555898c838fb55a0685bab543ddf478a..38638ba86b54a3c83274ba27671aec7d5a4df1c1 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Tekst til visning',
     'title' => 'Titel',
-    'open_link' => 'Åben link i...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Nuværende vindue',
     'open_link_new' => 'Nyt vindue',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index 22e850d5c323669687fb613a4b19c47b60481299..e0a95478cd17c16373ff62f5698a9423f7aa6642 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Søgeresultater',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Indsæt billede',
     'pages_md_insert_link' => 'Indsæt emnelink',
     'pages_md_insert_drawing' => 'Indsæt tegning',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Side er ikke i et kapitel',
     'pages_move' => 'Flyt side',
     'pages_move_success' => 'Flyt side til ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Sidetilladelser opdateret',
     'pages_revision' => 'Revision',
     'pages_revisions' => 'Sidserevisioner',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Siderevisioner for :pageName',
     'pages_revision_named' => 'Siderevision for :pageName',
     'pages_revision_restored_from' => 'Genoprettet fra #:id; :summary',
     'pages_revisions_created_by' => 'Oprettet af',
     'pages_revisions_date' => 'Revisionsdato',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revision #:id',
     'pages_revisions_numbered_changes' => 'Revision #:id ændringer',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Reoltags',
     'tag' => 'Tag',
     'tags' =>  'Tags',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Tagnavn',
     'tag_value' => 'Tagværdi (valgfri)',
     'tags_explain' => "Tilføj nogle tags for bedre at kategorisere dit indhold. \n Du kan tildele en værdi til et tag for mere dybdegående organisering.",
diff --git a/resources/lang/da/preferences.php b/resources/lang/da/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index ed882615bba6a5584985169e3d0e2669fa4c40f0..5d5396cf62b53a31ae65b420827f7f163bfaa2aa 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roller',
     'role_user_roles' => 'Brugerroller',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Opret en ny rolle',
     'role_create_success' => 'Rollen blev oprette korrekt',
     'role_delete' => 'Slet rolle',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Brugere',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Brugerprofil',
     'users_add_new' => 'Tilføj ny bruger',
     'users_search' => 'Søg efter brugere',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Opret ny Webhook',
     'webhooks_none_created' => 'Ingen webhooks er blevet oprettet endnu.',
     'webhooks_edit' => 'Rediger Webhook',
index f72ef56e3d974220e231eca5898004ffa8813415..85480f5eacc43d2336e07bb49cf97c2db5d90bde 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Leider konnte die für die Registrierung notwendige E-Mail zur Bestätigung Ihrer E-Mail-Adresse nicht versandt werden. Bitte kontaktieren Sie den Systemadministrator!',
     'email_confirm_success' => 'Ihre E-Mail wurde bestätigt! Sie sollten nun in der Lage sein, sich mit dieser E-Mail-Adresse anzumelden.',
     'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfen Sie Ihren Posteingang.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt',
     'email_not_confirmed_text' => 'Ihre E-Mail-Adresse ist bisher nicht bestätigt.',
index 645c4ad5871ed814f9822a360e020456da3efbd8..3ca22701a085e8e06f97bb6ad01ae887d3b7646f 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Aktionen',
     'view' => 'Anzeigen',
     'view_all' => 'Alle anzeigen',
+    'new' => 'New',
     'create' => 'Erstellen',
     'update' => 'Aktualisieren',
     'edit' => 'Bearbeiten',
@@ -80,12 +81,14 @@ return [
     'none' => 'Nichts',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Header-Menü erweitern',
     'profile_menu' => 'Profilmenü',
     'view_profile' => 'Profil ansehen',
     'edit_profile' => 'Profil bearbeiten',
     'dark_mode' => 'Dunkler Modus',
     'light_mode' => 'Heller Modus',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Info',
index 2c14b6410c5d6d9149b7a6e0bf74f6bcdd49a43f..72ede3377e7f27b20064f552ee449b421fabdb8d 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Anzuzeigender Text',
     'title' => 'Titel',
-    'open_link' => 'Link öffnen in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Aktuelles Fenster',
     'open_link_new' => 'Neues Fenster',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Einklappbarer Block einfügen',
     'collapsible_unwrap' => 'Auspacken',
     'edit_label' => 'Label bearbeiten',
index 5e48b01e07f224b187e7b6865c67d536dfbe9c85..dad280c6925cb75309fa652ea2c761c07c37df02 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Alle anderen',
     'permissions_role_everyone_else_desc' => 'Berechtigungen für alle Rollen setzen, die nicht explizit überschrieben wurden.',
     'permissions_role_override' => 'Berechtigungen für Rolle überschreiben',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Suchergebnisse',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Bild einfügen',
     'pages_md_insert_link' => 'Link zu einem Objekt einfügen',
     'pages_md_insert_drawing' => 'Zeichnung einfügen',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Seite ist in keinem Kapitel',
     'pages_move' => 'Seite verschieben',
     'pages_move_success' => 'Seite nach ":parentName" verschoben',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',
     'pages_revision' => 'Version',
     'pages_revisions' => 'Seitenversionen',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Seitenversionen von ":pageName"',
     'pages_revision_named' => 'Seitenversion von ":pageName"',
     'pages_revision_restored_from' => 'Wiederhergestellt von #:id; :summary',
     'pages_revisions_created_by' => 'Erstellt von',
     'pages_revisions_date' => 'Versionsdatum',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revision #:id',
     'pages_revisions_numbered_changes' => 'Revision #:id Änderungen',
     'pages_revisions_editor' => 'Editor-Typ',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Regal-Schlagwörter',
     'tag' => 'Schlagwort',
     'tags' =>  'Schlagwörter',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Schlagwort Name',
     'tag_value' => 'Inhalt (Optional)',
     'tags_explain' => "Fügen Sie Schlagwörter hinzu, um Ihren Inhalt zu kategorisieren.\nSie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.",
diff --git a/resources/lang/de/preferences.php b/resources/lang/de/preferences.php
new file mode 100644 (file)
index 0000000..cc757aa
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Tastenkürzel',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Gemeinsame Aktionen',
+    'shortcuts_save' => 'Tastenkürzel speichern',
+    'shortcuts_overlay_desc' => 'Hinweis: Wenn Tastenkürzel aktiviert sind, ist ein Hilfsoverlay durch Drücken von "?" verfügbar, welches die verfügbaren Tastenkürzel für Aktionen hervorhebt, die aktuell auf dem Bildschirm sichtbar sind.',
+    'shortcuts_update_success' => 'Tastenkürzel Einstellungen wurden aktualisiert!',
+];
\ No newline at end of file
index 7a095f5b7583f425ae3bd6a0c05be9fe4ee46894..9d449923ed7e7e231d22960a989a8afdeec00e18 100644 (file)
@@ -136,6 +136,11 @@ Hinweis: Benutzer können ihre E-Mail-Adresse nach erfolgreicher Registrierung 
     // Role Settings
     'roles' => 'Rollen',
     'role_user_roles' => 'Benutzer-Rollen',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Neue Rolle anlegen',
     'role_create_success' => 'Rolle erfolgreich angelegt',
     'role_delete' => 'Rolle löschen',
@@ -175,6 +180,7 @@ Hinweis: Benutzer können ihre E-Mail-Adresse nach erfolgreicher Registrierung 
 
     // Users
     'users' => 'Benutzer',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Benutzerprofil',
     'users_add_new' => 'Benutzer hinzufügen',
     'users_search' => 'Benutzer suchen',
@@ -244,6 +250,8 @@ Hinweis: Benutzer können ihre E-Mail-Adresse nach erfolgreicher Registrierung 
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Neuen Webhook erstellen',
     'webhooks_none_created' => 'Es wurden noch keine Webhooks erstellt.',
     'webhooks_edit' => 'Webhook bearbeiten',
index 9cff7961d0c102fa237c16802f1446a2ce2a2310..5e7dca202f76a1428178841b41f1fd501fcaa5db 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Leider konnte die für die Registrierung notwendige E-Mail zur Bestätigung Deiner E-Mail-Adresse nicht versandt werden. Bitte kontaktiere den Systemadministrator!',
     'email_confirm_success' => 'Ihre E-Mail wurde bestätigt! Sie sollten nun in der Lage sein, sich mit dieser E-Mail-Adresse anzumelden.',
     'email_confirm_resent' => 'Bestätigungs-E-Mail wurde erneut versendet, bitte überprüfe Deinen Posteingang.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt',
     'email_not_confirmed_text' => 'Deine E-Mail-Adresse ist bisher nicht bestätigt.',
index 5055c399ef033b4c4f10ba2b1893f76527760304..80ba2fb1b0560c141afc2ca905ff31886835106d 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Aktionen',
     'view' => 'Anzeigen',
     'view_all' => 'Alle anzeigen',
+    'new' => 'New',
     'create' => 'Anlegen',
     'update' => 'Aktualisieren',
     'edit' => 'Bearbeiten',
@@ -80,12 +81,14 @@ return [
     'none' => 'Keine',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Header-Menü erweitern',
     'profile_menu' => 'Profilmenü',
     'view_profile' => 'Profil ansehen',
     'edit_profile' => 'Profil bearbeiten',
     'dark_mode' => 'Dunkler Modus',
     'light_mode' => 'Heller Modus',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Info',
index f136579d8e2a3cb89f58b626ee7fd3f8c7e592f3..213380b7f8a588ed1bcac2e865dc10f5028654f8 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Anzuzeigender Text',
     'title' => 'Titel',
-    'open_link' => 'Link öffnen in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Aktuellem Fenster',
     'open_link_new' => 'Neuem Fenster',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Einklappbaren Block einfügen',
     'collapsible_unwrap' => 'Entfernen',
     'edit_label' => 'Beschriftung bearbeiten',
index 3d8842874244af8c9d90e50f35a3b7140612e066..f6a87363bd3b4fb326e36bf57b4bb7db428ae799 100644 (file)
@@ -42,14 +42,15 @@ return [
 
     // Permissions and restrictions
     'permissions' => 'Berechtigungen',
-    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',
-    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',
-    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',
+    'permissions_desc' => 'Legen Sie hier Berechtigungen fest, um die Standardberechtigungen von Benutzerrollen zu überschreiben.',
+    'permissions_book_cascade' => 'In Büchern festgelegte Berechtigungen werden automatisch in untergeordnete Kapitel und Seiten kaskadiert, es sei denn, sie haben eigene Berechtigungen definiert.',
+    'permissions_chapter_cascade' => 'In Kapiteln festgelegte Berechtigungen werden automatisch in untergeordnete Seiten kaskadiert, es sei denn, sie haben eigene Berechtigungen definiert.',
     'permissions_save' => 'Berechtigungen speichern',
     'permissions_owner' => 'Besitzer',
-    'permissions_role_everyone_else' => 'Everyone Else',
-    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
-    'permissions_role_override' => 'Override permissions for role',
+    'permissions_role_everyone_else' => 'Alle anderen',
+    'permissions_role_everyone_else_desc' => 'Berechtigungen für alle Rollen setzen, die nicht explizit überschrieben wurden.',
+    'permissions_role_override' => 'Berechtigungen für Rolle überschreiben',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Suchergebnisse',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Bild einfügen',
     'pages_md_insert_link' => 'Link zu einem Objekt einfügen',
     'pages_md_insert_drawing' => 'Zeichnung einfügen',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Seite ist in keinem Kapitel',
     'pages_move' => 'Seite verschieben',
     'pages_move_success' => 'Seite nach ":parentName" verschoben',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',
     'pages_revision' => 'Version',
     'pages_revisions' => 'Seitenversionen',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Seitenversionen von ":pageName"',
     'pages_revision_named' => 'Seitenversion von ":pageName"',
     'pages_revision_restored_from' => 'Wiederhergestellt von #:id; :summary',
     'pages_revisions_created_by' => 'Erstellt von',
     'pages_revisions_date' => 'Versionsdatum',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revision #:id',
     'pages_revisions_numbered_changes' => 'Revision #:id Änderungen',
     'pages_revisions_editor' => 'Editortyp',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Regal-Schlagwörter',
     'tag' => 'Schlagwort',
     'tags' =>  'Schlagwörter',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Schlagwort Name',
     'tag_value' => 'Inhalt (Optional)',
     'tags_explain' => "Füge Schlagwörter hinzu, um ihren Inhalt zu kategorisieren.\nDu kannst einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.",
diff --git a/resources/lang/de_informal/preferences.php b/resources/lang/de_informal/preferences.php
new file mode 100644 (file)
index 0000000..cc757aa
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Tastenkürzel',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Gemeinsame Aktionen',
+    'shortcuts_save' => 'Tastenkürzel speichern',
+    'shortcuts_overlay_desc' => 'Hinweis: Wenn Tastenkürzel aktiviert sind, ist ein Hilfsoverlay durch Drücken von "?" verfügbar, welches die verfügbaren Tastenkürzel für Aktionen hervorhebt, die aktuell auf dem Bildschirm sichtbar sind.',
+    'shortcuts_update_success' => 'Tastenkürzel Einstellungen wurden aktualisiert!',
+];
\ No newline at end of file
index fdcd31954997d86434aab1802e4fda0a5e079f46..a8f12fa0ea41bc55893bf0b85667b116e3a2392f 100644 (file)
@@ -136,6 +136,11 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
     // Role Settings
     'roles' => 'Rollen',
     'role_user_roles' => 'Benutzer-Rollen',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Neue Rolle anlegen',
     'role_create_success' => 'Rolle erfolgreich angelegt',
     'role_delete' => 'Rolle löschen',
@@ -175,6 +180,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
 
     // Users
     'users' => 'Benutzer',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Benutzerprofil',
     'users_add_new' => 'Benutzer hinzufügen',
     'users_search' => 'Benutzer suchen',
@@ -244,6 +250,8 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Neuen Webhook erstellen',
     'webhooks_none_created' => 'Es wurden noch keine Webhooks erstellt.',
     'webhooks_edit' => 'Webhook bearbeiten',
index a4cd8e02371b9a63c12c851efca93091da937cec..6f1e61db20308a2ea427055aa373166cfd1b4070 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Απαιτείται επιβεβαίωση μέσω email, αλλά το σύστημα δεν μπόρεσε να στείλει το email. Επικοινωνήστε με τον διαχειριστή για να βεβαιωθείτε ότι το email έχει ρυθμιστεί σωστά.',
     'email_confirm_success' => 'Το email σας επιβεβαιώθηκε! Θα πρέπει τώρα να μπορείτε να συνδεθείτε χρησιμοποιώντας αυτήν τη διεύθυνση email.',
     'email_confirm_resent' => 'Το email επιβεβαίωσης στάλθηκε εκ νέου. Ελέγξτε τα εισερχόμενά σας.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Η διεύθυνση email δεν επιβεβαιώθηκε',
     'email_not_confirmed_text' => 'Η διεύθυνση email σας δεν έχει ακόμη επιβεβαιωθεί.',
index 9837e53f0681ee97c3345f8ebbcc571d05f7b481..02be7a661d097a6daf8e1773aaccb5cfacd5fda2 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Ενέργειες',
     'view' => 'Προβολή',
     'view_all' => 'Προβολή όλων',
+    'new' => 'New',
     'create' => 'Δημιουργία',
     'update' => 'Ενημέρωση',
     'edit' => 'Επεξεργασία',
@@ -80,12 +81,14 @@ return [
     'none' => 'Κανένας',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Αναπτύξτε το Head Menu',
     'profile_menu' => 'Μενού Προφίλ',
     'view_profile' => 'Προβολή προφίλ',
     'edit_profile' => 'Επεξεργασία προφίλ',
     'dark_mode' => 'Σκουρόχρωμη εμφάνιση',
     'light_mode' => 'Ανοιχτόχρωμη εμφάνιση',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Πληροφορίες',
index 0f9489e8b1b319a8c2c8603aebb24000d5867c72..596a63b52ae2f876ab39688a368c2bb7dc16a090 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Κείμενο εμφάνισης',
     'title' => 'Τίτλος',
-    'open_link' => 'Άνοιγμα συνδέσμου σε...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Τρέχον παράθυρο',
     'open_link_new' => 'Νέο παράθυρο',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Εισαγωγή πτυσσόμενου μπλοκ',
     'collapsible_unwrap' => 'Μετατροπή πτυσσόμενου μπλοκ σε παράγραφο',
     'edit_label' => 'Επεξεργασία ετικέτας',
index f9f4aef70a3c9803fc09b754900ca1289a137c28..681fefda0b846b7ab409ac0ddaaf438340d3de28 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Αποτελέσματα αναζήτησης',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Εισαγωγή Εικόνας',
     'pages_md_insert_link' => 'Εισαγωγή/Επεξεργασία συνδέσμου',
     'pages_md_insert_drawing' => 'Εισαγωγή Σχεδίου',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Η σελίδα δεν είναι σε κεφάλαιο',
     'pages_move' => 'Μετακίνηση Σελίδας',
     'pages_move_success' => 'Η σελίδα μετακινήθηκε στο ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Τα δικαιώματα σελίδας ενημερώθηκαν',
     'pages_revision' => 'Αναθεώρηση',
     'pages_revisions' => 'Αναθεωρήσεις Σελίδας',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Αναθεωρήσεις σελίδας για :pageName',
     'pages_revision_named' => 'Αναθεώρηση σελίδας για :pageName',
     'pages_revision_restored_from' => 'Επαναφορά από #:id; :summary',
     'pages_revisions_created_by' => 'Δημιουργήθηκε από',
     'pages_revisions_date' => 'Ημερομηνία Αναθεώρησης',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Αναθεώρηση #',
     'pages_revisions_numbered_changes' => 'Αναθεώρηση #:id Αλλαγές',
     'pages_revisions_editor' => 'Τύπος Επεξεργαστή',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Ετικέτες Ραφιών',
     'tag' => 'Ετικέτα',
     'tags' =>  'Ετικέτες',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Όνομα Ετικέτας',
     'tag_value' => 'Τιμή Ετικέτας (Προαιρετικό)',
     'tags_explain' => "Προσθέστε μερικές ετικέτες για να κατηγοριοποιήσετε καλύτερα το περιεχόμενό σας. \n Μπορείτε να αντιστοιχίσετε μια τιμή σε μια ετικέτα για πιο αναλυτική οργάνωση.",
diff --git a/resources/lang/el/preferences.php b/resources/lang/el/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 7c5f571b6ad678d63a34f5d06d3597a40d46e316..e49f57aecb7ecb09c017d1ef6991531bee366865 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Ρόλοι',
     'role_user_roles' => 'Ρόλοι Χρηστών',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Δημιουργία νέου ρόλου',
     'role_create_success' => 'Ο Ρόλος δημιουργήθηκε με επιτυχία',
     'role_delete' => 'Διαγραφή Ρόλου',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Χρήστες',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Προφίλ Χρήστη',
     'users_add_new' => 'Προσθήκη νέου Χρήστη',
     'users_search' => 'Αναζήτηση Χρηστών',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Δημιουργία νέου Webhook',
     'webhooks_none_created' => 'Δεν έχουν δημιουργηθεί ακόμη webhook.',
     'webhooks_edit' => 'Επεξεργασία Webhook',
index c670106f9b64d79f28c666e8122e077e6b0f355d..dc4b242a09ee68f5e0411e3500f59e00c5d78ea4 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Email Address Not Confirmed',
     'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',
index 703a70c7e6aa5bea2b59f7319d4739c5f7162c13..c74dcc90775219416dfe1e56cd0c2d9c577c6f4d 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Actions',
     'view' => 'View',
     'view_all' => 'View All',
+    'new' => 'New',
     'create' => 'Create',
     'update' => 'Update',
     'edit' => 'Edit',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Expand Header Menu',
     'profile_menu' => 'Profile Menu',
     'view_profile' => 'View Profile',
     'edit_profile' => 'Edit Profile',
     'dark_mode' => 'Dark Mode',
     'light_mode' => 'Light Mode',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Info',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index bf6201900fc3653724e85d715411695325b6fd0a..fa2586f8d75cab171ec753065eaf73f4efcafe5d 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Search Results',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Insert Image',
     'pages_md_insert_link' => 'Insert Entity Link',
     'pages_md_insert_drawing' => 'Insert Drawing',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Page is not in a chapter',
     'pages_move' => 'Move Page',
     'pages_move_success' => 'Page moved to ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Page permissions updated',
     'pages_revision' => 'Revision',
     'pages_revisions' => 'Page Revisions',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Page Revisions for :pageName',
     'pages_revision_named' => 'Page Revision for :pageName',
     'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Created By',
     'pages_revisions_date' => 'Revision Date',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revision #:id',
     'pages_revisions_numbered_changes' => 'Revision #:id Changes',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Shelf Tags',
     'tag' => 'Tag',
     'tags' =>  'Tags',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Tag Name',
     'tag_value' => 'Tag Value (Optional)',
     'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
diff --git a/resources/lang/en/preferences.php b/resources/lang/en/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 1ad271e7c9ebd736f1d283bc2884b756294ea56d..f4204dd68bf05858a70205983d8aa35204366efa 100755 (executable)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roles',
     'role_user_roles' => 'User Roles',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Create New Role',
     'role_create_success' => 'Role successfully created',
     'role_delete' => 'Delete Role',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Users',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'User Profile',
     'users_add_new' => 'Add New User',
     'users_search' => 'Search Users',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 093b7e02a0bd203eac03f8fe16fcdf21546d06c9..63383f1ab9353fb4cd0d4f8215a9f075eabbc90e 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Confirmation de correo electrónico requerida pero el sistema no pudo enviar el correo. Contacte con el administrador para asegurarse de que el correo electrónico está configurado correctamente.',
     'email_confirm_success' => '¡Tu correo electrónico ha sido confirmado! Ahora deberías poder iniciar sesión usando esta dirección de correo electrónico.',
     'email_confirm_resent' => 'correo electrónico de confirmación reenviado, compruebe su bandeja de entrada.',
+    'email_confirm_thanks' => '¡Gracias por confirmar!',
+    'email_confirm_thanks_desc' => 'Por favor, espere un momento mientras se gestiona su confirmación. Si no es redirigido después de 3 segundos, pulse el enlace "Continuar" para continuar.',
 
     'email_not_confirmed' => 'Dirección de Correo Electrónico no confirmada',
     'email_not_confirmed_text' => 'Su Cuenta de Correo electrónico todavía no ha sido confirmada.',
@@ -69,12 +71,12 @@ return [
     'email_not_confirmed_resend_button' => 'Reenviar Correo Electrónico de confirmación',
 
     // User Invite
-    'user_invite_email_subject' => 'As sido invitado a unirte a :appName!',
+    'user_invite_email_subject' => '¡Has sido invitado a unirte a :appName!',
     'user_invite_email_greeting' => 'Se ha creado una cuenta para usted en :appName.',
-    'user_invite_email_text' => 'Clica en el botón a continuación para ajustar una contraseña y poder acceder:',
-    'user_invite_email_action' => 'Ajustar la Contraseña de la Cuenta',
+    'user_invite_email_text' => 'Haga clic en el botón de abajo para establecer una contraseña de cuenta y obtener acceso:',
+    'user_invite_email_action' => 'Establecer contraseña de la cuenta',
     'user_invite_page_welcome' => '¡Bienvenido a :appName!',
-    'user_invite_page_text' => 'Para completar la cuenta y tener acceso es necesario que configure una contraseña que se utilizará para entrar en :appName en futuros accesos.',
+    'user_invite_page_text' => 'Para finalizar tu cuenta y obtener acceso necesitas establecer una contraseña que se utilizará para iniciar sesión en :appName en futuras visitas.',
     'user_invite_page_confirm_button' => 'Confirmar Contraseña',
     'user_invite_success_login' => 'Contraseña guardada, ¡ahora debería ser capaz de iniciar sesión usando su contraseña establecida para acceder a :appName!',
 
index a58507bc62c6b0939f6059246151e386c8c15fab..c80a856cfb7c58419f9ca898495195802de241ca 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Acciones',
     'view' => 'Ver',
     'view_all' => 'Ver todos',
+    'new' => 'Nuevo',
     'create' => 'Crear',
     'update' => 'Actualizar',
     'edit' => 'Editar',
@@ -80,12 +81,14 @@ return [
     'none' => 'Ninguno',
 
     // Header
+    'homepage' => 'Página de Inicio',
     'header_menu_expand' => 'Expandir el Menú de la Cabecera',
     'profile_menu' => 'Menú de Perfil',
     'view_profile' => 'Ver Perfil',
     'edit_profile' => 'Editar Perfil',
     'dark_mode' => 'Modo Oscuro',
     'light_mode' => 'Modo Claro',
+    'global_search' => 'Búsqueda Global',
 
     // Layout tabs
     'tab_info' => 'Información',
index ed77927f489d89175873f1ff3b1dde46cda29471..1284cad6187217f3240714b53ff5f8ad78428589 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Texto para mostrar',
     'title' => 'Titulo',
-    'open_link' => 'Abrir enlace en...',
+    'open_link' => 'Abrir enlace',
+    'open_link_in' => 'Abrir enlace en...',
     'open_link_current' => 'Ventana actual',
     'open_link_new' => 'Nueva ventana',
+    'remove_link' => 'Eliminar enlace',
     'insert_collapsible' => 'Insertar bloque plegable',
     'collapsible_unwrap' => 'Desplegar',
     'edit_label' => 'Editar etiqueta',
index 2887e2cc63efce53a617be16538d6d247b915b8e..a149dc3e32850cecafaf221f4fd1539ab0dbc461 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Todos los demás',
     'permissions_role_everyone_else_desc' => 'Establecer permisos para todos los roles sin permisos específicos asignados.',
     'permissions_role_override' => 'Reemplazar permisos para el rol',
+    'permissions_inherit_defaults' => 'Heredar valores por defecto',
 
     // Search
     'search_results' => 'Resultados de búsqueda',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Insertar Imagen',
     'pages_md_insert_link' => 'Insertar link de entidad',
     'pages_md_insert_drawing' => 'Insertar Dibujo',
+    'pages_md_show_preview' => 'Mostrar vista previa',
+    'pages_md_sync_scroll' => 'Sincronizar desplazamiento de vista previa',
     'pages_not_in_chapter' => 'La página no está en un capítulo',
     'pages_move' => 'Mover página',
     'pages_move_success' => 'Página movida a ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Permisos de página actualizados',
     'pages_revision' => 'Revisión',
     'pages_revisions' => 'Revisiones de página',
+    'pages_revisions_desc' => 'A continuación se listan todas las revisiones pasadas de esta página. Puede volver la vista atrás, comparar y restaurar versiones antiguas de la página si los permisos lo permiten. Es posible que el historial completo de la página no se refleje en esta sección. Dependiendo de la configuración del sistema, las viejas revisiones podrían ser eliminadas automáticamente.',
     'pages_revisions_named' => 'Revisiones de página para :pageName',
     'pages_revision_named' => 'Revisión de página para :pageName',
     'pages_revision_restored_from' => 'Restaurado de #:id; :summary',
     'pages_revisions_created_by' => 'Creado por',
     'pages_revisions_date' => 'Fecha de revisión',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Número de Revisión',
     'pages_revisions_numbered' => 'Revisión #:id',
     'pages_revisions_numbered_changes' => 'Revisión #:id Cambios',
     'pages_revisions_editor' => 'Tipo de Editor',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Etiquetas de Estante',
     'tag' => 'Etiqueta',
     'tags' =>  'Etiquetas',
+    'tags_index_desc' => 'Las etiquetas se pueden aplicar al contenido dentro del sistema para aplicar una forma flexible de categorización. Las etiquetas pueden tener tanto una clave como un valor, siendo el valor opcional. Una vez aplicado, el contenido puede ser consultado usando el nombre y el valor de la etiqueta.',
     'tag_name' =>  'Nombre de la Etiqueta',
     'tag_value' => 'Valor de la etiqueta (Opcional)',
     'tags_explain' => "Agrege algunas etiquetas para mejorar la categorización de su contenido. \n Puede asignar un valor a una etiqueta para una organización a mayor detalle.",
diff --git a/resources/lang/es/preferences.php b/resources/lang/es/preferences.php
new file mode 100644 (file)
index 0000000..3735c5f
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Accesos rápidos',
+    'shortcuts_interface' => 'Atajos del Teclado de la Interfaz',
+    'shortcuts_toggle_desc' => 'Aquí puede activar o desactivar los accesos rápidos de la interfaz, utilizados para la navegación y las acciones.',
+    'shortcuts_customize_desc' => 'Puede personalizar cada uno de los accesos directos a continuación. Simplemente pulse la combinación de teclas deseada después de seleccionar la entrada para un acceso directo.',
+    'shortcuts_toggle_label' => 'Accesos directos habilitados',
+    'shortcuts_section_navigation' => 'Navegación',
+    'shortcuts_section_actions' => 'Acciones comunes',
+    'shortcuts_save' => 'Guardar atajos',
+    'shortcuts_overlay_desc' => 'Nota: Cuando se activan los accesos directos se puede mostrar la ayuda presionando la tecla "?" que resaltará los accesos rápidos disponibles para las acciones actualmente visibles en la pantalla.',
+    'shortcuts_update_success' => '¡Las preferencias de accesos rápidos han sido actualizadas!',
+];
\ No newline at end of file
index ba994e156e8756e98f6f93bb8263a3ce4f4148e6..7bb6e2f19f57ce9d21ebd410e420af56e594215f 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roles',
     'role_user_roles' => 'Roles de usuario',
+    'roles_index_desc' => 'Los roles se utilizan para agrupar usuarios y proporcionar permisos del sistema a sus miembros. Cuando un usuario es miembro de múltiples roles los privilegios otorgados se acumularán y el usuario heredará todas las habilidades.',
+    'roles_x_users_assigned' => '1 usuario asignado|:count usuarios asignados',
+    'roles_x_permissions_provided' => '1 permiso|:count permisos',
+    'roles_assigned_users' => 'Usuarios Asignados',
+    'roles_permissions_provided' => 'Permisos Proporcionados',
     'role_create' => 'Crear nuevo rol',
     'role_create_success' => 'Rol creado satisfactoriamente',
     'role_delete' => 'Borrar rol',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Usuarios',
+    'users_index_desc' => 'Crear y administrar cuentas de usuario individuales dentro del sistema. Las cuentas de usuario se utilizan para el inicio de sesión y atribución de contenido y actividad. Los permisos de acceso se basan principalmente en roles, pero la propiedad del contenido del usuario, entre otros factores, también puede afectar a los permisos y el acceso.',
     'user_profile' => 'Perfil de Usuario',
     'users_add_new' => 'Agregar Nuevo Usuario',
     'users_search' => 'Buscar usuarios',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Los Webhooks son una forma de enviar datos a URLs externas cuando ciertas acciones y eventos ocurren dentro del sistema, lo que permite la integración basada en eventos con plataformas externas como mensajería o sistemas de notificación.',
+    'webhooks_x_trigger_events' => '1 evento|:count eventos',
     'webhooks_create' => 'Crear Webhook',
     'webhooks_none_created' => 'No hay webhooks creados.',
     'webhooks_edit' => 'Editar Webhook',
index 2351be129d8aa89db827b33af23943681c067b03..19b86f67961ab6f207e82c35ec61d9f2bdae56d8 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Se pidió confirmación de correo electrónico pero el sistema no pudo enviar el correo electrónico. Contacte al administrador para asegurarse que el correo electrónico está configurado correctamente.',
     'email_confirm_success' => '¡Su correo electrónico ha sido confirmado! Ahora debería poder iniciar sesión usando esta dirección de correo electrónico.',
     'email_confirm_resent' => 'Correo electrónico de confirmación reenviado, Por favor verifique su bandeja de entrada.',
+    'email_confirm_thanks' => '¡Gracias por confirmar!',
+    'email_confirm_thanks_desc' => 'Por favor, espere un momento mientras se gestiona su confirmación. Si no se lo redirige después de 3 segundos, pulse el enlace "Continuar" para seguir.',
 
     'email_not_confirmed' => 'Dirección de correo electrónico no confirmada',
     'email_not_confirmed_text' => 'Su cuenta de correo electrónico todavía no ha sido confirmada.',
index 8c11712ae0e71da5767f7835c1cffb642d1948c3..9815db28f9c4fa10c744c2c079c285efbe840b89 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Acciones',
     'view' => 'Ver',
     'view_all' => 'Ver todo',
+    'new' => 'Nuevo',
     'create' => 'Crear',
     'update' => 'Actualizar',
     'edit' => 'Editar',
@@ -80,12 +81,14 @@ return [
     'none' => 'Ninguno',
 
     // Header
+    'homepage' => 'Página de Inicio',
     'header_menu_expand' => 'Expandir el Menú de Cabecera',
     'profile_menu' => 'Menu del Perfil',
     'view_profile' => 'Ver Perfil',
     'edit_profile' => 'Editar Perfil',
     'dark_mode' => 'Modo Oscuro',
     'light_mode' => 'Modo Claro',
+    'global_search' => 'Búsqueda Global',
 
     // Layout tabs
     'tab_info' => 'Información',
index d1acc03905af69d38e6e6c9450797fdf10413fce..df453f2f77107da97415d83bfbf8659c909352db 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Texto a mostrar',
     'title' => 'Título',
-    'open_link' => 'Abrir enlace en...',
+    'open_link' => 'Abrir enlace',
+    'open_link_in' => 'Abrir enlace en...',
     'open_link_current' => 'Ventana actual',
     'open_link_new' => 'Ventana nueva',
+    'remove_link' => 'Eliminar enlace',
     'insert_collapsible' => 'Insertar bloque desplegable',
     'collapsible_unwrap' => 'Desplegar',
     'edit_label' => 'Editar etiqueta',
index 828b7db602b3802e551428b5f61630d97bfb69b3..943297e5f7dea34a0ead121011eda1c7683e652e 100644 (file)
@@ -42,14 +42,15 @@ return [
 
     // Permissions and restrictions
     'permissions' => 'Permisos',
-    'permissions_desc' => 'Establezca los permisos aquí para anular los permisos por defecto proporcionados por los roles de usuario.',
-    'permissions_book_cascade' => 'Los permisos establecidos en los libros se aplicarán a sus capítulos y páginas, a menos que tengan sus propios permisos definidos.',
-    'permissions_chapter_cascade' => 'Los permisos establecidos en los capítulos se aplicarán a sus páginas, a menos que tengan sus propios permisos definidos.',
+    'permissions_desc' => 'Establezca los permisos aquí para reemplazar los permisos por defecto proporcionados por los roles de usuario.',
+    'permissions_book_cascade' => 'Los permisos establecidos en los libros se aplicarán a los capítulos contenidos y las páginas contenidas, a menos que tengan sus propios permisos definidos.',
+    'permissions_chapter_cascade' => 'Los permisos establecidos en los capítulos se aplicarán a las páginas contenidas, a menos que tengan sus propios permisos definidos.',
     'permissions_save' => 'Guardar permisos',
     'permissions_owner' => 'Propietario',
     'permissions_role_everyone_else' => 'Todos los demás',
-    'permissions_role_everyone_else_desc' => 'Establecer permisos para todos los roles sin permisos específicos asignados.',
+    'permissions_role_everyone_else_desc' => 'Establecer permisos para todos los roles no específicamente reemplazados.',
     'permissions_role_override' => 'Reemplazar permisos para el rol',
+    'permissions_inherit_defaults' => 'Heredar valores por defecto',
 
     // Search
     'search_results' => 'Buscar resultados',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Insertar Imagen',
     'pages_md_insert_link' => 'Insertar link de entidad',
     'pages_md_insert_drawing' => 'Insertar Dibujo',
+    'pages_md_show_preview' => 'Mostrar vista previa',
+    'pages_md_sync_scroll' => 'Sincronizar desplazamiento de vista previa',
     'pages_not_in_chapter' => 'La página no esá en el capítulo',
     'pages_move' => 'Mover página',
     'pages_move_success' => 'Página movida a ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Permisos de página actualizados',
     'pages_revision' => 'Revisión',
     'pages_revisions' => 'Revisiones de página',
+    'pages_revisions_desc' => 'A continuación se listan todas las revisiones anteriores de esta página. Puede volver la vista atrás, comparar y restaurar versiones antiguas de la página si los permisos lo permiten. Es posible que el historial completo de la página no se refleje en esta sección. Dependiendo de la configuración del sistema, las revisiones viejas podrían eliminarse automáticamente.',
     'pages_revisions_named' => 'Revisiones de página para :pageName',
     'pages_revision_named' => 'Revisión de ágina para :pageName',
     'pages_revision_restored_from' => 'Restaurado desde #:id; :summary',
     'pages_revisions_created_by' => 'Creado por',
     'pages_revisions_date' => 'Fecha de revisión',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Número de Revisión',
     'pages_revisions_numbered' => 'Revisión #:id',
     'pages_revisions_numbered_changes' => 'Cambios de Revisión #:id',
     'pages_revisions_editor' => 'Tipo de Editor',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Etiquetas de Estante',
     'tag' => 'Etiqueta',
     'tags' =>  'Etiquetas',
+    'tags_index_desc' => 'Las etiquetas se pueden aplicar al contenido dentro del sistema para lograr una forma flexible de categorización. Las etiquetas pueden tener tanto una clave como un valor, siendo el valor opcional. Una vez aplicado, el contenido puede ser consultado usando el nombre y el valor de la etiqueta.',
     'tag_name' =>  'Nombre de etiqueta',
     'tag_value' => 'Valor de la etiqueta (Opcional)',
     'tags_explain' => "Agregar algunas etiquetas para mejorar la categorización de su contenido. \n Se puede asignar un valor a una etiqueta para una organizacón con mayor detalle.",
diff --git a/resources/lang/es_AR/preferences.php b/resources/lang/es_AR/preferences.php
new file mode 100644 (file)
index 0000000..6c12454
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Atajos',
+    'shortcuts_interface' => 'Atajos del Teclado para la Interfaz',
+    'shortcuts_toggle_desc' => 'Aquí puede activar o desactivar los accesos rápidos de la interfaz, utilizados para la navegación y las acciones.',
+    'shortcuts_customize_desc' => 'Puede personalizar cada uno de los atajos a continuación. Simplemente pulse la combinación de teclas deseada después de seleccionar la entrada para un atajo.',
+    'shortcuts_toggle_label' => 'Atajos de teclado habilitados',
+    'shortcuts_section_navigation' => 'Navegación',
+    'shortcuts_section_actions' => 'Acciones comunes',
+    'shortcuts_save' => 'Guardar atajos',
+    'shortcuts_overlay_desc' => 'Nota: Cuando se activan los atajos de teclado se puede visualizar la ayuda presionando la tecla "?", que resaltará los atajos disponibles para las acciones visibles actualmente en la pantalla.',
+    'shortcuts_update_success' => '¡Se actualizaron las preferencias de atajos de teclado!',
+];
\ No newline at end of file
index 03e1f0308a8da54823c2f2d68fb5877ca7fad761..2f4c423991283fef43149fa51b2de323d1c7d1fc 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roles',
     'role_user_roles' => 'Roles de usuario',
+    'roles_index_desc' => 'Los roles se utilizan para agrupar usuarios y proporcionar permisos del sistema a sus miembros. Cuando un usuario es miembro de múltiples roles los privilegios otorgados se acumularán y el usuario heredará todas las habilidades.',
+    'roles_x_users_assigned' => '1 usuario assigned|:count usuarios asignados',
+    'roles_x_permissions_provided' => '1 permission|:count permisos',
+    'roles_assigned_users' => 'Usuarios Asignados',
+    'roles_permissions_provided' => 'Permisos Proporcionados',
     'role_create' => 'Crear nuevo rol',
     'role_create_success' => 'Rol creado satisfactoriamente',
     'role_delete' => 'Borrar rol',
@@ -173,6 +178,7 @@ return [
 
     // Users
     'users' => 'Usuarios',
+    'users_index_desc' => 'Crear y administrar cuentas de usuario individuales dentro del sistema. Las cuentas de usuario se utilizan para el inicio de sesión y atribución de contenido y actividad. Los permisos de acceso se basan principalmente en roles, pero la propiedad del contenido del usuario, entre otros factores, también puede afectar a los permisos y el acceso.',
     'user_profile' => 'Perfil de usuario',
     'users_add_new' => 'Agregar nuevo usuario',
     'users_search' => 'Buscar usuarios',
@@ -242,6 +248,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Los Webhooks son una forma de enviar datos a URLs externas cuando ciertas acciones y eventos ocurren dentro del sistema, lo que permite la integración basada en eventos con plataformas externas como mensajería o sistemas de notificación.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count evento desencadenante',
     'webhooks_create' => 'Crear nuevo Webhook',
     'webhooks_none_created' => 'No hay webhooks creados.',
     'webhooks_edit' => 'Editar Webhook',
index b8fb471a8224e4a55e69b42ae84b12043b592bb6..74e19a230ad0f4be17cf01a36de88b94976caa8e 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'E-posti aadressi kinnitamine on vajalik, aga e-kirja saatmine ebaõnnestus. Võta ühendust administraatoriga.',
     'email_confirm_success' => 'E-posti aadress on kinnitatud! Nüüd saad selle aadressiga sisse logida.',
     'email_confirm_resent' => 'Kinnituskiri on saadetud, vaata oma postkasti.',
+    'email_confirm_thanks' => 'Aitäh, et kinnitasid!',
+    'email_confirm_thanks_desc' => 'Palun oota hetke, kuni kinnitust töödeldakse. Kui sind 3 sekundi jooksul ümber ei suunata, klõpsa jätkamiseks allpool "Jätka" linki.',
 
     'email_not_confirmed' => 'E-posti aadress ei ole kinnitatud',
     'email_not_confirmed_text' => 'Sinu e-posti aadress ei ole veel kinnitatud.',
index f0a0811527df4eeff29b31ad5dc994aef27c6e6f..f5f7f982fad51d4b6d60c0d5b2fd37e7dbfdaa5f 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Tegevused',
     'view' => 'Vaata',
     'view_all' => 'Vaata kõiki',
+    'new' => 'Uus',
     'create' => 'Lisa',
     'update' => 'Uuenda',
     'edit' => 'Muuda',
@@ -80,12 +81,14 @@ return [
     'none' => 'Puudub',
 
     // Header
+    'homepage' => 'Avaleht',
     'header_menu_expand' => 'Laienda päisemenüü',
     'profile_menu' => 'Profiilimenüü',
     'view_profile' => 'Vaata profiili',
     'edit_profile' => 'Muuda profiili',
     'dark_mode' => 'Tume režiim',
     'light_mode' => 'Hele režiim',
+    'global_search' => 'Globaalne otsing',
 
     // Layout tabs
     'tab_info' => 'Info',
index 996cbad900fa0f2b2fbd4b4df6730fa95845bafa..fecff9af9fe9404f6a7ba50c7b6718a4b6f14cb8 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Kuvatav tekst',
     'title' => 'Pealkiri',
-    'open_link' => 'Ava link...',
+    'open_link' => 'Ava link',
+    'open_link_in' => 'Ava link...',
     'open_link_current' => 'Samas aknas',
     'open_link_new' => 'Uues aknas',
+    'remove_link' => 'Eemalda link',
     'insert_collapsible' => 'Lisa kokkupandav plokk',
     'collapsible_unwrap' => 'Paki lahti',
     'edit_label' => 'Muuda silti',
index 2b94d214f236f3daa129a0e0dde7f34e919da627..d9a0ef7dc6f2d07a89958ec0fb35e339bc8a616b 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Kõik muud',
     'permissions_role_everyone_else_desc' => 'Sea õigused kõigile rollidele, mida pole üle kirjutatud.',
     'permissions_role_override' => 'Kirjuta rolli õigused üle',
+    'permissions_inherit_defaults' => 'Päri vaikimisi seaded',
 
     // Search
     'search_results' => 'Otsingutulemused',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Lisa pilt',
     'pages_md_insert_link' => 'Lisa viide',
     'pages_md_insert_drawing' => 'Lisa joonis',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Leht ei kuulu peatüki alla',
     'pages_move' => 'Liiguta leht',
     'pages_move_success' => 'Leht liigutatud ":parentName" alla',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Lehe õigused muudetud',
     'pages_revision' => 'Redaktsioon',
     'pages_revisions' => 'Lehe redaktsioonid',
+    'pages_revisions_desc' => 'Allpool on toodud kõik selle lehe eelmised redaktsioonid. Kui õigused lubavad, saad sa vanu redaktsioone vaadata, võrrelda ja taastada. Siin ei pruugi kajastuda lehe täielik ajalugu, kuna sõltuvalt süsteemi seadistusest võidakse vanu redaktsioone automaatselt kustutada.',
     'pages_revisions_named' => 'Lehe :pageName redaktsioonid',
     'pages_revision_named' => 'Lehe :pageName redaktsioon',
     'pages_revision_restored_from' => 'Taastatud redaktsioonist #:id; :summary',
     'pages_revisions_created_by' => 'Autor',
     'pages_revisions_date' => 'Redaktsiooni aeg',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Redaktsiooni number',
     'pages_revisions_numbered' => 'Redaktsioon #:id',
     'pages_revisions_numbered_changes' => 'Redaktsiooni #:id muudatused',
     'pages_revisions_editor' => 'Redaktori tüüp',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Riiuli sildid',
     'tag' => 'Silt',
     'tags' =>  'Sildid',
+    'tags_index_desc' => 'Silte saab kasutada süsteemis oleva sisu paindlikuks kategoriseerimiseks. Siltidel saab olla nii võti kui väärtus, millest viimane on valikuline. Pärast määramist saab sisu otsida sildi nime ja väärtuse kaudu.',
     'tag_name' =>  'Sildi nimi',
     'tag_value' => 'Sildi väärtus (valikuline)',
     'tags_explain' => "Lisa silte, et sisu paremini organiseerida.\nVeel täpsemaks organiseerimiseks saad siltidele väärtuseid määrata.",
diff --git a/resources/lang/et/preferences.php b/resources/lang/et/preferences.php
new file mode 100644 (file)
index 0000000..05ba032
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Kiirklahvid',
+    'shortcuts_interface' => 'Kasutajaliidese kiirklahvid',
+    'shortcuts_toggle_desc' => 'Siit saad sisse ja välja lülitada navigeerimiseks ja tegevusteks kasutatavad kiirklahvid.',
+    'shortcuts_customize_desc' => 'Allpool saad iga kiirklahvi kohandada. Pärast kiirklahvile vastava tekstivälja valimist vajuta lihtsalt soovitud klahvikombinatsiooni.',
+    'shortcuts_toggle_label' => 'Kiirklahvid sisse lülitatud',
+    'shortcuts_section_navigation' => 'Navigatsioon',
+    'shortcuts_section_actions' => 'Sagedased tegevused',
+    'shortcuts_save' => 'Salvesta kiirklahvid',
+    'shortcuts_overlay_desc' => 'Märkus: Kui kiirklahvid on sisse lülitatud, saab "?" vajutades kuvada abiinfo, mis märgib ära kõigi hetkel ekraanil nähtavate tegevuste kiirklahvid.',
+    'shortcuts_update_success' => 'Kiirklahvide eelistused on salvestatud!',
+];
\ No newline at end of file
index 4c7c1d7d9396879bac8b5b165b687a561588a8eb..0dd7551d792ad78125d2aa99fe31dfe9b597aedc 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Rollid',
     'role_user_roles' => 'Kasutaja rollid',
+    'roles_index_desc' => 'Rolle saab kasutada kasutajate grupeerimiseks ja liikmetele süsteemsete õiguste andmiseks. Kui kasutaja on mitme rolli liige, siis õigused kombineeritakse ning kasutaja saab kõik õigused.',
+    'roles_x_users_assigned' => '1 kasutaja|:count kasutajat',
+    'roles_x_permissions_provided' => '1 õigus|:count õigust',
+    'roles_assigned_users' => 'Määratud kasutajad',
+    'roles_permissions_provided' => 'Antud õigused',
     'role_create' => 'Lisa uus roll',
     'role_create_success' => 'Roll on lisatud',
     'role_delete' => 'Kustuta roll',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Kasutajad',
+    'users_index_desc' => 'Loo ja halda süsteemi kasutajakontosid. Kontosid kasutatakse sisselogimiseks ning sisu ja tegevuse omistamiseks. Ligipääsuload on enamasti rollipõhised, aga sisu omandus ja muud faktorid võivad samuti mõjutada õiguseid ja ligipääsu.',
     'user_profile' => 'Kasutajaprofiil',
     'users_add_new' => 'Lisa uus kasutaja',
     'users_search' => 'Otsi kasutajaid',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Veebihaagid',
+    'webhooks_index_desc' => 'Veebihaakide abil saab teatud süsteemis toimunud tegevuste ja sündmuste puhul saata andmeid välistele URL-idele, mis võimaldab integreerida väliseid platvorme, nagu sõnumi- või teavitussüsteemid.',
+    'webhooks_x_trigger_events' => '1 sündmus|:count sündmust',
     'webhooks_create' => 'Lisa uus veebihaak',
     'webhooks_none_created' => 'Ühtegi veebihaaki pole lisatud.',
     'webhooks_edit' => 'Muuda veebihaaki',
index 0ef81239cc6b69b8db867d51f5e08e25c480edbe..d1f37804b6d80d25186b97fd7efed7ddf6797d0e 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Posta elektronikoaren baieztapenak behar da, baina sistemak ezin izan du posta elektronikoa bidali. Administratzailearekin harremanetan jarri email ezarpenak ongi dauden baieztatzeko.',
     'email_confirm_success' => 'Zure posta elektronikoa baieztatu da! Helbide elektroniko hau erabili dezakezu saioa hasteko.',
     'email_confirm_resent' => 'Eragiketa baieztatzeko email bat bidali dizugu. Mesedez, begiratu zure posta elektronikoa.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Email helbidea ez da baieztatu',
     'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',
index 9ad8e8b796aa01209ba20cc3103512441d4bc9b6..e3c3a40fc000af21ae3726f1596df4c9c7922641 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Ekintzak',
     'view' => 'Ikusi',
     'view_all' => 'Ikusi denak',
+    'new' => 'New',
     'create' => 'Sortu',
     'update' => 'Eguneratu',
     'edit' => 'Editatu',
@@ -80,12 +81,14 @@ return [
     'none' => 'Bat ere ez',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Zabaldu goiburuko menua',
     'profile_menu' => 'Perfileko menua',
     'view_profile' => 'Ikusi profila',
     'edit_profile' => 'Editatu profila',
     'dark_mode' => 'Modu iluna',
     'light_mode' => 'Modu argia',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Info',
index f951d629249d866d4f56808835f70dea21c63461..6972b991eb279ee978f360245329261b8f1b7284 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index c7ea1ab5f3a01e6701ddaf96fb79cc3d85cd995e..f1d3121a6948a722d1d9f3e38a473622895c6bbc 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Bilaketaren emaitzak',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Txertatu irudia',
     'pages_md_insert_link' => 'Insert Entity Link',
     'pages_md_insert_drawing' => 'Txertatu marrazki berria',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Page is not in a chapter',
     'pages_move' => 'Move Page',
     'pages_move_success' => 'Page moved to ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Page permissions updated',
     'pages_revision' => 'Revision',
     'pages_revisions' => 'Page Revisions',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Page Revisions for :pageName',
     'pages_revision_named' => 'Page Revision for :pageName',
     'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Sortzailea',
     'pages_revisions_date' => 'Berrikuspen data',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revision #:id',
     'pages_revisions_numbered_changes' => 'Revision #:id Changes',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Apalategi etiketak',
     'tag' => 'Etiketa',
     'tags' =>  'Etiketak',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Etiketa izena',
     'tag_value' => 'Tag Value (Optional)',
     'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
diff --git a/resources/lang/eu/preferences.php b/resources/lang/eu/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 89d0516fd6450de11b7d0e7c9b45ef658a23fbdb..ef38d964e3f39139a7dc0513e6700c9a53bbe6fb 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Rolak',
     'role_user_roles' => 'Erabiltzailearen rola',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Rol berria sortu',
     'role_create_success' => 'Rola ondo sortu da',
     'role_delete' => 'Ezabatu Rol-a',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Erabiltzaileak',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Erabiltzailearen profila',
     'users_add_new' => 'Erabiltzaile berri bat gehitu',
     'users_search' => 'Erabiltzaileak bilatu',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index dac1f39abec3a3bc67bdccec1b79275602451f92..77ab33a2e83ea865552d57f1def1eb9f180f1a3f 100644 (file)
@@ -6,7 +6,7 @@
 return [
 
     // Pages
-    'page_create'                 => 'صÙ\81Ø­Ù\87 Ø§Û\8cجاد Ø´Ø¯Ù\87',
+    'page_create'                 => 'تارÛ\8cØ® Ø§Û\8cجاد',
     'page_create_notification'    => 'صفحه با موفقیت ایجاد شد',
     'page_update'                 => 'به روزرسانی صفحه',
     'page_update_notification'    => 'صفحه با موفقیت به روزرسانی شد',
@@ -29,7 +29,7 @@ return [
     'book_create'                 => 'ایجاد کتاب',
     'book_create_notification'    => 'کتاب با موفقیت ایجاد شد',
     'book_create_from_chapter'              => 'تبدیل فصل به کتاب',
-    'book_create_from_chapter_notification' => 'فصل با موفقیت به یک کتاب تبدیل شد',
+    'book_create_from_chapter_notification' => 'کتاب با موفقیت به یک قفسه تبدیل شد',
     'book_update'                 => 'به روزرسانی کتاب',
     'book_update_notification'    => 'کتاب با موفقیت به روزرسانی شد',
     'book_delete'                 => 'حذف کتاب',
index 54819a733773e62f3d7adee956de80c7656a9e01..4de8b58002dd8c3f97e0ab4929aa97f619a8fcea 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'تایید پست الکترونیک الزامی می باشد، اما سیستم قادر به ارسال پیام نمی باشد.',
     'email_confirm_success' => 'ایمیل شما تایید شد! اکنون باید بتوانید با استفاده از این آدرس ایمیل وارد شوید.',
     'email_confirm_resent' => 'پیام تایید پست الکترونیک مجدد ارسال گردید، لطفا صندوق ورودی خود را بررسی نمایید.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'پست الکترونیک تایید نشده است',
     'email_not_confirmed_text' => 'پست الکترونیک شما هنوز تایید نشده است.',
index ebea44a338a120134d8cf9f6710c406538f24d8c..7aec33d8bf7807b93cbff6d202f18d88a8f6f94e 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'عملیات',
     'view' => 'نمایش',
     'view_all' => 'نمایش همه',
+    'new' => 'New',
     'create' => 'ایجاد',
     'update' => 'به‌روز رسانی',
     'edit' => 'ويرايش',
@@ -80,12 +81,14 @@ return [
     'none' => 'هیچکدام',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'گسترش منو',
     'profile_menu' => 'منو پروفایل',
     'view_profile' => 'مشاهده پروفایل',
     'edit_profile' => 'ویرایش پروفایل',
     'dark_mode' => 'حالت تاریک',
     'light_mode' => 'حالت روشن',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'اطلاعات',
index e7071498c658580f42a85b812f0087958adf30e4..0e358c5e67e0522bb659f5a5721167f9d823f7b2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'آدرس',
     'text_to_display' => 'متن جهت نمایش',
     'title' => 'عنوان',
-    'open_link' => 'باز کردن لینک در ...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'پنجره کنونی',
     'open_link_new' => 'پنجره جدید',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'درج بلوک جمع شونده',
     'collapsible_unwrap' => 'باز کردن',
     'edit_label' => 'ویرایش برچسب',
index 416e868c56e386706143d288f23e03ed65b7db08..d829ee4b5a831e3efb1ba9ee58543097e90cc141 100644 (file)
@@ -42,14 +42,15 @@ return [
 
     // Permissions and restrictions
     'permissions' => 'مجوزها',
-    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',
-    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',
-    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',
+    'permissions_desc' => 'مجوزها را در اینجا تنظیم کنید تا مجوزهای پیش فرض تنظیم شده برای نقش های کاربر را لغو کنید.',
+    'permissions_book_cascade' => 'مجوزهای تنظیم‌شده روی کتاب‌ها به‌طور خودکار به فصل‌ها و صفحات داخل آن اختصاص داده می‌شوند، مگر اینکه مجوزهای اختصاصی برای آن‌ها (فصل‌ها و صفحات) تعریف شده باشد.',
+    'permissions_chapter_cascade' => 'مجوزهای تنظیم‌شده روی فصل‌ها به‌طور خودکار به صفحات داخل آن اختصاص داده می‌شوند، مگر اینکه مجوزهای اختصاصی برای آن‌ها (صفحات) تعریف شده باشد.',
     'permissions_save' => 'ذخيره مجوزها',
     'permissions_owner' => 'مالک',
-    'permissions_role_everyone_else' => 'Everyone Else',
-    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
-    'permissions_role_override' => 'Override permissions for role',
+    'permissions_role_everyone_else' => 'سایر کاربران',
+    'permissions_role_everyone_else_desc' => 'مجوزها را برای نقش‌هایی تنظیم کنید که به طور خاص لغو نشده‌اند.',
+    'permissions_role_override' => 'لغو مجوز برای نقش',
+    'permissions_inherit_defaults' => 'ارث بردن از مجوزهای پیش‌فرض',
 
     // Search
     'search_results' => 'نتایج جستجو',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'درج تصویر',
     'pages_md_insert_link' => 'پیوند نهاد را درج کنید',
     'pages_md_insert_drawing' => 'درج طرح',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'صفحه در یک فصل نیست',
     'pages_move' => 'انتقال صفحه',
     'pages_move_success' => 'صفحه به ":parentName" منتقل شد',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'مجوزهای صفحه به روز شد',
     'pages_revision' => 'تجدید نظر',
     'pages_revisions' => 'ویرایش های صفحه',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'بازبینی صفحه برای :pageName',
     'pages_revision_named' => 'ویرایش صفحه برای :pageName',
     'pages_revision_restored_from' => 'بازیابی شده از #:id; :summary',
     'pages_revisions_created_by' => 'ایجاد شده توسط',
     'pages_revisions_date' => 'تاریخ تجدید نظر',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'تجدید نظر #:id',
     'pages_revisions_numbered_changes' => 'بازبینی #:id تغییرات',
     'pages_revisions_editor' => 'نوع ویرایشگر',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'برچسب های قفسه',
     'tag' => 'برچسب',
     'tags' =>  'برچسب ها',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'نام برچسب',
     'tag_value' => 'مقدار برچسب (اختیاری)',
     'tags_explain' => "برای دسته بندی بهتر مطالب خود چند برچسب اضافه کنید.\nمی توانید برای سازماندهی عمیق‌تر، یک مقدار به یک برچسب اختصاص دهید.",
diff --git a/resources/lang/fa/preferences.php b/resources/lang/fa/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index f3ff49eee49cb635e5e6cfc4ca3bf54a98f035ae..7a990684f2258e5fddf2dac3d6c2d0bfb2fe7a7f 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'نقش ها',
     'role_user_roles' => 'نقش های کاربر',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'نقش جدید ایجاد کنید',
     'role_create_success' => 'نقش با موفقیت ایجاد شد',
     'role_delete' => 'حذف نقش',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'کاربران',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'پرونده کاربر',
     'users_add_new' => 'افزودن کاربر جدید',
     'users_search' => 'جستجوی کاربران',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'وب‌هوک‌ها',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'ایجاد وب هوک جدید',
     'webhooks_none_created' => 'هنوز هیچ وب هوکی ایجاد نشده است.',
     'webhooks_edit' => 'ویرایش وب هوک',
index b3d1aa8d2c255de1ab6a3781f66f60281ebe90e0..32b8c27ec1c2c2da1338472919c5d990108fa074 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'La confirmation par e-mail est requise mais le système n\'a pas pu envoyer l\'e-mail. Contactez l\'administrateur système.',
     'email_confirm_success' => 'Votre adresse e-mail a été confirmée ! Vous devriez maintenant pouvoir vous connecter en utilisant cette adresse e-mail.',
     'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de réception.',
+    'email_confirm_thanks' => 'Merci d\'avoir confirmé !',
+    'email_confirm_thanks_desc' => 'Veuillez patienter un moment pendant que votre confirmation est traitée. Si vous n’êtes pas redirigé après 3 secondes, appuyez sur le lien « Continuer » ci-dessous pour continuer.',
 
     'email_not_confirmed' => 'Adresse e-mail non confirmée',
     'email_not_confirmed_text' => 'Votre adresse e-mail n\'a pas été confirmée.',
index 1256f16e3ef48a2e94990b2f7eb858e6571f997e..486b78eb6fb8399ff6b37637b11fb7c59ddf2155 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Actions',
     'view' => 'Voir',
     'view_all' => 'Tout afficher',
+    'new' => 'Nouveau',
     'create' => 'Créer',
     'update' => 'Modifier',
     'edit' => 'Éditer',
@@ -80,12 +81,14 @@ return [
     'none' => 'Aucun',
 
     // Header
+    'homepage' => 'Accueil',
     'header_menu_expand' => 'Développer le menu',
     'profile_menu' => 'Menu du profil',
     'view_profile' => 'Voir le profil',
     'edit_profile' => 'Modifier le profil',
     'dark_mode' => 'Mode sombre',
     'light_mode' => 'Mode clair',
+    'global_search' => 'Recherche',
 
     // Layout tabs
     'tab_info' => 'Informations',
index 75416808873e0ec13157c93f1c1670482177c85b..f28a1740e296badd80c857619eef4d2514f11fcd 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Texte à afficher',
     'title' => 'Titre',
-    'open_link' => 'Ouvrir le lien dans...',
+    'open_link' => 'Ouvrir le lien',
+    'open_link_in' => 'Ouvrir le lien dans...',
     'open_link_current' => 'Fenêtre actuelle',
     'open_link_new' => 'Nouvelle fenêtre',
+    'remove_link' => 'Retirer le lien',
     'insert_collapsible' => 'Insérer un bloc repliable',
     'collapsible_unwrap' => 'Dérouler',
     'edit_label' => 'Modifier le libellé',
index e58c556818ffbc3f611304e059ce5e6cff69cbbf..2136f22e43b0aafe5bf7a0800f63f1e147564461 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Tous les autres',
     'permissions_role_everyone_else_desc' => 'Définir les permissions pour tous les rôles qui ne sont pas spécifiquement remplacés.',
     'permissions_role_override' => 'Remplacer les permissions pour le rôle',
+    'permissions_inherit_defaults' => 'Hériter les valeurs par défaut',
 
     // Search
     'search_results' => 'Résultats de recherche',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Insérer une image',
     'pages_md_insert_link' => 'Insérer un lien',
     'pages_md_insert_drawing' => 'Insérer un dessin',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'La page n\'est pas dans un chapitre',
     'pages_move' => 'Déplacer la page',
     'pages_move_success' => 'Page déplacée à ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Permissions de la page mises à jour',
     'pages_revision' => 'Révision',
     'pages_revisions' => 'Révisions de la page',
+    'pages_revisions_desc' => 'Vous trouverez sur la page ci-dessous toutes les anciennes révisions. Vous pouvez regarder, comparer et restaurer les anciennes versions de page si les autorisations le permettent. L’historique complet de la page peut ne pas être entièrement affiché ici, car selon la configuration du système, les anciennes révisions peuvent être supprimées automatiquement.',
     'pages_revisions_named' => 'Révisions pour :pageName',
     'pages_revision_named' => 'Révision pour :pageName',
     'pages_revision_restored_from' => 'Restauré à partir de #:id; :summary',
     'pages_revisions_created_by' => 'Créé par',
     'pages_revisions_date' => 'Date de révision',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Numéro de révision',
     'pages_revisions_numbered' => 'Révision #:id',
     'pages_revisions_numbered_changes' => 'Modification #:id',
     'pages_revisions_editor' => 'Type d\'éditeur',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Mots-clés de l\'étagère',
     'tag' => 'Mot-clé',
     'tags' =>  'Mots-clés',
+    'tags_index_desc' => 'Les tags peuvent être appliqués au contenu du système pour appliquer une forme flexible de catégorisation. Les tags peuvent avoir à la fois une clé et une valeur, la valeur étant facultative. Une fois appliqué, le contenu peut ensuite être interrogé à l’aide du nom et de la valeur du tag.',
     'tag_name' =>  'Nom du tag',
     'tag_value' => 'Valeur du mot-clé (optionnel)',
     'tags_explain' => "Ajouter des mots-clés pour catégoriser votre contenu.",
diff --git a/resources/lang/fr/preferences.php b/resources/lang/fr/preferences.php
new file mode 100644 (file)
index 0000000..4030ccb
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Raccourcis',
+    'shortcuts_interface' => 'Raccourcis clavier',
+    'shortcuts_toggle_desc' => 'Ici vous pouvez activer ou désactiver les raccourcis clavier, utilisés pour la navigation et les actions.',
+    'shortcuts_customize_desc' => 'Vous pouvez personnaliser chaque raccourci ci-dessous. Il vous suffit d\'appuyer sur la combinaison de touche choisie après avoir sélectionné l\'entrée pour un raccourci.',
+    'shortcuts_toggle_label' => 'Raccourcis clavier activés',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Actions communes',
+    'shortcuts_save' => 'Sauvegarder les raccourcis',
+    'shortcuts_overlay_desc' => 'Note : Lorsque les raccourcis sont activés, assistant est disponible en appuyant sur "?" qui mettra en surbrillance les raccourcis disponibles pour les actions actuellement visibles à l\'écran.',
+    'shortcuts_update_success' => 'Les préférences de raccourci ont été mises à jour !',
+];
\ No newline at end of file
index ebcb10f2b757ed6a101be6a13a9db0c9229f4dcd..7f7731c2b414e840fff7ee6963dd79b05f4dc5db 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Rôles',
     'role_user_roles' => 'Rôles des utilisateurs',
+    'roles_index_desc' => 'Les rôles sont utilisés pour regrouper les utilisateurs et fournir une autorisation système à leurs membres. Lorsqu\'un utilisateur est membre de plusieurs rôles, les privilèges accordés se cumulent et l\'utilisateur hérite de tous les droits d\'accès.',
+    'roles_x_users_assigned' => '1 utilisateur affecté| : nombre d\'affectés',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Créer un nouveau rôle',
     'role_create_success' => 'Rôle créé avec succès',
     'role_delete' => 'Supprimer le rôle',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Utilisateurs',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Profil d\'utilisateur',
     'users_add_new' => 'Ajouter un nouvel utilisateur',
     'users_search' => 'Rechercher les utilisateurs',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Créer un nouveau Webhook',
     'webhooks_none_created' => 'Aucun webhook n\'a encore été créé.',
     'webhooks_edit' => 'Éditer le Webhook',
index 3c42ecef231297cbf84b2bc586b18543fb02317d..50dc2e47068b791c0e8af870e7e2fc2c1dcb8e5c 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'נדרש אימות אי-מייל אך שליחת האי-מייל אליך נכשלה. יש ליצור קשר עם מנהל המערכת כדי לוודא שאכן ניתן לשלוח מיילים.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'אימות נשלח לאי-מייל שלך, יש לבדוק בתיבת הדואר הנכנס',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'כתובת המייל לא אומתה',
     'email_not_confirmed_text' => 'כתובת המייל שלך טרם אומתה',
index 512bcf6c3ce7e2883b78094e6e1c57706a7def00..50731d5d5d1ad0b7931928e7994fa8d82bf1474e 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'פעולות',
     'view' => 'הצג',
     'view_all' => 'הצג הכל',
+    'new' => 'New',
     'create' => 'צור',
     'update' => 'עדכן',
     'edit' => 'ערוך',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'הרחב תפריט',
     'profile_menu' => 'Profile Menu',
     'view_profile' => 'הצג פרופיל',
     'edit_profile' => 'ערוך פרופיל',
     'dark_mode' => 'מצב לילה',
     'light_mode' => 'מצב יום',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'מידע',
index 6184ec00a6a501f9773d5b74440520cfeb521eb0..c258466426208b429929e81d28c7adbcdb05a023 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'קישור',
     'text_to_display' => 'טקסט שיוצג',
     'title' => 'כותרת',
-    'open_link' => 'פתח קישור ב...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'החלון הנוכחי',
     'open_link_new' => 'חלון חדש',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'הכנס מקטע הניתן לכיווץ',
     'collapsible_unwrap' => 'בטל גלישת שורות',
     'edit_label' => 'עריכת תווית',
index 399a010273ef4aa414878d7ccacf0cf783865e69..7b7bae296b7776c632764270ed7de2e8230337e9 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'תוצאות חיפוש',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'הכנס תמונה',
     'pages_md_insert_link' => 'הכנס קישור ליישות',
     'pages_md_insert_drawing' => 'הכנס סרטוט',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'דף אינו חלק מפרק',
     'pages_move' => 'העבר דף',
     'pages_move_success' => 'הדף הועבר אל ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'הרשאות הדף עודכנו',
     'pages_revision' => 'נוסח',
     'pages_revisions' => 'נוסחי דף',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'נוסחי דף עבור :pageName',
     'pages_revision_named' => 'נוסח דף עבור :pageName',
     'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'נוצר על ידי',
     'pages_revisions_date' => 'תאריך נוסח',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'נוסח #:id',
     'pages_revisions_numbered_changes' => 'שינויי נוסח #:id',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'תגיות מדף',
     'tag' => 'תגית',
     'tags' =>  'תגיות',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'שם התווית',
     'tag_value' => 'ערך התגית (אופציונאלי)',
     'tags_explain' => "הכנס תגיות על מנת לסדר את התוכן שלך. \n  ניתן לציין ערך לתגית על מנת לבצע סידור יסודי יותר",
diff --git a/resources/lang/he/preferences.php b/resources/lang/he/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 073d2f79064f7e4b9d16335a82f4696af64641cb..4be533f0ca567886690d9530aad1c7c7c4182eb8 100755 (executable)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'תפקידים',
     'role_user_roles' => 'תפקידי משתמשים',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'צור תפקיד משתמש חדש',
     'role_create_success' => 'התפקיד נוצר בהצלחה',
     'role_delete' => 'מחק תפקיד',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'משתמשים',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'פרופיל משתמש',
     'users_add_new' => 'הוסף משתמש חדש',
     'users_search' => 'חפש משתמשים',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index bd7f2b06e19be430ff1f7a6b6a970e52b38b8e10..ca0b996610b295154d2309616d21a08e32aee5a9 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Potvrda e-mail adrese je obavezna, ali sustav ne može poslati e-mail. Javite se administratoru kako bi provjerio vaš e-mail.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'Ponovno je poslana potvrda. Molimo, provjerite svoj inbox.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'E-mail adresa nije potvrđena.',
     'email_not_confirmed_text' => 'Vaša e-mail adresa još nije potvrđena.',
index ab2d36f3ff720611c643696e8b9df592101c7ee9..1527c4c5af116693aece181b4a725e0720fd5fec 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Aktivnost',
     'view' => 'Pogled',
     'view_all' => 'Pogledaj sve',
+    'new' => 'New',
     'create' => 'Stvori',
     'update' => 'Ažuriraj',
     'edit' => 'Uredi',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Proširi izbornik',
     'profile_menu' => 'Profil',
     'view_profile' => 'Vidi profil',
     'edit_profile' => 'Uredite profil',
     'dark_mode' => 'Tamni način',
     'light_mode' => 'Svijetli način',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Info',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index 32718562e9a85f512d221cc90f6f54a2abe92b9e..cad1d50ddc847a6b76a948bc5ea790789bcfda61 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Pretraži rezultate',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Umetni sliku',
     'pages_md_insert_link' => 'Umetni poveznicu',
     'pages_md_insert_drawing' => 'Umetni crtež',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Stranica nije u poglavlju',
     'pages_move' => 'Premjesti stranicu',
     'pages_move_success' => 'Stranica premještena u ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Ažurirana dopuštenja stranice',
     'pages_revision' => 'Revizija',
     'pages_revisions' => 'Revizija stranice',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Revizije stranice :pageName',
     'pages_revision_named' => 'Revizija stranice :pageName',
     'pages_revision_restored_from' => 'Oporavak iz #:id; :summary',
     'pages_revisions_created_by' => 'Stvoreno od',
     'pages_revisions_date' => 'Datum revizije',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revizija #:id',
     'pages_revisions_numbered_changes' => 'Revizija #:id Promjene',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Oznake polica',
     'tag' => 'Oznaka',
     'tags' =>  'Tags',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Tag Name',
     'tag_value' => 'Oznaka vrijednosti (neobavezno)',
     'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
diff --git a/resources/lang/hr/preferences.php b/resources/lang/hr/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 6fdab220e68b637134c9c23154f9dedacbd912d6..c13a5317040cb50548da99d2b92759d3c9460d87 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Uloge',
     'role_user_roles' => 'Uloge korisnika',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Stvori novu ulogu',
     'role_create_success' => 'Uloga uspješno stvorena',
     'role_delete' => 'Izbriši ulogu',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Korisnici',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Profil korisnika',
     'users_add_new' => 'Dodajte novog korisnika',
     'users_search' => 'Pretražite korisnike',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 6eafdd56583793a65c1590d29ba64bbe1d3628c1..f7c30551fd9b6d86596688a17fb5fee1961c368f 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Az email megerősítés kötelező, de a rendszer nem tudta elküldeni az emailt. Fel kell venni a kapcsolatot az adminisztrátorral és meg kell győződni róla, hogy az email beállítások megfelelőek.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'Megerősítő email újraküldve. Ellenőrizni kell a bejövő üzeneteket.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Az email cím nincs megerősítve',
     'email_not_confirmed_text' => 'Az email cím még nincs megerősítve.',
index 156844d9f53882e6e387eb7d98335e3122a1b83f..07a76681f034e65cd90ed9da72457686be1edb9b 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Műveletek',
     'view' => 'Megtekintés',
     'view_all' => 'Összes megtekintése',
+    'new' => 'New',
     'create' => 'Létrehozás',
     'update' => 'Frissítés',
     'edit' => 'Szerkesztés',
@@ -80,12 +81,14 @@ return [
     'none' => 'Egyik sem',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Expand Header Menu',
     'profile_menu' => 'Profil menü',
     'view_profile' => 'Profil megtekintése',
     'edit_profile' => 'Profil szerkesztése',
     'dark_mode' => 'Sötét mód',
     'light_mode' => 'Világos mód',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Információ',
index 828dea6e11ce563ee9600c7db8301a953843e3a0..4662cf4559c98fca7c0d37c52099d6b33536ac7c 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Megjelenő szöveg',
     'title' => 'Cím',
-    'open_link' => 'Hivatkozás megnyitása...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Aktuális ablak',
     'open_link_new' => 'Új ablak',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Címke szerkesztése',
index 55c1524ed2ae80ec4bb8b29696975a6b5e3a3779..0c91cdcd3cac08966c21d727f57761144ee3a09a 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Keresési eredmények',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Kép beillesztése',
     'pages_md_insert_link' => 'Entitás hivatkozás beillesztése',
     'pages_md_insert_drawing' => 'Rajz beillesztése',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Az oldal nincs fejezetben',
     'pages_move' => 'Oldal áthelyezése',
     'pages_move_success' => 'Oldal áthelyezve ide: ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Oldal jogosultságok frissítve',
     'pages_revision' => 'Változat',
     'pages_revisions' => 'Oldal változatai',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => ':pageName oldal változatai',
     'pages_revision_named' => ':pageName oldal változata',
     'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Létrehozta:',
     'pages_revisions_date' => 'Változat dátuma',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Változat #:id',
     'pages_revisions_numbered_changes' => '#:id változat módosításai',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Polc címkék',
     'tag' => 'Címke',
     'tags' =>  'Címkék',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Címkenév',
     'tag_value' => 'Címke érték (nem kötelező)',
     'tags_explain' => "Címkék hozzáadása a tartalom jobb kategorizálásához.\nA mélyebb szervezettség megvalósításához hozzá lehet rendelni egy értéket a címkéhez.",
diff --git a/resources/lang/hu/preferences.php b/resources/lang/hu/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 1707dee4989d6393a078b8aa2b03e0c23fab9f1c..2014c2a9de29886209bb1d66fa2e314ed01c2fdd 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Szerepkörök',
     'role_user_roles' => 'Felhasználói szerepkörök',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Új szerepkör létrehozása',
     'role_create_success' => 'Szerepkör sikeresen létrehozva',
     'role_delete' => 'Szerepkör törlése',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Felhasználók',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Felhasználói profil',
     'users_add_new' => 'Új felhasználó hozzáadása',
     'users_search' => 'Felhasználók keresése',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 694d6754fb26a9c49028561ff29a95fa4483c8a4..bc871c4963c31ddb32a536281cf56355f129bf58 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Konfirmasi email diperlukan tetapi sistem tidak dapat mengirim email. Hubungi admin untuk memastikan email disiapkan dengan benar.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'Email konfirmasi dikirim ulang, Harap periksa kotak masuk Anda.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Alamat Email Tidak Dikonfirmasi',
     'email_not_confirmed_text' => 'Alamat email Anda belum dikonfirmasi.',
index 8554e088eb4c555c0ff7656d9aa8de551523953d..c6e1fa8b14082c361ea688d713ad7b66d0e03cb4 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Tindakan',
     'view' => 'Lihat',
     'view_all' => 'Lihat Semua',
+    'new' => 'New',
     'create' => 'Buat',
     'update' => 'Perbarui',
     'edit' => 'Sunting',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Perluas Menu Tajuk',
     'profile_menu' => 'Menu Profil',
     'view_profile' => 'Tampilkan Profil',
     'edit_profile' => 'Sunting Profil',
     'dark_mode' => 'Mode Gelap',
     'light_mode' => 'Mode Terang',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Informasi',
index 42a7b10c52318ce56d25740cca7263f192587bb4..18d85733fa7d3c33f07643942e8cca9768c1a358 100644 (file)
@@ -45,12 +45,12 @@ return [
     'underline' => 'Garis Bawah',
     'strikethrough' => 'Strikethrough',
     'superscript' => 'Superscript',
-    'subscript' => 'Subscript',
-    'text_color' => 'Text color',
-    'custom_color' => 'Custom color',
-    'remove_color' => 'Remove color',
-    'background_color' => 'Background color',
-    'align_left' => 'Align left',
+    'subscript' => 'Berlangganan',
+    'text_color' => 'Warna teks',
+    'custom_color' => 'Warna khusus',
+    'remove_color' => 'Hapus Warna',
+    'background_color' => 'Warna latar',
+    'align_left' => 'Rata Kiri',
     'align_center' => 'Align center',
     'align_right' => 'Align right',
     'align_justify' => 'Justify',
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index db2d7f8b9dfbdf29c928aca526b1494b38a1d77b..fba8a1ed487bd1445e24806fb8e41321777803aa 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Hasil Pencarian',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Sisipkan Gambar',
     'pages_md_insert_link' => 'Sisipkan Tautan Entitas',
     'pages_md_insert_drawing' => 'Sisipkan Gambar',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Halaman tidak dalam satu bab',
     'pages_move' => 'Pindahkan Halaman',
     'pages_move_success' => 'Halaman dipindahkan ke ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Izin halaman diperbarui',
     'pages_revision' => 'Revisi',
     'pages_revisions' => 'Revisi Halaman',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Revisi Halaman untuk :pageName',
     'pages_revision_named' => 'Revisi Halaman untuk :pageName',
     'pages_revision_restored_from' => 'Dipulihkan dari #:id; :summary',
     'pages_revisions_created_by' => 'Dibuat Oleh',
     'pages_revisions_date' => 'Tanggal Revisi',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revisi #:id',
     'pages_revisions_numbered_changes' => 'Revisi #:id Berubah',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Tag Rak',
     'tag' => 'Tag',
     'tags' =>  'Semua Tag',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Nama Tag',
     'tag_value' => 'Nilai Tag (opsional)',
     'tags_explain' => "Tambahkan beberapa tag untuk mengkategorikan konten Anda dengan lebih baik.\n Anda dapat menetapkan nilai ke tag untuk pengaturan yang lebih mendalam.",
diff --git a/resources/lang/id/preferences.php b/resources/lang/id/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 2315e763fb1652db4d3945227dc9cefe36749f00..4da41917f272d98232c5bc35acf9d805d0f0559f 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Peran',
     'role_user_roles' => 'Peran Pengguna',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Buat Peran Baru',
     'role_create_success' => 'Peran berhasil dibuat',
     'role_delete' => 'Hapus Peran',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Pengguna',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Profil Pengguna',
     'users_add_new' => 'Tambahkan pengguna baru',
     'users_search' => 'Cari Pengguna',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 62a7a69438f815d5492a10699b84dbdd0a4a075b..b9bc685aac49ddd3471934c4e0da02432bb6e158 100755 (executable)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'La conferma della mail è richiesta ma non è stato possibile mandare la mail. Contatta l\'amministratore.',
     'email_confirm_success' => 'La tua email è stata confermata! Ora dovresti essere in grado di effettuare il login utilizzando questo indirizzo email.',
     'email_confirm_resent' => 'Mail di conferma reinviata, controlla la tua posta.',
+    'email_confirm_thanks' => 'Grazie per la conferma!',
+    'email_confirm_thanks_desc' => 'Attendere un momento mentre la conferma viene gestita. Se non si è reindirizzati dopo 3 secondi, premere il link "Continua" qui sotto per procedere.',
 
     'email_not_confirmed' => 'Indirizzo Email Non Confermato',
     'email_not_confirmed_text' => 'Il tuo indirizzo email non è ancora stato confermato.',
index b97016403549a584b0631ba28d69df9313dac06c..a63fcd718ccbd4126643c996c2f9932d4102c991 100755 (executable)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Azioni',
     'view' => 'Visualizza',
     'view_all' => 'Vedi tutto',
+    'new' => 'Nuovo',
     'create' => 'Crea',
     'update' => 'Aggiorna',
     'edit' => 'Modifica',
@@ -80,12 +81,14 @@ return [
     'none' => 'Nessuno',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Espandi Menù Intestazione',
     'profile_menu' => 'Menù del profilo',
     'view_profile' => 'Visualizza Profilo',
     'edit_profile' => 'Modifica Profilo',
     'dark_mode' => 'Modalità Scura',
     'light_mode' => 'Modalità Chiara',
+    'global_search' => 'Ricerca Globale',
 
     // Layout tabs
     'tab_info' => 'Info',
index 39ea9fefbed792a2dfdf61fd04b56cfbdb82f88f..6d3af834347b76a7106b3b25e5225a088c9e4b24 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Testo da visualizzare',
     'title' => 'Titolo',
-    'open_link' => 'Apri collegamento in...',
+    'open_link' => 'Apri collegamento',
+    'open_link_in' => 'Apri collegamento in...',
     'open_link_current' => 'Finestra corrente',
     'open_link_new' => 'Nuova finestra',
+    'remove_link' => 'Rimuovi collegamento',
     'insert_collapsible' => 'Inserisci blocco collassabile',
     'collapsible_unwrap' => 'Espandi',
     'edit_label' => 'Modifica etichetta',
index 41b792a7d852a6b8fc026f4cfd6dc703c36c98d9..2e8f76e27e62b97aad41b0c2ea83cd7c1ab18ba4 100755 (executable)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Tutti Gli Altri',
     'permissions_role_everyone_else_desc' => 'Imposta i permessi per tutti i ruoli non specificamente sovrascritti.',
     'permissions_role_override' => 'Sovrascrivere i permessi per il ruolo',
+    'permissions_inherit_defaults' => 'Eredita predefinite',
 
     // Search
     'search_results' => 'Risultati Ricerca',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Inserisci Immagina',
     'pages_md_insert_link' => 'Inserisci Link Entità',
     'pages_md_insert_drawing' => 'Inserisci Disegno',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'La pagina non è in un capitolo',
     'pages_move' => 'Muovi Pagina',
     'pages_move_success' => 'Pagina mossa in ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Permessi pagina aggiornati',
     'pages_revision' => 'Versione',
     'pages_revisions' => 'Versioni Pagina',
+    'pages_revisions_desc' => 'Di seguito sono elencate tutte le revisioni precedenti di questa pagina. È possibile consultare, confrontare e ripristinare le vecchie versioni della pagina, se le autorizzazioni lo consentono. La cronologia completa della pagina potrebbe non essere riportata qui, poiché, a seconda della configurazione del sistema, le vecchie revisioni potrebbero essere cancellate automaticamente.',
     'pages_revisions_named' => 'Versioni della pagina :pageName',
     'pages_revision_named' => 'Versione della pagina :pageName',
     'pages_revision_restored_from' => 'Ripristinato da #:id; :summary',
     'pages_revisions_created_by' => 'Creata Da',
     'pages_revisions_date' => 'Data Versione',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Numero Revisione',
     'pages_revisions_numbered' => 'Revisione #:id',
     'pages_revisions_numbered_changes' => 'Modifiche Revisione #:id',
     'pages_revisions_editor' => 'Tipo Di Editor',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Tag Libreria',
     'tag' => 'Tag',
     'tags' =>  'Tag',
+    'tags_index_desc' => 'I tag possono essere applicati ai contenuti del sistema per applicare una forma flessibile di categorizzazione. I tag possono avere contemporaneamente una chiave e un valore, mentre il valore è opzionale. Una volta applicati, i contenuti possono essere interrogati utilizzando il nome e il valore del tag.',
     'tag_name' =>  'Nome Tag',
     'tag_value' => 'Valore (Opzionale)',
     'tags_explain' => "Aggiungi tag per categorizzare meglio il contenuto. \n Puoi assegnare un valore ai tag per una migliore organizzazione.",
diff --git a/resources/lang/it/preferences.php b/resources/lang/it/preferences.php
new file mode 100644 (file)
index 0000000..06cc172
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Scorciatoie',
+    'shortcuts_interface' => 'Interfaccia scorciatoie da tastiera',
+    'shortcuts_toggle_desc' => 'Qui puoi abilitare o disabilitare le scorciatoie dell\'interfaccia di sistema da tastiera, utilizzate per la navigazione e le azioni.',
+    'shortcuts_customize_desc' => 'È possibile personalizzare ciascuna delle scorciatoie riportate di seguito. È sufficiente premere la combinazione di tasti desiderata dopo aver selezionato l\'input per una scelta rapida.',
+    'shortcuts_toggle_label' => 'Scorciatoie da tastiera attivate',
+    'shortcuts_section_navigation' => 'Navigazione',
+    'shortcuts_section_actions' => 'Azioni Comuni',
+    'shortcuts_save' => 'Salva Scorciatoie',
+    'shortcuts_overlay_desc' => 'Nota: quando le scorciatoie sono abilitate, premendo "?" è possibile visualizzare le scorciatoie disponibili per le azioni attualmente visibili sullo schermo.',
+    'shortcuts_update_success' => 'Le preferenze delle scorciatoie sono state aggiornate!',
+];
\ No newline at end of file
index 776d5edec8c5005457a5e009ac97c4d78a67b955..6776c2cb91344605321b16532ef69785a2f733f0 100755 (executable)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Ruoli',
     'role_user_roles' => 'Ruoli Utente',
+    'roles_index_desc' => 'I ruoli sono utilizzati per raggruppare gli utenti e fornire ai loro membri i permessi di sistema. Quando un utente è membro di più ruoli, i privilegi concessi si sovrappongono e l\'utente eredita tutte le abilità.',
+    'roles_x_users_assigned' => '1 utente assegnato|:count utenti assegnati',
+    'roles_x_permissions_provided' => '1 permesso|:count permessi',
+    'roles_assigned_users' => 'Utenti Assegnati',
+    'roles_permissions_provided' => 'Autorizzazioni fornite',
     'role_create' => 'Crea Nuovo Ruolo',
     'role_create_success' => 'Ruolo creato correttamente',
     'role_delete' => 'Elimina Ruolo',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Utenti',
+    'users_index_desc' => 'Crea e gestisci account utente individuali all\'interno del sistema. Gli account utente sono utilizzati per il login e l\'attribuzione di contenuti e attività. Le autorizzazioni di accesso sono principalmente basate sui ruoli, ma la proprietà dei contenuti dell\'utente, insieme ad altri fattori, può influenzare le autorizzazioni e l\'accesso.',
     'user_profile' => 'Profilo Utente',
     'users_add_new' => 'Aggiungi Nuovo Utente',
     'users_search' => 'Cerca Utenti',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'I webhook sono un modo per inviare dati a URL esterne quando si verificano determinate azioni ed eventi all\'interno del sistema, consentendo l\'integrazione basata sugli eventi con piattaforme esterne, come sistemi di messaggistica o di notifica.',
+    'webhooks_x_trigger_events' => '1 evento trigger|:count eventi trigger',
     'webhooks_create' => 'Crea Nuovo Webhook',
     'webhooks_none_created' => 'Nessun webhook è stato creato.',
     'webhooks_edit' => 'Modifica Webhook',
index 1598ee24bd1e17377fae3eb561a312ecea26e163..98c49822ce488095847e79e6f11752c4ee93f7dc 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Eメールの確認が必要でしたが、システム上でEメールの送信ができませんでした。管理者に連絡し、Eメールが正しく設定されていることを確認してください。',
     'email_confirm_success' => 'メールアドレスが確認されました!このメールアドレスでログインできるようになりました。',
     'email_confirm_resent' => '確認メールを再送信しました。受信トレイを確認してください。',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Eメールアドレスが確認できていません',
     'email_not_confirmed_text' => 'Eメールアドレスの確認が完了していません。',
index 82429bd766f6d69f2eee6379cd2407f6b3106ce0..90b587d54c1e38c94060e8dfe78ac2ae363f4bf1 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => '実行',
     'view' => '表示',
     'view_all' => 'すべて表示',
+    'new' => 'New',
     'create' => '作成',
     'update' => '更新',
     'edit' => '編集',
@@ -80,12 +81,14 @@ return [
     'none' => 'なし',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'ヘッダーメニューを展開',
     'profile_menu' => 'プロフィールメニュー',
     'view_profile' => 'プロフィール表示',
     'edit_profile' => 'プロフィール編集',
     'dark_mode' => 'ダークモード',
     'light_mode' => 'ライトモード',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => '情報',
index 0e13bb907137f046f200702f3d23c06d07a3442c..d3a36019de9f5c244021d1bae8a2f7e5116b2a1f 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'リンク先URL',
     'text_to_display' => 'リンク元テキスト',
     'title' => 'タイトル',
-    'open_link' => 'リンクの開き方...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => '同じウィンドウ',
     'open_link_new' => '新規ウィンドウ',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => '折りたたみブロックを追加',
     'collapsible_unwrap' => 'ブロックの解除',
     'edit_label' => 'ラベルを編集',
index ab0774d439741f6b6e4a13140ea34bcdd11c0296..e3613c38064127ea9739a151f219349c2a752413 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => '検索結果',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => '画像を挿入',
     'pages_md_insert_link' => 'エンティティへのリンクを挿入',
     'pages_md_insert_drawing' => '描画を追加',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'チャプターが設定されていません',
     'pages_move' => 'ページを移動',
     'pages_move_success' => 'ページを ":parentName" へ移動しました',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'ページの権限を更新しました',
     'pages_revision' => '編集履歴',
     'pages_revisions' => '編集履歴',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => ':pageName のリビジョン',
     'pages_revision_named' => ':pageName のリビジョン',
     'pages_revision_restored_from' => '#:id :summary から復元',
     'pages_revisions_created_by' => '作成者',
     'pages_revisions_date' => '日付',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'リビジョン #:id',
     'pages_revisions_numbered_changes' => 'リビジョン #:id の変更',
     'pages_revisions_editor' => 'エディタの種類',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => '本棚のタグ',
     'tag' => 'タグ',
     'tags' =>  'タグ',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'タグの名前',
     'tag_value' => '内容 (オプション)',
     'tags_explain' => "タグを設定すると、コンテンツの管理が容易になります。\nより高度な管理をしたい場合、タグに内容を設定できます。",
diff --git a/resources/lang/ja/preferences.php b/resources/lang/ja/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index ed2fc40fd527877a00dab7f939ceb60ae4ba0506..a362719498ed1accfa72b6a080248f97567d2938 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => '役割',
     'role_user_roles' => '役割',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => '役割を作成',
     'role_create_success' => '役割を作成しました',
     'role_delete' => '役割を削除',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'ユーザー',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'ユーザプロフィール',
     'users_add_new' => 'ユーザーを追加',
     'users_search' => 'ユーザー検索',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhook',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Webhookを作成',
     'webhooks_none_created' => 'Webhookはまだ作成されていません。',
     'webhooks_edit' => 'Webhookを編集',
diff --git a/resources/lang/ka/activities.php b/resources/lang/ka/activities.php
new file mode 100644 (file)
index 0000000..f348bff
--- /dev/null
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Activity text strings.
+ * Is used for all the text within activity logs & notifications.
+ */
+return [
+
+    // Pages
+    'page_create'                 => 'created page',
+    'page_create_notification'    => 'Page successfully created',
+    'page_update'                 => 'updated page',
+    'page_update_notification'    => 'Page successfully updated',
+    'page_delete'                 => 'deleted page',
+    'page_delete_notification'    => 'Page successfully deleted',
+    'page_restore'                => 'restored page',
+    'page_restore_notification'   => 'Page successfully restored',
+    'page_move'                   => 'moved page',
+
+    // Chapters
+    'chapter_create'              => 'created chapter',
+    'chapter_create_notification' => 'Chapter successfully created',
+    'chapter_update'              => 'updated chapter',
+    'chapter_update_notification' => 'Chapter successfully updated',
+    'chapter_delete'              => 'deleted chapter',
+    'chapter_delete_notification' => 'Chapter successfully deleted',
+    'chapter_move'                => 'moved chapter',
+
+    // Books
+    'book_create'                 => 'created book',
+    'book_create_notification'    => 'Book successfully created',
+    'book_create_from_chapter'              => 'converted chapter to book',
+    'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',
+    'book_update'                 => 'updated book',
+    'book_update_notification'    => 'Book successfully updated',
+    'book_delete'                 => 'deleted book',
+    'book_delete_notification'    => 'Book successfully deleted',
+    'book_sort'                   => 'sorted book',
+    'book_sort_notification'      => 'Book successfully re-sorted',
+
+    // Bookshelves
+    'bookshelf_create'            => 'created shelf',
+    'bookshelf_create_notification'    => 'Shelf successfully created',
+    'bookshelf_create_from_book'    => 'converted book to shelf',
+    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',
+    'bookshelf_update'                 => 'updated shelf',
+    'bookshelf_update_notification'    => 'Shelf successfully updated',
+    'bookshelf_delete'                 => 'deleted shelf',
+    'bookshelf_delete_notification'    => 'Shelf successfully deleted',
+
+    // Favourites
+    'favourite_add_notification' => '":name" has been added to your favourites',
+    'favourite_remove_notification' => '":name" has been removed from your favourites',
+
+    // MFA
+    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
+    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+
+    // Webhooks
+    'webhook_create' => 'created webhook',
+    'webhook_create_notification' => 'Webhook successfully created',
+    'webhook_update' => 'updated webhook',
+    'webhook_update_notification' => 'Webhook successfully updated',
+    'webhook_delete' => 'deleted webhook',
+    'webhook_delete_notification' => 'Webhook successfully deleted',
+
+    // Users
+    'user_update_notification' => 'User successfully updated',
+    'user_delete_notification' => 'User successfully removed',
+
+    // Other
+    'commented_on'                => 'commented on',
+    'permissions_update'          => 'updated permissions',
+];
diff --git a/resources/lang/ka/auth.php b/resources/lang/ka/auth.php
new file mode 100644 (file)
index 0000000..dc4b242
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+/**
+ * Authentication Language Lines
+ * The following language lines are used during authentication for various
+ * messages that we need to display to the user.
+ */
+return [
+
+    'failed' => 'These credentials do not match our records.',
+    'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
+
+    // Login & Register
+    'sign_up' => 'Sign up',
+    'log_in' => 'Log in',
+    'log_in_with' => 'Login with :socialDriver',
+    'sign_up_with' => 'Sign up with :socialDriver',
+    'logout' => 'Logout',
+
+    'name' => 'Name',
+    'username' => 'Username',
+    'email' => 'Email',
+    'password' => 'Password',
+    'password_confirm' => 'Confirm Password',
+    'password_hint' => 'Must be at least 8 characters',
+    'forgot_password' => 'Forgot Password?',
+    'remember_me' => 'Remember Me',
+    'ldap_email_hint' => 'Please enter an email to use for this account.',
+    'create_account' => 'Create Account',
+    'already_have_account' => 'Already have an account?',
+    'dont_have_account' => 'Don\'t have an account?',
+    'social_login' => 'Social Login',
+    'social_registration' => 'Social Registration',
+    'social_registration_text' => 'Register and sign in using another service.',
+
+    'register_thanks' => 'Thanks for registering!',
+    'register_confirm' => 'Please check your email and click the confirmation button to access :appName.',
+    'registrations_disabled' => 'Registrations are currently disabled',
+    'registration_email_domain_invalid' => 'That email domain does not have access to this application',
+    'register_success' => 'Thanks for signing up! You are now registered and signed in.',
+
+    // Login auto-initiation
+    'auto_init_starting' => 'Attempting Login',
+    'auto_init_starting_desc' => 'We\'re contacting your authentication system to start the login process. If there\'s no progress after 5 seconds you can try clicking the link below.',
+    'auto_init_start_link' => 'Proceed with authentication',
+
+    // Password Reset
+    'reset_password' => 'Reset Password',
+    'reset_password_send_instructions' => 'Enter your email below and you will be sent an email with a password reset link.',
+    'reset_password_send_button' => 'Send Reset Link',
+    'reset_password_sent' => 'A password reset link will be sent to :email if that email address is found in the system.',
+    'reset_password_success' => 'Your password has been successfully reset.',
+    'email_reset_subject' => 'Reset your :appName password',
+    'email_reset_text' => 'You are receiving this email because we received a password reset request for your account.',
+    'email_reset_not_requested' => 'If you did not request a password reset, no further action is required.',
+
+    // Email Confirmation
+    'email_confirm_subject' => 'Confirm your email on :appName',
+    'email_confirm_greeting' => 'Thanks for joining :appName!',
+    'email_confirm_text' => 'Please confirm your email address by clicking the button below:',
+    'email_confirm_action' => 'Confirm Email',
+    'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',
+    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
+    'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
+
+    'email_not_confirmed' => 'Email Address Not Confirmed',
+    'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',
+    'email_not_confirmed_click_link' => 'Please click the link in the email that was sent shortly after you registered.',
+    'email_not_confirmed_resend' => 'If you cannot find the email you can re-send the confirmation email by submitting the form below.',
+    'email_not_confirmed_resend_button' => 'Resend Confirmation Email',
+
+    // User Invite
+    'user_invite_email_subject' => 'You have been invited to join :appName!',
+    'user_invite_email_greeting' => 'An account has been created for you on :appName.',
+    'user_invite_email_text' => 'Click the button below to set an account password and gain access:',
+    'user_invite_email_action' => 'Set Account Password',
+    'user_invite_page_welcome' => 'Welcome to :appName!',
+    'user_invite_page_text' => 'To finalise your account and gain access you need to set a password which will be used to log-in to :appName on future visits.',
+    'user_invite_page_confirm_button' => 'Confirm Password',
+    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
+
+    // Multi-factor Authentication
+    'mfa_setup' => 'Setup Multi-Factor Authentication',
+    'mfa_setup_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'mfa_setup_configured' => 'Already configured',
+    'mfa_setup_reconfigure' => 'Reconfigure',
+    'mfa_setup_remove_confirmation' => 'Are you sure you want to remove this multi-factor authentication method?',
+    'mfa_setup_action' => 'Setup',
+    'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
+    'mfa_option_totp_title' => 'Mobile App',
+    'mfa_option_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_option_backup_codes_title' => 'Backup Codes',
+    'mfa_option_backup_codes_desc' => 'Securely store a set of one-time-use backup codes which you can enter to verify your identity.',
+    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
+    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
+    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
+    'mfa_gen_backup_codes_download' => 'Download Codes',
+    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
+    'mfa_gen_totp_title' => 'Mobile App Setup',
+    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
+    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
+    'mfa_gen_totp_verify_setup' => 'Verify Setup',
+    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
+    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
+    'mfa_verify_access' => 'Verify Access',
+    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
+    'mfa_verify_no_methods' => 'No Methods Configured',
+    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_verify_use_totp' => 'Verify using a mobile app',
+    'mfa_verify_use_backup_codes' => 'Verify using a backup code',
+    'mfa_verify_backup_code' => 'Backup Code',
+    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
+    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
+    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
+    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
+];
diff --git a/resources/lang/ka/common.php b/resources/lang/ka/common.php
new file mode 100644 (file)
index 0000000..c74dcc9
--- /dev/null
@@ -0,0 +1,107 @@
+<?php
+/**
+ * Common elements found throughout many areas of BookStack.
+ */
+return [
+
+    // Buttons
+    'cancel' => 'Cancel',
+    'confirm' => 'Confirm',
+    'back' => 'Back',
+    'save' => 'Save',
+    'continue' => 'Continue',
+    'select' => 'Select',
+    'toggle_all' => 'Toggle All',
+    'more' => 'More',
+
+    // Form Labels
+    'name' => 'Name',
+    'description' => 'Description',
+    'role' => 'Role',
+    'cover_image' => 'Cover image',
+    'cover_image_description' => 'This image should be approx 440x250px.',
+
+    // Actions
+    'actions' => 'Actions',
+    'view' => 'View',
+    'view_all' => 'View All',
+    'new' => 'New',
+    'create' => 'Create',
+    'update' => 'Update',
+    'edit' => 'Edit',
+    'sort' => 'Sort',
+    'move' => 'Move',
+    'copy' => 'Copy',
+    'reply' => 'Reply',
+    'delete' => 'Delete',
+    'delete_confirm' => 'Confirm Deletion',
+    'search' => 'Search',
+    'search_clear' => 'Clear Search',
+    'reset' => 'Reset',
+    'remove' => 'Remove',
+    'add' => 'Add',
+    'configure' => 'Configure',
+    'fullscreen' => 'Fullscreen',
+    'favourite' => 'Favourite',
+    'unfavourite' => 'Unfavourite',
+    'next' => 'Next',
+    'previous' => 'Previous',
+    'filter_active' => 'Active Filter:',
+    'filter_clear' => 'Clear Filter',
+    'download' => 'Download',
+    'open_in_tab' => 'Open in Tab',
+
+    // Sort Options
+    'sort_options' => 'Sort Options',
+    'sort_direction_toggle' => 'Sort Direction Toggle',
+    'sort_ascending' => 'Sort Ascending',
+    'sort_descending' => 'Sort Descending',
+    'sort_name' => 'Name',
+    'sort_default' => 'Default',
+    'sort_created_at' => 'Created Date',
+    'sort_updated_at' => 'Updated Date',
+
+    // Misc
+    'deleted_user' => 'Deleted User',
+    'no_activity' => 'No activity to show',
+    'no_items' => 'No items available',
+    'back_to_top' => 'Back to top',
+    'skip_to_main_content' => 'Skip to main content',
+    'toggle_details' => 'Toggle Details',
+    'toggle_thumbnails' => 'Toggle Thumbnails',
+    'details' => 'Details',
+    'grid_view' => 'Grid View',
+    'list_view' => 'List View',
+    'default' => 'Default',
+    'breadcrumb' => 'Breadcrumb',
+    'status' => 'Status',
+    'status_active' => 'Active',
+    'status_inactive' => 'Inactive',
+    'never' => 'Never',
+    'none' => 'None',
+
+    // Header
+    'homepage' => 'Homepage',
+    'header_menu_expand' => 'Expand Header Menu',
+    'profile_menu' => 'Profile Menu',
+    'view_profile' => 'View Profile',
+    'edit_profile' => 'Edit Profile',
+    'dark_mode' => 'Dark Mode',
+    'light_mode' => 'Light Mode',
+    'global_search' => 'Global Search',
+
+    // Layout tabs
+    'tab_info' => 'Info',
+    'tab_info_label' => 'Tab: Show Secondary Information',
+    'tab_content' => 'Content',
+    'tab_content_label' => 'Tab: Show Primary Content',
+
+    // Email Content
+    'email_action_help' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser:',
+    'email_rights' => 'All rights reserved',
+
+    // Footer Link Options
+    // Not directly used but available for convenience to users.
+    'privacy_policy' => 'Privacy Policy',
+    'terms_of_service' => 'Terms of Service',
+];
diff --git a/resources/lang/ka/components.php b/resources/lang/ka/components.php
new file mode 100644 (file)
index 0000000..48a0a32
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Text used in custom JavaScript driven components.
+ */
+return [
+
+    // Image Manager
+    'image_select' => 'Image Select',
+    'image_all' => 'All',
+    'image_all_title' => 'View all images',
+    'image_book_title' => 'View images uploaded to this book',
+    'image_page_title' => 'View images uploaded to this page',
+    'image_search_hint' => 'Search by image name',
+    'image_uploaded' => 'Uploaded :uploadedDate',
+    'image_load_more' => 'Load More',
+    'image_image_name' => 'Image Name',
+    'image_delete_used' => 'This image is used in the pages below.',
+    'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
+    'image_select_image' => 'Select Image',
+    'image_dropzone' => 'Drop images or click here to upload',
+    'images_deleted' => 'Images Deleted',
+    'image_preview' => 'Image Preview',
+    'image_upload_success' => 'Image uploaded successfully',
+    'image_update_success' => 'Image details successfully updated',
+    'image_delete_success' => 'Image successfully deleted',
+    'image_upload_remove' => 'Remove',
+
+    // Code Editor
+    'code_editor' => 'Edit Code',
+    'code_language' => 'Code Language',
+    'code_content' => 'Code Content',
+    'code_session_history' => 'Session History',
+    'code_save' => 'Save Code',
+];
diff --git a/resources/lang/ka/editor.php b/resources/lang/ka/editor.php
new file mode 100644 (file)
index 0000000..670c1c5
--- /dev/null
@@ -0,0 +1,174 @@
+<?php
+/**
+ * Page Editor Lines
+ * Contains text strings used within the user interface of the
+ * WYSIWYG page editor. Some Markdown editor strings may still
+ * exist in the 'entities' file instead since this was added later.
+ */
+return [
+    // General editor terms
+    'general' => 'General',
+    'advanced' => 'Advanced',
+    'none' => 'None',
+    'cancel' => 'Cancel',
+    'save' => 'Save',
+    'close' => 'Close',
+    'undo' => 'Undo',
+    'redo' => 'Redo',
+    'left' => 'Left',
+    'center' => 'Center',
+    'right' => 'Right',
+    'top' => 'Top',
+    'middle' => 'Middle',
+    'bottom' => 'Bottom',
+    'width' => 'Width',
+    'height' => 'Height',
+    'More' => 'More',
+    'select' => 'Select...',
+
+    // Toolbar
+    'formats' => 'Formats',
+    'header_large' => 'Large Header',
+    'header_medium' => 'Medium Header',
+    'header_small' => 'Small Header',
+    'header_tiny' => 'Tiny Header',
+    'paragraph' => 'Paragraph',
+    'blockquote' => 'Blockquote',
+    'inline_code' => 'Inline code',
+    'callouts' => 'Callouts',
+    'callout_information' => 'Information',
+    'callout_success' => 'Success',
+    'callout_warning' => 'Warning',
+    'callout_danger' => 'Danger',
+    'bold' => 'Bold',
+    'italic' => 'Italic',
+    'underline' => 'Underline',
+    'strikethrough' => 'Strikethrough',
+    'superscript' => 'Superscript',
+    'subscript' => 'Subscript',
+    'text_color' => 'Text color',
+    'custom_color' => 'Custom color',
+    'remove_color' => 'Remove color',
+    'background_color' => 'Background color',
+    'align_left' => 'Align left',
+    'align_center' => 'Align center',
+    'align_right' => 'Align right',
+    'align_justify' => 'Justify',
+    'list_bullet' => 'Bullet list',
+    'list_numbered' => 'Numbered list',
+    'list_task' => 'Task list',
+    'indent_increase' => 'Increase indent',
+    'indent_decrease' => 'Decrease indent',
+    'table' => 'Table',
+    'insert_image' => 'Insert image',
+    'insert_image_title' => 'Insert/Edit Image',
+    'insert_link' => 'Insert/edit link',
+    'insert_link_title' => 'Insert/Edit Link',
+    'insert_horizontal_line' => 'Insert horizontal line',
+    'insert_code_block' => 'Insert code block',
+    'edit_code_block' => 'Edit code block',
+    'insert_drawing' => 'Insert/edit drawing',
+    'drawing_manager' => 'Drawing manager',
+    'insert_media' => 'Insert/edit media',
+    'insert_media_title' => 'Insert/Edit Media',
+    'clear_formatting' => 'Clear formatting',
+    'source_code' => 'Source code',
+    'source_code_title' => 'Source Code',
+    'fullscreen' => 'Fullscreen',
+    'image_options' => 'Image options',
+
+    // Tables
+    'table_properties' => 'Table properties',
+    'table_properties_title' => 'Table Properties',
+    'delete_table' => 'Delete table',
+    'insert_row_before' => 'Insert row before',
+    'insert_row_after' => 'Insert row after',
+    'delete_row' => 'Delete row',
+    'insert_column_before' => 'Insert column before',
+    'insert_column_after' => 'Insert column after',
+    'delete_column' => 'Delete column',
+    'table_cell' => 'Cell',
+    'table_row' => 'Row',
+    'table_column' => 'Column',
+    'cell_properties' => 'Cell properties',
+    'cell_properties_title' => 'Cell Properties',
+    'cell_type' => 'Cell type',
+    'cell_type_cell' => 'Cell',
+    'cell_scope' => 'Scope',
+    'cell_type_header' => 'Header cell',
+    'merge_cells' => 'Merge cells',
+    'split_cell' => 'Split cell',
+    'table_row_group' => 'Row Group',
+    'table_column_group' => 'Column Group',
+    'horizontal_align' => 'Horizontal align',
+    'vertical_align' => 'Vertical align',
+    'border_width' => 'Border width',
+    'border_style' => 'Border style',
+    'border_color' => 'Border color',
+    'row_properties' => 'Row properties',
+    'row_properties_title' => 'Row Properties',
+    'cut_row' => 'Cut row',
+    'copy_row' => 'Copy row',
+    'paste_row_before' => 'Paste row before',
+    'paste_row_after' => 'Paste row after',
+    'row_type' => 'Row type',
+    'row_type_header' => 'Header',
+    'row_type_body' => 'Body',
+    'row_type_footer' => 'Footer',
+    'alignment' => 'Alignment',
+    'cut_column' => 'Cut column',
+    'copy_column' => 'Copy column',
+    'paste_column_before' => 'Paste column before',
+    'paste_column_after' => 'Paste column after',
+    'cell_padding' => 'Cell padding',
+    'cell_spacing' => 'Cell spacing',
+    'caption' => 'Caption',
+    'show_caption' => 'Show caption',
+    'constrain' => 'Constrain proportions',
+    'cell_border_solid' => 'Solid',
+    'cell_border_dotted' => 'Dotted',
+    'cell_border_dashed' => 'Dashed',
+    'cell_border_double' => 'Double',
+    'cell_border_groove' => 'Groove',
+    'cell_border_ridge' => 'Ridge',
+    'cell_border_inset' => 'Inset',
+    'cell_border_outset' => 'Outset',
+    'cell_border_none' => 'None',
+    'cell_border_hidden' => 'Hidden',
+
+    // Images, links, details/summary & embed
+    'source' => 'Source',
+    'alt_desc' => 'Alternative description',
+    'embed' => 'Embed',
+    'paste_embed' => 'Paste your embed code below:',
+    'url' => 'URL',
+    'text_to_display' => 'Text to display',
+    'title' => 'Title',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
+    'open_link_current' => 'Current window',
+    'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
+    'insert_collapsible' => 'Insert collapsible block',
+    'collapsible_unwrap' => 'Unwrap',
+    'edit_label' => 'Edit label',
+    'toggle_open_closed' => 'Toggle open/closed',
+    'collapsible_edit' => 'Edit collapsible block',
+    'toggle_label' => 'Toggle label',
+
+    // About view
+    'about' => 'About the editor',
+    'about_title' => 'About the WYSIWYG Editor',
+    'editor_license' => 'Editor License & Copyright',
+    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',
+    'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',
+    'save_continue' => 'Save Page & Continue',
+    'callouts_cycle' => '(Keep pressing to toggle through types)',
+    'link_selector' => 'Link to content',
+    'shortcuts' => 'Shortcuts',
+    'shortcut' => 'Shortcut',
+    'shortcuts_intro' => 'The following shortcuts are available in the editor:',
+    'windows_linux' => '(Windows/Linux)',
+    'mac' => '(Mac)',
+    'description' => 'Description',
+];
diff --git a/resources/lang/ka/entities.php b/resources/lang/ka/entities.php
new file mode 100644 (file)
index 0000000..fa2586f
--- /dev/null
@@ -0,0 +1,388 @@
+<?php
+/**
+ * Text used for 'Entities' (Document Structure Elements) such as
+ * Books, Shelves, Chapters & Pages
+ */
+return [
+
+    // Shared
+    'recently_created' => 'Recently Created',
+    'recently_created_pages' => 'Recently Created Pages',
+    'recently_updated_pages' => 'Recently Updated Pages',
+    'recently_created_chapters' => 'Recently Created Chapters',
+    'recently_created_books' => 'Recently Created Books',
+    'recently_created_shelves' => 'Recently Created Shelves',
+    'recently_update' => 'Recently Updated',
+    'recently_viewed' => 'Recently Viewed',
+    'recent_activity' => 'Recent Activity',
+    'create_now' => 'Create one now',
+    'revisions' => 'Revisions',
+    'meta_revision' => 'Revision #:revisionCount',
+    'meta_created' => 'Created :timeLength',
+    'meta_created_name' => 'Created :timeLength by :user',
+    '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',
+    'entity_select' => 'Entity Select',
+    'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
+    'images' => 'Images',
+    'my_recent_drafts' => 'My Recent Drafts',
+    'my_recently_viewed' => 'My Recently Viewed',
+    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
+    'my_favourites' => 'My Favourites',
+    'no_pages_viewed' => 'You have not viewed any pages',
+    'no_pages_recently_created' => 'No pages have been recently created',
+    'no_pages_recently_updated' => 'No pages have been recently updated',
+    'export' => 'Export',
+    'export_html' => 'Contained Web File',
+    'export_pdf' => 'PDF File',
+    'export_text' => 'Plain Text File',
+    'export_md' => 'Markdown File',
+
+    // Permissions and restrictions
+    'permissions' => 'Permissions',
+    'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',
+    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',
+    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',
+    'permissions_save' => 'Save Permissions',
+    'permissions_owner' => 'Owner',
+    'permissions_role_everyone_else' => 'Everyone Else',
+    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
+    'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
+
+    // Search
+    'search_results' => 'Search Results',
+    'search_total_results_found' => ':count result found|:count total results found',
+    'search_clear' => 'Clear Search',
+    'search_no_pages' => 'No pages matched this search',
+    'search_for_term' => 'Search for :term',
+    'search_more' => 'More Results',
+    'search_advanced' => 'Advanced Search',
+    'search_terms' => 'Search Terms',
+    'search_content_type' => 'Content Type',
+    'search_exact_matches' => 'Exact Matches',
+    'search_tags' => 'Tag Searches',
+    'search_options' => 'Options',
+    'search_viewed_by_me' => 'Viewed by me',
+    'search_not_viewed_by_me' => 'Not viewed by me',
+    'search_permissions_set' => 'Permissions set',
+    'search_created_by_me' => 'Created by me',
+    'search_updated_by_me' => 'Updated by me',
+    'search_owned_by_me' => 'Owned by me',
+    'search_date_options' => 'Date Options',
+    'search_updated_before' => 'Updated before',
+    'search_updated_after' => 'Updated after',
+    'search_created_before' => 'Created before',
+    'search_created_after' => 'Created after',
+    'search_set_date' => 'Set Date',
+    'search_update' => 'Update Search',
+
+    // Shelves
+    'shelf' => 'Shelf',
+    'shelves' => 'Shelves',
+    'x_shelves' => ':count Shelf|:count Shelves',
+    'shelves_empty' => 'No shelves have been created',
+    'shelves_create' => 'Create New Shelf',
+    'shelves_popular' => 'Popular Shelves',
+    'shelves_new' => 'New Shelves',
+    'shelves_new_action' => 'New Shelf',
+    'shelves_popular_empty' => 'The most popular shelves will appear here.',
+    'shelves_new_empty' => 'The most recently created shelves will appear here.',
+    'shelves_save' => 'Save Shelf',
+    'shelves_books' => 'Books on this shelf',
+    'shelves_add_books' => 'Add books to this shelf',
+    'shelves_drag_books' => 'Drag books below to add them to this shelf',
+    'shelves_empty_contents' => 'This shelf has no books assigned to it',
+    'shelves_edit_and_assign' => 'Edit shelf to assign books',
+    'shelves_edit_named' => 'Edit Shelf :name',
+    'shelves_edit' => 'Edit Shelf',
+    'shelves_delete' => 'Delete Shelf',
+    'shelves_delete_named' => 'Delete Shelf :name',
+    'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
+    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
+    'shelves_permissions' => 'Shelf Permissions',
+    '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_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.',
+    'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
+
+    // Books
+    'book' => 'Book',
+    'books' => 'Books',
+    'x_books' => ':count Book|:count Books',
+    'books_empty' => 'No books have been created',
+    'books_popular' => 'Popular Books',
+    'books_recent' => 'Recent Books',
+    'books_new' => 'New Books',
+    'books_new_action' => 'New Book',
+    'books_popular_empty' => 'The most popular books will appear here.',
+    'books_new_empty' => 'The most recently created books will appear here.',
+    'books_create' => 'Create New Book',
+    'books_delete' => 'Delete Book',
+    'books_delete_named' => 'Delete Book :bookName',
+    'books_delete_explain' => 'This will delete the book with the name \':bookName\'. All pages and chapters will be removed.',
+    'books_delete_confirmation' => 'Are you sure you want to delete this book?',
+    'books_edit' => 'Edit Book',
+    'books_edit_named' => 'Edit Book :bookName',
+    'books_form_book_name' => 'Book Name',
+    'books_save' => 'Save Book',
+    'books_permissions' => 'Book Permissions',
+    'books_permissions_updated' => 'Book Permissions Updated',
+    'books_empty_contents' => 'No pages or chapters have been created for this book.',
+    'books_empty_create_page' => 'Create a new page',
+    'books_empty_sort_current_book' => 'Sort the current book',
+    'books_empty_add_chapter' => 'Add a chapter',
+    'books_permissions_active' => 'Book Permissions Active',
+    'books_search_this' => 'Search this book',
+    'books_navigation' => 'Book Navigation',
+    'books_sort' => 'Sort Book Contents',
+    'books_sort_named' => 'Sort Book :bookName',
+    'books_sort_name' => 'Sort by Name',
+    'books_sort_created' => 'Sort by Created Date',
+    'books_sort_updated' => 'Sort by Updated Date',
+    'books_sort_chapters_first' => 'Chapters First',
+    'books_sort_chapters_last' => 'Chapters Last',
+    'books_sort_show_other' => 'Show Other Books',
+    'books_sort_save' => 'Save New Order',
+    'books_copy' => 'Copy Book',
+    'books_copy_success' => 'Book successfully copied',
+
+    // Chapters
+    'chapter' => 'Chapter',
+    'chapters' => 'Chapters',
+    'x_chapters' => ':count Chapter|:count Chapters',
+    'chapters_popular' => 'Popular Chapters',
+    'chapters_new' => 'New Chapter',
+    'chapters_create' => 'Create New Chapter',
+    'chapters_delete' => 'Delete Chapter',
+    'chapters_delete_named' => 'Delete Chapter :chapterName',
+    'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.',
+    'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?',
+    'chapters_edit' => 'Edit Chapter',
+    'chapters_edit_named' => 'Edit Chapter :chapterName',
+    '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',
+    'chapters_empty' => 'No pages are currently in this chapter.',
+    'chapters_permissions_active' => 'Chapter Permissions Active',
+    'chapters_permissions_success' => 'Chapter Permissions Updated',
+    'chapters_search_this' => 'Search this chapter',
+    'chapter_sort_book' => 'Sort Book',
+
+    // Pages
+    'page' => 'Page',
+    'pages' => 'Pages',
+    'x_pages' => ':count Page|:count Pages',
+    'pages_popular' => 'Popular Pages',
+    'pages_new' => 'New Page',
+    'pages_attachments' => 'Attachments',
+    'pages_navigation' => 'Page Navigation',
+    'pages_delete' => 'Delete Page',
+    'pages_delete_named' => 'Delete Page :pageName',
+    'pages_delete_draft_named' => 'Delete Draft Page :pageName',
+    'pages_delete_draft' => 'Delete Draft Page',
+    'pages_delete_success' => 'Page deleted',
+    'pages_delete_draft_success' => 'Draft page deleted',
+    '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_edit_draft_options' => 'Draft Options',
+    'pages_edit_save_draft' => 'Save Draft',
+    'pages_edit_draft' => 'Edit Page Draft',
+    'pages_editing_draft' => 'Editing Draft',
+    'pages_editing_page' => 'Editing Page',
+    'pages_edit_draft_save_at' => 'Draft saved at ',
+    'pages_edit_delete_draft' => 'Delete Draft',
+    'pages_edit_discard_draft' => 'Discard Draft',
+    'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
+    'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
+    'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
+    'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
+    'pages_edit_set_changelog' => 'Set Changelog',
+    'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
+    'pages_edit_enter_changelog' => 'Enter Changelog',
+    'pages_editor_switch_title' => 'Switch Editor',
+    'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
+    'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
+    'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
+    'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
+    'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
+    'pages_save' => 'Save Page',
+    'pages_title' => 'Page Title',
+    'pages_name' => 'Page Name',
+    'pages_md_editor' => 'Editor',
+    'pages_md_preview' => 'Preview',
+    'pages_md_insert_image' => 'Insert Image',
+    'pages_md_insert_link' => 'Insert Entity Link',
+    'pages_md_insert_drawing' => 'Insert Drawing',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
+    '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_permissions' => 'Page Permissions',
+    'pages_permissions_success' => 'Page permissions updated',
+    'pages_revision' => 'Revision',
+    'pages_revisions' => 'Page Revisions',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
+    'pages_revisions_named' => 'Page Revisions for :pageName',
+    'pages_revision_named' => 'Page Revision for :pageName',
+    'pages_revision_restored_from' => 'Restored from #:id; :summary',
+    'pages_revisions_created_by' => 'Created By',
+    'pages_revisions_date' => 'Revision Date',
+    'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
+    'pages_revisions_numbered' => 'Revision #:id',
+    'pages_revisions_numbered_changes' => 'Revision #:id Changes',
+    'pages_revisions_editor' => 'Editor Type',
+    'pages_revisions_changelog' => 'Changelog',
+    'pages_revisions_changes' => 'Changes',
+    'pages_revisions_current' => 'Current Version',
+    'pages_revisions_preview' => 'Preview',
+    'pages_revisions_restore' => 'Restore',
+    'pages_revisions_none' => 'This page has no revisions',
+    'pages_copy_link' => 'Copy Link',
+    'pages_edit_content_link' => 'Edit Content',
+    'pages_permissions_active' => 'Page Permissions Active',
+    'pages_initial_revision' => 'Initial publish',
+    'pages_references_update_revision' => 'System auto-update of internal links',
+    'pages_initial_name' => 'New Page',
+    'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.',
+    'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.',
+    'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
+    'pages_draft_edit_active' => [
+        'start_a' => ':count users have started editing this page',
+        'start_b' => ':userName has started editing this page',
+        'time_a' => 'since the page was last updated',
+        '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_specific' => 'Specific Page',
+    'pages_is_template' => 'Page Template',
+
+    // Editor Sidebar
+    'page_tags' => 'Page Tags',
+    'chapter_tags' => 'Chapter Tags',
+    'book_tags' => 'Book Tags',
+    'shelf_tags' => 'Shelf Tags',
+    'tag' => 'Tag',
+    'tags' =>  'Tags',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
+    'tag_name' =>  'Tag Name',
+    'tag_value' => 'Tag Value (Optional)',
+    'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
+    'tags_add' => 'Add another tag',
+    'tags_remove' => 'Remove this tag',
+    'tags_usages' => 'Total tag usages',
+    'tags_assigned_pages' => 'Assigned to Pages',
+    'tags_assigned_chapters' => 'Assigned to Chapters',
+    'tags_assigned_books' => 'Assigned to Books',
+    'tags_assigned_shelves' => 'Assigned to Shelves',
+    'tags_x_unique_values' => ':count unique values',
+    'tags_all_values' => 'All values',
+    'tags_view_tags' => 'View Tags',
+    'tags_view_existing_tags' => 'View existing tags',
+    'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
+    '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_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_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',
+    'attachment_link' => 'Attachment link',
+    'attachments_link_url' => 'Link to file',
+    'attachments_link_url_hint' => 'Url of site or file',
+    'attach' => 'Attach',
+    'attachments_insert_link' => 'Add Attachment Link to Page',
+    'attachments_edit_file' => 'Edit File',
+    'attachments_edit_file_name' => 'File Name',
+    'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite',
+    'attachments_order_updated' => 'Attachment order updated',
+    'attachments_updated_success' => 'Attachment details updated',
+    'attachments_deleted' => 'Attachment deleted',
+    'attachments_file_uploaded' => 'File successfully uploaded',
+    'attachments_file_updated' => 'File successfully updated',
+    'attachments_link_attached' => 'Link successfully attached to page',
+    'templates' => 'Templates',
+    'templates_set_as_template' => 'Page is a template',
+    'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.',
+    'templates_replace_content' => 'Replace page content',
+    'templates_append_content' => 'Append to page content',
+    'templates_prepend_content' => 'Prepend to page content',
+
+    // Profile View
+    'profile_user_for_x' => 'User for :time',
+    'profile_created_content' => 'Created Content',
+    'profile_not_created_pages' => ':userName has not created any pages',
+    'profile_not_created_chapters' => ':userName has not created any chapters',
+    'profile_not_created_books' => ':userName has not created any books',
+    'profile_not_created_shelves' => ':userName has not created any shelves',
+
+    // Comments
+    'comment' => 'Comment',
+    'comments' => 'Comments',
+    'comment_add' => 'Add Comment',
+    '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_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',
+
+    // 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
+    'copy_consider' => 'Please consider the below when copying content.',
+    'copy_consider_permissions' => 'Custom permission settings will not be copied.',
+    'copy_consider_owner' => 'You will become the owner of all copied content.',
+    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
+    'copy_consider_attachments' => 'Page attachments will not be copied.',
+    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
+
+    // Conversions
+    'convert_to_shelf' => 'Convert to Shelf',
+    'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',
+    'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',
+    'convert_book' => 'Convert Book',
+    'convert_book_confirm' => 'Are you sure you want to convert this book?',
+    'convert_undo_warning' => 'This cannot be as easily undone.',
+    'convert_to_book' => 'Convert to Book',
+    'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
+    'convert_chapter' => 'Convert Chapter',
+    'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
+
+    // References
+    '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.',
+];
diff --git a/resources/lang/ka/errors.php b/resources/lang/ka/errors.php
new file mode 100644 (file)
index 0000000..52f96cb
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Text shown in error messaging.
+ */
+return [
+
+    // Permissions
+    'permission' => 'You do not have permission to access the requested page.',
+    'permissionJson' => 'You do not have permission to perform the requested action.',
+
+    // Auth
+    'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.',
+    'email_already_confirmed' => 'Email has already been confirmed, Try logging in.',
+    'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.',
+    'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.',
+    'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed',
+    'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind',
+    'ldap_fail_authed' => 'LDAP access failed using given dn & password details',
+    'ldap_extension_not_installed' => 'LDAP PHP extension not installed',
+    'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
+    'saml_already_logged_in' => 'Already logged in',
+    'saml_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
+    'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
+    'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.',
+    'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
+    'oidc_already_logged_in' => 'Already logged in',
+    'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
+    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
+    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
+    'social_no_action_defined' => 'No action defined',
+    'social_login_bad_response' => "Error received during :socialAccount login: \n:error",
+    'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.',
+    'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.',
+    'social_account_existing' => 'This :socialAccount is already attached to your profile.',
+    'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.',
+    'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ',
+    'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.',
+    'social_driver_not_found' => 'Social driver not found',
+    'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
+    'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.',
+
+    // System
+    'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
+    'cannot_get_image_from_url' => 'Cannot get image from :url',
+    'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.',
+    'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.',
+    'uploaded'  => 'The server does not allow uploads of this size. Please try a smaller file size.',
+    'image_upload_error' => 'An error occurred uploading the image',
+    'image_upload_type_error' => 'The image type being uploaded is invalid',
+    'file_upload_timeout' => 'The file upload has timed out.',
+
+    // Attachments
+    'attachment_not_found' => 'Attachment not found',
+
+    // Pages
+    'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page',
+    'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage',
+
+    // Entities
+    'entity_not_found' => 'Entity not found',
+    'bookshelf_not_found' => 'Shelf not found',
+    'book_not_found' => 'Book not found',
+    'page_not_found' => 'Page not found',
+    'chapter_not_found' => 'Chapter not found',
+    'selected_book_not_found' => 'The selected book was not found',
+    'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found',
+    'guests_cannot_save_drafts' => 'Guests cannot save drafts',
+
+    // Users
+    'users_cannot_delete_only_admin' => 'You cannot delete the only admin',
+    'users_cannot_delete_guest' => 'You cannot delete the guest user',
+
+    // Roles
+    'role_cannot_be_edited' => 'This role cannot be edited',
+    'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',
+    'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',
+    'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.',
+
+    // Comments
+    'comment_list' => 'An error occurred while fetching the comments.',
+    'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.',
+    'comment_add' => 'An error occurred while adding / updating the comment.',
+    'comment_delete' => 'An error occurred while deleting the comment.',
+    'empty_comment' => 'Cannot add an empty comment.',
+
+    // Error pages
+    '404_page_not_found' => 'Page Not Found',
+    'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.',
+    'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.',
+    'image_not_found' => 'Image Not Found',
+    'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
+    'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
+    'return_home' => 'Return to home',
+    'error_occurred' => 'An Error Occurred',
+    'app_down' => ':appName is down right now',
+    'back_soon' => 'It will be back up soon.',
+
+    // API errors
+    'api_no_authorization_found' => 'No authorization token found on the request',
+    'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',
+    'api_user_token_not_found' => 'No matching API token was found for the provided authorization token',
+    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',
+    'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls',
+    'api_user_token_expired' => 'The authorization token used has expired',
+
+    // Settings & Maintenance
+    'maintenance_test_email_failure' => 'Error thrown when sending a test email:',
+
+];
diff --git a/resources/lang/ka/pagination.php b/resources/lang/ka/pagination.php
new file mode 100644 (file)
index 0000000..85bd12f
--- /dev/null
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Pagination Language Lines
+ * The following language lines are used by the paginator library to build
+ * the simple pagination links.
+ */
+return [
+
+    'previous' => '&laquo; Previous',
+    'next'     => 'Next &raquo;',
+
+];
diff --git a/resources/lang/ka/passwords.php b/resources/lang/ka/passwords.php
new file mode 100644 (file)
index 0000000..b408f3c
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Password Reminder Language Lines
+ * The following language lines are the default lines which match reasons
+ * that are given by the password broker for a password update attempt has failed.
+ */
+return [
+
+    'password' => 'Passwords must be at least eight characters and match the confirmation.',
+    'user' => "We can't find a user with that e-mail address.",
+    'token' => 'The password reset token is invalid for this email address.',
+    'sent' => 'We have e-mailed your password reset link!',
+    'reset' => 'Your password has been reset!',
+
+];
diff --git a/resources/lang/ka/preferences.php b/resources/lang/ka/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
diff --git a/resources/lang/ka/settings.php b/resources/lang/ka/settings.php
new file mode 100644 (file)
index 0000000..f4204dd
--- /dev/null
@@ -0,0 +1,324 @@
+<?php
+/**
+ * Settings text strings
+ * Contains all text strings used in the general settings sections of BookStack
+ * including users and roles.
+ */
+return [
+
+    // Common Messages
+    'settings' => 'Settings',
+    'settings_save' => 'Save Settings',
+    'settings_save_success' => 'Settings saved',
+    'system_version' => 'System Version',
+    'categories' => 'Categories',
+
+    // App Settings
+    'app_customization' => 'Customization',
+    'app_features_security' => 'Features & Security',
+    'app_name' => 'Application Name',
+    'app_name_desc' => 'This name is shown in the header and in any system-sent emails.',
+    'app_name_header' => 'Show name in header',
+    'app_public_access' => 'Public Access',
+    'app_public_access_desc' => 'Enabling this option will allow visitors, that are not logged-in, to access content in your BookStack instance.',
+    'app_public_access_desc_guest' => 'Access for public visitors can be controlled through the "Guest" user.',
+    'app_public_access_toggle' => 'Allow public access',
+    'app_public_viewing' => 'Allow public viewing?',
+    'app_secure_images' => 'Higher Security Image Uploads',
+    'app_secure_images_toggle' => 'Enable higher security image uploads',
+    'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.',
+    'app_default_editor' => 'Default Page Editor',
+    'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.',
+    'app_custom_html' => 'Custom HTML Head Content',
+    'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.',
+    'app_custom_html_disabled_notice' => 'Custom HTML head content is disabled on this settings page to ensure any breaking changes can be reverted.',
+    'app_logo' => 'Application Logo',
+    'app_logo_desc' => 'This image should be 43px in height. <br>Large images will be scaled down.',
+    'app_primary_color' => 'Application Primary Color',
+    'app_primary_color_desc' => 'Sets the primary color for the application including the banner, buttons, and links.',
+    'app_homepage' => 'Application Homepage',
+    'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
+    'app_homepage_select' => 'Select a page',
+    'app_footer_links' => 'Footer Links',
+    'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::<key>" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".',
+    'app_footer_links_label' => 'Link Label',
+    'app_footer_links_url' => 'Link URL',
+    'app_footer_links_add' => 'Add Footer Link',
+    'app_disable_comments' => 'Disable Comments',
+    'app_disable_comments_toggle' => 'Disable comments',
+    'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',
+
+    // Color settings
+    'content_colors' => 'Content Colors',
+    'content_colors_desc' => 'Sets colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.',
+    'bookshelf_color' => 'Shelf Color',
+    'book_color' => 'Book Color',
+    'chapter_color' => 'Chapter Color',
+    'page_color' => 'Page Color',
+    'page_draft_color' => 'Page Draft Color',
+
+    // Registration Settings
+    'reg_settings' => 'Registration',
+    'reg_enable' => 'Enable Registration',
+    'reg_enable_toggle' => 'Enable registration',
+    'reg_enable_desc' => 'When registration is enabled user will be able to sign themselves up as an application user. Upon registration they are given a single, default user role.',
+    'reg_default_role' => 'Default user role after registration',
+    'reg_enable_external_warning' => 'The option above is ignored while external LDAP or SAML authentication is active. User accounts for non-existing members will be auto-created if authentication, against the external system in use, is successful.',
+    'reg_email_confirmation' => 'Email Confirmation',
+    'reg_email_confirmation_toggle' => 'Require email confirmation',
+    'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and this option will be ignored.',
+    'reg_confirm_restrict_domain' => 'Domain Restriction',
+    'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.',
+    'reg_confirm_restrict_domain_placeholder' => 'No restriction set',
+
+    // Maintenance settings
+    'maint' => 'Maintenance',
+    'maint_image_cleanup' => 'Cleanup Images',
+    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',
+    'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions',
+    'maint_image_cleanup_run' => 'Run Cleanup',
+    'maint_image_cleanup_warning' => ':count potentially unused images were found. Are you sure you want to delete these images?',
+    'maint_image_cleanup_success' => ':count potentially unused images found and deleted!',
+    'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',
+    'maint_send_test_email' => 'Send a Test Email',
+    'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',
+    'maint_send_test_email_run' => 'Send test email',
+    'maint_send_test_email_success' => 'Email sent to :address',
+    'maint_send_test_email_mail_subject' => 'Test Email',
+    'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',
+    'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',
+    'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',
+    'maint_recycle_bin_open' => 'Open Recycle Bin',
+    'maint_regen_references' => 'Regenerate References',
+    'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
+    'maint_regen_references_success' => 'Reference index has been regenerated!',
+    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
+
+    // Recycle Bin
+    'recycle_bin' => 'Recycle Bin',
+    'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
+    'recycle_bin_deleted_item' => 'Deleted Item',
+    'recycle_bin_deleted_parent' => 'Parent',
+    'recycle_bin_deleted_by' => 'Deleted By',
+    'recycle_bin_deleted_at' => 'Deletion Time',
+    'recycle_bin_permanently_delete' => 'Permanently Delete',
+    'recycle_bin_restore' => 'Restore',
+    'recycle_bin_contents_empty' => 'The recycle bin is currently empty',
+    'recycle_bin_empty' => 'Empty Recycle Bin',
+    'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?',
+    'recycle_bin_destroy_confirm' => 'This action will permanently delete this item, along with any child elements listed below, from the system and you will not be able to restore this content. Are you sure you want to permanently delete this item?',
+    'recycle_bin_destroy_list' => 'Items to be Destroyed',
+    'recycle_bin_restore_list' => 'Items to be Restored',
+    'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.',
+    'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.',
+    'recycle_bin_restore_parent' => 'Restore Parent',
+    'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.',
+    'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.',
+
+    // Audit Log
+    'audit' => 'Audit Log',
+    'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
+    'audit_event_filter' => 'Event Filter',
+    'audit_event_filter_no_filter' => 'No Filter',
+    'audit_deleted_item' => 'Deleted Item',
+    'audit_deleted_item_name' => 'Name: :name',
+    'audit_table_user' => 'User',
+    'audit_table_event' => 'Event',
+    'audit_table_related' => 'Related Item or Detail',
+    'audit_table_ip' => 'IP Address',
+    'audit_table_date' => 'Activity Date',
+    'audit_date_from' => 'Date Range From',
+    'audit_date_to' => 'Date Range To',
+
+    // Role Settings
+    'roles' => 'Roles',
+    'role_user_roles' => 'User Roles',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
+    'role_create' => 'Create New Role',
+    'role_create_success' => 'Role successfully created',
+    'role_delete' => 'Delete Role',
+    'role_delete_confirm' => 'This will delete the role with the name \':roleName\'.',
+    'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',
+    'role_delete_no_migration' => "Don't migrate users",
+    'role_delete_sure' => 'Are you sure you want to delete this role?',
+    'role_delete_success' => 'Role successfully deleted',
+    'role_edit' => 'Edit Role',
+    'role_details' => 'Role Details',
+    'role_name' => 'Role Name',
+    'role_desc' => 'Short Description of Role',
+    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
+    'role_external_auth_id' => 'External Authentication IDs',
+    'role_system' => 'System Permissions',
+    'role_manage_users' => 'Manage users',
+    'role_manage_roles' => 'Manage roles & role permissions',
+    'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',
+    'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',
+    'role_manage_page_templates' => 'Manage page templates',
+    'role_access_api' => 'Access system API',
+    'role_manage_settings' => 'Manage app settings',
+    'role_export_content' => 'Export content',
+    'role_editor_change' => 'Change page editor',
+    'role_asset' => 'Asset Permissions',
+    'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
+    'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
+    'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',
+    'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.',
+    'role_all' => 'All',
+    'role_own' => 'Own',
+    'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
+    'role_save' => 'Save Role',
+    'role_update_success' => 'Role successfully updated',
+    'role_users' => 'Users in this role',
+    'role_users_none' => 'No users are currently assigned to this role',
+
+    // Users
+    'users' => 'Users',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
+    'user_profile' => 'User Profile',
+    'users_add_new' => 'Add New User',
+    'users_search' => 'Search Users',
+    'users_latest_activity' => 'Latest Activity',
+    'users_details' => 'User Details',
+    'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',
+    'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',
+    'users_role' => 'User Roles',
+    'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.',
+    'users_password' => 'User Password',
+    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
+    'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.',
+    'users_send_invite_option' => 'Send user invite email',
+    'users_external_auth_id' => 'External Authentication ID',
+    'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.',
+    'users_password_warning' => 'Only fill the below if you would like to change your password.',
+    'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
+    'users_delete' => 'Delete User',
+    'users_delete_named' => 'Delete user :userName',
+    'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
+    'users_delete_confirm' => 'Are you sure you want to delete this user?',
+    'users_migrate_ownership' => 'Migrate Ownership',
+    'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',
+    'users_none_selected' => 'No user selected',
+    'users_edit' => 'Edit User',
+    'users_edit_profile' => 'Edit Profile',
+    'users_avatar' => 'User Avatar',
+    'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.',
+    'users_preferred_language' => 'Preferred Language',
+    'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.',
+    'users_social_accounts' => 'Social Accounts',
+    'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.',
+    'users_social_connect' => 'Connect Account',
+    'users_social_disconnect' => 'Disconnect Account',
+    'users_social_connected' => ':socialAccount account was successfully attached to your profile.',
+    'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',
+    'users_api_tokens' => 'API Tokens',
+    'users_api_tokens_none' => 'No API tokens have been created for this user',
+    'users_api_tokens_create' => 'Create Token',
+    'users_api_tokens_expires' => 'Expires',
+    'users_api_tokens_docs' => 'API Documentation',
+    'users_mfa' => 'Multi-Factor Authentication',
+    'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.',
+    'users_mfa_x_methods' => ':count method configured|:count methods configured',
+    'users_mfa_configure' => 'Configure Methods',
+
+    // API Tokens
+    'user_api_token_create' => 'Create API Token',
+    'user_api_token_name' => 'Name',
+    'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.',
+    'user_api_token_expiry' => 'Expiry Date',
+    'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.',
+    'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.',
+    'user_api_token_create_success' => 'API token successfully created',
+    'user_api_token_update_success' => 'API token successfully updated',
+    'user_api_token' => 'API Token',
+    'user_api_token_id' => 'Token ID',
+    'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.',
+    'user_api_token_secret' => 'Token Secret',
+    'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.',
+    'user_api_token_created' => 'Token created :timeAgo',
+    'user_api_token_updated' => 'Token updated :timeAgo',
+    'user_api_token_delete' => 'Delete Token',
+    'user_api_token_delete_warning' => 'This will fully delete this API token with the name \':tokenName\' from the system.',
+    'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?',
+    'user_api_token_delete_success' => 'API token successfully deleted',
+
+    // Webhooks
+    'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
+    'webhooks_create' => 'Create New Webhook',
+    'webhooks_none_created' => 'No webhooks have yet been created.',
+    'webhooks_edit' => 'Edit Webhook',
+    'webhooks_save' => 'Save Webhook',
+    'webhooks_details' => 'Webhook Details',
+    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
+    'webhooks_events' => 'Webhook Events',
+    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
+    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
+    'webhooks_events_all' => 'All system events',
+    'webhooks_name' => 'Webhook Name',
+    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
+    'webhooks_endpoint' => 'Webhook Endpoint',
+    'webhooks_active' => 'Webhook Active',
+    'webhook_events_table_header' => 'Events',
+    'webhooks_delete' => 'Delete Webhook',
+    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
+    'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',
+    'webhooks_format_example' => 'Webhook Format Example',
+    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
+    'webhooks_status' => 'Webhook Status',
+    'webhooks_last_called' => 'Last Called:',
+    'webhooks_last_errored' => 'Last Errored:',
+    'webhooks_last_error_message' => 'Last Error Message:',
+
+
+    //! If editing translations files directly please ignore this in all
+    //! languages apart from en. Content will be auto-copied from en.
+    //!////////////////////////////////
+    'language_select' => [
+        'en' => 'English',
+        'ar' => 'العربية',
+        'bg' => 'Bǎlgarski',
+        'bs' => 'Bosanski',
+        'ca' => 'Català',
+        'cs' => 'Česky',
+        'da' => 'Dansk',
+        'de' => 'Deutsch (Sie)',
+        'de_informal' => 'Deutsch (Du)',
+        'el' => 'ελληνικά',
+        'es' => 'Español',
+        'es_AR' => 'Español Argentina',
+        'et' => 'Eesti keel',
+        'eu' => 'Euskara',
+        'fa' => 'فارسی',
+        'fr' => 'Français',
+        'he' => 'עברית',
+        'hr' => 'Hrvatski',
+        'hu' => 'Magyar',
+        'id' => 'Bahasa Indonesia',
+        'it' => 'Italian',
+        'ja' => '日本語',
+        'ko' => '한국어',
+        'lt' => 'Lietuvių Kalba',
+        'lv' => 'Latviešu Valoda',
+        'nl' => 'Nederlands',
+        'nb' => 'Norsk (Bokmål)',
+        'pl' => 'Polski',
+        'pt' => 'Português',
+        'pt_BR' => 'Português do Brasil',
+        'ro' => 'Română',
+        'ru' => 'Русский',
+        'sk' => 'Slovensky',
+        'sl' => 'Slovenščina',
+        'sv' => 'Svenska',
+        'tr' => 'Türkçe',
+        'uk' => 'Українська',
+        'vi' => 'Tiếng Việt',
+        'zh_CN' => '简体中文',
+        'zh_TW' => '繁體中文',
+    ],
+    //!////////////////////////////////
+];
diff --git a/resources/lang/ka/validation.php b/resources/lang/ka/validation.php
new file mode 100644 (file)
index 0000000..2a676c7
--- /dev/null
@@ -0,0 +1,117 @@
+<?php
+/**
+ * Validation Lines
+ * The following language lines contain the default error messages used by
+ * the validator class. Some of these rules have multiple versions such
+ * as the size rules. Feel free to tweak each of these messages here.
+ */
+return [
+
+    // Standard laravel validation lines
+    'accepted'             => 'The :attribute must be accepted.',
+    'active_url'           => 'The :attribute is not a valid URL.',
+    'after'                => 'The :attribute must be a date after :date.',
+    'alpha'                => 'The :attribute may only contain letters.',
+    'alpha_dash'           => 'The :attribute may only contain letters, numbers, dashes and underscores.',
+    'alpha_num'            => 'The :attribute may only contain letters and numbers.',
+    'array'                => 'The :attribute must be an array.',
+    'backup_codes'         => 'The provided code is not valid or has already been used.',
+    'before'               => 'The :attribute must be a date before :date.',
+    'between'              => [
+        'numeric' => 'The :attribute must be between :min and :max.',
+        'file'    => 'The :attribute must be between :min and :max kilobytes.',
+        'string'  => 'The :attribute must be between :min and :max characters.',
+        'array'   => 'The :attribute must have between :min and :max items.',
+    ],
+    'boolean'              => 'The :attribute field must be true or false.',
+    'confirmed'            => 'The :attribute confirmation does not match.',
+    'date'                 => 'The :attribute is not a valid date.',
+    'date_format'          => 'The :attribute does not match the format :format.',
+    'different'            => 'The :attribute and :other must be different.',
+    'digits'               => 'The :attribute must be :digits digits.',
+    'digits_between'       => 'The :attribute must be between :min and :max digits.',
+    'email'                => 'The :attribute must be a valid email address.',
+    'ends_with' => 'The :attribute must end with one of the following: :values',
+    'file'                 => 'The :attribute must be provided as a valid file.',
+    'filled'               => 'The :attribute field is required.',
+    'gt'                   => [
+        'numeric' => 'The :attribute must be greater than :value.',
+        'file'    => 'The :attribute must be greater than :value kilobytes.',
+        'string'  => 'The :attribute must be greater than :value characters.',
+        'array'   => 'The :attribute must have more than :value items.',
+    ],
+    'gte'                  => [
+        'numeric' => 'The :attribute must be greater than or equal :value.',
+        'file'    => 'The :attribute must be greater than or equal :value kilobytes.',
+        'string'  => 'The :attribute must be greater than or equal :value characters.',
+        'array'   => 'The :attribute must have :value items or more.',
+    ],
+    'exists'               => 'The selected :attribute is invalid.',
+    'image'                => 'The :attribute must be an image.',
+    'image_extension'      => 'The :attribute must have a valid & supported image extension.',
+    'in'                   => 'The selected :attribute is invalid.',
+    'integer'              => 'The :attribute must be an integer.',
+    'ip'                   => 'The :attribute must be a valid IP address.',
+    'ipv4'                 => 'The :attribute must be a valid IPv4 address.',
+    'ipv6'                 => 'The :attribute must be a valid IPv6 address.',
+    'json'                 => 'The :attribute must be a valid JSON string.',
+    'lt'                   => [
+        'numeric' => 'The :attribute must be less than :value.',
+        'file'    => 'The :attribute must be less than :value kilobytes.',
+        'string'  => 'The :attribute must be less than :value characters.',
+        'array'   => 'The :attribute must have less than :value items.',
+    ],
+    'lte'                  => [
+        'numeric' => 'The :attribute must be less than or equal :value.',
+        'file'    => 'The :attribute must be less than or equal :value kilobytes.',
+        'string'  => 'The :attribute must be less than or equal :value characters.',
+        'array'   => 'The :attribute must not have more than :value items.',
+    ],
+    'max'                  => [
+        'numeric' => 'The :attribute may not be greater than :max.',
+        'file'    => 'The :attribute may not be greater than :max kilobytes.',
+        'string'  => 'The :attribute may not be greater than :max characters.',
+        'array'   => 'The :attribute may not have more than :max items.',
+    ],
+    'mimes'                => 'The :attribute must be a file of type: :values.',
+    'min'                  => [
+        'numeric' => 'The :attribute must be at least :min.',
+        'file'    => 'The :attribute must be at least :min kilobytes.',
+        'string'  => 'The :attribute must be at least :min characters.',
+        'array'   => 'The :attribute must have at least :min items.',
+    ],
+    'not_in'               => 'The selected :attribute is invalid.',
+    'not_regex'            => 'The :attribute format is invalid.',
+    'numeric'              => 'The :attribute must be a number.',
+    'regex'                => 'The :attribute format is invalid.',
+    'required'             => 'The :attribute field is required.',
+    'required_if'          => 'The :attribute field is required when :other is :value.',
+    'required_with'        => 'The :attribute field is required when :values is present.',
+    'required_with_all'    => 'The :attribute field is required when :values is present.',
+    'required_without'     => 'The :attribute field is required when :values is not present.',
+    'required_without_all' => 'The :attribute field is required when none of :values are present.',
+    'same'                 => 'The :attribute and :other must match.',
+    'safe_url'             => 'The provided link may not be safe.',
+    'size'                 => [
+        'numeric' => 'The :attribute must be :size.',
+        'file'    => 'The :attribute must be :size kilobytes.',
+        'string'  => 'The :attribute must be :size characters.',
+        'array'   => 'The :attribute must contain :size items.',
+    ],
+    'string'               => 'The :attribute must be a string.',
+    'timezone'             => 'The :attribute must be a valid zone.',
+    'totp'                 => 'The provided code is not valid or has expired.',
+    'unique'               => 'The :attribute has already been taken.',
+    'url'                  => 'The :attribute format is invalid.',
+    'uploaded'             => 'The file could not be uploaded. The server may not accept files of this size.',
+
+    // Custom validation lines
+    'custom' => [
+        'password-confirm' => [
+            'required_with' => 'Password confirmation required',
+        ],
+    ],
+
+    // Custom validation attributes
+    'attributes' => [],
+];
index d3ef939a5912754a7a0501c9ee8b151b7ec623c1..dc32c1355a209afa0e067df4532312c8826710ef 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => '메일을 보낼 수 없었습니다.',
     'email_confirm_success' => '메일 인증을 성공했습니다. 이 메일 주소로 로그인할 수 있습니다.',
     'email_confirm_resent' => '다시 보냈습니다. 메일함을 확인하세요.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => '인증하지 않았습니다.',
     'email_not_confirmed_text' => '인증을 완료하지 않았습니다.',
index 0fdc748d7b3a08b4d0c8af8c9058c89ef01280db..876a0029dd0d6c78db67652faca7dce901daf53d 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => '활동',
     'view' => '보기',
     'view_all' => '모두 보기',
+    'new' => 'New',
     'create' => '만들기',
     'update' => '바꾸기',
     'edit' => '수정',
@@ -80,12 +81,14 @@ return [
     'none' => '없음',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => '헤더 메뉴 펼치기',
     'profile_menu' => '프로필',
     'view_profile' => '프로필 보기',
     'edit_profile' => '프로필 바꾸기',
     'dark_mode' => '어두운 테마',
     'light_mode' => '밝은 테마',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => '정보',
index c86f56bb89241e3cbf87f56ea6ff0c5d98356451..8bd2ffacdc0e210024c9bcb52bedab0e67521362 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => '표시할 텍스트',
     'title' => '제목',
-    'open_link' => '링크 열기...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => '현재 창',
     'open_link_new' => '새 창',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => '레이블 수정',
index a3bd3f36fcd3c2c6780f700430811afdcf91a461..3b73e085888c84c5b982ad5a16aee1f84c71118d 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => '검색 결과',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => '이미지 추가',
     'pages_md_insert_link' => '내부 링크',
     'pages_md_insert_drawing' => '드로잉 추가',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => '챕터에 있는 문서가 아닙니다.',
     'pages_move' => '문서 이동하기',
     'pages_move_success' => ':parentName(으)로 옮김',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => '문서 권한 바꿈',
     'pages_revision' => '수정본',
     'pages_revisions' => '문서 수정본',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => ':pageName 수정본',
     'pages_revision_named' => ':pageName 수정본',
     'pages_revision_restored_from' => '#:id; :summary에서 복구함',
     'pages_revisions_created_by' => '만든 사용자',
     'pages_revisions_date' => '수정한 날짜',
     'pages_revisions_number' => 'No.',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => '수정본 :id',
     'pages_revisions_numbered_changes' => '수정본 :id에서 바꾼 부분',
     'pages_revisions_editor' => '편집기 유형',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => '책꽂이 꼬리표',
     'tag' => '꼬리표',
     'tags' =>  '꼬리표',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  '꼬리표 이름',
     'tag_value' => '리스트 값 (선택 사항)',
     'tags_explain' => "꼬리표로 문서를 분류하세요.",
diff --git a/resources/lang/ko/preferences.php b/resources/lang/ko/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index c69b5d20742dc65ccb79861532b01cda883cafd0..76e835a4276cb59d74b0fb320c57b8bcd4d2ef16 100755 (executable)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => '권한',
     'role_user_roles' => '사용자 권한',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => '권한 만들기',
     'role_create_success' => '권한 만듦',
     'role_delete' => '권한 제거',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => '사용자',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => '사용자 프로필',
     'users_add_new' => '사용자 만들기',
     'users_search' => '사용자 검색',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => '웹 훅',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => '웹 훅 만들기',
     'webhooks_none_created' => '웹 훅이 없습니다.',
     'webhooks_edit' => '웹 훅 수정',
index fb30e57ed7a8d99e1c62343dd8a376610737b09f..02e64e25035a2c329d6423f42b87ce9634946e2b 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Būtinas elektroninio laiško patviritnimas, bet sistema negali išsiųsti laiško. Susisiekite su administratoriumi, kad užtikrintumėte, jog elektroninis paštas atsinaujino teisingai.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'Elektroninio pašto patvirtinimas persiųstas, prašome patikrinti pašto dėžutę.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Elektroninis paštas nepatvirtintas',
     'email_not_confirmed_text' => 'Jūsų elektroninis paštas dar vis nepatvirtintas.',
index 962d2d9d6c80f3481d34b5537e90c7ef03aee828..d2ec44d90123c7787d075a1b51f548629d95dee2 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Veiksmai',
     'view' => 'Rodyti',
     'view_all' => 'Rodyti visus',
+    'new' => 'New',
     'create' => 'Sukurti',
     'update' => 'Atnaujinti',
     'edit' => 'Redaguoti',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Plėsti antraštės meniu',
     'profile_menu' => 'Profilio meniu',
     'view_profile' => 'Rodyti porofilį',
     'edit_profile' => 'Redaguoti profilį',
     'dark_mode' => 'Tamsus rėžimas',
     'light_mode' => 'Šviesus rėžimas',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Informacija',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index ec89cef9fbb4b930873bd71f1159dcc8d2ec2932..80eb479186bc68e65f07388e6be85bab42fd3ae4 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Ieškoti rezultatų',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Įterpti nuotrauką',
     'pages_md_insert_link' => 'Įterpti subjekto nuorodą',
     'pages_md_insert_drawing' => 'Įterpti piešinį',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Puslapio nėra skyriuje',
     'pages_move' => 'Perkelti puslapį',
     'pages_move_success' => 'Puslapis perkeltas į ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Puslapio leidimai atnaujinti',
     'pages_revision' => 'Peržiūra',
     'pages_revisions' => 'Puslapio peržiūros',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Peržiūros puslapio :pageName',
     'pages_revision_named' => 'Peržiūra puslapio :pageName',
     'pages_revision_restored_from' => 'Atkurta iš #:id; :summary',
     'pages_revisions_created_by' => 'Sukurta',
     'pages_revisions_date' => 'Peržiūros data',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Peržiūra #:id',
     'pages_revisions_numbered_changes' => 'Peržiūros #:id pokyčiai',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Lentynų žymos',
     'tag' => 'Žymos',
     'tags' =>  'Žymės',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Žymės pavadinimas',
     'tag_value' => 'Žymos vertė (neprivaloma)',
     'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
diff --git a/resources/lang/lt/preferences.php b/resources/lang/lt/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index f3957c4c2c47a18fb131049bcc72838d6816d97d..10c98aa9b527ac2e8dba57d0bf7c13c1dbff6026 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Vaidmenys',
     'role_user_roles' => 'Naudotojo vaidmenys',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Sukurti naują vaidmenį',
     'role_create_success' => 'Vaidmuo sukurtas sėkmingai',
     'role_delete' => 'Ištrinti vaidmenį',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Naudotojai',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Naudotojo profilis',
     'users_add_new' => 'Pridėti naują naudotoją',
     'users_search' => 'Ieškoti naudotojų',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 009da6cb7092d72f4bb7776d54cb42da92db4ab0..1ace7f720f67a82263111f2d73677eae945350a4 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'E-pasta apriprināšana ir nepieciešama, bet sistēma nevarēja e-pastu nosūtīt. Lūdzu sazinaties ar administratoru, lai pārliecinātos, ka e-pasts ir iestatīts pareizi.',
     'email_confirm_success' => 'Jūsu epasta adrese ir apstiprināta! Jums tagad jābūt iespējai pieslēgties, izmantojot šo epasta adresi.',
     'email_confirm_resent' => 'Apstiprinājuma vēstule tika nosūtīta. Lūdzu, pārbaudiet jūsu e-pastu.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'E-pasts nav apstiprināts',
     'email_not_confirmed_text' => 'Jūsu e-pasta adrese vēl nav apstiprināta.',
index 64a7421c200949fb1f4a28f66cd950ec3d6e49a5..ce8c8ead76a7f5704b47a3aae3b3cb00a9be3012 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Darbības',
     'view' => 'Skatīt',
     'view_all' => 'Skatīt visus',
+    'new' => 'New',
     'create' => 'Izveidot',
     'update' => 'Atjaunināt',
     'edit' => 'Rediģēt',
@@ -80,12 +81,14 @@ return [
     'none' => 'Neviens',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Izvērst galvenes izvēlni',
     'profile_menu' => 'Profila izvēlne',
     'view_profile' => 'Apskatīt profilu',
     'edit_profile' => 'Rediģēt profilu',
     'dark_mode' => 'Tumšais režīms',
     'light_mode' => 'Gaišais režīms',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Informācija',
index d2fe32acb5c055bed61dabf3de9d62b723750246..baba6cb6e1cf0ebc545bfa9778ee82a84df7df0b 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Attēlojamais teksts',
     'title' => 'Nosaukums',
-    'open_link' => 'Atvērt saiti...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Šis logs',
     'open_link_new' => 'Jauns logs',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Ievietot sakļaujamu bloku',
     'collapsible_unwrap' => 'Attīt',
     'edit_label' => 'Rediģēt marķējumu',
index 6c87000a04e22ec7fcdeec9d28bd83f087ccd566..9ef618f5261297e3b6fdb11f022db28fbca44ba4 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Meklēšanas rezultāti',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Ievietot attēlu',
     'pages_md_insert_link' => 'Ievietot vienuma saiti',
     'pages_md_insert_drawing' => 'Ievietot zīmējumu',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Lapa nav nodaļā',
     'pages_move' => 'Pārvietot lapu',
     'pages_move_success' => 'Lapa pārvietota uz ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Lapas atļaujas atjauninātas',
     'pages_revision' => 'Revīzijas',
     'pages_revisions' => 'Lapas revīzijas',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => ':pageName lapas revīzijas',
     'pages_revision_named' => ':pageName lapas revīzija',
     'pages_revision_restored_from' => 'Atjaunots no #:id; :summary',
     'pages_revisions_created_by' => 'Izveidoja',
     'pages_revisions_date' => 'Revīzijas datums',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revīzija #:id',
     'pages_revisions_numbered_changes' => 'Revīzijas #:id izmaiņas',
     'pages_revisions_editor' => 'Redaktora veids',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Plauktu birkas',
     'tag' => 'Birka',
     'tags' =>  'Birkas',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Birkas nosaukums',
     'tag_value' => 'Birkas papildvērtība (neobligāta)',
     'tags_explain' => "Pievieno birkas, lai precīzāk grupētu saturu.\n Tu vari pievienot papildus vērtību birkai vēl precīzākai grupēšanai.",
diff --git a/resources/lang/lv/preferences.php b/resources/lang/lv/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 49a98cf1e922799946a5ecfc866904621c52d095..ec4009b5d06ac09474a3e86c9b3d5b3def04b6be 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Grupas',
     'role_user_roles' => 'Lietotāju grupas',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Izveidot jaunu grupu',
     'role_create_success' => 'Grupa veiksmīgi izveidota',
     'role_delete' => 'Dzēst grupu',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Lietotāji',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Lietotāja profils',
     'users_add_new' => 'Pievienot jaunu lietotāju',
     'users_search' => 'Meklēt lietotājus',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhook',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Izveidot jaunu webhook',
     'webhooks_none_created' => 'Nav izveidots neviens webhook.',
     'webhooks_edit' => 'Labot webhook',
index afeb51e54e9b7d9e4713746e72d6f4ea5f5a6192..968ec2302d9f60aec8bfc85894bb65844423cce0 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Bekreftelse er krevd av systemet, men systemet kan ikke sende disse. Kontakt admin for å løse problemet.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'Bekreftelsespost ble sendt, sjekk innboksen din.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'E-posten er ikke bekreftet.',
     'email_not_confirmed_text' => 'Epost-adressen er ennå ikke bekreftet.',
index 43de015ac8490cddbaeb40e70b48457eb76250e3..af7d0635dd220887b211d5ff8afeb248e8d3f2b3 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Handlinger',
     'view' => 'Vis',
     'view_all' => 'Vis alle',
+    'new' => 'New',
     'create' => 'Opprett',
     'update' => 'Oppdater',
     'edit' => 'Rediger',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Utvid toppmeny',
     'profile_menu' => 'Profilmeny',
     'view_profile' => 'Vis profil',
     'edit_profile' => 'Endre Profile',
     'dark_mode' => 'Kveldsmodus',
     'light_mode' => 'Dagmodus',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Informasjon',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index 39ad6d035c563a7f6ef2319ed204bedf7d6cba72..c0f5b0e05014959872e373a77dd05eb7eeab8651 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Søkeresultater',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Lim inn bilde',
     'pages_md_insert_link' => 'Lim in lenke',
     'pages_md_insert_drawing' => 'Lim inn tegning',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Siden tilhører ingen kapittel',
     'pages_move' => 'Flytt side',
     'pages_move_success' => 'Siden ble flyttet til ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Sidens tilganger ble endret',
     'pages_revision' => 'Revisjon',
     'pages_revisions' => 'Sidens revisjoner',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Revisjoner for :pageName',
     'pages_revision_named' => 'Revisjoner for :pageName',
     'pages_revision_restored_from' => 'Gjenopprettet fra #:id; :summary',
     'pages_revisions_created_by' => 'Skrevet av',
     'pages_revisions_date' => 'Revideringsdato',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revisjon #:id',
     'pages_revisions_numbered_changes' => 'Endringer på revisjon #:id',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Hyllemerker',
     'tag' => 'Merke',
     'tags' =>  'Merker',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Merketittel',
     'tag_value' => 'Merkeverdi (Valgfritt)',
     'tags_explain' => "Legg til merker for å kategorisere innholdet ditt. \n Du kan legge til merkeverdier for å beskrive dem ytterligere.",
diff --git a/resources/lang/nb/preferences.php b/resources/lang/nb/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 6526a7ec01efdcad703197ce3c4829c91f900a9b..54cb7228ce2da4a8c3c6fc28d69502dab6feffd9 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roller',
     'role_user_roles' => 'Kontoroller',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Opprett ny rolle',
     'role_create_success' => 'Rolle opprettet',
     'role_delete' => 'Rolle slettet',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Brukere',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Profil',
     'users_add_new' => 'Register ny konto',
     'users_search' => 'Søk i kontoer',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 1f3b39f4805f1a7122c4c049b4504855950387e5..e0bbe36a7463c89f6d49ebec079ca7ac24503bc3 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'E-mail bevestiging is vereisd maar het systeem kon geen mail verzenden. Neem contact op met de beheerder.',
     'email_confirm_success' => 'Uw e-mailadres is bevestigd! U zou nu moeten kunnen inloggen met dit e-mailadres.',
     'email_confirm_resent' => 'De bevestigingse-mails is opnieuw verzonden. Controleer je inbox.',
+    'email_confirm_thanks' => 'Bedankt voor de bevestiging!',
+    'email_confirm_thanks_desc' => 'Wacht even terwijl uw bevestiging wordt behandeld. Als u na 3 seconden niet wordt doorverwezen, drukt u op de onderstaande link "Doorgaan" om verder te gaan.',
 
     'email_not_confirmed' => 'E-mailadres nog niet bevestigd',
     'email_not_confirmed_text' => 'Je e-mailadres is nog niet bevestigd.',
index ed0e21ad5b8b65ebba8f097fdc7527c8f207181b..3f86fccceb1a0380c9b2aed3899b2a4b402b85e6 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Acties',
     'view' => 'Bekijk',
     'view_all' => 'Bekijk Alle',
+    'new' => 'Nieuw',
     'create' => 'Aanmaken',
     'update' => 'Bijwerken',
     'edit' => 'Bewerk',
@@ -80,12 +81,14 @@ return [
     'none' => 'Geen',
 
     // Header
+    'homepage' => 'Startpagina',
     'header_menu_expand' => 'Header menu uitvouwen',
     'profile_menu' => 'Profiel menu',
     'view_profile' => 'Profiel weergeven',
     'edit_profile' => 'Profiel bewerken',
     'dark_mode' => 'Donkere modus',
     'light_mode' => 'Lichte modus',
+    'global_search' => 'Algemene zoekopdracht',
 
     // Layout tabs
     'tab_info' => 'Info',
index 8b154b96291740a09fb567e5346c0cd9601a7fff..6a3852a0764d0aa3a9c3c8b3d53403560a470173 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Weer te geven tekst',
     'title' => 'Titel',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open koppeling',
+    'open_link_in' => 'Open koppeling in...',
     'open_link_current' => 'Huidig venster',
     'open_link_new' => 'Nieuw venster',
+    'remove_link' => 'Verwijder koppeling',
     'insert_collapsible' => 'Voeg inklapbaar blok toe',
     'collapsible_unwrap' => 'Uitpakken',
     'edit_label' => 'Bewerk label',
index 6786daeccea21f24dabbbacd796134932e3a799c..09471feb83d9b2cd0253f657adc875b47dd08d62 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'De rest',
     'permissions_role_everyone_else_desc' => 'Stel machtigingen in voor alle rollen die niet specifiek overschreven worden.',
     'permissions_role_override' => 'Overschrijf machtigingen voor rol',
+    'permissions_inherit_defaults' => 'Standaardwaarden overnemen',
 
     // Search
     'search_results' => 'Zoekresultaten',
@@ -62,7 +63,7 @@ return [
     'search_terms' => 'Zoektermen',
     'search_content_type' => 'Inhoudstype',
     'search_exact_matches' => 'Exacte matches',
-    'search_tags' => 'Zoek tags',
+    'search_tags' => 'Label Zoekopdrachten',
     'search_options' => 'Opties',
     'search_viewed_by_me' => 'Bekeken door mij',
     'search_not_viewed_by_me' => 'Niet bekeken door mij',
@@ -214,7 +215,7 @@ return [
     'pages_editor_switch_consider_following' => 'Houd rekening met het volgende als u van bewerker verandert:',
     'pages_editor_switch_consideration_a' => 'Eenmaal opgeslagen, zal de nieuwe bewerker keuze gebruikt worden door alle toekomstige gebruikers, ook diegene die zelf niet van bewerker type kunnen veranderen.',
     'pages_editor_switch_consideration_b' => 'Dit kan mogelijks tot een verlies van detail en syntax leiden in bepaalde omstandigheden.',
-    'pages_editor_switch_consideration_c' => 'De veranderingen aan Tags of aan het wijzigingslogboek, sinds de laatste keer opslaan, zullen niet behouden blijven met deze wijziging.',
+    'pages_editor_switch_consideration_c' => 'De veranderingen aan Labels of aan het wijzigingslogboek, sinds de laatste keer opslaan, zullen niet behouden blijven met deze wijziging.',
     'pages_save' => 'Pagina opslaan',
     'pages_title' => 'Pagina titel',
     'pages_name' => 'Pagina naam',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Afbeelding invoegen',
     'pages_md_insert_link' => 'Entity link invoegen',
     'pages_md_insert_drawing' => 'Tekening invoegen',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Deze pagina staat niet in een hoofdstuk',
     'pages_move' => 'Pagina verplaatsten',
     'pages_move_success' => 'Pagina verplaatst naar ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Pagina machtigingen bijgewerkt',
     'pages_revision' => 'Revisie',
     'pages_revisions' => 'Pagina revisies',
+    'pages_revisions_desc' => 'Hieronder staan alle vorige versies van deze pagina. U kunt oude paginaversies terugkijken, vergelijken en herstellen indien de rechten dit toelaten. De volledige geschiedenis van de pagina wordt hier mogelijk niet volledig weergegeven omdat, afhankelijk van de systeemconfiguratie, oude versies al automatisch verwijderd kunnen zijn.',
     'pages_revisions_named' => 'Pagina revisies voor :pageName',
     'pages_revision_named' => 'Pagina revisie voor :pageName',
     'pages_revision_restored_from' => 'Hersteld van #:id; :samenvatting',
     'pages_revisions_created_by' => 'Aangemaakt door',
     'pages_revisions_date' => 'Revisiedatum',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Versie Nummer',
     'pages_revisions_numbered' => 'Revisie #:id',
     'pages_revisions_numbered_changes' => 'Revisie #:id wijzigingen',
     'pages_revisions_editor' => 'Bewerker Type',
@@ -270,26 +275,27 @@ return [
 
     // Editor Sidebar
     'page_tags' => 'Pagina Labels',
-    'chapter_tags' => 'Labels van hoofdstuk',
-    'book_tags' => 'Labels van boeken',
-    'shelf_tags' => 'Labels van boekenplanken',
+    'chapter_tags' => 'Hoofdstuk Labels',
+    'book_tags' => 'Boek Labels',
+    'shelf_tags' => 'Boekenplank Labels',
     'tag' => 'Label',
     'tags' =>  'Labels',
-    'tag_name' =>  'Naam label',
+    'tags_index_desc' => 'Labels kunnen worden toegepast op inhoud binnen het systeem om een flexibele vorm van categorisering toe te passen. Labels kunnen zowel een sleutel als een waarde hebben, waarbij de waarde optioneel is. Eenmaal toegepast, kan de inhoud worden opgevraagd aan de hand van de naam en de waarde van het label.',
+    'tag_name' =>  'Labelnaam',
     'tag_value' => 'Labelwaarde (Optioneel)',
-    'tags_explain' => "Voeg labels toe om de inhoud te categoriseren. \n Je kunt meerdere labels toevoegen.",
-    'tags_add' => 'Voeg een extra label toe',
-    'tags_remove' => 'Dit label verwijderen',
-    'tags_usages' => 'Totaal tag gebruik',
+    'tags_explain' => "Voeg enkele labels toe om uw inhoud beter te categoriseren. \nJe kunt een waarde aan een label toekennen voor een meer gedetailleerde organisatie.",
+    'tags_add' => 'Voeg nog een label toe',
+    'tags_remove' => 'Verwijder deze label',
+    'tags_usages' => 'Totaal aantal gebruikte labels',
     'tags_assigned_pages' => 'Toegewezen aan pagina\'s',
     'tags_assigned_chapters' => 'Toegewezen aan hoofdstukken',
     'tags_assigned_books' => 'Toegewezen aan boeken',
     'tags_assigned_shelves' => 'Toegewezen aan boekenplanken',
     'tags_x_unique_values' => ':count unieke waarden',
     'tags_all_values' => 'Alle waarden',
-    'tags_view_tags' => 'Bekijk Tags',
-    'tags_view_existing_tags' => 'Bekijk bestaande tags',
-    'tags_list_empty_hint' => 'Tags kunnen worden toegekend via de zijbalk van de pagina-bewerker of tijdens het bewerken van de details van een boek, hoofdstuk of boekenplank.',
+    'tags_view_tags' => 'Bekijk Labels',
+    'tags_view_existing_tags' => 'Bekijk bestaande labels',
+    'tags_list_empty_hint' => 'Labels kunnen worden toegekend via de zijbalk van de pagina-bewerker of tijdens het bewerken van de details van een boek, hoofdstuk of boekenplank.',
     'attachments' => 'Bijlages',
     'attachments_explain' => 'Upload bijlages of voeg een link toe. Deze worden zichtbaar in het navigatiepaneel.',
     'attachments_explain_instant_save' => 'Wijzigingen worden meteen opgeslagen.',
diff --git a/resources/lang/nl/preferences.php b/resources/lang/nl/preferences.php
new file mode 100644 (file)
index 0000000..f587e99
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Snelkoppelingen',
+    'shortcuts_interface' => 'Toetsencombinaties voor de gebruikersinterface',
+    'shortcuts_toggle_desc' => 'Hier kunt u toetscombinaties voor de gebruikersinterface in- of uitschakelen voor navigatie en acties.',
+    'shortcuts_customize_desc' => 'U kunt elk van de onderstaande toetsencombinaties aanpassen. Druk simpelweg op de gewenste toetscombinatie na het selecteren van de invoer voor een toetscombinatie.',
+    'shortcuts_toggle_label' => 'Toetsencombinaties ingeschakeld',
+    'shortcuts_section_navigation' => 'Navigatie',
+    'shortcuts_section_actions' => 'Gebruikelijke acties',
+    'shortcuts_save' => 'Sla Toetsencombinaties Op',
+    'shortcuts_overlay_desc' => 'Opmerking: Wanneer toetsencombinaties zijn ingeschakeld, is een overlay beschikbaar door op "?" te drukken, die de momenteel beschikbare toetscombinaties voor acties op het scherm markeert.',
+    'shortcuts_update_success' => 'Toetsencombinatievoorkeuren zijn bijgewerkt!',
+];
\ No newline at end of file
index 413d6c4aaf4c747945359a5df6764ea4e13a4bea..470805053b05206a7e1a6d63866eb4de52286932 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Rollen',
     'role_user_roles' => 'Gebruikersrollen',
+    'roles_index_desc' => 'Rollen worden gebruikt om gebruikers te groeperen en systeemrechten te geven. Wanneer een gebruiker lid is van meerdere rollen worden de toegekende rechten samengevoegd en erft de gebruiker alle mogelijkheden.',
+    'roles_x_users_assigned' => '1 gebruiker toegewezen|:count gebruikers toegewezen',
+    'roles_x_permissions_provided' => '1 machtiging|:count machtigingen',
+    'roles_assigned_users' => 'Toegewezen Gebruikers',
+    'roles_permissions_provided' => 'Verleende Machtigingen',
     'role_create' => 'Nieuwe Rol Maken',
     'role_create_success' => 'Rol succesvol aangemaakt',
     'role_delete' => 'Rol Verwijderen',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Gebruikers',
+    'users_index_desc' => 'Creëer en beheer individuele gebruikersaccounts binnen het systeem. Gebruikersaccounts worden gebruikt voor aanmelding en toekenning van inhoud en activiteiten. Toegangsmachtigingen zijn voornamelijk gebaseerd op rollen, maar het eigendom van gebruikersinhoud en andere factoren kunnen ook van invloed zijn op machtigingen en toegang.',
     'user_profile' => 'Gebruikersprofiel',
     'users_add_new' => 'Gebruiker toevoegen',
     'users_search' => 'Gebruiker zoeken',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks zijn een manier om gegevens naar externe URL\'s te sturen wanneer bepaalde acties en gebeurtenissen in het systeem plaatsvinden, wat op gebeurtenissen gebaseerde integratie met externe platforms zoals berichten- of notificatiesystemen mogelijk maakt.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Nieuwe Webhook Maken',
     'webhooks_none_created' => 'Er zijn nog geen webhooks aangemaakt.',
     'webhooks_edit' => 'Bewerk Webhook',
index df9ba4bd6ab75878e3ecff3bc4a245a5cbd00941..d3e8c09b9801bb67f88285be269673f15a44272c 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Wymagane jest potwierdzenie hasła, lecz wiadomość nie mogła zostać wysłana. Skontaktuj się z administratorem w celu upewnienia się, że skrzynka została skonfigurowana prawidłowo.',
     'email_confirm_success' => 'Twój e-mail został potwierdzony! Powinieneś teraz mieć możliwość zalogowania się za pomocą tego adresu e-mail.',
     'email_confirm_resent' => 'E-mail z potwierdzeniem został wysłany ponownie, sprawdź swoją skrzynkę odbiorczą.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Adres e-mail nie został potwierdzony',
     'email_not_confirmed_text' => 'Twój adres e-mail nie został jeszcze potwierdzony.',
index e6c08dfa598b228edc52192c7e774ca0a703cb04..61adf67b6142afa6d8244f641baa976d2b5a91f7 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Akcje',
     'view' => 'Widok',
     'view_all' => 'Zobacz wszystkie',
+    'new' => 'Nowe',
     'create' => 'Utwórz',
     'update' => 'Zaktualizuj',
     'edit' => 'Edytuj',
@@ -80,12 +81,14 @@ return [
     'none' => 'Brak',
 
     // Header
+    'homepage' => 'Strona domowa',
     'header_menu_expand' => 'Rozwiń menu nagłówka',
     'profile_menu' => 'Menu profilu',
     'view_profile' => 'Zobacz profil',
     'edit_profile' => 'Edytuj profil',
     'dark_mode' => 'Tryb ciemny',
     'light_mode' => 'Tryb jasny',
+    'global_search' => 'Wyszukiwanie globalne',
 
     // Layout tabs
     'tab_info' => 'Informacje',
index ceaf52375621f09a69ad72662e83a39745fec8d6..9b19e266768a34c5e56a2fbb6d6f913763296cdb 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'Adres URL',
     'text_to_display' => 'Tekst do wyświetlenia',
     'title' => 'Tytuł',
-    'open_link' => 'Otwórz link za pomocą...',
+    'open_link' => 'Otwórz link',
+    'open_link_in' => 'Otwórz link w...',
     'open_link_current' => 'Bieżące okno',
     'open_link_new' => 'Nowe okno',
+    'remove_link' => 'Usuń link',
     'insert_collapsible' => 'Wstaw zwijalny blok',
     'collapsible_unwrap' => 'Rozwiń',
     'edit_label' => 'Edytuj etykietę',
index f50787892419a8bcf955c3d4db5c4364e157c1aa..58ae3ac7a394006af52c77b04221fce81504fdf9 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Wszyscy inni',
     'permissions_role_everyone_else_desc' => 'Ustaw uprawnienia dla wszystkich nienadpisywanych specjalnie ról.',
     'permissions_role_override' => 'Nadpisz uprawnienia dla roli',
+    'permissions_inherit_defaults' => 'Dziedzicz wartości domyślne',
 
     // Search
     'search_results' => 'Wyniki wyszukiwania',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Wstaw obrazek',
     'pages_md_insert_link' => 'Wstaw łącze do obiektu',
     'pages_md_insert_drawing' => 'Wstaw rysunek',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Strona nie została umieszczona w rozdziale',
     'pages_move' => 'Przenieś stronę',
     'pages_move_success' => 'Strona przeniesiona do ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Zaktualizowano uprawnienia strony',
     'pages_revision' => 'Wersja',
     'pages_revisions' => 'Wersje strony',
+    'pages_revisions_desc' => 'Poniżej wymieniono wszystkie poprzednie wersje tej strony. Możesz cofnąć się wstecz, porównać i przywrócić stare wersje stron, jeśli pozwalają na to uprawnienia. Pełna historia strony może nie być całkowicie odzwierciedlona, ponieważ w zależności od konfiguracji systemu stare wersje mogą zostać automatycznie usunięte.',
     'pages_revisions_named' => 'Wersje strony :pageName',
     'pages_revision_named' => 'Wersja strony :pageName',
     'pages_revision_restored_from' => 'Przywrócono z #:id; :summary',
     'pages_revisions_created_by' => 'Utworzona przez',
     'pages_revisions_date' => 'Data wersji',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Numer wersji',
     'pages_revisions_numbered' => 'Wersja #:id',
     'pages_revisions_numbered_changes' => 'Zmiany w wersji #:id',
     'pages_revisions_editor' => 'Typ edytora',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Tagi półki',
     'tag' => 'Tag',
     'tags' =>  'Tagi',
+    'tags_index_desc' => 'Tagi mogą być stosowane do treści w systemie w celu umożliwienia elastycznej formy kategoryzacji. Tagi mogą mieć zarówno klucz, jak i wartość, przy czym wartość jest opcjonalna. Po zastosowaniu tagu treść może być wyszukana przy użyciu nazwy i wartości tagu.',
     'tag_name' =>  'Nazwa tagu',
     'tag_value' => 'Wartość tagu (opcjonalnie)',
     'tags_explain' => "Dodaj tagi by skategoryzować zawartość. \n W celu dokładniejszej organizacji zawartości możesz dodać wartości do tagów.",
diff --git a/resources/lang/pl/preferences.php b/resources/lang/pl/preferences.php
new file mode 100644 (file)
index 0000000..df34b21
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Skróty',
+    'shortcuts_interface' => 'Interfejs Skrótów Klawiszowych',
+    'shortcuts_toggle_desc' => 'Tutaj możesz włączyć lub wyłączyć interfejs skrótów klawiszowych używanych do nawigacji i akcji.',
+    'shortcuts_customize_desc' => 'Możesz dostosować każdy z poniższych skrótów. Wystarczy nacisnąć wybraną kombinację klawiszy po wybraniu wprowadzania dla danego skrótu.',
+    'shortcuts_toggle_label' => 'Skróty klawiszowe włączone',
+    'shortcuts_section_navigation' => 'Nawigacja',
+    'shortcuts_section_actions' => 'Częste działania',
+    'shortcuts_save' => 'Zapisz skróty',
+    'shortcuts_overlay_desc' => 'Uwaga: Gdy skróty są włączone, przez naciśnięcie "?" może być otworzona nakładka pomocnicza, która podświetli dostępne skróty dla akcji widocznych obecnie na ekranie.',
+    'shortcuts_update_success' => 'Ustawienia skrótów zostały zaktualizowane!',
+];
\ No newline at end of file
index 263a5644fad29499c97c09d0d3332e91af876d1b..598f4dcd6e11d928f4c62aaf1ec1e181474b9e7c 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Role',
     'role_user_roles' => 'Role użytkowników',
+    'roles_index_desc' => 'Role są używane do grupowania użytkowników i udzielania uprawnień systemowych ich członkom. Gdy użytkownik jest członkiem wielu ról, przyznane uprawnienia będą gromadzone, a użytkownik odziedziczy wszystkie możliwości.',
+    'roles_x_users_assigned' => '1 użytkownik przypisany|:count użytkowników przypisanych',
+    'roles_x_permissions_provided' => '1 uprawnienie|:count uprawnień',
+    'roles_assigned_users' => 'Przypisani Użytkownicy',
+    'roles_permissions_provided' => 'Przyznawane Uprawnienia',
     'role_create' => 'Utwórz nową rolę',
     'role_create_success' => 'Rola utworzona pomyślnie',
     'role_delete' => 'Usuń rolę',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Użytkownicy',
+    'users_index_desc' => 'Twórz indywidualne konta użytkowników w systemie i zarządzaj nimi. Konta użytkowników są używane do logowania i przypisywania treści i aktywności. Uprawnienia dostępu są przede wszystkim oparte na roli, ale posiadanie przez użytkownika zawartości, podobnie jak inne czynniki może również wpływać na uprawnienia i dostęp.',
     'user_profile' => 'Profil użytkownika',
     'users_add_new' => 'Dodaj użytkownika',
     'users_search' => 'Wyszukaj użytkownika',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooki',
+    'webhooks_index_desc' => 'Webhooki to sposób na wysyłanie danych do zewnętrznych adresów URL, gdy pewne działania i zdarzenia zachodzą w ramach systemu, co umożliwia integrację zdarzeń w systemie z zewnętrznymi platformami, takimi jak systemy wysyłania wiadomości lub powiadamiania.',
+    'webhooks_x_trigger_events' => '1 zdarzenie wyzwalacza|:count zdarzeń wyzwalacza',
     'webhooks_create' => 'Utwórz nowy Webhook',
     'webhooks_none_created' => 'Nie utworzono jeszcze żadnych webhooków.',
     'webhooks_edit' => 'Edytuj Webhook',
index a815b9ccad2fbdf852a403e10f8d78ee77c7391c..a88bf6ab87d9763574c355c1205d4018db3aead6 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'A confirmação do endereço de e-mail é requerida, mas o sistema não pôde enviar a mensagem. Por favor, entre em contacto com o administrador para se certificar que o serviço de envio de e-mails está corretamente configurado.',
     'email_confirm_success' => 'O seu endereço de email foi confirmado! Neste momento já poderá entrar usando este endereço de email.',
     'email_confirm_resent' => 'E-mail de confirmação reenviado. Por favor, verifique a sua caixa de entrada.',
+    'email_confirm_thanks' => 'Obrigado por confirmar!',
+    'email_confirm_thanks_desc' => 'Por favor, aguarde um momento enquanto a sua confirmação é tratada. Se não for redirecionado após 3 segundos pressione "Continuar" para prosseguir.',
 
     'email_not_confirmed' => 'Endereço de E-mail Não Confirmado',
     'email_not_confirmed_text' => 'O seu endereço de e-mail ainda não foi confirmado.',
index 828c285470c3e862c5e4935cfe423711b6747766..0ee11149f02376a637389536081b3f886a2342da 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Ações',
     'view' => 'Visualizar',
     'view_all' => 'Visualizar Todos',
+    'new' => 'Novo',
     'create' => 'Criar',
     'update' => 'Atualizar',
     'edit' => 'Editar',
@@ -80,12 +81,14 @@ return [
     'none' => 'Nenhum',
 
     // Header
+    'homepage' => 'Página inicial',
     'header_menu_expand' => 'Expandir Menu de Cabeçalho',
     'profile_menu' => 'Menu de Perfil',
     'view_profile' => 'Visualizar Perfil',
     'edit_profile' => 'Editar Perfil',
     'dark_mode' => 'Modo Escuro',
     'light_mode' => 'Modo Claro',
+    'global_search' => 'Pesquisa global',
 
     // Layout tabs
     'tab_info' => 'Informações',
index cbd84106b9a37f5cc04d0351b9a6e31d25396550..f7bc6c894cdb6ad68001099a8ee22eb7d6f70d63 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Texto a ser exibido',
     'title' => 'Título',
-    'open_link' => 'Abrir link em...',
+    'open_link' => 'Abrir ligação',
+    'open_link_in' => 'Abrir ligação em...',
     'open_link_current' => 'Janela atual',
     'open_link_new' => 'Nova janela',
+    'remove_link' => 'Remover ligação',
     'insert_collapsible' => 'Inserir bloco colapsável',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Editar etiqueta',
index f3751ad133a5735e6d90111b80527afe34596354..9fab3949a03235304b96453a083bb57b3651d3b9 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Restante',
     'permissions_role_everyone_else_desc' => 'Definir permissões para todos os papéis não substituídos especificamente.',
     'permissions_role_override' => 'Substituir permissões para o papel',
+    'permissions_inherit_defaults' => 'Herdar padrões',
 
     // Search
     'search_results' => 'Resultado(s) da Pesquisa',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Inserir Imagem',
     'pages_md_insert_link' => 'Inserir Link para Entidade',
     'pages_md_insert_drawing' => 'Inserir Desenho',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'A página não está dentro de um capítulo',
     'pages_move' => 'Mover Página',
     'pages_move_success' => 'Pagina movida para ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Permissões da Página atualizadas',
     'pages_revision' => 'Revisão',
     'pages_revisions' => 'Revisões da Página',
+    'pages_revisions_desc' => 'Abaixo estão listadas todas as revisões anteriores desta página. Pode analisar, comparar e restaurar versões de páginas antigas se as permissões o permitirem. O histórico completo da página pode não ser totalmente refletido aqui, uma vez que, dependendo da configuração do sistema, revisões antigas podem ser auto-excluídas.',
     'pages_revisions_named' => 'Revisões de Página para :pageName',
     'pages_revision_named' => 'Revisão de Página para :pageName',
     'pages_revision_restored_from' => 'Recuperado de #:id; :summary',
     'pages_revisions_created_by' => 'Criado por',
     'pages_revisions_date' => 'Data da Revisão',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Número da Revisão',
     'pages_revisions_numbered' => 'Revisão #:id',
     'pages_revisions_numbered_changes' => 'Alterações da Revisão #:id',
     'pages_revisions_editor' => 'Tipo de editor',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Etiquetas da Prateleira',
     'tag' => 'Etiqueta',
     'tags' =>  'Etiquetas',
+    'tags_index_desc' => 'As etiquetas podem ser aplicadas a conteúdos do sistema para aplicar uma forma flexível de categorização. As etiquetas podem ter uma chave e um valor, sendo o valor opcional. Uma vez aplicado, o conteúdo pode ser consultado através do nome e/ou do valor da etiqueta.',
     'tag_name' =>  'Nome da Etiqueta',
     'tag_value' => 'Valor da Etiqueta (Opcional)',
     'tags_explain' => "Adicione algumas etiquetas para melhor categorizar o seu conteúdo. \n Você poderá atribuir valores às etiquetas para uma organização mais complexa.",
diff --git a/resources/lang/pt/preferences.php b/resources/lang/pt/preferences.php
new file mode 100644 (file)
index 0000000..4784aef
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Atalhos',
+    'shortcuts_interface' => 'Atalhos de Teclado',
+    'shortcuts_toggle_desc' => 'Aqui pode ativar ou desativar os atalhos de teclado do sistema, usados para navegação e ações.',
+    'shortcuts_customize_desc' => 'Pode personalizar cada um dos atalhos abaixo. Pressione a combinação de tecla desejada após selecionar a entrada para um atalho.',
+    'shortcuts_toggle_label' => 'Atalhos de teclado ativados',
+    'shortcuts_section_navigation' => 'Navegação',
+    'shortcuts_section_actions' => 'Ações comuns',
+    'shortcuts_save' => 'Salvar Atalhos',
+    'shortcuts_overlay_desc' => 'Nota: Quando os atalhos estão ativados, um balão de ajuda ficará disponível pressionando "?" destacando os atalhos disponíveis para ações atualmente visíveis na tela.',
+    'shortcuts_update_success' => 'As suas preferências de atalhos foram guardadas!',
+];
\ No newline at end of file
index 2811575198c7105738d714bf30b84ea9427f82b8..c88864fa8b0d2456b10aa68208cb46abd9602bcf 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Cargos',
     'role_user_roles' => 'Cargos de Utilizador',
+    'roles_index_desc' => 'Papéis são usados para agrupar utilizadores & fornecer permissão ao sistema para os seus membros. Quando um utilizador é membro de múltiplas funções, os privilégios concedidos irão acumular e o utilizador herdará todas as habilidades.',
+    'roles_x_users_assigned' => '1 utilizador atribuído|:count utilizadores atribuídos',
+    'roles_x_permissions_provided' => '1 permissão|:count permissões',
+    'roles_assigned_users' => 'Utilizadores atribuídos',
+    'roles_permissions_provided' => 'Permissões fornecidas',
     'role_create' => 'Criar novo Cargo',
     'role_create_success' => 'Cargo criado com sucesso',
     'role_delete' => 'Excluir Cargo',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Utilizadores',
+    'users_index_desc' => 'Crie & gira individualmente contas de utilizador no sistema. Contas de utilizador são usadas para iniciar sessão e atribuição de conteúdo & atividade. As permissões de acesso são principalmente baseadas em funções, mas a propriedade de conteúdo do utilizador, entre outros fatores, também pode afetar permissões e acesso.',
     'user_profile' => 'Perfil do Utilizador',
     'users_add_new' => 'Adicionar Novo Utilizador',
     'users_search' => 'Pesquisar Utilizadores',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks são uma maneira de enviar dados para URLs externas quando certas ações e eventos ocorrem no sistema. Isto permite uma integração baseada em eventos com plataformas externas como mensagens ou sistemas de notificação.',
+    'webhooks_x_trigger_events' => '1 acionador|:count acionadores',
     'webhooks_create' => 'Criar um novo webhook',
     'webhooks_none_created' => 'Ainda nenhum webhooks foi criado.',
     'webhooks_edit' => 'Editar Webhook',
index 60812af0ffff53ba0833bf4f13249103d070f9d7..71d8b73a2b3ce663e7e31034e0a8df8e9fbf521f 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'A confirmação de e-mail é requerida, mas o sistema não pôde enviar a mensagem. Por favor, entre em contato com o administrador para se certificar que o serviço de envio de e-mails está corretamente configurado.',
     'email_confirm_success' => 'Seu e-mail foi confirmado! Agora você pode de entrar usando este endereço de e-mail.',
     'email_confirm_resent' => 'E-mail de confirmação reenviado. Por favor, verifique sua caixa de entrada.',
+    'email_confirm_thanks' => 'Obrigado por confirmar!',
+    'email_confirm_thanks_desc' => 'Aguarde um momento enquanto sua confirmação é processada. Se você não for redirecionado após 3 segundos, pressione o link "Continuar" abaixo para continuar.',
 
     'email_not_confirmed' => 'Endereço de E-mail Não Confirmado',
     'email_not_confirmed_text' => 'Seu endereço de e-mail ainda não foi confirmado.',
index 99aecfd923d049914a144690f4cfd779652e2065..9baa4185c2f3e28e4b58855fecfbacc00980329f 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Ações',
     'view' => 'Visualizar',
     'view_all' => 'Visualizar Tudo',
+    'new' => 'Novo',
     'create' => 'Criar',
     'update' => 'Atualizar',
     'edit' => 'Editar',
@@ -80,12 +81,14 @@ return [
     'none' => 'Nenhum',
 
     // Header
+    'homepage' => 'Página inicial',
     'header_menu_expand' => 'Expandir Cabeçalho do Menu',
     'profile_menu' => 'Menu de Perfil',
     'view_profile' => 'Visualizar Perfil',
     'edit_profile' => 'Editar Perfil',
     'dark_mode' => 'Modo Escuro',
     'light_mode' => 'Modo Claro',
+    'global_search' => 'Pesquisa Global',
 
     // Layout tabs
     'tab_info' => 'Informações',
index c0f0d406bec6bc8d69b5a8001a68612f8cc2e506..71e284475b623e070eefcdd13cb01ea8d544547b 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Texto de exibição',
     'title' => 'Título',
-    'open_link' => 'Abrir link em...',
+    'open_link' => 'Link aberto',
+    'open_link_in' => 'Abrir link em...',
     'open_link_current' => 'Janelas atuais',
     'open_link_new' => 'Nova janela',
+    'remove_link' => 'Remover link',
     'insert_collapsible' => 'Inserir bloco colapsável',
     'collapsible_unwrap' => 'Desembrulhar',
     'edit_label' => 'Editar etiqueta',
index 68f95c420bd5416c77affac49cee5e0e2a0b69fa..20fd038e4c666cb3a11714902789944b507e532a 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Todos os outros',
     'permissions_role_everyone_else_desc' => 'Defina permissões para todas as funções não especificamente substituídas.',
     'permissions_role_override' => 'Substituir permissões para função',
+    'permissions_inherit_defaults' => 'Herdar padrões',
 
     // Search
     'search_results' => 'Resultado(s) da Pesquisa',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Inserir Imagem',
     'pages_md_insert_link' => 'Inserir Link para Entidade',
     'pages_md_insert_drawing' => 'Inserir Desenho',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Página não está dentro de um capítulo',
     'pages_move' => 'Mover Página',
     'pages_move_success' => 'Pagina movida para ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Permissões da Página atualizadas',
     'pages_revision' => 'Revisão',
     'pages_revisions' => 'Revisões da Página',
+    'pages_revisions_desc' => 'Listadas abaixo estão todas as revisões anteriores desta página. Você pode rever, comparar e restaurar versões antigas de páginas se as permissões permitirem. O histórico completo da página pode não ser totalmente refletido aqui, pois, dependendo da configuração do sistema, as revisões antigas podem ser excluídas automaticamente.',
     'pages_revisions_named' => 'Revisões de Página para :pageName',
     'pages_revision_named' => 'Revisão de Página para :pageName',
     'pages_revision_restored_from' => 'Restaurado de #:id; :summary',
     'pages_revisions_created_by' => 'Criada por',
     'pages_revisions_date' => 'Data da Revisão',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Número de revisão',
     'pages_revisions_numbered' => 'Revisão #:id',
     'pages_revisions_numbered_changes' => 'Alterações da Revisão #:id',
     'pages_revisions_editor' => 'Tipo de editor',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Tags de Prateleira',
     'tag' => 'Tag',
     'tags' =>  'Tags',
+    'tags_index_desc' => 'As tags podem ser aplicadas ao conteúdo dentro do sistema para aplicar uma forma flexível de categorização. As tags podem ter uma chave e um valor, sendo o valor opcional. Depois de aplicado, o conteúdo pode ser consultado usando o nome e o valor da tag.',
     'tag_name' =>  'Nome da Tag',
     'tag_value' => 'Valor da Tag (Opcional)',
     'tags_explain' => "Adicione algumas tags para melhor categorizar seu conteúdo. \n Você pode atribuir valores às tags para uma organização mais complexa.",
diff --git a/resources/lang/pt_BR/preferences.php b/resources/lang/pt_BR/preferences.php
new file mode 100644 (file)
index 0000000..a10e3dd
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Atalhos',
+    'shortcuts_interface' => 'Atalhos de Teclado da Interface',
+    'shortcuts_toggle_desc' => 'Aqui você pode habilitar ou desabilitar os atalhos da interface do sistema de teclado, usados para navegação e ações.',
+    'shortcuts_customize_desc' => 'Você pode personalizar cada um dos atalhos abaixo. Basta pressionar a combinação de teclas desejada após selecionar a entrada para um atalho.',
+    'shortcuts_toggle_label' => 'Atalhos de teclado ativados',
+    'shortcuts_section_navigation' => 'Navegação',
+    'shortcuts_section_actions' => 'Ações Comuns',
+    'shortcuts_save' => 'Salvar Atalhos',
+    'shortcuts_overlay_desc' => 'Observação: quando os atalhos estão ativados, uma sobreposição auxiliar está disponível pressionando "?" que destacará os atalhos disponíveis para ações atualmente visíveis na tela.',
+    'shortcuts_update_success' => 'As preferências de atalho foram atualizadas!',
+];
\ No newline at end of file
index 4f2f1d3482efddd3993080a07b8d4517963640f5..05698d46de08886214932e151ee1394612418275 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Cargos',
     'role_user_roles' => 'Cargos de Usuário',
+    'roles_index_desc' => 'As funções são usadas para agrupar usuários & fornecer permissão de sistema a seus membros. Quando um usuário é membro de várias funções, os privilégios concedidos serão acumulados e o usuário herdará todas as habilidades.',
+    'roles_x_users_assigned' => '1 usuário atribuído|:count usuários atribuídos',
+    'roles_x_permissions_provided' => '1 permissão|:count permissões',
+    'roles_assigned_users' => 'Usuários atribuídos',
+    'roles_permissions_provided' => 'Permissões fornecidas',
     'role_create' => 'Criar novo Cargo',
     'role_create_success' => 'Cargo criado com sucesso',
     'role_delete' => 'Excluir Cargo',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Usuários',
+    'users_index_desc' => 'Crie e gerencie contas de usuários individuais dentro do sistema. As contas de usuário são usadas para login e atribuição de conteúdo e atividade. As permissões de acesso são baseadas principalmente na função, mas a propriedade do conteúdo do usuário, entre outros fatores, também pode afetar as permissões e o acesso.',
     'user_profile' => 'Perfil do Usuário',
     'users_add_new' => 'Adicionar Novo Usuário',
     'users_search' => 'Pesquisar Usuários',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Os webhooks são uma maneira de enviar dados para URLs externos quando certas ações e eventos ocorrem dentro do sistema, o que permite a integração baseada em eventos com plataformas externas, como sistemas de mensagens ou notificação.',
+    'webhooks_x_trigger_events' => '1 evento de gatilho|:count evento de gatilho',
     'webhooks_create' => 'Criar novo webhook',
     'webhooks_none_created' => 'Nenhum webhooks foi criado ainda.',
     'webhooks_edit' => 'Editar webhook',
index 05a01c6c64615e0669faef58c5492e9236e359b6..d21c0e4b1f5617500ff6bb4cba0ba0708e7787ac 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Este necesară confirmarea prin e-mail, dar sistemul nu a putut trimite e-mailul. Contactează administratorul pentru a te asigura că e-mailul este configurat corect.',
     'email_confirm_success' => 'E-mailul a fost confirmat! Acum ar trebui să te poți autentifica folosind această adresă de e-mail.',
     'email_confirm_resent' => 'E-mailul de confirmare a fost retrimis, te rugăm să îți verifici căsuța de e-mail.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Adresa de e-mail neconfirmată',
     'email_not_confirmed_text' => 'Adresa ta de e-mail nu a fost încă confirmată.',
index a98a20beef5ebdfb167ec0e620b7a0830b76cfb3..76216a4546443daa959445d2fb843eb9b5160c2c 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Acțiuni',
     'view' => 'Vizualizare',
     'view_all' => 'Vizualizează tot',
+    'new' => 'New',
     'create' => 'Crează',
     'update' => 'Actualzează',
     'edit' => 'Editează',
@@ -80,12 +81,14 @@ return [
     'none' => 'Niciunul',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Extindere meniu antet',
     'profile_menu' => 'Meniu profil',
     'view_profile' => 'Vezi profil',
     'edit_profile' => 'Editare profil',
     'dark_mode' => 'Mod întunecat',
     'light_mode' => 'Mod luminos',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Informații',
index eef26658f3e77bd018daa056a2d29afcb03db2d2..3b3c61c1a41de85c3ef665435dc8fcf07b23c55f 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text de afișat',
     'title' => 'Titlu',
-    'open_link' => 'Deschie link în...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Fereastra curentă',
     'open_link_new' => 'Fereastră nouă',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Inserează bloc colapsabil',
     'collapsible_unwrap' => 'Desfășoară',
     'edit_label' => 'Editează eticheta',
index fec3966022eb35e6fb5d99ae97be72ea92854b33..f500e84c77db8af2325229aa8963ec9c23a99029 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Rezultatele căutării',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Inserare imagine',
     'pages_md_insert_link' => 'Inserează link-ul entității',
     'pages_md_insert_drawing' => 'Inserează desen',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Pagina nu este într-un capitol',
     'pages_move' => 'Mută pagina',
     'pages_move_success' => 'Pagina mutată la ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Permisiuni pagină actualizate',
     'pages_revision' => 'Revizuire',
     'pages_revisions' => 'Revizuiri pagină',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Revizuiri pagină pentru :pageName',
     'pages_revision_named' => 'Revizuire pagină pentru :pageName',
     'pages_revision_restored_from' => 'Restaurat de la #:id; :summary',
     'pages_revisions_created_by' => 'Creat de',
     'pages_revisions_date' => 'Data revizuirii',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revizuire #:id',
     'pages_revisions_numbered_changes' => 'Revizuirea #:id Modificări',
     'pages_revisions_editor' => 'Tip editor',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Etichete raft',
     'tag' => 'Etichetă',
     'tags' =>  'Etichete',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Nume etichetă',
     'tag_value' => 'Valoare etichetă (opțional)',
     'tags_explain' => "Adăugați unele etichete pentru a clasifica mai bine conținutul. \n Puteți atribui o valoare unei etichete pentru o organizare mai aprofundată.",
diff --git a/resources/lang/ro/preferences.php b/resources/lang/ro/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 91c1502a5f02e33af6307c134c5558aef9612a51..6a2ed62889a8949a5560a3d780746d9cbcf54a36 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roluri',
     'role_user_roles' => 'Roluri utilizator',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Crează rol nou',
     'role_create_success' => 'Rol creat cu succes',
     'role_delete' => 'Șterge rolul',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Utilizatori',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Profil utilizator',
     'users_add_new' => 'Adaugă utilizator nou',
     'users_search' => 'Căutare utilizatori',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhook-uri',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Creează un nou Webhook',
     'webhooks_none_created' => 'Nu au fost create webhook-uri.',
     'webhooks_edit' => 'Editare Webhook',
index 40756f7c3be08130a01429d1a2c5aaeb9e2f1782..50ada48a2f165d1105cc58dc3fbf7a5ff47e884f 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Требуется подтверждение электронной почты, но система не может отправить письмо. Свяжитесь с администратором, чтобы убедиться, что адрес электронной почты настроен правильно.',
     'email_confirm_success' => 'Ваш адрес электронной почты был подтвержден! Теперь вы можете войти в систему, используя этот адрес электронной почты.',
     'email_confirm_resent' => 'Письмо с подтверждение выслано снова. Пожалуйста, проверьте ваш почтовый ящик.',
+    'email_confirm_thanks' => 'Спасибо за подтверждение!',
+    'email_confirm_thanks_desc' => 'Подождите, пока обработка вашего подтверждения будет завершена. Если вы не будете перенаправлены через 3 секунды, нажмите на ссылку «Продолжить» для продолжения.',
 
     'email_not_confirmed' => 'Адрес электронной почты не подтвержден',
     'email_not_confirmed_text' => 'Ваш email адрес все еще не подтвержден.',
index 8a684bc4fdb17ed90678104c568d064d87666dff..c372ab0174d920120330df5ddfcf41ca81070422 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Действия',
     'view' => 'Просмотр',
     'view_all' => 'Показать все',
+    'new' => 'Новый',
     'create' => 'Создание',
     'update' => 'Обновление',
     'edit' => 'Редактировать',
@@ -80,12 +81,14 @@ return [
     'none' => 'Нет',
 
     // Header
+    'homepage' => 'Главная страница',
     'header_menu_expand' => 'Развернуть меню заголовка',
     'profile_menu' => 'Меню профиля',
     'view_profile' => 'Посмотреть профиль',
     'edit_profile' => 'Редактировать профиль',
     'dark_mode' => 'Темный режим',
     'light_mode' => 'Светлый режим',
+    'global_search' => 'Глобальный поиск',
 
     // Layout tabs
     'tab_info' => 'Информация',
index 2de8e5fc003ade121e7e4e874807412e4d6e3c26..cc26f75dca082185f6caa6f2869dbf3fad2d0820 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL-адрес',
     'text_to_display' => 'Текст для отображения',
     'title' => 'Заголовок',
-    'open_link' => 'Открыть ссылку в...',
+    'open_link' => 'Открыть ссылку',
+    'open_link_in' => 'Открыть ссылку в...',
     'open_link_current' => 'В текущем окне',
     'open_link_new' => 'В новом окне',
+    'remove_link' => 'Удалить ссылку',
     'insert_collapsible' => 'Вставить свернутый блок',
     'collapsible_unwrap' => 'Удалить блок',
     'edit_label' => 'Изменить метку',
index 3172ddd5fc9d4f43724b90e58c1b0de93e213d0b..3d0122ee23136b86eeb65020cc0a23ece8a18d58 100644 (file)
@@ -43,13 +43,14 @@ return [
     // Permissions and restrictions
     'permissions' => 'Разрешения',
     'permissions_desc' => 'Установите права доступа для переопределения прав по умолчанию, предоставленных ролями пользователей.',
-    'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',
-    'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',
+    'permissions_book_cascade' => 'Разрешения, установленные для книг, автоматически распространяются на дочерние главы и страницы, если для них не определены собственные разрешения.',
+    'permissions_chapter_cascade' => 'Разрешения, установленные для глав, автоматически распространяются на дочерние страницы, если для них не определены собственные разрешения.',
     'permissions_save' => 'Сохранить разрешения',
     'permissions_owner' => 'Владелец',
     'permissions_role_everyone_else' => 'Все остальные',
     'permissions_role_everyone_else_desc' => 'Установить права доступа для всех ролей, которые не были специально переопределены.',
     'permissions_role_override' => 'Переопределить права доступа для роли',
+    'permissions_inherit_defaults' => 'Наследовать по умолчанию',
 
     // Search
     'search_results' => 'Результаты поиска',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Вставить изображение',
     'pages_md_insert_link' => 'Вставить ссылку на объект',
     'pages_md_insert_drawing' => 'Вставить рисунок',
+    'pages_md_show_preview' => 'Предпросмотр',
+    'pages_md_sync_scroll' => 'Синхронизировать прокрутку',
     'pages_not_in_chapter' => 'Страница не находится в главе',
     'pages_move' => 'Переместить страницу',
     'pages_move_success' => 'Страница перемещена в \':parentName\'',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Pазрешения страницы обновлены',
     'pages_revision' => 'Версия',
     'pages_revisions' => 'Версии страницы',
+    'pages_revisions_desc' => 'Ниже перечислены все предыдущие изменения этой страницы. Вы можете посмотреть резервные копии, сравнить и восстановить старые версии страниц, если позволяют разрешения. Полная история страницы не может быть полностью отражена здесь, поскольку, в зависимости от системной конфигурации, старые версии могут быть удалены автоматически.',
     'pages_revisions_named' => 'Версии страницы для :pageName',
     'pages_revision_named' => 'Версия страницы для :pageName',
     'pages_revision_restored_from' => 'Восстановлено из #:id; :summary',
     'pages_revisions_created_by' => 'Создана',
     'pages_revisions_date' => 'Дата версии',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Номер ревизии',
     'pages_revisions_numbered' => 'Версия #:id',
     'pages_revisions_numbered_changes' => 'Изменения в версии #:id',
     'pages_revisions_editor' => 'Тип редактора',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Теги полки',
     'tag' => 'Тег',
     'tags' =>  'Теги',
+    'tags_index_desc' => 'Теги могут быть применены к содержимому для гибкой категоризации. Теги могут иметь как ключ, так и значение, значение является необязательным. После применения содержимое может быть найдено с помощью имени тега и значения.',
     'tag_name' =>  'Имя тега',
     'tag_value' => 'Значение тега (опционально)',
     'tags_explain' => "Добавьте теги, чтобы лучше классифицировать ваш контент. \\n Вы можете присвоить значение тегу для более глубокой организации.",
diff --git a/resources/lang/ru/preferences.php b/resources/lang/ru/preferences.php
new file mode 100644 (file)
index 0000000..9b51f8e
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Горячие клавиши',
+    'shortcuts_interface' => 'Горячие клавиши интерфейса',
+    'shortcuts_toggle_desc' => 'Здесь вы можете включить или отключить горячие клавиши системного интерфейса, используемые для навигации и действий.',
+    'shortcuts_customize_desc' => 'Вы можете настроить каждую из горячих клавиш ниже. Просто нажмите комбинацию клавиш после выбора вставки для горячих клавиш.',
+    'shortcuts_toggle_label' => 'Горячие клавиши включены',
+    'shortcuts_section_navigation' => 'Навигация',
+    'shortcuts_section_actions' => 'Общие действия',
+    'shortcuts_save' => 'Сохранить горячие клавиши',
+    'shortcuts_overlay_desc' => 'Примечание: Когда горячие клавиши включены, вспомогательное наложение доступно через нажатие "?", которая будет подсвечивать доступные горячие клавиши для действий, видимых в настоящее время на экране.',
+    'shortcuts_update_success' => 'Настройки горячих клавиш были обновлены!',
+];
\ No newline at end of file
index 3aaad197841a1a12d1e2014c712ec59ffa4219a9..15c98d1a67f3414ac3b3c2410a57d70f800f0269 100755 (executable)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Роли',
     'role_user_roles' => 'Роли пользователей',
+    'roles_index_desc' => 'Роли используются для группировки пользователей и предоставления системных разрешений их участникам. Когда пользователь является членом нескольких ролей, предоставленные разрешения объединяются, и пользователь наследует все возможности.',
+    'roles_x_users_assigned' => '1 пользователь назначен|:count назначенных пользователей',
+    'roles_x_permissions_provided' => '1 разрешение|:count разрешений',
+    'roles_assigned_users' => 'Назначенные пользователи',
+    'roles_permissions_provided' => 'Предоставленные разрешения',
     'role_create' => 'Добавить роль',
     'role_create_success' => 'Роль успешно добавлена',
     'role_delete' => 'Удалить роль',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Пользователи',
+    'users_index_desc' => 'Создание и управление индивидуальными учетными записями пользователей в системе. Учетные записи пользователя используются для входа и атрибуции контента и активности. Разрешения доступа в первую очередь основываются на роли, но владельцы контента могут влиять на разрешения и доступ.',
     'user_profile' => 'Профиль пользователя',
     'users_add_new' => 'Добавить пользователя',
     'users_search' => 'Поиск пользователей',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Вебхуки',
+    'webhooks_index_desc' => 'Webhooks - это способ посылать данные на внешние URL-адреса при возникновении определенных действий и событий в системе, которые позволяют интегрировать события с внешними платформами, такими как системы обмена сообщениями или уведомлениями.',
+    'webhooks_x_trigger_events' => '1 событие триггер|:count событий триггера',
     'webhooks_create' => 'Создать вебхук',
     'webhooks_none_created' => 'Вебхуки еще не созданы.',
     'webhooks_edit' => 'Редактировать вебхук',
index de1484d4494e58c358fa97c80d1d4e303a48fb3c..2f5bd5c40fff8e68b09b05e20bb10e52267499df 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Je požadované overenie e-mailu, ale systém nemohol e-mail odoslať. Kontaktujte administrátora, aby ste sa uistili, že je e-mail nastavený správne.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'Potvrdzujúci e-mail bol poslaný znovu, skontrolujte prosím svoju e-mailovú schránku.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'E-mailová adresa nebola overená',
     'email_not_confirmed_text' => 'Vaša e-mailová adresa nebola zatiaľ overená.',
index 85e6a7c206d1803f05d58a06936c538ac9e17a88..e4725b39b1588345a2e74621d322705dddece35e 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Akcie',
     'view' => 'Zobraziť',
     'view_all' => 'Zobraziť všetko',
+    'new' => 'New',
     'create' => 'Vytvoriť',
     'update' => 'Aktualizovať',
     'edit' => 'Editovať',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Rozbaliť menu v záhlaví',
     'profile_menu' => 'Menu profilu',
     'view_profile' => 'Zobraziť profil',
     'edit_profile' => 'Upraviť profil',
     'dark_mode' => 'Tmavý režim',
     'light_mode' => 'Svetlý režim',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Informácie',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index c64756761576f5719077c3f169307e5823cbefbe..50125231a3d5961490d759bbc35364f8cc9a1161 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Výsledky hľadania',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Vložiť obrázok',
     'pages_md_insert_link' => 'Vložiť odkaz na entitu',
     'pages_md_insert_drawing' => 'Vložiť kresbu',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Stránka nie je v kapitole',
     'pages_move' => 'Presunúť stránku',
     'pages_move_success' => 'Stránka presunutá do ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Oprávnenia stránky aktualizované',
     'pages_revision' => 'Revízia',
     'pages_revisions' => 'Revízie stránky',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Revízie stránky :pageName',
     'pages_revision_named' => 'Revízia stránky :pageName',
     'pages_revision_restored_from' => 'Obnovené z #:id; :summary',
     'pages_revisions_created_by' => 'Vytvoril',
     'pages_revisions_date' => 'Dátum revízie',
     'pages_revisions_number' => 'č.',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revízia č. :id',
     'pages_revisions_numbered_changes' => 'Zmeny revízie č. ',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Štítky knižníc',
     'tag' => 'Štítok',
     'tags' =>  'Štítky',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Názov štítku',
     'tag_value' => 'Hodnota štítku (Voliteľné)',
     'tags_explain' => "Pridajte pár štítkov pre uľahčenie kategorizácie Vášho obsahu. \n Štítku môžete priradiť hodnotu pre ešte lepšiu organizáciu.",
diff --git a/resources/lang/sk/preferences.php b/resources/lang/sk/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index afe6a780073381db5a04fb5f77395a970a73defa..50e2b9509a54711a9c9ab0b7e756b86cbb80db05 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roly',
     'role_user_roles' => 'Používateľské roly',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Vytvoriť novú rolu',
     'role_create_success' => 'Rola úspešne vytvorená',
     'role_delete' => 'Zmazať rolu',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Používatelia',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Profil používateľa',
     'users_add_new' => 'Pridať nového používateľa',
     'users_search' => 'Hľadať medzi používateľmi',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index e667cb71ca59bc03b6217f26118f14c35a2f960c..ed7b5d4a0504bee191f9262a26ebac06b4522eff 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'E-poštna potrditev je zahtevana ampak sistem ni mogel poslati e-pošte. Kontaktirajte administratorja, da zagotovite, da je e-pošta pravilno nastavljena.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'Poslali smo vam potrditveno sporočilo. Prosimo preverite svojo elektronsko pošto.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Elektronski naslov ni potrjen',
     'email_not_confirmed_text' => 'Vaš e-naslov še ni bil potrjen.',
index 4e352828ec80520685d40a9bc223a60dc906aa5a..2053109d9da8a81b0672d7008f9c6be3a53cbff3 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Dejanja',
     'view' => 'Pogled',
     'view_all' => 'Prikaži vse',
+    'new' => 'New',
     'create' => 'Ustvari',
     'update' => 'Posodobi',
     'edit' => 'Uredi',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Expand Header Menu',
     'profile_menu' => 'Meni profila',
     'view_profile' => 'Ogled profila',
     'edit_profile' => 'Uredi profil',
     'dark_mode' => 'Način temnega zaslona',
     'light_mode' => 'Način svetlega zaslona',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Informacije',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index 2a6c721cb4ca78c1f9f9f96af7608fdc0f9b61ca..8e7300ae76a654dbb87f41ee80dc3260a5e61862 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Rezultati iskanja',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Vstavi sliko',
     'pages_md_insert_link' => 'Vnesi povezavo do objekta',
     'pages_md_insert_drawing' => 'Vstavi risbo',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Stran ni v poglavju',
     'pages_move' => 'Premakni stran',
     'pages_move_success' => 'Stran premaknjena v ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Posodobljena dovoljenja strani',
     'pages_revision' => 'Revizija',
     'pages_revisions' => 'Pregled strani',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Pregledi strani za :pageName',
     'pages_revision_named' => 'Pregled strani za :pageName',
     'pages_revision_restored_from' => 'Obnovljeno iz #:id; :summary',
     'pages_revisions_created_by' => 'Ustvaril',
     'pages_revisions_date' => 'Datum revizije',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revizija št. :id',
     'pages_revisions_numbered_changes' => 'Revizija št. #:id Changes',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Oznake police',
     'tag' => 'Oznaka',
     'tags' =>  'Oznake',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Ime oznake',
     'tag_value' => 'Vrednost oznake (opcijsko)',
     'tags_explain' => "Dodajte nekaj oznak za boljšo kategorizacijo vaše vsebine.\nZ dodelitvijo oznake lahko poskrbite za bolj poglobljeno organizacijo.",
diff --git a/resources/lang/sl/preferences.php b/resources/lang/sl/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 97effa32ab6adb1bef1448a64bda911d9ce40930..c8f7f177b0b8fcb15dc42d4deb41f1edef651df7 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Vloge',
     'role_user_roles' => 'Vloge uporabnika',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Ustvari novo vlogo',
     'role_create_success' => 'Vloga uspešno ustvarjena',
     'role_delete' => 'Brisanje vloge',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Uporabniki',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Uporabniški profil',
     'users_add_new' => 'Dodaj novega uporabnika',
     'users_search' => 'Išči uporabnike',
@@ -242,6 +248,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 2fbb10e43f250b5f8819c71e14621126f2db0fe8..b480fd03d8092871582f05e82ea1baabc7420ccc 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'E-posten behöver bekräftas men systemet kan inte skicka mail. Kontakta adminstratören för att kontrollera att allt är konfigurerat korrekt.',
     'email_confirm_success' => 'Din e-postadress har bekräftats! Du bör nu kunna logga in med denna e-postadress.',
     'email_confirm_resent' => 'Bekräftelsemailet har skickats på nytt, kolla din mail',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'E-posadress ej bekräftad',
     'email_not_confirmed_text' => 'Din e-postadress har inte bekräftats ännu.',
index cc81b71d8b3c8e077cf4c4a3a293ca48a4355e1c..cd28dc39512fd6e5451480d6d162d157c6a7a37f 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Åtgärder',
     'view' => 'Visa',
     'view_all' => 'Visa alla',
+    'new' => 'New',
     'create' => 'Skapa',
     'update' => 'Uppdatera',
     'edit' => 'Redigera',
@@ -80,12 +81,14 @@ return [
     'none' => 'Inga',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Expandera sidhuvudsmenyn',
     'profile_menu' => 'Profilmeny',
     'view_profile' => 'Visa profil',
     'edit_profile' => 'Redigera profil',
     'dark_mode' => 'Mörkt läge',
     'light_mode' => 'Ljust läge',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Information',
index 0df009710f014a0013f8eef3db61eeee99318bad..b1ed182dc581c3a8ee4cf32d061e8ff2a9c22828 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text som ska visas',
     'title' => 'Titel',
-    'open_link' => 'Öppna länk i...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Aktuellt fönster',
     'open_link_new' => 'Nytt fönster',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Infoga hopfällbart block',
     'collapsible_unwrap' => 'Expandera',
     'edit_label' => 'Redigera etikett',
index 6289e14e69cd05e4b67bcf3f961f2aa0e9fda4a9..9ebe28d347fbfebdec3183e4168db2bd3ea4cfc0 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Alla andra',
     'permissions_role_everyone_else_desc' => 'Ställ in rättigheter för alla roller som inte uttryckligen har åsidosatts.',
     'permissions_role_override' => 'Åsidosätt rättigheter för roll',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Sökresultat',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Infoga bild',
     'pages_md_insert_link' => 'Infoga länk',
     'pages_md_insert_drawing' => 'Infoga teckning',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Sidan ligger inte i något kapitel',
     'pages_move' => 'Flytta sida',
     'pages_move_success' => 'Sidan har flyttats till ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Rättigheterna för sidan har uppdaterats',
     'pages_revision' => 'Revidering',
     'pages_revisions' => 'Sidrevisioner',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Sidrevisioner för :pageName',
     'pages_revision_named' => 'Sidrevision för :pageName',
     'pages_revision_restored_from' => 'Återställd från #:id; :summary',
     'pages_revisions_created_by' => 'Skapad av',
     'pages_revisions_date' => 'Revisionsdatum',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revisions #:id',
     'pages_revisions_numbered_changes' => 'Revision #:id ändringar',
     'pages_revisions_editor' => 'Typ av redigerare',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Hylltaggar',
     'tag' => 'Tagg',
     'tags' =>  'Taggar',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Etikettnamn',
     'tag_value' => 'Taggvärde (Frivilligt)',
     'tags_explain' => "Lägg till taggar för att kategorisera ditt innehåll bättre. \n Du kan tilldela ett värde till en tagg för ännu bättre organisering.",
index dfd13f25da07cca77d642e5e37e332d5dde4ee3a..1e2cf6f0a359e67609eb914e42e5af4e7d6f2cb5 100644 (file)
@@ -11,7 +11,7 @@ return [
     // Auth
     'error_user_exists_different_creds' => 'En användare med adressen :email finns redan.',
     'email_already_confirmed' => 'E-posten har redan bekräftats, prova att logga in.',
-    'email_confirmation_invalid' => 'Denna bekräftelsekod är inte giltig eller har redan använts. Vänligen prova att registera dig på nytt',
+    'email_confirmation_invalid' => 'Denna bekräftelsekod är inte giltig eller har redan använts. Vänligen prova att registrera dig på nytt.',
     'email_confirmation_expired' => 'Denna bekräftelsekod har gått ut. Vi har skickat dig en ny.',
     'email_confirmation_awaiting' => 'E-postadressen för det konto som används måste bekräftas',
     'ldap_fail_anonymous' => 'LDAP-inloggning misslyckades med anonym bindning',
diff --git a/resources/lang/sv/preferences.php b/resources/lang/sv/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index d44b43f701533207f54732a3a60163aa8f679ea5..16ca555b3e075bdf4cdd59027de7dc146e59b29a 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roller',
     'role_user_roles' => 'Användarroller',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Skapa ny roll',
     'role_create_success' => 'Rollen har skapats',
     'role_delete' => 'Ta bort roll',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Användare',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Användarprofil',
     'users_add_new' => 'Lägg till användare',
     'users_search' => 'Sök användare',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Skapa ny webhook',
     'webhooks_none_created' => 'Inga webhooks har skapats än.',
     'webhooks_edit' => 'Redigera webhook',
index 28144df2dfcb197806fc3e81755efdc3c0020579..0d04a4ab858a84b46045793abe48678c10d000da 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'E-posta adresinin doğrulanması gerekiyor fakat sistem, doğrulama bağlantısını göndermeyi başaramadı. E-posta adresinin doğru bir şekilde ayarlığından emin olmak için yöneticiyle iletişime geçin.',
     'email_confirm_success' => 'Email hesabınız onaylandı. Email adresinizi kullanarak giriş yapabilirsiniz.',
     'email_confirm_resent' => 'Doğrulama e-postası tekrar gönderildi, lütfen gelen kutunuzu kontrol ediniz.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'E-posta Adresi Doğrulanmadı',
     'email_not_confirmed_text' => 'E-posta adresiniz henüz doğrulanmadı.',
index fe68799237789f4afbf23d01da49c520791f1198..2efb18e1bf6bb57a8192928d78350c3c1dcd00a5 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'İşlemler',
     'view' => 'Görüntüle',
     'view_all' => 'Hepsini Göster',
+    'new' => 'New',
     'create' => 'Oluştur',
     'update' => 'Güncelle',
     'edit' => 'Düzenle',
@@ -80,12 +81,14 @@ return [
     'none' => 'Hiçbiri',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Başlık Menüsünü Genişlet',
     'profile_menu' => 'Profil Menüsü',
     'view_profile' => 'Profili Görüntüle',
     'edit_profile' => 'Profili Düzenle',
     'dark_mode' => 'Gece Modu',
     'light_mode' => 'Aydınlık Modu',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Bilgi',
index 7324de26a0bf33755449b71623aff29890fa9182..503049d004f139aa586350fcd3ac6e6467526902 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Görüntülenecek metin',
     'title' => 'Başlık',
-    'open_link' => 'Bağlantıyı şurada aç...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Geçerli pencere',
     'open_link_new' => 'Yeni pencere',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Etiketi düzenle',
index e361b3c00a6d4db9c4c60bafe8a9c50bed4ff406..d90c5070d36947cc0e792ef5457f778cf37c47ae 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Arama Sonuçları',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Görsel Ekle',
     'pages_md_insert_link' => 'Öge Bağlantısı Ekle',
     'pages_md_insert_drawing' => 'Çizim Ekle',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Bu sayfa, bir bölüme ait değil',
     'pages_move' => 'Sayfayı Taşı',
     'pages_move_success' => 'Sayfa şuraya taşındı: :parentName',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Sayfa izinleri güncellendi',
     'pages_revision' => 'Revizyon',
     'pages_revisions' => 'Sayfa Revizyonları',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => ':pageName için Sayfa Revizyonları',
     'pages_revision_named' => ':pageName için Sayfa Revizyonu',
     'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Revize Eden',
     'pages_revisions_date' => 'Revizyon Tarihi',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revizyon #:id',
     'pages_revisions_numbered_changes' => 'Revizyon #:id Değişiklikleri',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Kitaplık Etiketleri',
     'tag' => 'Etiket',
     'tags' =>  'Etiketler',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Etiket İsmi',
     'tag_value' => 'Etiket Değeri (Opsiyonel)',
     'tags_explain' => "İçeriğinizi daha iyi kategorize etmek için etiket ekleyin. Etiketlere değer atayarak daha derinlemesine bir düzen elde edebilirsiniz.",
diff --git a/resources/lang/tr/preferences.php b/resources/lang/tr/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 8bf1eac7d9c5d52a5697ee2ddf077e6cda029fc4..a823556930896a5d22fc17926a5f848adee5599c 100755 (executable)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roller',
     'role_user_roles' => 'Kullanıcı Rolleri',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Yeni Rol Oluştur',
     'role_create_success' => 'Rol, başarıyla oluşturuldu',
     'role_delete' => 'Rolü Sil',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Kullanıcılar',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Kullanıcı Profili',
     'users_add_new' => 'Yeni Kullanıcı Ekle',
     'users_search' => 'Kullanıcı Ara',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 7cda943c0714aaea3c6cfb49caa350f7496e3742..15aecaab57d03fb87f9997072d057d36dd7f5f58 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Необхідно підтвердження електронною поштою, але система не змогла надіслати електронний лист. Зверніться до адміністратора, щоб правильно налаштувати електронну пошту.',
     'email_confirm_success' => 'Ваша адреса електронної пошти була підтверджена! Тепер ви можете увійти в систему, використовуючи цю адресу електронної пошти.',
     'email_confirm_resent' => 'Лист з підтвердженням надіслано, перевірте свою пошту.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Адресу електронної скриньки не підтверджено',
     'email_not_confirmed_text' => 'Ваша електронна адреса ще не підтверджена.',
index 8f1cac6097c614caf7083f03394c58cf7e3ca702..9e6b1da0787afeb3825b836b7eb4493ef94bdf52 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Дії',
     'view' => 'Подивитись',
     'view_all' => 'Подивитись все',
+    'new' => 'New',
     'create' => 'Створити',
     'update' => 'Оновити',
     'edit' => 'Редагувати',
@@ -80,12 +81,14 @@ return [
     'none' => 'Відсутньо',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Розгорнути меню заголовка',
     'profile_menu' => 'Меню профілю',
     'view_profile' => 'Переглянути профіль',
     'edit_profile' => 'Редагувати профіль',
     'dark_mode' => 'Темний режим',
     'light_mode' => 'Світлий режим',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Інфо',
index 2c71dc62833e2c1d34b03ef08714a0481d968620..f006bbfe3ac1cacb57ff8c64e7b12410b6aedb46 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'Адреса URL',
     'text_to_display' => 'Текст для показу',
     'title' => 'Назва',
-    'open_link' => 'Відкрити посилання за допомогою...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Поточне вікно',
     'open_link_new' => 'Нове вікно',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Вставити згорнутий блок',
     'collapsible_unwrap' => 'Розгорнути',
     'edit_label' => 'Редагувати мітку',
index 3db380b156868b2eba74ed3b4717e01317e4fbdd..1d9f366576a0056d01ed21496f08560c55b9db53 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Всі інші',
     'permissions_role_everyone_else_desc' => 'Встановити дозвіл для всіх ролей не спеціально перевизначений.',
     'permissions_role_override' => 'Змінити права доступу для ролі',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Результати пошуку',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Вставити зображення',
     'pages_md_insert_link' => 'Вставити посилання на об\'єкт',
     'pages_md_insert_drawing' => 'Вставити малюнок',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Сторінка не знаходиться в розділі',
     'pages_move' => 'Перемістити сторінку',
     'pages_move_success' => 'Сторінку переміщено до ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Дозволи на сторінку оновлено',
     'pages_revision' => 'Версія',
     'pages_revisions' => 'Версія сторінки',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Версії сторінки для :pageName',
     'pages_revision_named' => 'Версія сторінки для :pageName',
     'pages_revision_restored_from' => 'Відновлено з #:id; :summary',
     'pages_revisions_created_by' => 'Створена',
     'pages_revisions_date' => 'Дата версії',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Версія #:id',
     'pages_revisions_numbered_changes' => 'Зміни версії #:id',
     'pages_revisions_editor' => 'Тип редактора',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Теги полиць',
     'tag' => 'Тег',
     'tags' =>  'Теги',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Назва тегу',
     'tag_value' => 'Значення тегу (необов\'язково)',
     'tags_explain' => "Додайте кілька тегів, щоб краще класифікувати ваш вміст. \n Ви можете присвоїти значення тегу для більш глибокої організації.",
diff --git a/resources/lang/uk/preferences.php b/resources/lang/uk/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 0c69fc8e629fa954b21d0416b90366e949ed532b..6de7169a9bf8501b4877bd162733b59c455edaa8 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Ролі',
     'role_user_roles' => 'Ролі користувача',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Створити нову роль',
     'role_create_success' => 'Роль успішно створена',
     'role_delete' => 'Видалити роль',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Користувачі',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Профіль користувача',
     'users_add_new' => 'Додати нового користувача',
     'users_search' => 'Пошук користувачів',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Веб-хуки',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Створити новий Веб-хук',
     'webhooks_none_created' => 'Немає створених Веб-хуків.',
     'webhooks_edit' => 'Редагувати Веб-хук',
index 08929d39d703803480bfe122204d7c6058b1b11f..e9a48ab32adc16d21e2137398cd3793a44fac10a 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Email confirmation required but the system could not send the email. Contact the admin to ensure email is set up correctly.',
     'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
     'email_confirm_resent' => 'Confirmation email resent, Please check your inbox.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Email Address Not Confirmed',
     'email_not_confirmed_text' => 'Your email address has not yet been confirmed.',
index 703a70c7e6aa5bea2b59f7319d4739c5f7162c13..c74dcc90775219416dfe1e56cd0c2d9c577c6f4d 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Actions',
     'view' => 'View',
     'view_all' => 'View All',
+    'new' => 'New',
     'create' => 'Create',
     'update' => 'Update',
     'edit' => 'Edit',
@@ -80,12 +81,14 @@ return [
     'none' => 'None',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Expand Header Menu',
     'profile_menu' => 'Profile Menu',
     'view_profile' => 'View Profile',
     'edit_profile' => 'Edit Profile',
     'dark_mode' => 'Dark Mode',
     'light_mode' => 'Light Mode',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Info',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index b747a7f67464679ae08a982d99c310e0d18164ef..90318b902fb7a0b30255f1acfeabea7cc356f951 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Qidiruv natijalari',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Insert Image',
     'pages_md_insert_link' => 'Insert Entity Link',
     'pages_md_insert_drawing' => 'Insert Drawing',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Page is not in a chapter',
     'pages_move' => 'Move Page',
     'pages_move_success' => 'Page moved to ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Page permissions updated',
     'pages_revision' => 'Revision',
     'pages_revisions' => 'Page Revisions',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Page Revisions for :pageName',
     'pages_revision_named' => 'Page Revision for :pageName',
     'pages_revision_restored_from' => 'Restored from #:id; :summary',
     'pages_revisions_created_by' => 'Created By',
     'pages_revisions_date' => 'Revision Date',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Revision #:id',
     'pages_revisions_numbered_changes' => 'Revision #:id Changes',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Shelf Tags',
     'tag' => 'Tag',
     'tags' =>  'Tags',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Tag Name',
     'tag_value' => 'Tag Value (Optional)',
     'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
diff --git a/resources/lang/uz/preferences.php b/resources/lang/uz/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 1ad271e7c9ebd736f1d283bc2884b756294ea56d..f4204dd68bf05858a70205983d8aa35204366efa 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Roles',
     'role_user_roles' => 'User Roles',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Create New Role',
     'role_create_success' => 'Role successfully created',
     'role_delete' => 'Delete Role',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Users',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'User Profile',
     'users_add_new' => 'Add New User',
     'users_search' => 'Search Users',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Create New Webhook',
     'webhooks_none_created' => 'No webhooks have yet been created.',
     'webhooks_edit' => 'Edit Webhook',
index 0d63fb759b604bc045b264600d348c5e4b244d77..4cee662dece21612d72047eb305c80f31857d7df 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => 'Email xác nhận cần gửi nhưng hệ thống đã không thể gửi được email. Liên hệ với quản trị viên để chắc chắn email được thiết lập đúng.',
     'email_confirm_success' => 'Email của bạn đã được xác nhận! Bạn có thể đăng nhập với email này ngay bây giờ.',
     'email_confirm_resent' => 'Email xác nhận đã được gửi lại, Vui lòng kiểm tra hộp thư.',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Địa chỉ email chưa được xác nhận',
     'email_not_confirmed_text' => 'Địa chỉ email của bạn hiện vẫn chưa được xác nhận.',
index 23718b6b6d66c5da9278dcdbe34d0c12cc50aa34..aa2893107d6f9f1f514bd902d79f2975d91f48ad 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => 'Hành động',
     'view' => 'Xem',
     'view_all' => 'Xem tất cả',
+    'new' => 'New',
     'create' => 'Tạo',
     'update' => 'Cập nhật',
     'edit' => 'Sửa',
@@ -80,12 +81,14 @@ return [
     'none' => 'Không',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => 'Mở rộng Header Menu',
     'profile_menu' => 'Menu Hồ sơ',
     'view_profile' => 'Xem Hồ sơ',
     'edit_profile' => 'Sửa Hồ sơ',
     'dark_mode' => 'Chế độ Tối',
     'light_mode' => 'Chế độ Sáng',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => 'Thông tin',
index faf351da2e878e929722ac15ff93edf17464632e..670c1c5e1acd0995de08f07c7cca695821a290c2 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => 'Edit label',
index 32a4653c431128c2812a3d29b9f338c564f23fd9..85dab34bf8b3721478134ab1d5fd0f6e0d7cf4ec 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => 'Kết quả Tìm kiếm',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => 'Chèn hình ảnh',
     'pages_md_insert_link' => 'Chèn liên kết thực thể',
     'pages_md_insert_drawing' => 'Chèn bản vẽ',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Trang không nằm trong một chương',
     'pages_move' => 'Di chuyển Trang',
     'pages_move_success' => 'Trang đã chuyển tới ":parentName"',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => 'Quyền hạn Trang được cập nhật',
     'pages_revision' => 'Phiên bản',
     'pages_revisions' => 'Phiên bản Trang',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => 'Phiên bản Trang cho :pageName',
     'pages_revision_named' => 'Phiên bản Trang cho :pageName',
     'pages_revision_restored_from' => 'Khôi phục từ #:id; :summary',
     'pages_revisions_created_by' => 'Tạo bởi',
     'pages_revisions_date' => 'Ngày của Phiên bản',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => 'Phiên bản #:id',
     'pages_revisions_numbered_changes' => 'Các thay đổi của phiên bản #:id',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => 'Các Thẻ Kệ',
     'tag' => 'Nhãn',
     'tags' =>  'Các Thẻ',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  'Tên Nhãn',
     'tag_value' => 'Giá trị Thẻ (Tùy chọn)',
     'tags_explain' => "Thêm vài thẻ để phân loại nội dung của bạn tốt hơn. \n Bạn có thể đặt giá trị cho thẻ để quản lí kĩ càng hơn.",
diff --git a/resources/lang/vi/preferences.php b/resources/lang/vi/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index 7e4d2040e47079b64461a4a293860c7c61baf2b2..a135bdd9d0bd9a4a7ea399e446a81b97cdb260e7 100644 (file)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => 'Quyền',
     'role_user_roles' => 'Quyền người dùng',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => 'Tạo quyền mới',
     'role_create_success' => 'Quyền mới đã được tạo thành công',
     'role_delete' => 'Xóa quyền',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => 'Người dùng',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => 'Hồ sơ người dùng',
     'users_add_new' => 'Thêm người dùng mới',
     'users_search' => 'Tìm kiếm người dùng',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => 'Tạo Webhook mới',
     'webhooks_none_created' => 'Chưa có webhooks nào được tạo.',
     'webhooks_edit' => 'Chỉnh sửa Webhook',
index 4c0c587fe06262c2da44610c079bc00366995105..3e944a7fa4addd96a9d96eec29b05387bd4532dc 100644 (file)
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => '需要Email验证,但系统无法发送电子邮件,请联系网站管理员。',
     'email_confirm_success' => '您已成功验证电子邮件地址!您现在可以使用此电子邮件地址登录。',
     'email_confirm_resent' => '验证邮件已重新发送,请检查收件箱。',
+    'email_confirm_thanks' => 'Thanks for confirming!',
+    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
 
     'email_not_confirmed' => 'Email地址未验证',
     'email_not_confirmed_text' => '您的电子邮件地址尚未确认。',
index 4889faf116c088d1609a6707f18b2ef7b3d7c5f3..be36fcecaff52acb926b9d8d68c175b2a1c2cc44 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => '操作',
     'view' => '浏览',
     'view_all' => '查看全部',
+    'new' => 'New',
     'create' => '创建',
     'update' => '更新',
     'edit' => '编辑',
@@ -80,12 +81,14 @@ return [
     'none' => '无',
 
     // Header
+    'homepage' => 'Homepage',
     'header_menu_expand' => '展开标头菜单',
     'profile_menu' => '个人资料',
     'view_profile' => '查看个人资料',
     'edit_profile' => '编辑个人资料',
     'dark_mode' => '夜间模式',
     'light_mode' => '日间模式',
+    'global_search' => 'Global Search',
 
     // Layout tabs
     'tab_info' => '信息',
index 267d552f32c92a13f1524aecb48e3e873e3d4c54..174ba142535d8eb341081957a06a35469320c04d 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => '网址',
     'text_to_display' => '要显示的文本',
     'title' => '标题',
-    'open_link' => '打开方式...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => '覆盖当前窗口',
     'open_link_new' => '新建窗口',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => '插入可折叠块',
     'collapsible_unwrap' => '展开',
     'edit_label' => '编辑标签',
index c7fbd3d9197c4cffaa1f859c4e2dae27b266b5d4..f3ff05700638f54c6069c176e54ac3449e8cc0c6 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => '其他所有人',
     'permissions_role_everyone_else_desc' => '为所有未被特别覆盖的角色设置权限。',
     'permissions_role_override' => '覆盖角色权限',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => '搜索结果',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => '插入图片',
     'pages_md_insert_link' => '插入项目链接',
     'pages_md_insert_drawing' => '插入图表',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => '本页面不在某章节中',
     'pages_move' => '移动页面',
     'pages_move_success' => '页面已移动到「:parentName」',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => '页面权限已更新',
     'pages_revision' => '修订',
     'pages_revisions' => '页面修订',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => '“:pageName”页面修订',
     'pages_revision_named' => '“:pageName”页面修订',
     'pages_revision_restored_from' => '恢复到 #:id :summary',
     'pages_revisions_created_by' => '创建者',
     'pages_revisions_date' => '修订日期',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => '修订 #:id',
     'pages_revisions_numbered_changes' => '修改 #:id ',
     'pages_revisions_editor' => '编辑器类型',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => '书架标签',
     'tag' => '标签',
     'tags' =>  '标签',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  '标签名称',
     'tag_value' => '标签值 (可选)',
     'tags_explain' => "添加一些标签以更好地对您的内容进行分类。\n您可以为标签分配一个值,以进行更好的进行管理。",
diff --git a/resources/lang/zh_CN/preferences.php b/resources/lang/zh_CN/preferences.php
new file mode 100644 (file)
index 0000000..e9a4746
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => 'Shortcuts',
+    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
+    'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
+    'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.',
+    'shortcuts_toggle_label' => 'Keyboard shortcuts enabled',
+    'shortcuts_section_navigation' => 'Navigation',
+    'shortcuts_section_actions' => 'Common Actions',
+    'shortcuts_save' => 'Save Shortcuts',
+    'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.',
+    'shortcuts_update_success' => 'Shortcut preferences have been updated!',
+];
\ No newline at end of file
index af5fd2949224d5f4e8e0fd9c1052a782854c3471..22b626ed1f31a37ba299284f8deee9ffdd9975a0 100755 (executable)
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => '角色',
     'role_user_roles' => '用户角色',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => '创建角色',
     'role_create_success' => '角色创建成功',
     'role_delete' => '删除角色',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => '用户',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => '用户资料',
     'users_add_new' => '添加用户',
     'users_search' => '搜索用户',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => '新建 Webhook',
     'webhooks_none_created' => '尚未创建任何 Webhook。',
     'webhooks_edit' => '编辑 Webhook',
index d231ea54abc128d4f8c1cead763602b5f2bb8323..7a3d8031d47ffa73aa93453e07363fb52a35ab81 100644 (file)
@@ -39,9 +39,9 @@ return [
     'register_success' => '感謝您註冊!您已註冊完成並可登入。',
 
     // Login auto-initiation
-    'auto_init_starting' => 'Attempting Login',
-    'auto_init_starting_desc' => 'We\'re contacting your authentication system to start the login process. If there\'s no progress after 5 seconds you can try clicking the link below.',
-    'auto_init_start_link' => 'Proceed with authentication',
+    'auto_init_starting' => '嘗試登入中',
+    'auto_init_starting_desc' => '正在與認證系統連線以開始流程,若 5 秒鐘仍無回應,請嘗試點擊以下的連結。',
+    'auto_init_start_link' => '進行認證',
 
     // Password Reset
     'reset_password' => '重設密碼',
@@ -61,6 +61,8 @@ return [
     'email_confirm_send_error' => '需要電子郵件驗證,但系統無法傳送電子郵件。請與管理員聯絡以確保電子郵件正確設定。',
     'email_confirm_success' => '您的電子郵箱已確認成功!您可以使用該電子郵箱地址進行登入了。',
     'email_confirm_resent' => '確認電子郵件已重新傳送。請檢查您的收件匣。',
+    'email_confirm_thanks' => '完成驗證,謝謝。',
+    'email_confirm_thanks_desc' => '正在處理您的確認,請稍候。若三秒後沒有重新導向,請按下方的「繼續」連結繼續。',
 
     'email_not_confirmed' => '電子郵件地址未確認',
     'email_not_confirmed_text' => '您的電子郵件位址尚未確認。',
@@ -90,26 +92,26 @@ return [
     'mfa_option_totp_desc' => '您必須在行動裝置上安裝了支援TOTP的身份驗證程式(例如Google Authenticator, Authy 或是 Microsoft Authenticator)才能使用雙重身份驗證。',
     'mfa_option_backup_codes_title' => '備用驗證碼',
     'mfa_option_backup_codes_desc' => '妥善保存好您的一次性備用驗證碼,以便日後驗證您的身份。',
-    'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
-    'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
-    'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
-    'mfa_gen_backup_codes_download' => 'Download Codes',
-    'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
-    'mfa_gen_totp_title' => 'Mobile App Setup',
-    'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
-    'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
-    'mfa_gen_totp_verify_setup' => 'Verify Setup',
-    'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
-    'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
-    'mfa_verify_access' => 'Verify Access',
-    'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
-    'mfa_verify_no_methods' => 'No Methods Configured',
-    'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
+    'mfa_gen_confirm_and_enable' => '確認並啟用',
+    'mfa_gen_backup_codes_title' => '備援代碼設定',
+    'mfa_gen_backup_codes_desc' => '將以下代碼列表儲存在安全的地方。存取系統時,您可以使用其中一個代碼作為第二個身份驗證機制。',
+    'mfa_gen_backup_codes_download' => '下載代碼',
+    'mfa_gen_backup_codes_usage_warning' => '每個代碼都只能使用一次',
+    'mfa_gen_totp_title' => '行動裝置應用程式設定',
+    'mfa_gen_totp_desc' => '您必須在行動裝置上安裝支援 TOTP 的身份驗證應用程式(例如 Google Authenticator、Authy 或 Microsoft Authenticator)。',
+    'mfa_gen_totp_scan' => '使用您偏好的身份驗證應用程式掃描下方的 QR code 以開始流程。',
+    'mfa_gen_totp_verify_setup' => '驗證設定',
+    'mfa_gen_totp_verify_setup_desc' => '透過在下方的輸入方塊中輸入您的身份驗證應用程式中產生的代碼來驗證一切都正常:',
+    'mfa_gen_totp_provide_code_here' => '在此處填入您的應用程式產生的代碼',
+    'mfa_verify_access' => '驗證存取權',
+    'mfa_verify_access_desc' => '您的使用者帳號在您取得存取權前需要您透過額外的驗證層級確認您的身份。使用您設定的其中一種驗證方式繼續。',
+    'mfa_verify_no_methods' => '未設定任何方式',
+    'mfa_verify_no_methods_desc' => '您的帳號中找不到多重步驟驗證方式。在取得存取權前,您必須設定至少一種方式。',
     'mfa_verify_use_totp' => '使用您的行動裝置進行驗證',
     'mfa_verify_use_backup_codes' => '使用您的備用驗證碼進行驗證',
     'mfa_verify_backup_code' => '備用驗證碼',
-    'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
-    'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
-    'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
-    'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
+    'mfa_verify_backup_code_desc' => '在下方輸入您剩下的其中一個備援代碼:',
+    'mfa_verify_backup_code_enter_here' => '在此處輸入備援代碼',
+    'mfa_verify_totp_desc' => '在下方輸入使用您行動裝置應用程式產生的代碼:',
+    'mfa_setup_login_notification' => '多因素認證已設定,請使用新的設定登入',
 ];
index ef768a08c891335963bb053a1adae34b96b581ba..6a0fa40e42939e23b671833d2b1652568b20163b 100644 (file)
@@ -25,6 +25,7 @@ return [
     'actions' => '動作',
     'view' => '檢視',
     'view_all' => '檢視全部',
+    'new' => '新增',
     'create' => '建立',
     'update' => '更新',
     'edit' => '編輯',
@@ -47,8 +48,8 @@ return [
     'previous' => '上一頁',
     'filter_active' => '使用中的過濾器',
     'filter_clear' => '清理過濾',
-    'download' => 'Download',
-    'open_in_tab' => 'Open in Tab',
+    'download' => '下載',
+    'open_in_tab' => '在新分頁中開啟',
 
     // Sort Options
     'sort_options' => '排序選項',
@@ -80,12 +81,14 @@ return [
     'none' => '無',
 
     // Header
+    'homepage' => '首頁',
     'header_menu_expand' => '展開選單',
     'profile_menu' => '個人資料選單',
     'view_profile' => '檢視個人資料',
     'edit_profile' => '編輯個人資料',
     'dark_mode' => '深色模式',
     'light_mode' => '淺色模式',
+    'global_search' => '全域搜尋',
 
     // Layout tabs
     'tab_info' => '資訊',
index efefc15bc2aec5610cace2369dfc246728f44fa4..903b49a0be586105021b9ad7d31ceca8aaf4bd9d 100644 (file)
@@ -144,9 +144,11 @@ return [
     'url' => '網址',
     'text_to_display' => 'Text to display',
     'title' => 'Title',
-    'open_link' => 'Open link in...',
+    'open_link' => 'Open link',
+    'open_link_in' => 'Open link in...',
     'open_link_current' => 'Current window',
     'open_link_new' => 'New window',
+    'remove_link' => 'Remove link',
     'insert_collapsible' => 'Insert collapsible block',
     'collapsible_unwrap' => 'Unwrap',
     'edit_label' => '編輯標記',
index a9a8c89c3eb0d30f585c8036decebe085372f078..bd3ac13d283c9dd236a2d1e0967e876bd362a6ac 100644 (file)
@@ -50,6 +50,7 @@ return [
     'permissions_role_everyone_else' => 'Everyone Else',
     'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
     'permissions_role_override' => 'Override permissions for role',
+    'permissions_inherit_defaults' => 'Inherit defaults',
 
     // Search
     'search_results' => '搜尋結果',
@@ -99,8 +100,8 @@ return [
     'shelves_edit' => 'Edit Shelf',
     'shelves_delete' => 'Delete Shelf',
     'shelves_delete_named' => 'Delete Shelf :name',
-    'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
-    'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
+    'shelves_delete_explain' => "這將刪除名為「:name」的書架。但其中的書本不會被刪除。",
+    'shelves_delete_confirmation' => '您確定要刪除此書架嗎?',
     'shelves_permissions' => 'Shelf Permissions',
     'shelves_permissions_updated' => 'Shelf Permissions Updated',
     'shelves_permissions_active' => 'Shelf Permissions Active',
@@ -223,6 +224,8 @@ return [
     'pages_md_insert_image' => '插入圖片',
     'pages_md_insert_link' => '插入連結',
     'pages_md_insert_drawing' => '插入繪圖',
+    'pages_md_show_preview' => 'Show preview',
+    'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => '頁面不在章節中',
     'pages_move' => '移動頁面',
     'pages_move_success' => '頁面已移動到「:parentName」',
@@ -233,12 +236,14 @@ return [
     'pages_permissions_success' => '頁面權限已更新',
     'pages_revision' => '修訂版本',
     'pages_revisions' => '頁面修訂版本',
+    'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
     'pages_revisions_named' => ':pageName 頁面修訂版本',
     'pages_revision_named' => ':pageName 頁面修訂版本',
     'pages_revision_restored_from' => '從 #:id; :summary 復原',
     'pages_revisions_created_by' => '建立者',
     'pages_revisions_date' => '修訂日期',
     'pages_revisions_number' => '#',
+    'pages_revisions_sort_number' => 'Revision Number',
     'pages_revisions_numbered' => '修訂版本 #:id',
     'pages_revisions_numbered_changes' => '修訂版本 #:id 變更',
     'pages_revisions_editor' => 'Editor Type',
@@ -275,6 +280,7 @@ return [
     'shelf_tags' => '書架標籤',
     'tag' => '標籤',
     'tags' =>  '標籤',
+    'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
     'tag_name' =>  '標籤名稱',
     'tag_value' => '標籤值(選擇性)',
     'tags_explain' => "加入一些標籤以更好地對您的內容進行分類。 \n 您可以為標籤分配一個值,以進行更深入的組織。",
@@ -287,8 +293,8 @@ return [
     'tags_assigned_shelves' => 'Assigned to Shelves',
     'tags_x_unique_values' => ':count unique values',
     'tags_all_values' => 'All values',
-    'tags_view_tags' => 'View Tags',
-    'tags_view_existing_tags' => 'View existing tags',
+    'tags_view_tags' => '檢視標籤',
+    'tags_view_existing_tags' => '檢視已存在的標籤',
     'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
     'attachments' => '附件',
     'attachments_explain' => '上傳一些檔案或附加連結以顯示在您的網頁上。將顯示在在頁面的側邊欄。',
index b1735038d4d6e855efd37f3a277a568cc3f851c7..623c66cba6a67de6f7c936347ee6cd5a88346616 100644 (file)
@@ -23,10 +23,10 @@ return [
     'saml_no_email_address' => '在外部認證系統提供的資料中找不到該使用者的電子郵件地址',
     'saml_invalid_response_id' => '此應用程式啟動的處理程序無法識別來自外部認證系統的請求。登入後回上一頁可能會造成此問題。',
     'saml_fail_authed' => '使用 :system 登入失敗,系統未提供成功的授權',
-    'oidc_already_logged_in' => 'Already logged in',
-    'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
-    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
-    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
+    'oidc_already_logged_in' => '已登入',
+    'oidc_user_not_registered' => '使用者 :name 未註冊,並已停用自動註冊',
+    'oidc_no_email_address' => '在外部認證系統提供的資料中找不到該使用者的電子郵件地址',
+    'oidc_fail_authed' => '使用 :system 登入失敗,系統未提供成功的授權',
     'social_no_action_defined' => '未定義動作',
     'social_login_bad_response' => "在 :socialAccount 登入時遇到錯誤: \n:error",
     'social_account_in_use' => ':socialAccount 帳號已被使用,請嘗試透過 :socialAccount 選項登入。',
diff --git a/resources/lang/zh_TW/preferences.php b/resources/lang/zh_TW/preferences.php
new file mode 100644 (file)
index 0000000..ac13742
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Text used for user-preference specific views within bookstack.
+ */
+
+return [
+    'shortcuts' => '快捷鍵',
+    'shortcuts_interface' => '介面鍵盤快捷鍵',
+    'shortcuts_toggle_desc' => '您可以在此處啟用或停用鍵盤系統介面快捷鍵,這些快捷鍵用於導覽與操作。',
+    'shortcuts_customize_desc' => '您可以自訂下方的每個快捷鍵。只要在選取快捷鍵輸入後按下您想要使用的按鍵組合即可。',
+    'shortcuts_toggle_label' => '鍵盤快捷鍵已啟用',
+    'shortcuts_section_navigation' => '導覽',
+    'shortcuts_section_actions' => '通用動作',
+    'shortcuts_save' => '儲存快捷鍵',
+    'shortcuts_overlay_desc' => '注意:當快捷鍵啟用時,可以按下「?」來使用小幫手覆蓋畫面。這將會在目前的畫面上突顯可見動作的快捷鍵。',
+    'shortcuts_update_success' => '快捷鍵偏好設定已更新!',
+];
\ No newline at end of file
index 12cff0c6ae2aeb230cf22bc82c3dd6e4b104aa58..913b8d6c16082cb24f08407b37db3ec1eb1bece3 100644 (file)
@@ -92,7 +92,7 @@ return [
     'maint_regen_references' => 'Regenerate References',
     'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.',
     'maint_regen_references_success' => 'Reference index has been regenerated!',
-    'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.',
+    'maint_timeout_command_note' => '備註:這項操作需要較長的時間,可能導致多數的網路環境發生連線逾時的問題。若有需要,可以透過終端機指令來替代。',
 
     // Recycle Bin
     'recycle_bin' => '資源回收桶',
@@ -133,6 +133,11 @@ return [
     // Role Settings
     'roles' => '角色',
     'role_user_roles' => '使用者角色',
+    'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.',
+    'roles_x_users_assigned' => '1 user assigned|:count users assigned',
+    'roles_x_permissions_provided' => '1 permission|:count permissions',
+    'roles_assigned_users' => 'Assigned Users',
+    'roles_permissions_provided' => 'Provided Permissions',
     'role_create' => '建立新角色',
     'role_create_success' => '角色建立成功',
     'role_delete' => '刪除角色',
@@ -172,6 +177,7 @@ return [
 
     // Users
     'users' => '使用者',
+    'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.',
     'user_profile' => '使用者個人資料',
     'users_add_new' => '新增使用者',
     'users_search' => '搜尋使用者',
@@ -241,6 +247,8 @@ return [
 
     // Webhooks
     'webhooks' => 'Webhooks',
+    'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.',
+    'webhooks_x_trigger_events' => '1 trigger event|:count trigger events',
     'webhooks_create' => '建立 Webhook',
     'webhooks_none_created' => '沒有已建立的 Webhook',
     'webhooks_edit' => '設置 Webhook',
index 2053674a63af899265d4eb3f1c183dc2393ebd0d..71ba5acadf8726e4bfc2ef3949f71f1aeb8d01ce 100644 (file)
@@ -15,7 +15,7 @@ return [
     'alpha_dash'           => ':attribute 只能包含字母、數字、破折號與底線。',
     'alpha_num'            => ':attribute 只能包含字母和數字。',
     'array'                => ':attribute 必須是陣列。',
-    'backup_codes'         => 'The provided code is not valid or has already been used.',
+    'backup_codes'         => '提供的代碼無效或已被使用。',
     'before'               => ':attribute 必須是在 :date 前的日期。',
     'between'              => [
         'numeric' => ':attribute 必須在 :min 到 :max 之間。',
@@ -32,7 +32,7 @@ return [
     'digits_between'       => ':attribute 必須為 :min 到 :max 位數。',
     'email'                => ':attribute 必須是有效的電子郵件地址。',
     'ends_with' => ':attribute必須以下列之一結尾::values',
-    'file'                 => 'The :attribute must be provided as a valid file.',
+    'file'                 => ':attribute 必須作為有效檔案提供。',
     'filled'               => ':attribute 欄位必填。',
     'gt'                   => [
         'numeric' => ':attribute 必須大於 :value。',
@@ -100,7 +100,7 @@ return [
     ],
     'string'               => ':attribute 必須是字元串。',
     'timezone'             => ':attribute 必須是有效的區域。',
-    'totp'                 => 'The provided code is not valid or has expired.',
+    'totp'                 => '提供的代碼無效或已過期。',
     'unique'               => ':attribute 已經被使用。',
     'url'                  => ':attribute 格式無效。',
     'uploaded'             => '無法上傳文件, 服務器可能不接受此大小的文件。',
index 85fd96206393a64581c08b829587d01cc09b3e06..eb9f4e767a4e9cf604287c411b8cee55299b4faa 100644 (file)
   }
 }
 
-.anim.searchResult {
-  opacity: 0;
-  transform: translate3d(580px, 0, 0);
-  animation-name: searchResult;
-  animation-duration: 220ms;
+.search-suggestions-animation{
+  animation-name: searchSuggestions;
+  animation-duration: 120ms;
   animation-fill-mode: forwards;
   animation-timing-function: cubic-bezier(.62, .28, .23, .99);
 }
 
-@keyframes searchResult {
+@keyframes searchSuggestions {
   0% {
-    opacity: 0;
-    transform: translate3d(400px, 0, 0);
+    opacity: .5;
+    transform: scale(0.9);
   }
   100% {
     opacity: 1;
-    transform: translate3d(0, 0, 0);
+    transform: scale(1);
   }
 }
 
index 0398224ca52a406ee29d7ae160fa63a00caae4b4..2794dd95459841ecf0bd17b43b3fddfe625d563b 100644 (file)
 .card-title a {
   line-height: 1;
 }
-.card-footer-link {
+.card-footer-link, button.card-footer-link  {
   display: block;
   padding: $-s $-m;
   line-height: 1;
   border-top: 1px solid;
+  width: 100%;
+  text-align: left;
   @include lightDark(border-color, #DDD, #555);
   border-radius: 0 0 3px 3px;
   font-size: 0.9em;
     text-decoration: none;
     @include lightDark(background-color, #f2f2f2, #2d2d2d);
   }
+  &:focus {
+    @include lightDark(background-color, #eee, #222);
+    outline: 1px dotted #666;
+    outline-offset: -2px;
+  }
 }
 
 .card.border-card {
-  border: 1px solid #DDD;
+  border: 1px solid;
+  @include lightDark(border-color, #ddd, #000);
 }
 
 .card.drag-card {
     flex-grow: 0;
     padding: 0 $-xs;
     &:hover {
-      background-color: #EEE;
+      @include lightDark(background-color, #eee, #2d2d2d);
     }
     .svg-icon {
       margin-inline-end: 0px;
   margin-bottom: 0;
 }
 
-td .tag-item {
+.item-list-row .tag-item {
   margin-bottom: 0;
 }
 
-/**
- * Pill boxes
- */
-
-.pill {
-  display: inline-block;
-  border: 1px solid currentColor;
-  padding: .2em .8em;
-  font-size: 0.8em;
-  border-radius: 1rem;
-  position: relative;
-  overflow: hidden;
-  line-height: 1.4;
-  &:before {
-    content: '';
-    background-color: currentColor;
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    opacity: 0.1;
-  }
-}
-
 /**
  * API Docs
  */
index 9fdd5a6117eb2b650965603bc2fac4159bc717db..ab1d506c755765fa7f4317213b0f82ad8293d6ff 100644 (file)
@@ -1,6 +1,6 @@
 
 // System wide notifications
-[notification] {
+.notification {
   position: fixed;
   top: 0;
   right: 0;
@@ -798,37 +798,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   max-width: 500px;
 }
 
-.content-permissions {
-  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
-}
-.content-permissions-row {
-  border: 1.5px solid;
-  @include lightDark(border-color, #E2E2E2, #444);
-  border-bottom-width: 0;
-  label {
-    padding-bottom: 0;
-  }
-  &:hover {
-    @include lightDark(background-color, #F2F2F2, #333);
-  }
-}
-.content-permissions-row:first-child {
-  border-radius: 4px 4px 0 0;
-}
-.content-permissions-row:last-child {
-  border-radius: 0 0 4px 4px;
-  border-bottom-width: 1.5px;
-}
-.content-permissions-row:first-child:last-child {
-  border-radius: 4px;
-}
-.content-permissions-row-toggle-all {
-  visibility: hidden;
-}
-.content-permissions-row:hover .content-permissions-row-toggle-all {
-  visibility: visible;
-}
-
 .template-item {
   cursor: pointer;
   position: relative;
@@ -843,14 +812,16 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
     height: 100%;
     display: flex;
     flex-direction: column;
-    border-inline-start: 1px solid #DDD;
+    border-inline-start: 1px solid;
+    @include lightDark(border-color, #ddd, #000);
   }
   .template-item-actions button {
     cursor: pointer;
     flex: 1;
-    background: #FFF;
+    @include lightDark(background-color, #FFF, #222);
     border: 0;
-    border-top: 1px solid #DDD;
+    border-top: 1px solid;
+    @include lightDark(border-color, #DDD, #000);
   }
   .template-item-actions button svg {
     margin: 0;
@@ -969,4 +940,113 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
   .dropdown-search-dropdown .dropdown-search-list {
     max-height: 240px;
   }
+}
+
+.item-list {
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
+}
+.item-list-row {
+  border: 1.5px solid;
+  @include lightDark(border-color, #E2E2E2, #444);
+  border-bottom-width: 0;
+  label {
+    padding-bottom: 0;
+  }
+  &:hover {
+    @include lightDark(background-color, #F6F6F6, #333);
+  }
+}
+.item-list-row:first-child {
+  border-radius: 4px 4px 0 0;
+}
+.item-list-row:last-child {
+  border-radius: 0 0 4px 4px;
+  border-bottom-width: 1.5px;
+}
+.item-list-row:first-child:last-child {
+  border-radius: 4px;
+}
+.item-list-row-toggle-all {
+  visibility: hidden;
+}
+.item-list-row:hover .item-list-row-toggle-all {
+  visibility: visible;
+}
+
+.status-indicator-active, .status-indicator-inactive {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  display: inline-block;
+}
+.status-indicator-active {
+  background-color: $positive;
+}
+.status-indicator-inactive {
+  background-color: $negative;
+}
+
+.shortcut-container {
+  background-color: rgba(0, 0, 0, 0.25);
+  pointer-events: none;
+  position: fixed;
+  left: 0;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 99;
+}
+.shortcut-linkage {
+  position: fixed;
+  box-shadow: 0 0 4px 0 #FFF;
+  border-radius: 3px;
+}
+.shortcut-hint {
+  position: fixed;
+  padding: $-xxs $-xxs;
+  font-size: .85rem;
+  font-weight: 700;
+  line-height: 1;
+  background-color: #eee;
+  border-radius: 3px;
+  border: 1px solid #b4b4b4;
+  box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
+  color: #333;
+}
+
+// Back to top link
+$btt-size: 40px;
+.back-to-top {
+  background-color: var(--color-primary);
+  position: fixed;
+  bottom: $-m;
+  right: $-l;
+  padding: 5px 7px;
+  cursor: pointer;
+  color: #FFF;
+  fill: #FFF;
+  svg {
+    width: math.div($btt-size, 1.5);
+    height: math.div($btt-size, 1.5);
+    margin-inline-end: 4px;
+  }
+  width: $btt-size;
+  height: $btt-size;
+  border-radius: $btt-size;
+  transition: all ease-in-out 180ms;
+  opacity: 0;
+  z-index: 999;
+  overflow: hidden;
+  &:hover {
+    width: $btt-size*3.4;
+    opacity: 1 !important;
+  }
+  .inner {
+    width: $btt-size*3.4;
+  }
+  span {
+    position: relative;
+    vertical-align: top;
+    line-height: 2;
+  }
 }
\ No newline at end of file
index 7e0f72355f3ee1f07275e07260483ecf8f168dea..ef14f62210f5460ef9fc94e8273482f82e433abd 100644 (file)
       outline: 0;
     }
   }
-  .markdown-display, .markdown-editor-wrap {
-    flex: 1;
-    position: relative;
-  }
-  .markdown-editor-wrap {
-    display: flex;
-    flex-direction: column;
-    border: 1px solid #DDD;
-    @include lightDark(border-color, #ddd, #000);
-    width: 50%;
-    max-width: 50%;
-  }
   &.fullscreen {
     position: fixed;
     top: 0;
   }
 }
 
+.markdown-editor-wrap {
+  border-top: 1px solid #DDD;
+  border-bottom: 1px solid #DDD;
+  @include lightDark(border-color, #ddd, #000);
+  position: relative;
+  flex: 1;
+}
+.markdown-editor-wrap + .markdown-editor-wrap {
+  flex-basis: 50%;
+  flex-shrink: 0;
+  flex-grow: 0;
+}
+
+.markdown-panel-divider {
+  width: 2px;
+  @include lightDark(background-color, #ddd, #000);
+  cursor: col-resize;
+}
+
 @include smaller-than($m) {
   #markdown-editor {
     flex-direction: column;
     width: 100%;
     max-width: 100%;
     flex-grow: 1;
-  }
-  #markdown-editor .editor-toolbar {
-    padding: 0;
-  }
-  #markdown-editor .editor-toolbar > * {
-    padding: $-xs $-s;
+    flex-basis: auto !important;
   }
   .editor-toolbar-label {
     float: none !important;
-    border-bottom: 1px solid #DDD;
+    @include lightDark(border-color, #DDD, #555);
     display: block;
   }
   .markdown-editor-wrap:not(.active) .editor-toolbar + div,
   }
 }
 
-.markdown-display {
-  margin-inline-start: -1px;
-}
-
 .markdown-editor-display {
   background-color: #fff;
   body {
@@ -138,8 +136,8 @@ html.markdown-editor-display.dark-mode {
 }
 
 .editor-toolbar {
+  height: 32px;
   width: 100%;
-  padding: $-xs $-m;
   font-size: 11px;
   line-height: 1.6;
   border-bottom: 1px solid #DDD;
@@ -147,11 +145,6 @@ html.markdown-editor-display.dark-mode {
   @include lightDark(background-color, #eee, #111);
   @include lightDark(border-color, #ddd, #000);
   flex: none;
-  &:after {
-    content: '';
-    display: block;
-    clear: both;
-  }
   @include whenDark {
     button {
       color: #AAA;
@@ -159,6 +152,30 @@ html.markdown-editor-display.dark-mode {
   }
 }
 
+.editor-toolbar .buttons {
+  font-size: $fs-m;
+  .dropdown-menu {
+    padding: 0;
+  }
+  .toggle-switch {
+    margin: $-s 0;
+  }
+}
+
+.editor-toolbar .buttons button {
+  font-size: .9rem;
+  width: 2rem;
+  text-align: center;
+  border-left: 1px solid;
+  @include lightDark(border-color, #DDD, #555);
+  svg {
+    margin-inline-end: 0;
+  }
+  &:hover {
+    @include lightDark(background-color, #DDD, #222);
+  }
+}
+
 
 label {
   @include lightDark(color, #666, #ddd);
@@ -328,7 +345,7 @@ input[type=color] {
   }
 }
 
-.form-group[collapsible] {
+.form-group.collapsible {
   padding: 0 $-m;
   border: 1px solid;
   @include lightDark(border-color, #DDD, #000);
@@ -412,7 +429,7 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
 .search-box {
   max-width: 100%;
   position: relative;
-  button {
+  button[tabindex="-1"] {
     background-color: transparent;
     border: none;
     @include lightDark(color, #666, #AAA);
@@ -473,4 +490,10 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
 .custom-file-input:focus + label {
   border-color: var(--color-primary);
   outline: 1px solid var(--color-primary);
+}
+
+input.shortcut-input {
+  width: auto;
+  max-width: 120px;
+  height: auto;
 }
\ No newline at end of file
index 923f026c2663f3f9f09d301b89cb947c3c03188e..aa560e8e050e3556b636d5d9bdae6a33d6080f5e 100644 (file)
@@ -108,21 +108,6 @@ header .search-box {
       border: 1px solid rgba(255, 255, 255, 0.4);
     }
   }
-  button {
-    z-index: 1;
-    left: 16px;
-    top: 10px;
-    color: #FFF;
-    opacity: 0.6;
-    @include lightDark(color, rgba(255, 255, 255, 0.8), #AAA);
-    @include rtl {
-      left: auto;
-      right: 16px;
-    }
-    svg {
-      margin-block-end: 0;
-    }
-  }
   input::placeholder {
     color: #FFF;
     opacity: 0.6;
@@ -130,10 +115,67 @@ header .search-box {
   @include between($l, $xl) {
     max-width: 200px;
   }
-  &:focus-within button {
+  &:focus-within #header-search-box-button {
     opacity: 1;
   }
 }
+#header-search-box-button {
+  z-index: 1;
+  inset-inline-start: 16px;
+  top: 10px;
+  color: #FFF;
+  opacity: 0.6;
+  @include lightDark(color, rgba(255, 255, 255, 0.8), #AAA);
+  svg {
+    margin-inline-end: 0;
+  }
+}
+
+.global-search-suggestions {
+  display: none;
+  position: absolute;
+  top: -$-s;
+  left: 0;
+  right: 0;
+  z-index: -1;
+  margin-left: -$-xxl;
+  margin-right: -$-xxl;
+  padding-top: 56px;
+  border-radius: 3px;
+  box-shadow: $bs-hover;
+  transform-origin: top center;
+  opacity: .5;
+  transform: scale(0.9);
+  .entity-item-snippet p  {
+    display: none;
+  }
+  .entity-item-snippet {
+    font-size: .8rem;
+  }
+  .entity-list-item-name {
+    font-size: .9rem;
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 2;
+    overflow: hidden;
+  }
+  .global-search-loading {
+    position: absolute;
+    width: 100%;
+  }
+}
+header .search-box.search-active:focus-within {
+  .global-search-suggestions {
+    display: block;
+  }
+  input {
+    @include lightDark(background-color, #EEE, #333);
+    @include lightDark(border-color, #DDD, #111);
+  }
+  #header-search-box-button, input {
+    @include lightDark(color, #444, #AAA);
+  }
+}
 
 .logo {
   display: inline-flex;
index cfb8397c99207e4ba3a436ed35c556c39caab22f..4c7de600b84c8f33787720051d4e8fe058ee913a 100644 (file)
@@ -144,6 +144,10 @@ body.flexbox {
   flex-direction: column;
 }
 
+.flex-container-row.inline, .flex-container-column.inline {
+  display: inline-flex !important;
+}
+
 .flex-container-column.wrap, .flex-container-row.wrap {
   flex-wrap: wrap;
 }
@@ -156,6 +160,23 @@ body.flexbox {
     flex-basis: auto;
     flex-grow: 0;
   }
+  &.fill-area {
+    flex-grow: 1;
+    flex-shrink: 0;
+    min-width: fit-content;
+  }
+}
+
+.flex-2 {
+  min-height: 0;
+  flex: 2;
+  max-width: 100%;
+}
+
+.flex-3 {
+  min-height: 0;
+  flex: 3;
+  max-width: 100%;
 }
 
 .flex-none {
@@ -177,7 +198,40 @@ body.flexbox {
 .items-center {
   align-items: center;
 }
+.items-stretch {
+  align-items: stretch;
+}
 
+/**
+ * Min width utilities
+ */
+.min-width-xxxxs {
+  min-width: 60px;
+}
+.min-width-xxxs {
+  min-width: 80px;
+}
+.min-width-xxs {
+  min-width: 100px;
+}
+.min-width-xs {
+  min-width: 120px;
+}
+.min-width-s {
+  min-width: 160px;
+}
+.min-width-m {
+  min-width: 200px;
+}
+.min-width-l {
+  min-width: 240px;
+}
+.min-width-xl {
+  min-width: 280px;
+}
+.min-width-xxl {
+  min-width: 320px;
+}
 
 /**
  * Display and float utilities
index 8926eb7f98308ba5c06078929218a077a7587dbd..86a89051f05976ee55d34e5013bf0701ceb71bfd 100644 (file)
     padding-inline-start: $-m;
   }
   li {
-    border: 1px solid #DDD;
+    border: 1px solid;
+    @include lightDark(border-color, #DDD, #666);
     margin-top: -1px;
     min-height: 38px;
   }
diff --git a/resources/sass/_opacity.scss b/resources/sass/_opacity.scss
new file mode 100644 (file)
index 0000000..235aed4
--- /dev/null
@@ -0,0 +1,28 @@
+
+.opacity-10 {
+  opacity: 0.1;
+}
+.opacity-20 {
+  opacity: 0.2;
+}
+.opacity-30 {
+  opacity: 0.3;
+}
+.opacity-40 {
+  opacity: 0.4;
+}
+.opacity-50 {
+  opacity: 0.5;
+}
+.opacity-60 {
+  opacity: 0.6;
+}
+.opacity-70 {
+  opacity: 0.7;
+}
+.opacity-80 {
+  opacity: 0.8;
+}
+.opacity-90 {
+  opacity: 0.9;
+}
\ No newline at end of file
index eeb51ebb511def1001cdd55f3cfa76ae539e02a9..720203a422aef9b5c7480d2a694a3a3aed685a7c 100755 (executable)
@@ -278,16 +278,16 @@ body.tox-fullscreen, body.markdown-fullscreen {
   &.open {
     width: 480px;
   }
-  [toolbox-toggle] svg {
+  .toolbox-toggle svg {
     transition: transform ease-in-out 180ms;
   }
-  [toolbox-toggle] {
+  .toolbox-toggle {
     transition: background-color ease-in-out 180ms;
   }
-  &.open [toolbox-toggle] {
+  &.open .toolbox-toggle {
     background-color: rgba(255, 0, 0, 0.29);
   }
-  &.open [toolbox-toggle] svg {
+  &.open .toolbox-toggle svg {
     transform: rotate(180deg);
   }
   > div {
@@ -357,7 +357,7 @@ body.tox-fullscreen, body.markdown-fullscreen {
   }
 }
 
-[toolbox-tab-content] {
+.toolbox-tab-content {
   display: none;
 }
 
index dd585733ce4b20b79a7748a9afc442c68c2dbbd7..2aaa74d5f4a42ff74478633db1322cc4460d2f32 100644 (file)
@@ -65,7 +65,7 @@ table.no-style {
 }
 
 table.list-table {
-  margin: 0 -$-xs;
+  margin: 0 (-$-xs);
   td {
     border: 0;
     vertical-align: middle;
index 4eab8959a6931822dede2b488f24b0dd036a5cb1..6c68bd12b08b285af52fbfe2591feee9f7e4a8fc 100644 (file)
@@ -5,7 +5,7 @@
 body, button, input, select, label, textarea {
   font-family: $text;
 }
-.Codemirror, pre, #markdown-editor-input, .editor-toolbar, .code-base {
+.Codemirror, pre, #markdown-editor-input, .text-mono, .code-base {
   font-family: $mono;
 }
 
index 3cb2dd4eddc5eda6ce1437fbd86ef96dc7c2ef5c..e1242bddaf8a6b93d243de84d6822ca9453178cc 100644 (file)
@@ -51,6 +51,10 @@ $fs-s: 12px;
 
 :root.dark-mode {
   --bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(255, 255, 255,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E");
+  color-scheme: only dark;
+}
+:root:not(.dark-mode) {
+  color-scheme: only light;
 }
 
 $positive: #0f7d15;
index ab97466a5aa260e50c7e24a09c04d6526f82b810..23959d1f85699c878d68361dfc518a44144e0570 100644 (file)
@@ -4,6 +4,7 @@
 @import "variables";
 @import "mixins";
 @import "spacing";
+@import "opacity";
 @import "html";
 @import "text";
 @import "colors";
@@ -99,43 +100,6 @@ $loadingSize: 10px;
   }
 }
 
-// Back to top link
-$btt-size: 40px;
-[back-to-top] {
-  background-color: var(--color-primary);
-  position: fixed;
-  bottom: $-m;
-  right: $-l;
-  padding: 5px 7px;
-  cursor: pointer;
-  color: #FFF;
-  fill: #FFF;
-  svg {
-    width: math.div($btt-size, 1.5);
-    height: math.div($btt-size, 1.5);
-    margin-inline-end: 4px;
-  }
-  width: $btt-size;
-  height: $btt-size;
-  border-radius: $btt-size;
-  transition: all ease-in-out 180ms;
-  opacity: 0;
-  z-index: 999;
-  overflow: hidden;
-  &:hover {
-    width: $btt-size*3.4;
-    opacity: 1 !important;
-  }
-  .inner {
-    width: $btt-size*3.4;
-  }
-  span {
-    position: relative;
-    vertical-align: top;
-    line-height: 2;
-  }
-}
-
 .skip-to-content-link {
   position: fixed;
   top: -52px;
@@ -352,15 +316,4 @@ input.scroll-box-search, .scroll-box-header-item {
       transform: rotate(180deg);
     }
   }
-}
-
-table.table .table-user-item {
-  display: grid;
-  grid-template-columns: 42px 1fr;
-  align-items: center;
-}
-table.table .table-entity-item {
-  display: grid;
-  grid-template-columns: 36px 1fr;
-  align-items: center;
 }
\ No newline at end of file
index 8ce24baaec5e8e447d541f7010ffcc2420c5fe5b..9345a7bcead45420fdb3825b1cf51227edb21861 100644 (file)
@@ -38,7 +38,7 @@
 
             <div style="overflow: auto;">
 
-                <section code-highlighter class="card content-wrap auto-height">
+                <section component="code-highlighter" class="card content-wrap auto-height">
                     @include('api-docs.parts.getting-started')
                 </section>
 
index 6e3d93659d2e76af07a2a2cc868e8663f4958b74..60c478fe563deaacd6c60c368c8f8cf16fafee6d 100644 (file)
 @endif
 
 @if($endpoint['example_request'] ?? false)
-    <details details-highlighter class="mb-m">
+    <details component="details-highlighter" class="mb-m">
         <summary class="text-muted">Example Request</summary>
         <pre><code class="language-json">{{ $endpoint['example_request'] }}</code></pre>
     </details>
 @endif
 
 @if($endpoint['example_response'] ?? false)
-    <details details-highlighter class="mb-m">
+    <details component="details-highlighter" class="mb-m">
         <summary class="text-muted">Example Response</summary>
         <pre><code class="language-json">{{ $endpoint['example_response'] }}</code></pre>
     </details>
index 024cb583c522e8147fca04626f1a67ba09e4e31a..724ca9c8eb93c38de37c75610aeaf31583732617 100644 (file)
@@ -1,6 +1,9 @@
-<div style="display: block;" toolbox-tab-content="files"
+<div style="display: block;"
+     refs="editor-toolbox@tab-content"
+     data-tab-content="files"
      component="attachments"
-     option:attachments:page-id="{{ $page->id ?? 0 }}">
+     option:attachments:page-id="{{ $page->id ?? 0 }}"
+     class="toolbox-tab-content">
 
     <h4>{{ trans('entities.attachments') }}</h4>
     <div class="px-l files">
index de99bb3f29feeb55babaa3f13020c474978f4a1f..6278adcd7a89eeb19c626b4ed4d1907f2d64e2cc 100644 (file)
@@ -9,6 +9,8 @@
         <div class="card content-wrap auto-height">
             <h1 class="list-heading">{{ Str::title(trans('auth.log_in')) }}</h1>
 
+            @include('auth.parts.login-message')
+
             @include('auth.parts.login-form-' . $authMethod)
 
             @if(count($socialDrivers) > 0)
diff --git a/resources/views/auth/parts/login-message.blade.php b/resources/views/auth/parts/login-message.blade.php
new file mode 100644 (file)
index 0000000..4711989
--- /dev/null
@@ -0,0 +1,2 @@
+{{-- This is a placeholder template file provided as a --}}
+{{-- convenience to users of the visual theme system. --}}
\ No newline at end of file
diff --git a/resources/views/auth/parts/register-message.blade.php b/resources/views/auth/parts/register-message.blade.php
new file mode 100644 (file)
index 0000000..4711989
--- /dev/null
@@ -0,0 +1,2 @@
+{{-- This is a placeholder template file provided as a --}}
+{{-- convenience to users of the visual theme system. --}}
\ No newline at end of file
diff --git a/resources/views/auth/register-confirm-accept.blade.php b/resources/views/auth/register-confirm-accept.blade.php
new file mode 100644 (file)
index 0000000..e52bdb4
--- /dev/null
@@ -0,0 +1,27 @@
+@extends('layouts.simple')
+
+@section('content')
+
+    <div class="container very-small mt-xl">
+        <div class="card content-wrap auto-height">
+            <h1 class="list-heading">{{ trans('auth.email_confirm_thanks') }}</h1>
+
+            <p class="mb-none">{{ trans('auth.email_confirm_thanks_desc') }}</p>
+
+            <div class="flex-container-row items-center wrap">
+                <div class="flex min-width-s">
+                    @include('common.loading-icon')
+                </div>
+                <div class="flex min-width-s text-s-right">
+                    <form component="auto-submit" action="{{ url('/register/confirm/accept') }}" method="post">
+                        {{ csrf_field() }}
+                        <input type="hidden" name="token" value="{{ $token }}">
+                        <button class="text-button">{{ trans('common.continue') }}</button>
+                    </form>
+                </div>
+            </div>
+
+        </div>
+    </div>
+
+@stop
index 91ec0b621f12f2190edd676c31e3ff1310db7dc2..d345b037aaa0c0dfb18927d845f6af7a9a07a4bb 100644 (file)
@@ -8,6 +8,8 @@
         <div class="card content-wrap auto-height">
             <h1 class="list-heading">{{ Str::title(trans('auth.sign_up')) }}</h1>
 
+            @include('auth.parts.register-message')
+
             <form action="{{ url("/register") }}" method="POST" class="mt-l stretch-inputs">
                 {!! csrf_field() !!}
 
index 6573bbe6a87adef2838aa2808d863fd08b96d7d6..dc51a3a80af86cac004f03fb76335aec4af6b6ac 100644 (file)
@@ -1,7 +1,7 @@
 @extends('layouts.tri')
 
 @section('body')
-    @include('books.parts.list', ['books' => $books, 'view' => $view])
+    @include('books.parts.list', ['books' => $books, 'view' => $view, 'listOptions' => $listOptions])
 @stop
 
 @section('left')
@@ -37,7 +37,7 @@
         <h5>{{ trans('common.actions') }}</h5>
         <div class="icon-list text-primary">
             @if(user()->can('book-create-all'))
-                <a href="{{ url("/create-book") }}" class="icon-list-item">
+                <a href="{{ url("/create-book") }}" data-shortcut="new" class="icon-list-item">
                     <span>@icon('add')</span>
                     <span>{{ trans('entities.books_create') }}</span>
                 </a>
index bb87089b272ac3a30e62f5846c7611c67ca8bb4a..e893bceadc55e10acf2a5b986cf9fcdceb6fe98f 100644 (file)
     @include('form.textarea', ['name' => 'description'])
 </div>
 
-<div class="form-group" collapsible id="logo-control">
-    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
         <label>{{ trans('common.cover_image') }}</label>
     </button>
-    <div class="collapse-content" collapsible-content>
+    <div refs="collapsible@content" class="collapse-content">
         <p class="small">{{ trans('common.cover_image_description') }}</p>
 
         @include('form.image-picker', [
     </div>
 </div>
 
-<div class="form-group" collapsible id="tags-control">
-    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="tags-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
         <label for="tag-manager">{{ trans('entities.book_tags') }}</label>
     </button>
-    <div class="collapse-content" collapsible-content>
+    <div refs="collapsible@content" class="collapse-content">
         @include('entities.tag-manager', ['entity' => $book ?? null])
     </div>
 </div>
index 30b0766135ccff81c87fb2d3fa7c1a5ba9d2de4a..2cf83dfa91b6d55d16c6becd2435dbb5c0267480 100644 (file)
@@ -2,13 +2,7 @@
     <div class="grid half v-center no-row-gap">
         <h1 class="list-heading">{{ trans('entities.books') }}</h1>
         <div class="text-m-right my-m">
-
-            @include('entities.sort', ['options' => [
-                'name' => trans('common.sort_name'),
-                'created_at' => trans('common.sort_created_at'),
-                'updated_at' => trans('common.sort_updated_at'),
-            ], 'order' => $order, 'sort' => $sort, 'type' => 'books'])
-
+            @include('common.sort', $listOptions->getSortControlData())
         </div>
     </div>
     @if(count($books) > 0)
                 @endforeach
             </div>
         @else
-             <div class="grid third">
+            <div class="grid third">
                 @foreach($books as $key => $book)
                     @include('entities.grid-item', ['entity' => $book])
                 @endforeach
-             </div>
+            </div>
         @endif
         <div>
             {!! $books->render() !!}
index f043735bbf4c9c0df001d5f40fc565756b467550..ef9929e46486b9e9fd944ca8cc1652a5551f366d 100644 (file)
@@ -4,11 +4,11 @@
         <span>{{ $book->name }}</span>
     </h5>
     <div class="sort-box-options pb-sm">
-        <a href="#" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</a>
-        <a href="#" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</a>
-        <a href="#" data-sort="updated" class="button outline small">{{ trans('entities.books_sort_updated') }}</a>
-        <a href="#" data-sort="chaptersFirst" class="button outline small">{{ trans('entities.books_sort_chapters_first') }}</a>
-        <a href="#" data-sort="chaptersLast" class="button outline small">{{ trans('entities.books_sort_chapters_last') }}</a>
+        <button type="button" data-sort="name" class="button outline small">{{ trans('entities.books_sort_name') }}</button>
+        <button type="button" data-sort="created" class="button outline small">{{ trans('entities.books_sort_created') }}</button>
+        <button type="button" data-sort="updated" class="button outline small">{{ trans('entities.books_sort_updated') }}</button>
+        <button type="button" data-sort="chaptersFirst" class="button outline small">{{ trans('entities.books_sort_chapters_first') }}</button>
+        <button type="button" data-sort="chaptersLast" class="button outline small">{{ trans('entities.books_sort_chapters_last') }}</button>
     </div>
     <ul class="sortable-page-list sort-list">
 
index b95b69d1b73748fc61985d3ecefd7dedfc9882b3..884082456b6c10dc39e05948590367349112c24f 100644 (file)
         <div class="icon-list text-primary">
 
             @if(userCan('page-create', $book))
-                <a href="{{ $book->getUrl('/create-page') }}" class="icon-list-item">
+                <a href="{{ $book->getUrl('/create-page') }}" data-shortcut="new" class="icon-list-item">
                     <span>@icon('add')</span>
                     <span>{{ trans('entities.pages_new') }}</span>
                 </a>
             @endif
             @if(userCan('chapter-create', $book))
-                <a href="{{ $book->getUrl('/create-chapter') }}" class="icon-list-item">
+                <a href="{{ $book->getUrl('/create-chapter') }}" data-shortcut="new" class="icon-list-item">
                     <span>@icon('add')</span>
                     <span>{{ trans('entities.chapters_new') }}</span>
                 </a>
             <hr class="primary-background">
 
             @if(userCan('book-update', $book))
-                <a href="{{ $book->getUrl('/edit') }}" class="icon-list-item">
+                <a href="{{ $book->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
                     <span>@icon('edit')</span>
                     <span>{{ trans('common.edit') }}</span>
                 </a>
-                <a href="{{ $book->getUrl('/sort') }}" class="icon-list-item">
+                <a href="{{ $book->getUrl('/sort') }}" data-shortcut="sort" class="icon-list-item">
                     <span>@icon('sort')</span>
                     <span>{{ trans('common.sort') }}</span>
                 </a>
             @endif
             @if(userCan('book-create-all'))
-                <a href="{{ $book->getUrl('/copy') }}" class="icon-list-item">
+                <a href="{{ $book->getUrl('/copy') }}" data-shortcut="copy" class="icon-list-item">
                     <span>@icon('copy')</span>
                     <span>{{ trans('common.copy') }}</span>
                 </a>
             @endif
             @if(userCan('restrictions-manage', $book))
-                <a href="{{ $book->getUrl('/permissions') }}" class="icon-list-item">
+                <a href="{{ $book->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
                     <span>@icon('lock')</span>
                     <span>{{ trans('entities.permissions') }}</span>
                 </a>
             @endif
             @if(userCan('book-delete', $book))
-                <a href="{{ $book->getUrl('/delete') }}" class="icon-list-item">
+                <a href="{{ $book->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
                     <span>@icon('delete')</span>
                     <span>{{ trans('common.delete') }}</span>
                 </a>
index a24bd8959203c9b5f24b263aec0374d8fced88c2..077da101d13bddb518f141ad2cde2fe60e2173e1 100644 (file)
 
         <div class="grid left-focus gap-xl">
             <div>
-                <div book-sort class="card content-wrap">
+                <div component="book-sort" class="card content-wrap">
                     <h1 class="list-heading mb-l">{{ trans('entities.books_sort') }}</h1>
-                    <div book-sort-boxes>
+                    <div refs="book-sort@sortContainer">
                         @include('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
                     </div>
 
                     <form action="{{ $book->getUrl('/sort') }}" method="POST">
                         {!! csrf_field() !!}
                         <input type="hidden" name="_method" value="PUT">
-                        <input book-sort-input type="hidden" name="sort-tree">
+                        <input refs="book-sort@input" type="hidden" name="sort-tree">
                         <div class="list text-right">
                             <a href="{{ $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
                             <button class="button" type="submit">{{ trans('entities.books_sort_save') }}</button>
index 3908d0693fb7a3ec635be5d609dfb61f579c3d7c..068c033ab90fb67864b56955e8b6f445b12bd109 100644 (file)
     @include('form.textarea', ['name' => 'description'])
 </div>
 
-<div class="form-group" collapsible id="logo-control">
-    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
         <label for="tags">{{ trans('entities.chapter_tags') }}</label>
     </button>
-    <div class="collapse-content" collapsible-content>
+    <div refs="collapsible@content" class="collapse-content">
         @include('entities.tag-manager', ['entity' => $chapter ?? null])
     </div>
 </div>
index b3496eae23717f8bb648430ba149d06a84ab7945..d2f8cec97f7353548d948ba6fec308c6f6303a4c 100644 (file)
         <div class="icon-list text-primary">
 
             @if(userCan('page-create', $chapter))
-                <a href="{{ $chapter->getUrl('/create-page') }}" class="icon-list-item">
+                <a href="{{ $chapter->getUrl('/create-page') }}" data-shortcut="new" class="icon-list-item">
                     <span>@icon('add')</span>
                     <span>{{ trans('entities.pages_new') }}</span>
                 </a>
             <hr class="primary-background"/>
 
             @if(userCan('chapter-update', $chapter))
-                <a href="{{ $chapter->getUrl('/edit') }}" class="icon-list-item">
+                <a href="{{ $chapter->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
                     <span>@icon('edit')</span>
                     <span>{{ trans('common.edit') }}</span>
                 </a>
             @endif
             @if(userCanOnAny('create', \BookStack\Entities\Models\Book::class) || userCan('chapter-create-all') || userCan('chapter-create-own'))
-                <a href="{{ $chapter->getUrl('/copy') }}" class="icon-list-item">
+                <a href="{{ $chapter->getUrl('/copy') }}" data-shortcut="copy" class="icon-list-item">
                     <span>@icon('copy')</span>
                     <span>{{ trans('common.copy') }}</span>
                 </a>
             @endif
             @if(userCan('chapter-update', $chapter) && userCan('chapter-delete', $chapter))
-                <a href="{{ $chapter->getUrl('/move') }}" class="icon-list-item">
+                <a href="{{ $chapter->getUrl('/move') }}" data-shortcut="move" class="icon-list-item">
                     <span>@icon('folder')</span>
                     <span>{{ trans('common.move') }}</span>
                 </a>
             @endif
             @if(userCan('restrictions-manage', $chapter))
-                <a href="{{ $chapter->getUrl('/permissions') }}" class="icon-list-item">
+                <a href="{{ $chapter->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
                     <span>@icon('lock')</span>
                     <span>{{ trans('entities.permissions') }}</span>
                 </a>
             @endif
             @if(userCan('chapter-delete', $chapter))
-                <a href="{{ $chapter->getUrl('/delete') }}" class="icon-list-item">
+                <a href="{{ $chapter->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
                     <span>@icon('delete')</span>
                     <span>{{ trans('common.delete') }}</span>
                 </a>
 
             @if($chapter->book && userCan('book-update', $chapter->book))
                 <hr class="primary-background"/>
-                <a href="{{ $chapter->book->getUrl('/sort') }}" class="icon-list-item">
+                <a href="{{ $chapter->book->getUrl('/sort') }}" data-shortcut="sort" class="icon-list-item">
                     <span>@icon('sort')</span>
                     <span>{{ trans('entities.chapter_sort_book') }}</span>
                 </a>
index 0812e487a06799971359e8acda65265c8cab5b9d..d6ecbc4d624c94e9b0f5e5197586ad21f1af0545 100644 (file)
@@ -1,4 +1,4 @@
-<form action="{{ url('/settings/users/toggle-dark-mode') }}" method="post">
+<form action="{{ url('/preferences/toggle-dark-mode') }}" method="post">
     {{ csrf_field() }}
     {{ method_field('patch') }}
     @if(setting()->getForCurrentUser('dark-mode-enabled'))
index 197b80c27ec10c20da9399bd195d0e190d8e69e8..71b73215b7ebb9a59252904536407dd1f3630c95 100644 (file)
@@ -2,7 +2,7 @@
     <div class="grid mx-l">
 
         <div>
-            <a href="{{ url('/') }}" class="logo">
+            <a href="{{ url('/') }}" data-shortcut="home_view" class="logo">
                 @if(setting('app-logo', '') !== 'none')
                     <img class="logo-image" src="{{ setting('app-logo', '') === '' ? url('/logo.png') : url(setting('app-logo', '')) }}" alt="Logo">
                 @endif
 
         <div class="flex-container-column items-center justify-center hide-under-l">
             @if (hasAppAccess())
-            <form action="{{ url('/search') }}" method="GET" class="search-box" role="search">
-                <button id="header-search-box-button" type="submit" aria-label="{{ trans('common.search') }}" tabindex="-1">@icon('search') </button>
-                <input id="header-search-box-input" type="text" name="term"
+            <form component="global-search" action="{{ url('/search') }}" method="GET" class="search-box" role="search">
+                <button id="header-search-box-button"
+                        refs="global-search@button"
+                        type="submit"
+                        aria-label="{{ trans('common.search') }}"
+                        tabindex="-1">@icon('search')</button>
+                <input id="header-search-box-input"
+                       refs="global-search@input"
+                       type="text"
+                       name="term"
+                       data-shortcut="global_search"
+                       autocomplete="off"
                        aria-label="{{ trans('common.search') }}" placeholder="{{ trans('common.search') }}"
-                       value="{{ isset($searchTerm) ? $searchTerm : '' }}">
+                       value="{{ $searchTerm ?? '' }}">
+                <div refs="global-search@suggestions" class="global-search-suggestions card">
+                    <div refs="global-search@loading" class="text-center px-m global-search-loading">@include('common.loading-icon')</div>
+                    <div refs="global-search@suggestion-results" class="px-m"></div>
+                    <button class="text-button card-footer-link" type="submit">{{ trans('common.view_all') }}</button>
+                </div>
             </form>
             @endif
         </div>
                 @if (hasAppAccess())
                     <a class="hide-over-l" href="{{ url('/search') }}">@icon('search'){{ trans('common.search') }}</a>
                     @if(userCanOnAny('view', \BookStack\Entities\Models\Bookshelf::class) || userCan('bookshelf-view-all') || userCan('bookshelf-view-own'))
-                        <a href="{{ url('/shelves') }}">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
+                        <a href="{{ url('/shelves') }}" data-shortcut="shelves_view">@icon('bookshelf'){{ trans('entities.shelves') }}</a>
                     @endif
-                    <a href="{{ url('/books') }}">@icon('books'){{ trans('entities.books') }}</a>
+                    <a href="{{ url('/books') }}" data-shortcut="books_view">@icon('books'){{ trans('entities.books') }}</a>
                     @if(signedInUser() && userCan('settings-manage'))
-                        <a href="{{ url('/settings') }}">@icon('settings'){{ trans('settings.settings') }}</a>
+                        <a href="{{ url('/settings') }}" data-shortcut="settings_view">@icon('settings'){{ trans('settings.settings') }}</a>
                     @endif
                     @if(signedInUser() && userCan('users-manage') && !userCan('settings-manage'))
-                        <a href="{{ url('/settings/users') }}">@icon('users'){{ trans('settings.users') }}</a>
+                        <a href="{{ url('/settings/users') }}" data-shortcut="settings_view">@icon('users'){{ trans('settings.users') }}</a>
                     @endif
                 @endif
 
                         </span>
                     <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
                         <li>
-                            <a href="{{ url('/favourites') }}" class="icon-item">
+                            <a href="{{ url('/favourites') }}" data-shortcut="favourites_view" class="icon-item">
                                 @icon('star')
                                 <div>{{ trans('entities.my_favourites') }}</div>
                             </a>
                         </li>
                         <li>
-                            <a href="{{ $currentUser->getProfileUrl() }}" class="icon-item">
+                            <a href="{{ $currentUser->getProfileUrl() }}" data-shortcut="profile_view" class="icon-item">
                                 @icon('user')
                                 <div>{{ trans('common.view_profile') }}</div>
                             </a>
                             <form action="{{ url(config('auth.method') === 'saml2' ? '/saml2/logout' : '/logout') }}"
                                   method="post">
                                 {{ csrf_field() }}
-                                <button class="icon-item">
+                                <button class="icon-item" data-shortcut="logout">
                                     @icon('logout')
                                     <div>{{ trans('auth.logout') }}</div>
                                 </button>
                             </form>
                         </li>
                         <li><hr></li>
+                        <li>
+                            <a href="{{ url('/preferences/shortcuts') }}" class="icon-item">
+                                @icon('shortcuts')
+                                <div>{{ trans('preferences.shortcuts') }}</div>
+                            </a>
+                        </li>
                         <li>
                             @include('common.dark-mode-toggle', ['classes' => 'icon-item'])
                         </li>
index 752920917570a9a11d974f7088bf7e572a5e6a0f..e06bd5fd13861b0e85a03ebdbea69817fc8644f6 100644 (file)
@@ -1,11 +1,29 @@
-<div notification="success" style="display: none;" data-autohide class="pos" role="alert" @if(session()->has('success')) data-show @endif>
+<div component="notification"
+     option:notification:type="success"
+     option:notification:auto-hide="true"
+     option:notification:show="{{ session()->has('success') ? 'true' : 'false' }}"
+     style="display: none;"
+     class="notification pos"
+     role="alert">
     @icon('check-circle') <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span><div class="dismiss">@icon('close')</div>
 </div>
 
-<div notification="warning" style="display: none;" class="warning" role="alert" @if(session()->has('warning')) data-show @endif>
+<div component="notification"
+     option:notification:type="warning"
+     option:notification:auto-hide="false"
+     option:notification:show="{{ session()->has('warning') ? 'true' : 'false' }}"
+     style="display: none;"
+     class="notification warning"
+     role="alert">
     @icon('info') <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span><div class="dismiss">@icon('close')</div>
 </div>
 
-<div notification="error" style="display: none;" class="neg" role="alert" @if(session()->has('error')) data-show @endif>
+<div component="notification"
+     option:notification:type="error"
+     option:notification:auto-hide="false"
+     option:notification:show="{{ session()->has('error') ? 'true' : 'false' }}"
+     style="display: none;"
+     class="notification neg"
+     role="alert">
     @icon('danger') <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span><div class="dismiss">@icon('close')</div>
-</div>
+</div>
\ No newline at end of file
similarity index 52%
rename from resources/views/entities/sort.blade.php
rename to resources/views/common/sort.blade.php
index f81ed797f652f940a28cef225357ad751e3360a1..29dfa60a1ac593246ec8760c0ebeb1fe08b60bfc 100644 (file)
@@ -2,25 +2,40 @@
     $selectedSort = (isset($sort) && array_key_exists($sort, $options)) ? $sort : array_keys($options)[0];
     $order = (isset($order) && in_array($order, ['asc', 'desc'])) ? $order : 'asc';
 ?>
-<div class="list-sort-container" list-sort-control>
+<div component="list-sort-control" class="list-sort-container">
     <div class="list-sort-label">{{ trans('common.sort') }}</div>
-    <form action="{{ url("/settings/users/". user()->id ."/change-sort/{$type}") }}" method="post">
+    <form refs="list-sort-control@form"
+          @if($useQuery ?? false)
+              action="{{ url()->current() }}"
+              method="get"
+          @else
+              action="{{ url("/preferences/change-sort/{$type}") }}"
+              method="post"
+          @endif
+    >
 
-        {!! csrf_field() !!}
-        {!! method_field('PATCH') !!}
-        <input type="hidden" value="{{ $selectedSort }}" name="sort">
-        <input type="hidden" value="{{ $order }}" name="order">
+        @if($useQuery ?? false)
+            @foreach(array_filter(request()->except(['sort', 'order'])) as $key => $value)
+                <input type="hidden" name="{{ $key }}" value="{{ $value }}">
+            @endforeach
+        @else
+            {!! method_field('PATCH') !!}
+            {!! csrf_field() !!}
+        @endif
+
+        <input refs="list-sort-control@sort" type="hidden" value="{{ $selectedSort }}" name="sort">
+        <input refs="list-sort-control@order" type="hidden" value="{{ $order }}" name="order">
 
         <div class="list-sort">
             <div component="dropdown" class="list-sort-type dropdown-container">
                 <div refs="dropdown@toggle" aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('common.sort_options') }}" tabindex="0">{{ $options[$selectedSort] }}</div>
-                <ul refs="dropdown@menu" class="dropdown-menu">
+                <ul refs="dropdown@menu list-sort-control@menu" class="dropdown-menu">
                     @foreach($options as $key => $label)
                         <li @if($key === $selectedSort) class="active" @endif><a href="#" data-sort-value="{{$key}}" class="text-item">{{ $label }}</a></li>
                     @endforeach
                 </ul>
             </div>
-            <button href="#" class="list-sort-dir" type="button" data-sort-dir
+            <button class="list-sort-dir" type="button" data-sort-dir
                     aria-label="{{ trans('common.sort_direction_toggle') }} - {{ $order === 'asc' ? trans('common.sort_ascending') : trans('common.sort_descending') }}" tabindex="0">
                 @icon($order === 'desc' ? 'sort-up' : 'sort-down')
             </button>
diff --git a/resources/views/common/status-indicator.blade.php b/resources/views/common/status-indicator.blade.php
new file mode 100644 (file)
index 0000000..ba9b1b4
--- /dev/null
@@ -0,0 +1,3 @@
+<span title="{{ trans('common.status_' . ($status ? 'active' : 'inactive')) }}"
+      class="status-indicator-{{ $status ? 'active' : 'inactive' }}"
+></span>
\ No newline at end of file
index bac240b1eed0c2a493b3ba8b09727067c08bf4c6..a55ab56d199cf174b6144f26bf0b7306bfaaa72d 100644 (file)
@@ -2,8 +2,13 @@
      class="dropdown-container"
      id="export-menu">
 
-    <div refs="dropdown@toggle" class="icon-list-item"
-         aria-haspopup="true" aria-expanded="false" aria-label="{{ trans('entities.export') }}" tabindex="0">
+    <div refs="dropdown@toggle"
+         class="icon-list-item"
+         aria-haspopup="true"
+         aria-expanded="false"
+         aria-label="{{ trans('entities.export') }}"
+         data-shortcut="export"
+         tabindex="0">
         <span>@icon('export')</span>
         <span>{{ trans('entities.export') }}</span>
     </div>
index 49ba6aa5db426224bfc0e9c6d367e4b31ab23a29..24bd40950e87e7ee5560f4ea2d29a940ee033a84 100644 (file)
@@ -5,7 +5,7 @@
     {{ csrf_field() }}
     <input type="hidden" name="type" value="{{ get_class($entity) }}">
     <input type="hidden" name="id" value="{{ $entity->id }}">
-    <button type="submit" class="icon-list-item text-primary">
+    <button type="submit" data-shortcut="favourite" class="icon-list-item text-primary">
         <span>@icon($isFavourite ? 'star' : 'star-outline')</span>
         <span>{{ $isFavourite ? trans('common.unfavourite') : trans('common.favourite') }}</span>
     </button>
index b65d5df944c2e399650a2015b4a671355ee21c27..1a1f785e6652605cbb44e9fad446460854728be5 100644 (file)
@@ -5,6 +5,6 @@
     <form refs="entity-search@searchForm" class="search-box flexible" role="search">
         <input refs="entity-search@searchInput" type="text"
                aria-label="{{ $label }}" name="term" placeholder="{{ $label }}">
-        <button type="submit" aria-label="{{ trans('common.search') }}">@icon('search')</button>
+        <button tabindex="-1" type="submit" aria-label="{{ trans('common.search') }}">@icon('search')</button>
     </form>
 </div>
\ No newline at end of file
index 1f64bac3e5d56aa13f7733b0c0d24b98a67d15b9..28a9cb0292aed07c8e0d27d981644fc3b301973a 100644 (file)
@@ -1,7 +1,7 @@
 <div id="sibling-navigation" class="grid half collapse-xs items-center mb-m px-m no-row-gap fade-in-when-active print-hidden">
     <div>
         @if($previous)
-            <a href="{{  $previous->getUrl()  }}" class="outline-hover no-link-style block rounded">
+            <a href="{{  $previous->getUrl()  }}" data-shortcut="previous" class="outline-hover no-link-style block rounded">
                 <div class="px-m pt-xs text-muted">{{ trans('common.previous') }}</div>
                 <div class="inline-block">
                     <div class="icon-list-item no-hover">
@@ -14,7 +14,7 @@
     </div>
     <div>
         @if($next)
-            <a href="{{  $next->getUrl()  }}" class="outline-hover no-link-style block rounded text-xs-right">
+            <a href="{{  $next->getUrl()  }}" data-shortcut="next" class="outline-hover no-link-style block rounded text-xs-right">
                 <div class="px-m pt-xs text-muted text-xs-right">{{ trans('common.next') }}</div>
                 <div class="inline block">
                     <div class="icon-list-item no-hover">
index 9ff1b49277d035c17f2df0169f90eab1ddbd2bfc..e376b878dda1c8f240e471279db8c005c05030af 100644 (file)
@@ -1,15 +1,15 @@
 <div>
-    <form action="{{ url("/settings/users/". user()->id ."/switch-${type}-view") }}" method="POST" class="inline">
+    <form action="{{ url("/preferences/change-view/" . $type) }}" method="POST" class="inline">
         {!! csrf_field() !!}
         {!! method_field('PATCH') !!}
-        <input type="hidden" value="{{ $view === 'list'? 'grid' : 'list' }}" name="view_type">
+
         @if ($view === 'list')
-            <button type="submit" class="icon-list-item text-primary">
+            <button type="submit" name="view" value="grid" class="icon-list-item text-primary">
                 <span class="icon">@icon('grid')</span>
                 <span>{{ trans('common.grid_view') }}</span>
             </button>
         @else
-            <button type="submit" class="icon-list-item text-primary">
+            <button type="submit" name="view" value="list" class="icon-list-item text-primary">
                 <span>@icon('list')</span>
                 <span>{{ trans('common.list_view') }}</span>
             </button>
index de3ffe922c13145c04a2e08c87d5ca80d33857b0..7750c6f00078b5a89b59fa0d269e29d6e19fffb5 100644 (file)
@@ -4,7 +4,7 @@ $value
 $checked
 $label
 --}}
-<label custom-checkbox class="toggle-switch @if($errors->has($name)) text-neg @endif">
+<label component="custom-checkbox" class="toggle-switch @if($errors->has($name)) text-neg @endif">
     <input type="checkbox" name="{{$name}}" value="{{ $value }}" @if($checked) checked="checked" @endif @if($disabled ?? false) disabled="disabled" @endif>
     <span tabindex="0" role="checkbox"
           aria-checked="{{ $checked ? 'true' : 'false' }}"
index d2e6a475631ce6a9becada220d9d4c0b8403ccd0..1cfb6ec981c356418e52ca1cf56f529d6e405474 100644 (file)
@@ -5,7 +5,7 @@ $permission - The entity permission containing the permissions.
 $inheriting - Boolean if the current row should be marked as inheriting default permissions. Used for "Everyone Else" role.
 --}}
 
-<div component="permissions-table" class="content-permissions-row flex-container-row justify-space-between wrap">
+<div component="permissions-table" class="item-list-row flex-container-row justify-space-between wrap">
     <div class="gap-x-m flex-container-row items-center px-l py-m flex">
         <div class="text-large" title="{{ $role->id === 0 ? trans('entities.permissions_role_everyone_else') : trans('common.role') }}">
             @icon($role->id === 0 ? 'groups' : 'role')
@@ -16,7 +16,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
         </span>
         @if($role->id !== 0)
             <button type="button"
-                class="ml-auto flex-none text-small text-primary text-button hover-underline content-permissions-row-toggle-all hide-under-s"
+                class="ml-auto flex-none text-small text-primary text-button hover-underline item-list-row-toggle-all hide-under-s"
                 refs="permissions-table@toggle-all"
                 ><strong>{{ trans('common.toggle_all') }}</strong></button>
         @endif
@@ -25,7 +25,7 @@ $inheriting - Boolean if the current row should be marked as inheriting default
         <div class="px-l flex-container-row items-center" refs="entity-permissions@everyone-inherit">
             @include('form.custom-checkbox', [
                 'name' => 'entity-permissions-inherit',
-                'label' => 'Inherit defaults',
+                'label' => trans('entities.permissions_inherit_defaults'),
                 'value' => 'true',
                 'checked' => $inheriting
             ])
index 724d0fb393658b4f5152331d8ba3117d4386ca2b..9bf309fb802952f16f9dabee3f076006087cac63 100644 (file)
@@ -35,7 +35,7 @@
 
     <hr>
 
-    <div refs="entity-permissions@role-container" class="content-permissions mt-m mb-m">
+    <div refs="entity-permissions@role-container" class="item-list mt-m mb-m">
         @foreach($data->permissionsWithRoles() as $permission)
             @include('form.entity-permissions-row', [
                 'permission' => $permission,
@@ -58,7 +58,7 @@
         </div>
     </div>
 
-    <div class="content-permissions mt-m mb-xl">
+    <div class="item-list mt-m mb-xl">
         @include('form.entity-permissions-row', [
                 'role' => $data->everyoneElseRole(),
                 'permission' => $data->everyoneElseEntityPermission(),
index 9c2661cccbd3d19c412a39af93837ad0be8ab205..f7e2c6bb97bd321a6f2b892f8044f05aab81006b 100644 (file)
@@ -1,26 +1,27 @@
-<div class="image-picker @if($errors->has($name)) has-error @endif"
-     image-picker="{{$name}}"
-     data-default-image="{{ $defaultImage }}">
+<div component="image-picker"
+     option:image-picker:default-image="{{ $defaultImage }}"
+     class="image-picker @if($errors->has($name)) has-error @endif">
 
     <div class="grid half">
         <div class="text-center">
-            <img @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif  class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
+            <img refs="image-picker@image"
+                @if($currentImage && $currentImage !== 'none') src="{{$currentImage}}" @else src="{{$defaultImage}}" @endif
+                class="{{$imageClass}} @if($currentImage=== 'none') none @endif" alt="{{ trans('components.image_preview') }}">
         </div>
         <div class="text-center">
-
-            <input type="file" class="custom-file-input" accept="image/*" name="{{ $name }}" id="{{ $name }}">
+            <input refs="image-picker@image-input" type="file" class="custom-file-input" accept="image/*" name="{{ $name }}" id="{{ $name }}">
             <label for="{{ $name }}" class="button outline">{{ trans('components.image_select_image') }}</label>
-            <input type="hidden" data-reset-input name="{{ $name }}_reset" value="true" disabled="disabled">
+            <input refs="image-picker@reset-input" type="hidden" name="{{ $name }}_reset" value="true" disabled="disabled">
             @if(isset($removeName))
-                <input type="hidden" data-remove-input name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
+                <input refs="image-picker@remove-input" type="hidden" name="{{ $removeName }}" value="{{ $removeValue }}" disabled="disabled">
             @endif
 
             <br>
-            <button class="text-button text-muted" data-action="reset-image" type="button">{{ trans('common.reset') }}</button>
+            <button refs="image-picker@reset-button" class="text-button text-muted" type="button">{{ trans('common.reset') }}</button>
 
             @if(isset($removeName))
                 <span class="sep">|</span>
-                <button class="text-button text-muted" data-action="remove-image" type="button">{{ trans('common.remove') }}</button>
+                <button refs="image-picker@remove-button" class="text-button text-muted" type="button">{{ trans('common.remove') }}</button>
             @endif
         </div>
     </div>
index a5eec30051b32a9bcd9e983c0fd98a40c4356ca9..375eda3d7b5cfaea2ece5fd44ab0f3f1590cfe60 100644 (file)
@@ -1,4 +1,4 @@
-<label toggle-switch="{{$name}}" custom-checkbox class="toggle-switch">
+<label components="custom-checkbox toggle-switch" class="toggle-switch">
     <input type="hidden" name="{{$name}}" value="{{$value?'true':'false'}}"/>
     <input type="checkbox" @if($value) checked="checked" @endif>
     <span tabindex="0" role="checkbox"
index 8ed7ff6e036167cb97a7428a57bb906d2f2e7f1e..73d21f5f7b9e1e4d4be38d44471973d2577e8b8a 100644 (file)
@@ -3,10 +3,12 @@ $target - CSS selector of items to expand
 $key - Unique key for checking existing stored state.
 --}}
 <?php $isOpen = setting()->getForCurrentUser('section_expansion#'. $key); ?>
-<button type="button" expand-toggle="{{ $target }}"
-   expand-toggle-update-endpoint="{{ url('/settings/users/'. user()->id .'/update-expansion-preference/' . $key) }}"
-   expand-toggle-is-open="{{ $isOpen ? 'yes' : 'no' }}"
-   class="icon-list-item {{ $classes ?? '' }}">
+<button component="expand-toggle"
+        option:expand-toggle:target-selector="{{ $target }}"
+        option:expand-toggle:update-endpoint="{{ url('/preferences/change-expansion/' . $key) }}"
+        option:expand-toggle:is-open="{{ $isOpen ? 'true' : 'false' }}"
+        type="button"
+        class="icon-list-item {{ $classes ?? '' }}">
     <span>@icon('expand-text')</span>
     <span>{{ trans('common.toggle_details') }}</span>
 </button>
index c525643b9c0fcb568f842067465d8e2a1061c174..fc99b915f3b3964722fcec359e3def557dace6b5 100644 (file)
@@ -18,7 +18,7 @@
                     <span>{{ trans('entities.shelves_new_action') }}</span>
                 </a>
             @endif
-            @include('entities.view-toggle', ['view' => $view, 'type' => 'shelves'])
+            @include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelves'])
             @include('home.parts.expand-toggle', ['classes' => 'text-primary', 'target' => '.entity-list.compact .entity-item-snippet', 'key' => 'home-details'])
             @include('common.dark-mode-toggle', ['classes' => 'icon-list-item text-primary'])
         </div>
index 936433b276adb445df7a3713396b645d7728d04f..5c8e65b251148f9cadb731b5cddce0bbb818f726 100644 (file)
@@ -3,7 +3,9 @@
 @section('body')
     <div class="mt-m">
         <main class="content-wrap card">
-            <div class="page-content" page-display="{{ $customHomepage->id }}">
+            <div component="page-display"
+                 option:page-display:page-id="{{ $customHomepage->id }}"
+                 class="page-content">
                 @include('pages.parts.page-display', ['page' => $customHomepage])
             </div>
         </main>
index 9f6e9f89af0f99fd54c19d26a92c4e1ccefb2ae4..76d220952924b6f382453411e0e375845f032412 100644 (file)
     <!-- Translations for JS -->
     @stack('translations')
 </head>
-<body class="@stack('body-class')">
+<body
+    @if(setting()->getForCurrentUser('ui-shortcuts-enabled', false))
+        component="shortcuts"
+        option:shortcuts:key-map="{{ \BookStack\Settings\UserShortcutMap::fromUserPreferences()->toJson() }}"
+    @endif
+      class="@stack('body-class')">
 
     @include('layouts.parts.base-body-start')
     @include('common.skip-to-content')
@@ -44,7 +49,7 @@
 
     @include('common.footer')
 
-    <div back-to-top class="primary-background print-hidden">
+    <div component="back-to-top" class="back-to-top print-hidden">
         <div class="inner">
             @icon('chevron-up') <span>{{ trans('common.back_to_top') }}</span>
         </div>
index 770ed48406ca547e80696d73160df0d9e10d0928..18c9ad4231e9b82a5f211c36260f74ac5a2d1d06 100644 (file)
                     <div refs="code-editor@language-options-container" class="lang-options">
                         @php
                             $languages = [
-                                'Bash', 'CSS', 'C', 'C++', 'C#', 'Diff', 'Fortran', 'F#', 'Go', 'Haskell', 'HTML', 'INI',
+                                'Bash', 'CSS', 'C', 'C++', 'C#', 'Dart', 'Diff', 'Fortran', 'F#', 'Go', 'Haskell', 'HTML', 'INI',
                                 'Java', 'JavaScript', 'JSON', 'Julia', 'Kotlin', 'LaTeX', 'Lua', 'MarkDown', 'MATLAB', 'Nginx', 'OCaml',
-                                'Octave', 'Pascal', 'Perl', 'PHP', 'Powershell', 'Python', 'Ruby', 'Rust', 'Shell', 'SQL', 'TypeScript',
-                                'VBScript', 'VB.NET', 'XML', 'YAML',
+                                'Octave', 'Pascal', 'Perl', 'PHP', 'Powershell', 'Python', 'Ruby', 'Rust', 'Shell', 'SQL', 'Swift',
+                                 'TypeScript', 'VBScript', 'VB.NET', 'XML', 'YAML',
                             ];
                         @endphp
 
index f3b54ddcd6eb0efb0978dea032555a54feeeab1d..8f332702486105f35c523e5e5660167eded8dfc2 100644 (file)
@@ -1,15 +1,15 @@
-<div editor-toolbox class="floating-toolbox">
+<div component="editor-toolbox" class="floating-toolbox">
 
     <div class="tabs primary-background-light">
-        <button type="button" toolbox-toggle aria-expanded="false">@icon('caret-left-circle')</button>
-        <button type="button" toolbox-tab-button="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</button>
+        <button type="button" refs="editor-toolbox@toggle" aria-expanded="false" class="toolbox-toggle">@icon('caret-left-circle')</button>
+        <button type="button" refs="editor-toolbox@tab-button" data-tab="tags" title="{{ trans('entities.page_tags') }}" class="active">@icon('tag')</button>
         @if(userCan('attachment-create-all'))
-            <button type="button" toolbox-tab-button="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
+            <button type="button" refs="editor-toolbox@tab-button" data-tab="files" title="{{ trans('entities.attachments') }}">@icon('attach')</button>
         @endif
-        <button type="button" toolbox-tab-button="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
+        <button type="button" refs="editor-toolbox@tab-button" data-tab="templates" title="{{ trans('entities.templates') }}">@icon('template')</button>
     </div>
 
-    <div toolbox-tab-content="tags">
+    <div refs="editor-toolbox@tab-content" data-tab-content="tags" class="toolbox-tab-content">
         <h4>{{ trans('entities.page_tags') }}</h4>
         <div class="px-l">
             @include('entities.tag-manager', ['entity' => $page])
@@ -20,7 +20,7 @@
         @include('attachments.manager', ['page' => $page])
     @endif
 
-    <div toolbox-tab-content="templates">
+    <div refs="editor-toolbox@tab-content" data-tab-content="templates" class="toolbox-tab-content">
         <h4>{{ trans('entities.templates') }}</h4>
 
         <div class="px-l">
index 39d628e17d0ce6ec503d6f8ea76051fa7d1238a2..fd8a20a0484f68722ad026f6dcf1245187b5fdb4 100644 (file)
@@ -5,24 +5,34 @@
      option:markdown-editor:server-upload-limit-text="{{ trans('errors.server_upload_limit') }}"
      class="flex-fill flex code-fill">
 
-    <div class="markdown-editor-wrap active">
-        <div class="editor-toolbar">
-            <span class="float left editor-toolbar-label">{{ trans('entities.pages_md_editor') }}</span>
-            <div class="float right buttons">
+    <div class="markdown-editor-wrap active flex-container-column">
+        <div class="editor-toolbar flex-container-row items-stretch justify-space-between">
+            <div class="editor-toolbar-label text-mono px-m py-xs flex-container-row items-center flex">
+                <span>{{ trans('entities.pages_md_editor') }}</span>
+            </div>
+            <div component="dropdown" class="buttons flex-container-row items-stretch">
                 @if(config('services.drawio'))
-                    <button class="text-button" type="button" data-action="insertDrawing">@icon('drawing'){{ trans('entities.pages_md_insert_drawing') }}</button>
-                    <span class="mx-xs text-muted">|</span>
+                    <button class="text-button" type="button" data-action="insertDrawing" title="{{ trans('entities.pages_md_insert_drawing') }}">@icon('drawing')</button>
                 @endif
-                <button class="text-button" type="button" data-action="insertImage">@icon('image'){{ trans('entities.pages_md_insert_image') }}</button>
-                <span class="mx-xs text-muted">|</span>
-                <button class="text-button" type="button" data-action="insertLink">@icon('link'){{ trans('entities.pages_md_insert_link') }}</button>
-                <span class="mx-xs text-muted">|</span>
-                <button class="text-button" type="button" data-action="fullscreen">@icon('fullscreen'){{ trans('common.fullscreen') }}</button>
+                <button class="text-button" type="button" data-action="insertImage" title="{{ trans('entities.pages_md_insert_image') }}">@icon('image')</button>
+                <button class="text-button" type="button" data-action="insertLink" title="{{ trans('entities.pages_md_insert_link') }}">@icon('link')</button>
+                <button class="text-button" type="button" data-action="fullscreen" title="{{ trans('common.fullscreen') }}">@icon('fullscreen')</button>
+                <button refs="dropdown@toggle" class="text-button" type="button" title="{{ trans('common.more') }}">@icon('more')</button>
+                <div refs="dropdown@menu markdown-editor@setting-container" class="dropdown-menu" role="menu">
+                    <div class="px-m">
+                        @include('form.custom-checkbox', ['name' => 'md-showPreview', 'label' => trans('entities.pages_md_show_preview'), 'value' => true, 'checked' => true])
+                    </div>
+                    <hr class="m-none">
+                    <div class="px-m">
+                        @include('form.custom-checkbox', ['name' => 'md-scrollSync', 'label' => trans('entities.pages_md_sync_scroll'), 'value' => true, 'checked' => true])
+                    </div>
+                </div>
             </div>
         </div>
 
         <div markdown-input class="flex flex-fill">
             <textarea id="markdown-editor-input"
+                      refs="markdown-editor@input"
                       @if($errors->has('markdown')) class="text-neg" @endif
                       name="markdown"
                       rows="5">@if(isset($model) || old('markdown')){{ old('markdown') ?? ($model->markdown === '' ? $model->html : $model->markdown) }}@endif</textarea>
 
     </div>
 
-    <div class="markdown-editor-wrap">
-        <div class="editor-toolbar">
-            <div class="editor-toolbar-label">{{ trans('entities.pages_md_preview') }}</div>
+    <div refs="markdown-editor@display-wrap" class="markdown-editor-wrap flex-container-row items-stretch" style="display: none">
+        <div refs="markdown-editor@divider" class="markdown-panel-divider flex-fill"></div>
+        <div class="flex-container-column flex flex-fill">
+            <div class="editor-toolbar">
+                <div class="editor-toolbar-label text-mono px-m py-xs">{{ trans('entities.pages_md_preview') }}</div>
+            </div>
+            <iframe src="about:blank"
+                    refs="markdown-editor@display"
+                    class="markdown-display flex flex-fill"
+                    sandbox="allow-same-origin"></iframe>
         </div>
-        <iframe src="about:blank" class="markdown-display" sandbox="allow-same-origin"></iframe>
     </div>
 </div>
 
similarity index 62%
rename from resources/views/pages/parts/revision-table-row.blade.php
rename to resources/views/pages/parts/revisions-index-row.blade.php
index 24301adc376991a9bbe4412440f627fd2ee14388..597b53234198cef25addab495078237721302a61 100644 (file)
@@ -1,38 +1,43 @@
-<tr>
-    <td>{{ $revision->revision_number == 0 ? '' : $revision->revision_number }}</td>
-    <td>
+<div class="item-list-row flex-container-row items-center wrap">
+    <div class="flex fit-content min-width-xxxxs px-m py-xs">
+        <span class="hide-over-l">{{ trans('entities.pages_revisions_number') }}</span>
+        {{ $revision->revision_number == 0 ? '' : $revision->revision_number }}
+    </div>
+    <div class="flex-2 px-m py-xs min-width-s">
         {{ $revision->name }}
         <br>
-        <small class="text-muted">({{ $revision->is_markdown ? 'Markdown' : 'WYSIWYG' }})</small>
-    </td>
-    <td style="line-height: 0;" width="30">
-        @if($revision->createdBy)
-            <img class="avatar" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
-        @endif
-    </td>
-    <td width="260">
-        @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif
-        <br>
-        <div class="text-muted">
-            <small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }}</small>
-            <small>({{ $revision->created_at->diffForHumans() }})</small>
+        <small class="text-muted">(<strong class="hide-over-l">{{ trans('entities.pages_revisions_editor') }}: </strong>{{ $revision->is_markdown ? 'Markdown' : 'WYSIWYG' }})</small>
+    </div>
+    <div class="flex-3 px-m py-xs min-width-l">
+        <div class="flex-container-row items-center gap-s">
+            @if($revision->createdBy)
+                <img class="avatar flex-none" height="30" width="30" src="{{ $revision->createdBy->getAvatar(30) }}" alt="{{ $revision->createdBy->name }}">
+            @endif
+            <div>
+                @if($revision->createdBy) {{ $revision->createdBy->name }} @else {{ trans('common.deleted_user') }} @endif
+                <br>
+                <div class="text-muted">
+                    <small>{{ $revision->created_at->formatLocalized('%e %B %Y %H:%M:%S') }}</small>
+                    <small>({{ $revision->created_at->diffForHumans() }})</small>
+                </div>
+            </div>
         </div>
-    </td>
-    <td>
+    </div>
+    <div class="flex-2 px-m py-xs min-width-m text-small">
         {{ $revision->summary }}
-    </td>
-    <td class="actions text-small text-right">
+    </div>
+    <div class="flex-2 px-m py-xs actions text-small text-l-right min-width-l">
         <a href="{{ $revision->getUrl('changes') }}" target="_blank" rel="noopener">{{ trans('entities.pages_revisions_changes') }}</a>
-        <span class="text-muted">&nbsp;|&nbsp;</span>
+        <span class="text-muted opacity-70">&nbsp;|&nbsp;</span>
 
 
-        @if ($index === 0)
+        @if ($current)
             <a target="_blank" rel="noopener" href="{{ $revision->page->getUrl() }}"><i>{{ trans('entities.pages_revisions_current') }}</i></a>
         @else
             <a href="{{ $revision->getUrl() }}" target="_blank" rel="noopener">{{ trans('entities.pages_revisions_preview') }}</a>
 
             @if(userCan('page-update', $revision->page))
-                <span class="text-muted">&nbsp;|&nbsp;</span>
+                <span class="text-muted opacity-70">&nbsp;|&nbsp;</span>
                 <div component="dropdown" class="dropdown-container">
                     <a refs="dropdown@toggle" href="#" aria-haspopup="true" aria-expanded="false">{{ trans('entities.pages_revisions_restore') }}</a>
                     <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
@@ -52,7 +57,7 @@
             @endif
 
             @if(userCan('page-delete', $revision->page))
-                <span class="text-muted">&nbsp;|&nbsp;</span>
+                <span class="text-muted opacity-70">&nbsp;|&nbsp;</span>
                 <div component="dropdown" class="dropdown-container">
                     <a refs="dropdown@toggle" href="#" aria-haspopup="true" aria-expanded="false">{{ trans('common.delete') }}</a>
                     <ul refs="dropdown@menu" class="dropdown-menu" role="menu">
@@ -71,5 +76,5 @@
                 </div>
             @endif
         @endif
-    </td>
-</tr>
\ No newline at end of file
+    </div>
+</div>
\ No newline at end of file
index 66d53ae7e9fc29d845ef3158fd7933d8a3de0403..c209626cdbf24054e70c6ecb65620adf302b272b 100644 (file)
@@ -1,4 +1,4 @@
-<div template-manager>
+<div component="template-manager">
     @if(userCan('templates-manage'))
         <p class="text-muted small mb-none">
             {{ trans('entities.templates_explain_set_as_template') }}
         <hr>
     @endif
 
-    @if(count($templates) > 0)
-        <div class="search-box flexible mb-m">
-            <input type="text" name="template-search" placeholder="{{ trans('common.search') }}">
-            <button type="button">@icon('search')</button>
-            <button class="search-box-cancel text-neg hidden" type="button">@icon('close')</button>
-        </div>
-    @endif
+    <div class="search-box flexible mb-m" style="display: {{ count($templates) > 0 ? 'block' : 'none' }}">
+        <input refs="template-manager@searchInput" type="text" name="template-search" placeholder="{{ trans('common.search') }}">
+        <button refs="template-manager@searchButton" tabindex="-1" type="button">@icon('search')</button>
+        <button refs="template-manager@searchCancel" class="search-box-cancel text-neg" type="button" style="display: none">@icon('close')</button>
+    </div>
 
-    <div template-manager-list>
+    <div refs="template-manager@list">
         @include('pages.parts.template-manager-list', ['templates' => $templates])
     </div>
 </div>
\ No newline at end of file
index 3e7edad997fe1c8cdb823abd2762ca67475a7d64..9f462e930ebb6e7e054a303a2664f807ada4948a 100644 (file)
 
         <main class="card content-wrap">
             <h1 class="list-heading">{{ trans('entities.pages_revisions') }}</h1>
-            @if(count($revisions) > 0)
 
-                <table class="table">
-                    <tr>
-                        <th width="56">{{ trans('entities.pages_revisions_number') }}</th>
-                        <th>
-                            {{ trans('entities.pages_name') }} / {{ trans('entities.pages_revisions_editor') }}
-                        </th>
-                        <th colspan="2">{{ trans('entities.pages_revisions_created_by') }} / {{ trans('entities.pages_revisions_date') }}</th>
-                        <th>{{ trans('entities.pages_revisions_changelog') }}</th>
-                        <th class="text-right">{{ trans('common.actions') }}</th>
-                    </tr>
+            <p class="text-muted">{{ trans('entities.pages_revisions_desc') }}</p>
+
+            <div class="flex-container-row my-m items-center justify-space-between wrap gap-x-m gap-y-s">
+                {{ $revisions->links() }}
+                <div>
+                    @include('common.sort', $listOptions->getSortControlData())
+                </div>
+            </div>
+
+            @if(count($revisions) > 0)
+                <div class="item-list">
+                    <div class="item-list-row flex-container-row items-center strong hide-under-l">
+                        <div class="flex fit-content min-width-xxxxs px-m py-xs">{{ trans('entities.pages_revisions_number') }}</div>
+                        <div class="flex-2 px-m py-xs">{{ trans('entities.pages_name') }} / {{ trans('entities.pages_revisions_editor') }}</div>
+                        <div class="flex-3 px-m py-xs">{{ trans('entities.pages_revisions_created_by') }} / {{ trans('entities.pages_revisions_date') }}</div>
+                        <div class="flex-2 px-m py-xs">{{ trans('entities.pages_revisions_changelog') }}</div>
+                        <div class="flex-2 px-m py-xs text-right">{{ trans('common.actions') }}</div>
+                    </div>
                     @foreach($revisions as $index => $revision)
-                        @include('pages.parts.revision-table-row', ['revision' => $revision])
+                        @include('pages.parts.revisions-index-row', ['revision' => $revision, 'current' => $page->revision_count === $revision->revision_number])
                     @endforeach
-                </table>
-
+                </div>
             @else
                 <p>{{ trans('entities.pages_revisions_none') }}</p>
             @endif
+
+            <div class="my-m">
+                {{ $revisions->links() }}
+            </div>
         </main>
 
     </div>
index 66c3762a89f675bd0d2083864a1fafb44df81d38..e05d9d7393fce09b93662b498a77290562dc7642 100644 (file)
@@ -17,7 +17,9 @@
     </div>
 
     <main class="content-wrap card">
-        <div class="page-content clearfix" page-display="{{ $page->id }}">
+        <div component="page-display"
+             option:page-display:page-id="{{ $page->id }}"
+             class="page-content clearfix">
             @include('pages.parts.page-display')
         </div>
         @include('pages.parts.pointer', ['page' => $page])
 
             {{--User Actions--}}
             @if(userCan('page-update', $page))
-                <a href="{{ $page->getUrl('/edit') }}" class="icon-list-item">
+                <a href="{{ $page->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
                     <span>@icon('edit')</span>
                     <span>{{ trans('common.edit') }}</span>
                 </a>
             @endif
             @if(userCanOnAny('create', \BookStack\Entities\Models\Book::class) || userCanOnAny('create', \BookStack\Entities\Models\Chapter::class) || userCan('page-create-all') || userCan('page-create-own'))
-                <a href="{{ $page->getUrl('/copy') }}" class="icon-list-item">
+                <a href="{{ $page->getUrl('/copy') }}" data-shortcut="copy" class="icon-list-item">
                     <span>@icon('copy')</span>
                     <span>{{ trans('common.copy') }}</span>
                 </a>
             @endif
             @if(userCan('page-update', $page))
                 @if(userCan('page-delete', $page))
-                       <a href="{{ $page->getUrl('/move') }}" class="icon-list-item">
+                       <a href="{{ $page->getUrl('/move') }}" data-shortcut="move" class="icon-list-item">
                            <span>@icon('folder')</span>
                            <span>{{ trans('common.move') }}</span>
                        </a>
                 @endif
             @endif
-            <a href="{{ $page->getUrl('/revisions') }}" class="icon-list-item">
+            <a href="{{ $page->getUrl('/revisions') }}" data-shortcut="revisions" class="icon-list-item">
                 <span>@icon('history')</span>
                 <span>{{ trans('entities.revisions') }}</span>
             </a>
             @if(userCan('restrictions-manage', $page))
-                <a href="{{ $page->getUrl('/permissions') }}" class="icon-list-item">
+                <a href="{{ $page->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
                     <span>@icon('lock')</span>
                     <span>{{ trans('entities.permissions') }}</span>
                 </a>
             @endif
             @if(userCan('page-delete', $page))
-                <a href="{{ $page->getUrl('/delete') }}" class="icon-list-item">
+                <a href="{{ $page->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
                     <span>@icon('delete')</span>
                     <span>{{ trans('common.delete') }}</span>
                 </a>
diff --git a/resources/views/search/parts/entity-suggestion-list.blade.php b/resources/views/search/parts/entity-suggestion-list.blade.php
new file mode 100644 (file)
index 0000000..4a8e838
--- /dev/null
@@ -0,0 +1,21 @@
+<div class="entity-list">
+    @if(count($entities) > 0)
+        @foreach($entities as $index => $entity)
+
+            @include('entities.list-item', [
+                'entity' => $entity,
+                'showPath' => true,
+                'locked' => false,
+            ])
+        
+            @if($index !== count($entities) - 1)
+                <hr>
+            @endif
+
+        @endforeach
+    @else
+        <div class="text-muted px-m py-m">
+            {{ trans('common.no_items') }}
+        </div>
+    @endif
+</div>
\ No newline at end of file
index 3fbfa18fef25fadf32adb306ceb6401bcc964df9..dfcc80269e6fee6d5d5712e5b7235d79e9ef6b99 100644 (file)
@@ -2,25 +2,24 @@
 @type - Type of term (exact, tag)
 @currentList
 --}}
-<table component="add-remove-rows"
+<div component="add-remove-rows"
        option:add-remove-rows:remove-selector="button.text-neg"
-       option:add-remove-rows:row-selector="tr"
-       class="no-style">
+       option:add-remove-rows:row-selector=".flex-container-row"
+        class="flex-container-column gap-xs">
     @foreach(array_merge($currentList, ['']) as $term)
-        <tr @if(empty($term)) class="hidden" refs="add-remove-rows@model" @endif>
-            <td class="pb-s pr-m">
+        <div @if(empty($term)) refs="add-remove-rows@model" @endif
+            class="{{ $term ? '' : 'hidden' }} flex-container-row items-center gap-x-xs">
+            <div>
                 <input class="exact-input outline" type="text" name="{{$type}}[]" value="{{ $term }}">
-            </td>
-            <td>
-                <button type="button" class="text-neg text-button">@icon('close')</button>
-            </td>
-        </tr>
+            </div>
+            <div>
+                <button type="button" class="text-neg text-button icon-button p-xs">@icon('close')</button>
+            </div>
+        </div>
     @endforeach
-    <tr>
-        <td colspan="2">
-            <button refs="add-remove-rows@add" type="button" class="text-button">
-                @icon('add-circle'){{ trans('common.add') }}
-            </button>
-        </td>
-    </tr>
-</table>
\ No newline at end of file
+    <div class="flex py-xs">
+        <button refs="add-remove-rows@add" type="button" class="text-button">
+            @icon('add-circle'){{ trans('common.add') }}
+        </button>
+    </div>
+</div>
\ No newline at end of file
index 2daeb8a8253bca070997fccdb164120e6f5d0908..abb9c2771116bfe4363b74e6d6ffdee128970855 100644 (file)
@@ -9,7 +9,11 @@
         <h1 class="list-heading">{{ trans('settings.audit') }}</h1>
         <p class="text-muted">{{ trans('settings.audit_desc') }}</p>
 
-        <form action="{{ url('/settings/audit') }}" method="get" class="flex-container-row wrap justify-flex-start gap-m">
+        <form action="{{ url('/settings/audit') }}" method="get" class="flex-container-row wrap justify-flex-start gap-x-m gap-y-xs">
+
+            @foreach(request()->only(['order', 'sort']) as $key => $val)
+                <input type="hidden" name="{{ $key }}" value="{{ $val }}">
+            @endforeach
 
             <div component="dropdown" class="list-sort-type dropdown-container">
                 <label for="">{{ trans('settings.audit_event_filter') }}</label>
                         aria-haspopup="true"
                         aria-expanded="false"
                         aria-label="{{ trans('common.sort_options') }}"
-                        class="input-base text-left">{{ $listDetails['event'] ?: trans('settings.audit_event_filter_no_filter') }}</button>
+                        class="input-base text-left">{{ $filters['event'] ?: trans('settings.audit_event_filter_no_filter') }}</button>
                 <ul refs="dropdown@menu" class="dropdown-menu">
-                    <li @if($listDetails['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => '']) }}" class="text-item">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
+                    <li @if($filters['event'] === '') class="active" @endif><a href="{{ sortUrl('/settings/audit', array_filter(request()->except('page')), ['event' => '']) }}" class="text-item">{{ trans('settings.audit_event_filter_no_filter') }}</a></li>
                     @foreach($activityTypes as $type)
-                        <li @if($type === $listDetails['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', $listDetails, ['event' => $type]) }}" class="text-item">{{ $type }}</a></li>
+                        <li @if($type === $filters['event']) class="active" @endif><a href="{{ sortUrl('/settings/audit', array_filter(request()->except('page')), ['event' => $type]) }}" class="text-item">{{ $type }}</a></li>
                     @endforeach
                 </ul>
             </div>
 
-            @if(!empty($listDetails['event']))
-                <input type="hidden" name="event" value="{{ $listDetails['event'] }}">
+            @if(!empty($filters['event']))
+                <input type="hidden" name="event" value="{{ $filters['event'] }}">
             @endif
 
             @foreach(['date_from', 'date_to'] as $filterKey)
@@ -38,7 +42,7 @@
                            component="submit-on-change"
                            type="date"
                            name="{{ $filterKey }}"
-                           value="{{ $listDetails[$filterKey] ?? '' }}">
+                           value="{{ $filters[$filterKey] ?? '' }}">
                 </div>
             @endforeach
 
                  component="submit-on-change"
                  option:submit-on-change:filter='[name="user"]'>
                 <label for="owner">{{ trans('settings.audit_table_user') }}</label>
-                @include('form.user-select', ['user' => $listDetails['user'] ? \BookStack\Auth\User::query()->find($listDetails['user']) : null, 'name' => 'user'])
+                @include('form.user-select', ['user' => $filters['user'] ? \BookStack\Auth\User::query()->find($filters['user']) : null, 'name' => 'user'])
             </div>
 
 
             <div class="form-group">
                 <label for="ip">{{ trans('settings.audit_table_ip') }}</label>
-                @include('form.text', ['name' => 'ip', 'model' => (object) $listDetails])
+                @include('form.text', ['name' => 'ip', 'model' => (object) $filters])
                 <input type="submit" style="display: none">
             </div>
         </form>
 
-        <hr class="mt-l mb-s">
-
-        {{ $activities->links() }}
-
-        <table class="table">
-            <tbody>
-            <tr>
-                <th>{{ trans('settings.audit_table_user') }}</th>
-                <th>
-                    <a href="{{ sortUrl('/settings/audit', $listDetails, ['sort' => 'key']) }}">{{ trans('settings.audit_table_event') }}</a>
-                </th>
-                <th>{{ trans('settings.audit_table_related') }}</th>
-                <th>{{ trans('settings.audit_table_ip') }}</th>
-                <th>
-                    <a href="{{ sortUrl('/settings/audit', $listDetails, ['sort' => 'created_at']) }}">{{ trans('settings.audit_table_date') }}</a></th>
-            </tr>
+        <hr class="mt-m mb-s">
+
+        <div class="flex-container-row justify-space-between items-center wrap">
+            <div class="flex-2 min-width-xl">{{ $activities->links() }}</div>
+            <div class="flex-none min-width-m py-m">
+                @include('common.sort', array_merge($listOptions->getSortControlData(), ['useQuery' => true]))
+            </div>
+        </div>
+
+        <div class="item-list">
+            <div class="item-list-row flex-container-row items-center bold hide-under-m">
+                <div class="flex-2 px-m py-xs flex-container-row items-center">{{ trans('settings.audit_table_user') }}</div>
+                <div class="flex-2 px-m py-xs">{{ trans('settings.audit_table_event') }}</div>
+                <div class="flex-3 px-m py-xs">{{ trans('settings.audit_table_related') }}</div>
+                <div class="flex-container-row flex-3">
+                    <div class="flex px-m py-xs">{{ trans('settings.audit_table_ip') }}</div>
+                    <div class="flex-2 px-m py-xs text-right">{{ trans('settings.audit_table_date') }}</div>
+                </div>
+            </div>
             @foreach($activities as $activity)
-                <tr>
-                    <td>
+                <div class="item-list-row flex-container-row items-center wrap py-xxs">
+                    <div class="flex-2 px-m py-xxs flex-container-row items-center min-width-m">
                         @include('settings.parts.table-user', ['user' => $activity->user, 'user_id' => $activity->user_id])
-                    </td>
-                    <td>{{ $activity->type }}</td>
-                    <td width="40%">
+                    </div>
+                    <div class="flex-2 px-m py-xxs min-width-m"><strong class="mr-xs hide-over-m">{{ trans('settings.audit_table_event') }}:</strong> {{ $activity->type }}</div>
+                    <div class="flex-3 px-m py-xxs min-width-l">
                         @if($activity->entity)
-                            <a href="{{ $activity->entity->getUrl() }}" class="table-entity-item">
-                                <span role="presentation" class="icon text-{{$activity->entity->getType()}}">@icon($activity->entity->getType())</span>
-                                <div class="text-{{ $activity->entity->getType() }}">
+                            <a href="{{ $activity->entity->getUrl() }}" class="flex-container-row items-center">
+                                <span role="presentation" class="icon flex-none text-{{$activity->entity->getType()}}">@icon($activity->entity->getType())</span>
+                                <div class="flex text-{{ $activity->entity->getType() }}">
                                     {{ $activity->entity->name }}
                                 </div>
                             </a>
                         @elseif($activity->detail && $activity->isForEntity())
-                            <div class="px-m">
+                            <div>
                                 {{ trans('settings.audit_deleted_item') }} <br>
                                 {{ trans('settings.audit_deleted_item_name', ['name' => $activity->detail]) }}
                             </div>
                         @elseif($activity->detail)
-                            <div class="px-m">{{ $activity->detail }}</div>
+                            <div>{{ $activity->detail }}</div>
                         @endif
-                    </td>
-                    <td>{{ $activity->ip }}</td>
-                    <td>{{ $activity->created_at }}</td>
-                </tr>
+                    </div>
+                    <div class="flex-container-row flex-3">
+                        <div class="flex px-m py-xxs min-width-xs"><strong class="mr-xs hide-over-m">{{ trans('settings.audit_table_ip') }}:<br></strong> {{ $activity->ip }}</div>
+                        <div class="flex-2 px-m py-xxs text-m-right min-width-xs"><strong class="mr-xs hide-over-m">{{ trans('settings.audit_table_date') }}:<br></strong> {{ $activity->created_at }}</div>
+                    </div>
+                </div>
             @endforeach
-            </tbody>
-        </table>
+        </div>
 
-        {{ $activities->links() }}
+        <div class="py-m">
+            {{ $activities->links() }}
+        </div>
     </div>
 
 </div>
index a7392196b68be6ad0329a57dffbb9c0191ea4052..3748267df8869c74c658afebe704a3219ed91557 100644 (file)
                     <label class="setting-list-label">{{ trans('settings.app_primary_color') }}</label>
                     <p class="small">{!! trans('settings.app_primary_color_desc') !!}</p>
                 </div>
-                <div setting-app-color-picker class="text-m-right pt-xs">
-                    <input type="color" data-default="#206ea7" data-current="{{ setting('app-color') }}" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#206ea7">
-                    <input type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
+                <div component="setting-app-color-picker setting-color-picker"
+                     option:setting-color-picker:default="#206ea7"
+                     option:setting-color-picker:current="{{ setting('app-color') }}"
+                     class="text-m-right pt-xs">
+                    <input refs="setting-color-picker@input setting-app-color-picker@input" type="color" value="{{ setting('app-color') }}" name="setting-app-color" id="setting-app-color" placeholder="#206ea7">
+                    <input refs="setting-app-color-picker@light-input" type="hidden" value="{{ setting('app-color-light') }}" name="setting-app-color-light" id="setting-app-color-light">
                     <div class="pr-s">
-                        <button type="button" class="text-button text-muted mt-s" setting-app-color-picker-default>{{ trans('common.default') }}</button>
+                        <button refs="setting-color-picker@default-button" type="button" class="text-button text-muted mt-s">{{ trans('common.default') }}</button>
                         <span class="sep">|</span>
-                        <button type="button" class="text-button text-muted mt-s" setting-app-color-picker-reset>{{ trans('common.reset') }}</button>
+                        <button refs="setting-color-picker@reset-button" type="button" class="text-button text-muted mt-s">{{ trans('common.reset') }}</button>
                     </div>
 
                 </div>
                 </div>
             </div>
 
-            <div homepage-control id="homepage-control" class="grid half gap-xl items-center">
+            <div component="setting-homepage-control" id="homepage-control" class="grid half gap-xl items-center">
                 <div>
                     <label for="setting-app-homepage-type" class="setting-list-label">{{ trans('settings.app_homepage') }}</label>
                     <p class="small">{{ trans('settings.app_homepage_desc') }}</p>
                 </div>
                 <div>
-                    <select name="setting-app-homepage-type" id="setting-app-homepage-type">
+                    <select refs="setting-homepage-control@type-control"
+                            name="setting-app-homepage-type"
+                            id="setting-app-homepage-type">
                         <option @if(setting('app-homepage-type') === 'default') selected @endif value="default">{{ trans('common.default') }}</option>
                         <option @if(setting('app-homepage-type') === 'books') selected @endif value="books">{{ trans('entities.books') }}</option>
                         <option @if(setting('app-homepage-type') === 'bookshelves') selected @endif value="bookshelves">{{ trans('entities.shelves') }}</option>
                         <option @if(setting('app-homepage-type') === 'page') selected @endif value="page">{{ trans('entities.pages_specific') }}</option>
                     </select>
 
-                    <div page-picker-container style="display: none;" class="mt-m">
+                    <div refs="setting-homepage-control@page-picker-container" style="display: none;" class="mt-m">
                         @include('settings.parts.page-picker', ['name' => 'setting-app-homepage', 'placeholder' => trans('settings.app_homepage_select'), 'value' => setting('app-homepage')])
                     </div>
                 </div>
index 0df42e3cef9993f12c7552881a44a76ebbfbcb03..d599a19ab6a1cf87d76fecd1ad0c3221f7f3691c 100644 (file)
@@ -1,13 +1,13 @@
 
 {{--Depends on entity selector popup--}}
-<div page-picker>
+<div component="page-picker">
     <div class="input-base">
-        <span @if($value) style="display: none" @endif page-picker-default class="text-muted italic">{{ $placeholder }}</span>
-        <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" rel="noopener" class="text-page" page-picker-display>#{{$value}}, {{$value ? \BookStack\Entities\Models\Page::find($value)->name : '' }}</a>
+        <span @if($value) style="display: none" @endif refs="page-picker@default-display" class="text-muted italic">{{ $placeholder }}</span>
+        <a @if(!$value) style="display: none" @endif href="{{ url('/link/' . $value) }}" target="_blank" rel="noopener" class="text-page" refs="page-picker@display">#{{$value}}, {{$value ? \BookStack\Entities\Models\Page::find($value)->name : '' }}</a>
     </div>
     <br>
-    <input type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
-    <button @if(!$value) style="display: none" @endif type="button" page-picker-reset class="text-button">{{ trans('common.reset') }}</button>
-    <span @if(!$value) style="display: none" @endif class="sep">|</span>
-    <button type="button" page-picker-select class="text-button">{{ trans('common.select') }}</button>
+    <input refs="page-picker@input" type="hidden" value="{{$value}}" name="{{$name}}" id="{{$name}}">
+    <button @if(!$value) style="display: none" @endif type="button" refs="page-picker@reset-button" class="text-button">{{ trans('common.reset') }}</button>
+    <span refs="page-picker@button-seperator" @if(!$value) style="display: none" @endif class="sep">|</span>
+    <button type="button" refs="page-picker@select-button" class="text-button">{{ trans('common.select') }}</button>
 </div>
\ No newline at end of file
index 3b99d0b7cd681a130f83a9dea5c65da7515136f0..e7bfc3fe9f360432d5db717542ece7fc9ae7ee08 100644 (file)
@@ -1,17 +1,19 @@
 {{--
     @type - Name of entity type
 --}}
-<div setting-color-picker class="grid no-break half mb-l">
+<div component="setting-color-picker"
+     option:setting-color-picker:default="{{ config('setting-defaults.'. $type .'-color') }}"
+     option:setting-color-picker:current="{{ setting($type .'-color') }}"
+     class="grid no-break half mb-l">
     <div>
         <label for="setting-{{ $type }}-color" class="text-dark">{{ trans('settings.'. str_replace('-', '_', $type) .'_color') }}</label>
-        <button type="button" class="text-button text-muted" setting-color-picker-default>{{ trans('common.default') }}</button>
+        <button refs="setting-color-picker@default-button" type="button" class="text-button text-muted">{{ trans('common.default') }}</button>
         <span class="sep">|</span>
-        <button type="button" class="text-button text-muted" setting-color-picker-reset>{{ trans('common.reset') }}</button>
+        <button refs="setting-color-picker@reset-button" type="button" class="text-button text-muted">{{ trans('common.reset') }}</button>
     </div>
     <div>
         <input type="color"
-               data-default="{{ config('setting-defaults.'. $type .'-color') }}"
-               data-current="{{ setting($type .'-color') }}"
+               refs="setting-color-picker@input"
                value="{{ setting($type .'-color') }}"
                name="setting-{{ $type }}-color"
                id="setting-{{ $type }}-color"
index a8f2777f03247cf27655fb5fbb2119409e561834..d29ad1979a0fafb95c5e8612d447a7cc89494522 100644 (file)
@@ -3,9 +3,9 @@ $user - User mode to display, Can be null.
 $user_id - Id of user to show. Must be provided.
 --}}
 @if($user)
-    <a href="{{ $user->getEditUrl() }}" class="table-user-item">
-        <div><img class="avatar block" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></div>
-        <div>{{ $user->name }}</div>
+    <a href="{{ $user->getEditUrl() }}" class="flex-container-row inline gap-s items-center">
+        <div class="flex-none"><img width="40" height="40" class="avatar block" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></div>
+        <div class="flex">{{ $user->name }}</div>
     </a>
 @else
     [ID: {{ $user_id }}] {{ trans('common.deleted_user') }}
index 56e2437fe0d88987776cc8d84edcbc41553ac92c..9e82ba4678d13c80d26dd6092e4307d4236e0bc4 100644 (file)
@@ -8,11 +8,11 @@
         <div class="card content-wrap auto-height">
             <h2 class="list-heading">{{ trans('settings.recycle_bin') }}</h2>
 
-            <div class="grid half left-focus">
-                <div>
-                    <p class="text-muted">{{ trans('settings.recycle_bin_desc') }}</p>
+            <div class="flex-container-row items-center gap-x-l gap-y-m wrap">
+                <div class="flex-2 min-width-l">
+                    <p class="text-muted mb-none">{{ trans('settings.recycle_bin_desc') }}</p>
                 </div>
-                <div class="text-right">
+                <div class="flex text-m-right min-width-m">
                     <div component="dropdown" class="dropdown-container">
                         <button refs="dropdown@toggle"
                                 type="button"
                 </div>
             </div>
 
-
             <hr class="mt-l mb-s">
 
-            {!! $deletions->links() !!}
+            <div class="py-m">
+                {!! $deletions->links() !!}
+            </div>
 
-            <table class="table">
-                <tr>
-                    <th width="30%">{{ trans('settings.recycle_bin_deleted_item') }}</th>
-                    <th width="20%">{{ trans('settings.recycle_bin_deleted_parent') }}</th>
-                    <th width="20%">{{ trans('settings.recycle_bin_deleted_by') }}</th>
-                    <th width="15%">{{ trans('settings.recycle_bin_deleted_at') }}</th>
-                    <th width="15%"></th>
-                </tr>
+            <div class="item-list">
+                <div class="item-list-row flex-container-row items-center px-s bold hide-under-l">
+                    <div class="flex-2 px-m py-xs">{{ trans('settings.audit_deleted_item') }}</div>
+                    <div class="flex-2 px-m py-xs">{{ trans('settings.recycle_bin_deleted_parent') }}</div>
+                    <div class="flex-2 px-m py-xs">{{ trans('settings.recycle_bin_deleted_by') }}</div>
+                    <div class="flex px-m py-xs">{{ trans('settings.recycle_bin_deleted_at') }}</div>
+                    <div class="flex px-m py-xs text-right"></div>
+                </div>
                 @if(count($deletions) === 0)
-                    <tr>
-                        <td colspan="5">
-                            <p class="text-muted"><em>{{ trans('settings.recycle_bin_contents_empty') }}</em></p>
-                        </td>
-                    </tr>
+                    <div class="item-list-row px-l py-m">
+                        <p class="text-muted mb-none"><em>{{ trans('settings.recycle_bin_contents_empty') }}</em></p>
+                    </div>
                 @endif
                 @foreach($deletions as $deletion)
-                <tr>
-                    <td>
-                        <div class="table-entity-item">
-                            <span role="presentation" class="icon text-{{$deletion->deletable->getType()}}">@icon($deletion->deletable->getType())</span>
-                            <div class="text-{{ $deletion->deletable->getType() }}">
-                                {{ $deletion->deletable->name }}
-                            </div>
-                        </div>
-                        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
-                            <div class="mb-m"></div>
-                        @endif
-                        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book)
-                            <div class="pl-xl block inline">
-                                <div class="text-chapter">
-                                    @icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }}
-                                </div>
-                            </div>
-                        @endif
-                        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
-                        <div class="pl-xl block inline">
-                            <div class="text-page">
-                                @icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }}
-                            </div>
-                        </div>
-                        @endif
-                    </td>
-                    <td>
-                        @if($deletion->deletable->getParent())
-                        <div class="table-entity-item">
-                            <span role="presentation" class="icon text-{{$deletion->deletable->getParent()->getType()}}">@icon($deletion->deletable->getParent()->getType())</span>
-                            <div class="text-{{ $deletion->deletable->getParent()->getType() }}">
-                                {{ $deletion->deletable->getParent()->name }}
-                            </div>
-                        </div>
-                        @endif
-                    </td>
-                    <td>@include('settings.parts.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])</td>
-                    <td width="200">{{ $deletion->created_at }}</td>
-                    <td width="150" class="text-right">
-                        <div component="dropdown" class="dropdown-container">
-                            <button type="button" refs="dropdown@toggle" class="button outline">{{ trans('common.actions') }}</button>
-                            <ul refs="dropdown@menu" class="dropdown-menu">
-                                <li><a class="text-item" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
-                                <li><a class="text-item" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
-                            </ul>
-                        </div>
-                    </td>
-                </tr>
+                    @include('settings.recycle-bin.parts.recycle-bin-list-item', ['deletion' => $deletion])
                 @endforeach
-            </table>
+            </div>
 
-            {!! $deletions->links() !!}
+            <div class="py-m">
+                {!! $deletions->links() !!}
+            </div>
 
         </div>
 
diff --git a/resources/views/settings/recycle-bin/parts/recycle-bin-list-item.blade.php b/resources/views/settings/recycle-bin/parts/recycle-bin-list-item.blade.php
new file mode 100644 (file)
index 0000000..8af598b
--- /dev/null
@@ -0,0 +1,48 @@
+<div class="item-list-row flex-container-row items-center px-s wrap">
+    <div class="flex-2 px-m py-xs min-width-xl">
+        <div class="flex-container-row items-center py-xs">
+            <span role="presentation" class="flex-none icon text-{{$deletion->deletable->getType()}}">@icon($deletion->deletable->getType())</span>
+            <div class="text-{{ $deletion->deletable->getType() }}">
+                {{ $deletion->deletable->name }}
+            </div>
+        </div>
+        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book)
+            <div class="pl-l block inline">
+                <div class="text-chapter">
+                    @icon('chapter') {{ trans_choice('entities.x_chapters', $deletion->deletable->chapters()->withTrashed()->count()) }}
+                </div>
+            </div>
+        @endif
+        @if($deletion->deletable instanceof \BookStack\Entities\Models\Book || $deletion->deletable instanceof \BookStack\Entities\Models\Chapter)
+            <div class="pl-l block inline">
+                <div class="text-page">
+                    @icon('page') {{ trans_choice('entities.x_pages', $deletion->deletable->pages()->withTrashed()->count()) }}
+                </div>
+            </div>
+        @endif
+    </div>
+    <div class="flex-2 px-m py-xs min-width-m">
+        @if($deletion->deletable->getParent())
+            <strong class="hide-over-l">{{ trans('settings.recycle_bin_deleted_parent') }}:<br></strong>
+            <div class="flex-container-row items-center">
+                <span role="presentation" class="flex-none icon text-{{$deletion->deletable->getParent()->getType()}}">@icon($deletion->deletable->getParent()->getType())</span>
+                <div class="text-{{ $deletion->deletable->getParent()->getType() }}">
+                    {{ $deletion->deletable->getParent()->name }}
+                </div>
+            </div>
+        @endif
+    </div>
+    <div class="flex-2 px-m py-xs flex-container-row items-center min-width-m">
+        <div><strong class="hide-over-l">{{ trans('settings.recycle_bin_deleted_by') }}:<br></strong>@include('settings.parts.table-user', ['user' => $deletion->deleter, 'user_id' => $deletion->deleted_by])</div>
+    </div>
+    <div class="flex px-m py-xs min-width-s"><strong class="hide-over-l">{{ trans('settings.recycle_bin_deleted_at') }}:<br></strong>{{ $deletion->created_at }}</div>
+    <div class="flex px-m py-xs text-m-right min-width-s">
+        <div component="dropdown" class="dropdown-container">
+            <button type="button" refs="dropdown@toggle" class="button outline">{{ trans('common.actions') }}</button>
+            <ul refs="dropdown@menu" class="dropdown-menu">
+                <li><a class="text-item" href="{{ $deletion->getUrl('/restore') }}">{{ trans('settings.recycle_bin_restore') }}</a></li>
+                <li><a class="text-item" href="{{ $deletion->getUrl('/destroy') }}">{{ trans('settings.recycle_bin_permanently_delete') }}</a></li>
+            </ul>
+        </div>
+    </div>
+</div>
\ No newline at end of file
index 4c3b5625aa9e9ad3785218185595f33479b1973d..27ee9ce3f9022a2af5dd302e9024715410796e00 100644 (file)
                 <h1 class="list-heading">{{ trans('settings.role_user_roles') }}</h1>
 
                 <div class="text-right">
-                    <a href="{{ url("/settings/roles/new") }}" class="button outline">{{ trans('settings.role_create') }}</a>
+                    <a href="{{ url("/settings/roles/new") }}" class="button outline my-none">{{ trans('settings.role_create') }}</a>
                 </div>
             </div>
 
-            <table class="table">
-                <tr>
-                    <th>{{ trans('settings.role_name') }}</th>
-                    <th></th>
-                    <th class="text-center">{{ trans('settings.users') }}</th>
-                </tr>
+            <p class="text-muted">{{ trans('settings.roles_index_desc') }}</p>
+
+            <div class="flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap">
+                <div>
+                    <div class="block inline mr-xs">
+                        <form method="get" action="{{ url("/settings/roles") }}">
+                            <input type="text"
+                                   name="search"
+                                   placeholder="{{ trans('common.search') }}"
+                                   value="{{ $listOptions->getSearch() }}">
+                        </form>
+                    </div>
+                </div>
+                <div class="justify-flex-end">
+                    @include('common.sort', $listOptions->getSortControlData())
+                </div>
+            </div>
+
+            <div class="item-list">
                 @foreach($roles as $role)
-                    <tr>
-                        <td><a href="{{ url("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a></td>
-                        <td>
-                            @if($role->mfa_enforced)
-                                <span title="{{ trans('settings.role_mfa_enforced') }}">@icon('lock') </span>
-                            @endif
-                            {{ $role->description }}
-                        </td>
-                        <td class="text-center">{{ $role->users->count() }}</td>
-                    </tr>
+                    @include('settings.roles.parts.roles-list-item', ['role' => $role])
                 @endforeach
-            </table>
+            </div>
 
+            <div class="mb-m">
+                {{ $roles->links() }}
+            </div>
 
         </div>
     </div>
diff --git a/resources/views/settings/roles/parts/asset-permissions-row.blade.php b/resources/views/settings/roles/parts/asset-permissions-row.blade.php
new file mode 100644 (file)
index 0000000..df179a9
--- /dev/null
@@ -0,0 +1,32 @@
+<div class="item-list-row flex-container-row items-center wrap">
+    <div class="flex py-s px-m min-width-s">
+        <strong>{{ $title }}</strong> <br>
+        <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.create') }}<br></small>
+        @if($permissionPrefix === 'page' || $permissionPrefix === 'chapter')
+            @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-own', 'label' => trans('settings.role_own')])
+            <br>
+        @endif
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-all', 'label' => trans('settings.role_all')])
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.view') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-view-own', 'label' => trans('settings.role_own')])
+        <br>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-view-all', 'label' => trans('settings.role_all')])
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.edit') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-own', 'label' => trans('settings.role_own')])
+        <br>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-all', 'label' => trans('settings.role_all')])
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.delete') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-own', 'label' => trans('settings.role_own')])
+        <br>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-all', 'label' => trans('settings.role_all')])
+    </div>
+</div>
\ No newline at end of file
index 044b4ceb47eba02fe6efd48a65548472f0cfbc87..8534b7fdbb15817b65e02fd787a8d1a2addcdceb 100644 (file)
             <p class="text-warn">{{ trans('settings.role_asset_admins') }}</p>
         @endif
 
-        <table component="permissions-table" class="table toggle-switch-list compact permissions-table">
-            <tr>
-                <th width="20%">
+        <div component="permissions-table"
+             option:permissions-table:cell-selector=".item-list-row > div"
+             option:permissions-table:row-selector=".item-list-row"
+             class="item-list toggle-switch-list">
+            <div class="item-list-row flex-container-row items-center hide-under-m bold">
+                <div class="flex py-s px-m min-width-s">
                     <a href="#" refs="permissions-table@toggle-all" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </th>
-                <th width="20%" refs="permissions-table@toggle-column">{{ trans('common.create') }}</th>
-                <th width="20%" refs="permissions-table@toggle-column">{{ trans('common.view') }}</th>
-                <th width="20%" refs="permissions-table@toggle-column">{{ trans('common.edit') }}</th>
-                <th width="20%" refs="permissions-table@toggle-column">{{ trans('common.delete') }}</th>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.shelves') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-create-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-view-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'bookshelf-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.books') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-create-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-view-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-view-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'book-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.chapters') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-create-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-view-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'chapter-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.pages') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-create-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-create-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-view-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-view-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'page-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.images') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>@include('settings.roles.parts.checkbox', ['permission' => 'image-create-all', 'label' => ''])</td>
-                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}<sup>1</sup></small></td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'image-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'image-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'image-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.attachments') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>@include('settings.roles.parts.checkbox', ['permission' => 'attachment-create-all', 'label' => ''])</td>
-                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'attachment-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-            <tr>
-                <td>
-                    <div>{{ trans('entities.comments') }}</div>
-                    <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
-                </td>
-                <td>@include('settings.roles.parts.checkbox', ['permission' => 'comment-create-all', 'label' => ''])</td>
-                <td style="line-height:1.2;"><small class="faded">{{ trans('settings.role_controlled_by_asset') }}</small></td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-update-all', 'label' => trans('settings.role_all')])
-                </td>
-                <td>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-own', 'label' => trans('settings.role_own')])
-                    <br>
-                    @include('settings.roles.parts.checkbox', ['permission' => 'comment-delete-all', 'label' => trans('settings.role_all')])
-                </td>
-            </tr>
-        </table>
+                </div>
+                <div refs="permissions-table@toggle-column" class="flex py-s px-m min-width-xxs">{{ trans('common.create') }}</div>
+                <div refs="permissions-table@toggle-column" class="flex py-s px-m min-width-xxs">{{ trans('common.view') }}</div>
+                <div refs="permissions-table@toggle-column" class="flex py-s px-m min-width-xxs">{{ trans('common.edit') }}</div>
+                <div refs="permissions-table@toggle-column" class="flex py-s px-m min-width-xxs">{{ trans('common.delete') }}</div>
+            </div>
+            @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.shelves'), 'permissionPrefix' => 'bookshelf'])
+            @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.books'), 'permissionPrefix' => 'book'])
+            @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.chapters'), 'permissionPrefix' => 'chapter'])
+            @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.pages'), 'permissionPrefix' => 'page'])
+            @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.images'), 'permissionPrefix' => 'image', 'refMark' => '1'])
+            @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.attachments'), 'permissionPrefix' => 'attachment'])
+            @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.comments'), 'permissionPrefix' => 'comment'])
+        </div>
 
         <div>
-            <p class="text-muted text-small px-m">
+            <p class="text-muted text-small p-m">
                 <sup>1</sup> {{ trans('settings.role_asset_image_view_note') }}
             </p>
         </div>
diff --git a/resources/views/settings/roles/parts/related-asset-permissions-row.blade.php b/resources/views/settings/roles/parts/related-asset-permissions-row.blade.php
new file mode 100644 (file)
index 0000000..1ec3d22
--- /dev/null
@@ -0,0 +1,26 @@
+<div class="item-list-row flex-container-row items-center wrap">
+    <div class="flex py-s px-m min-width-s">
+        <strong>{{ $title }}</strong> <br>
+        <a href="#" refs="permissions-table@toggle-row" class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.create') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-create-all', 'label' => ''])
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.view') }}<br></small>
+        <small class="faded">{{ trans('settings.role_controlled_by_asset') }}@if($refMark ?? false)<sup>{{ $refMark }}</sup>@endif</small>
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.edit') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-own', 'label' => trans('settings.role_own')])
+        <br>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-update-all', 'label' => trans('settings.role_all')])
+    </div>
+    <div class="flex py-s px-m min-width-xxs">
+        <small class="hide-over-m bold">{{ trans('common.delete') }}<br></small>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-own', 'label' => trans('settings.role_own')])
+        <br>
+        @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-delete-all', 'label' => trans('settings.role_all')])
+    </div>
+</div>
\ No newline at end of file
diff --git a/resources/views/settings/roles/parts/roles-list-item.blade.php b/resources/views/settings/roles/parts/roles-list-item.blade.php
new file mode 100644 (file)
index 0000000..43e8dc8
--- /dev/null
@@ -0,0 +1,14 @@
+<div class="item-list-row flex-container-row py-xs items-center">
+    <div class="py-xs px-m flex-2">
+        <a href="{{ url("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a><br>
+        @if($role->mfa_enforced)
+            <small title="{{ trans('settings.role_mfa_enforced') }}">@icon('lock') </small>
+        @endif
+        <small>{{ $role->description }}</small>
+    </div>
+    <div class="text-right flex py-xs px-m text-muted">
+        {{ trans_choice('settings.roles_x_users_assigned', $role->users_count, ['count' => $role->users_count]) }}
+        <br>
+        {{ trans_choice('settings.roles_x_permissions_provided', $role->permissions_count, ['count' => $role->permissions_count]) }}
+    </div>
+</div>
\ No newline at end of file
index bbe58453f38caa2d843e689d4861d994e595ff03..a564effe2b10107783a7bf24352604b428a7b598 100644 (file)
@@ -8,48 +8,48 @@
 
         <div class="card content-wrap auto-height">
 
-            <div class="grid half v-center">
+            <div class="flex-container-row items-center justify-space-between wrap">
                 <h1 class="list-heading">{{ trans('settings.webhooks') }}</h1>
 
-                <div class="text-right">
+                <div>
                     <a href="{{ url("/settings/webhooks/create") }}"
                        class="button outline">{{ trans('settings.webhooks_create') }}</a>
                 </div>
             </div>
 
-            @if(count($webhooks) > 0)
+            <p class="text-muted">{{ trans('settings.webhooks_index_desc') }}</p>
+
+            <div class="flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap">
+                <div>
+                    <div class="block inline mr-xs">
+                        <form method="get" action="{{ url("/settings/webhooks") }}">
+                            <input type="text"
+                                   name="search"
+                                   placeholder="{{ trans('common.search') }}"
+                                   value="{{ $listOptions->getSearch() }}">
+                        </form>
+                    </div>
+                </div>
+                <div class="justify-flex-end">
+                    @include('common.sort', $listOptions->getSortControlData())
+                </div>
+            </div>
 
-                <table class="table">
-                    <tr>
-                        <th>{{ trans('common.name') }}</th>
-                        <th width="100">{{ trans('settings.webhook_events_table_header') }}</th>
-                        <th width="100">{{ trans('common.status') }}</th>
-                    </tr>
+            @if(count($webhooks) > 0)
+                <div class="item-list">
                     @foreach($webhooks as $webhook)
-                        <tr>
-                            <td>
-                                <a href="{{ $webhook->getUrl() }}">{{ $webhook->name }}</a> <br>
-                                <span class="small text-muted italic">{{ $webhook->endpoint }}</span>
-                            </td>
-                            <td>
-                                @if($webhook->tracksEvent('all'))
-                                    {{ trans('settings.webhooks_events_all') }}
-                                @else
-                                    {{ $webhook->trackedEvents->count() }}
-                                @endif
-                            </td>
-                            <td>
-                                {{ trans('common.status_' . ($webhook->active ? 'active' : 'inactive')) }}
-                            </td>
-                        </tr>
+                        @include('settings.webhooks.parts.webhooks-list-item', ['webhook' => $webhook])
                     @endforeach
-                </table>
+                </div>
             @else
                 <p class="text-muted empty-text px-none">
                     {{ trans('settings.webhooks_none_created') }}
                 </p>
             @endif
 
+            <div class="my-m">
+                {{ $webhooks->links() }}
+            </div>
 
         </div>
     </div>
diff --git a/resources/views/settings/webhooks/parts/webhooks-list-item.blade.php b/resources/views/settings/webhooks/parts/webhooks-list-item.blade.php
new file mode 100644 (file)
index 0000000..0ba6131
--- /dev/null
@@ -0,0 +1,18 @@
+<div class="item-list-row py-s">
+    <div class="flex-container-row">
+        <div class="flex-2 px-m flex-container-row items-center gap-xs">
+            @include('common.status-indicator', ['status' => $webhook->active])
+            <div>&nbsp;<a href="{{ $webhook->getUrl() }}">{{ $webhook->name }}</a></div>
+        </div>
+        <div class="flex px-m text-right text-muted">
+            @if($webhook->tracksEvent('all'))
+                {{ trans('settings.webhooks_events_all') }}
+            @else
+                {{ trans_choice('settings.webhooks_x_trigger_events', $webhook->tracked_events_count, ['count' =>  $webhook->tracked_events_count]) }}
+            @endif
+        </div>
+    </div>
+    <div class="px-m text-muted italic text-limit-lines-1">
+        <small>{{ $webhook->endpoint }}</small>
+    </div>
+</div>
\ No newline at end of file
index ee52769aa0dc11ab0b23fc9aefdab43ea8bc665e..df3ca83eba2ba05bece88c35e3a46c258308e1de 100644 (file)
@@ -1,7 +1,7 @@
 @extends('layouts.tri')
 
 @section('body')
-    @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view])
+    @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view, 'listOptions' => $listOptions])
 @stop
 
 @section('right')
         <h5>{{ trans('common.actions') }}</h5>
         <div class="icon-list text-primary">
             @if(userCan('bookshelf-create-all'))
-                <a href="{{ url("/create-shelf") }}" class="icon-list-item">
+                <a href="{{ url("/create-shelf") }}" data-shortcut="new" class="icon-list-item">
                     <span>@icon('add')</span>
                     <span>{{ trans('entities.shelves_new_action') }}</span>
                 </a>
             @endif
 
-            @include('entities.view-toggle', ['view' => $view, 'type' => 'shelves'])
+            @include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelves'])
 
             <a href="{{ url('/tags') }}" class="icon-list-item">
                 <span>@icon('tag')</span>
index 1dcc4192f04acaddfd1e51198defee4e5be4f294..a5445270314ea0b3df2351a4f3e36f54fc3aa198 100644 (file)
 
 
 
-<div class="form-group" collapsible id="logo-control">
-    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="logo-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
         <label>{{ trans('common.cover_image') }}</label>
     </button>
-    <div class="collapse-content" collapsible-content>
+    <div refs="collapsible@content" class="collapse-content">
         <p class="small">{{ trans('common.cover_image_description') }}</p>
 
         @include('form.image-picker', [
     </div>
 </div>
 
-<div class="form-group" collapsible id="tags-control">
-    <button type="button" class="collapse-title text-primary" collapsible-trigger aria-expanded="false">
+<div class="form-group collapsible" component="collapsible" id="tags-control">
+    <button refs="collapsible@trigger" type="button" class="collapse-title text-primary" aria-expanded="false">
         <label for="tag-manager">{{ trans('entities.shelf_tags') }}</label>
     </button>
-    <div class="collapse-content" collapsible-content>
+    <div refs="collapsible@content" class="collapse-content">
         @include('entities.tag-manager', ['entity' => $shelf ?? null])
     </div>
 </div>
index d78606ac700e081818d7c183e880116692a6b938..da9c06d9208a585224e823e3125fa0ad3c8759d5 100644 (file)
@@ -1,10 +1,9 @@
-
 <main class="content-wrap mt-m card">
 
     <div class="grid half v-center">
         <h1 class="list-heading">{{ trans('entities.shelves') }}</h1>
         <div class="text-right">
-            @include('entities.sort', ['options' => $sortOptions, 'order' => $order, 'sort' => $sort, 'type' => 'bookshelves'])
+            @include('common.sort', $listOptions->getSortControlData())
         </div>
     </div>
 
@@ -31,7 +30,8 @@
     @else
         <p class="text-muted">{{ trans('entities.shelves_empty') }}</p>
         @if(userCan('bookshelf-create-all'))
-            <a href="{{ url("/create-shelf") }}" class="button outline">@icon('edit'){{ trans('entities.create_now') }}</a>
+            <a href="{{ url("/create-shelf") }}"
+               class="button outline">@icon('edit'){{ trans('entities.create_now') }}</a>
         @endif
     @endif
 
index 37d2889563063be922a8683d60ee655787bcf7df..1ea37992ae477f826c0fd36e64de8c5c6048ce23 100644 (file)
             <h1 class="flex fit-content break-text">{{ $shelf->name }}</h1>
             <div class="flex"></div>
             <div class="flex fit-content text-m-right my-m ml-m">
-                @include('entities.sort', ['options' => [
-                    'default' => trans('common.sort_default'),
-                    'name' => trans('common.sort_name'),
-                    'created_at' => trans('common.sort_created_at'),
-                    'updated_at' => trans('common.sort_updated_at'),
-                ], 'order' => $order, 'sort' => $sort, 'type' => 'shelf_books'])
+                @include('common.sort', $listOptions->getSortControlData())
             </div>
         </div>
 
         <div class="icon-list text-primary">
 
             @if(userCan('book-create-all') && userCan('bookshelf-update', $shelf))
-                <a href="{{ $shelf->getUrl('/create-book') }}" class="icon-list-item">
+                <a href="{{ $shelf->getUrl('/create-book') }}" data-shortcut="new" class="icon-list-item">
                     <span class="icon">@icon('add')</span>
                     <span>{{ trans('entities.books_new_action') }}</span>
                 </a>
             @endif
 
-            @include('entities.view-toggle', ['view' => $view, 'type' => 'shelf'])
+            @include('entities.view-toggle', ['view' => $view, 'type' => 'bookshelf'])
 
             <hr class="primary-background">
 
             @if(userCan('bookshelf-update', $shelf))
-                <a href="{{ $shelf->getUrl('/edit') }}" class="icon-list-item">
+                <a href="{{ $shelf->getUrl('/edit') }}" data-shortcut="edit" class="icon-list-item">
                     <span>@icon('edit')</span>
                     <span>{{ trans('common.edit') }}</span>
                 </a>
             @endif
 
             @if(userCan('restrictions-manage', $shelf))
-                <a href="{{ $shelf->getUrl('/permissions') }}" class="icon-list-item">
+                <a href="{{ $shelf->getUrl('/permissions') }}" data-shortcut="permissions" class="icon-list-item">
                     <span>@icon('lock')</span>
                     <span>{{ trans('entities.permissions') }}</span>
                 </a>
             @endif
 
             @if(userCan('bookshelf-delete', $shelf))
-                <a href="{{ $shelf->getUrl('/delete') }}" class="icon-list-item">
+                <a href="{{ $shelf->getUrl('/delete') }}" data-shortcut="delete" class="icon-list-item">
                     <span>@icon('delete')</span>
                     <span>{{ trans('common.delete') }}</span>
                 </a>
index c88449ce7ad1e9492bb4b303cc683bfbe76f0d75..b6b3325e0516a15d3c9ab6aabd4ebad5afcdd739 100644 (file)
@@ -5,25 +5,28 @@
 
         <main class="card content-wrap mt-xxl">
 
-            <div class="flex-container-row wrap justify-space-between items-center mb-s">
-                <h1 class="list-heading">{{ trans('entities.tags') }}</h1>
-
-                <div>
-                    <div class="block inline mr-xs">
-                        <form method="get" action="{{ url("/tags") }}">
-                            @include('form.request-query-inputs', ['params' => ['name']])
-                            <input type="text"
-                                   name="search"
-                                   placeholder="{{ trans('common.search') }}"
-                                   value="{{ $search }}">
-                        </form>
-                    </div>
+            <h1 class="list-heading">{{ trans('entities.tags') }}</h1>
+
+            <p class="text-muted">{{ trans('entities.tags_index_desc') }}</p>
+
+            <div class="flex-container-row wrap justify-space-between items-center mb-s gap-m">
+                <div class="block inline mr-xs">
+                    <form method="get" action="{{ url("/tags") }}">
+                        @include('form.request-query-inputs', ['params' => ['name']])
+                        <input type="text"
+                               name="search"
+                               placeholder="{{ trans('common.search') }}"
+                               value="{{ $listOptions->getSearch() }}">
+                    </form>
+                </div>
+                <div class="block inline">
+                    @include('common.sort', $listOptions->getSortControlData())
                 </div>
             </div>
 
             @if($nameFilter)
-                <div class="mb-m">
-                    <span class="mr-xs">{{ trans('common.filter_active') }}</span>
+                <div class="my-m">
+                    <strong class="mr-xs">{{ trans('common.filter_active') }}</strong>
                     @include('entities.tag', ['tag' => new \BookStack\Actions\Tag(['name' => $nameFilter])])
                     <form method="get" action="{{ url("/tags") }}" class="inline block">
                         @include('form.request-query-inputs', ['params' => ['search']])
             @endif
 
             @if(count($tags) > 0)
-                <table class="table expand-to-padding mt-m">
+                <div class="item-list mt-m">
                     @foreach($tags as $tag)
-                        @include('tags.parts.table-row', ['tag' => $tag, 'nameFilter' => $nameFilter])
+                        @include('tags.parts.tags-list-item', ['tag' => $tag, 'nameFilter' => $nameFilter])
                     @endforeach
-                </table>
+                </div>
 
-                <div>
+                <div class="my-m">
                     {{ $tags->links() }}
                 </div>
             @else
diff --git a/resources/views/tags/parts/table-row.blade.php b/resources/views/tags/parts/table-row.blade.php
deleted file mode 100644 (file)
index aa04959..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-<tr>
-    <td>
-        <span class="text-bigger mr-xl">@include('entities.tag', ['tag' => $tag])</span>
-    </td>
-    <td width="70" class="px-xs">
-        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() }}"
-           title="{{ trans('entities.tags_usages') }}"
-           class="pill text-muted">@icon('leaderboard'){{ $tag->usages }}</a>
-    </td>
-    <td width="70" class="px-xs">
-        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:page}' }}"
-           title="{{ trans('entities.tags_assigned_pages') }}"
-           class="pill text-page">@icon('page'){{ $tag->page_count }}</a>
-    </td>
-    <td width="70" class="px-xs">
-        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:chapter}' }}"
-           title="{{ trans('entities.tags_assigned_chapters') }}"
-           class="pill text-chapter">@icon('chapter'){{ $tag->chapter_count }}</a>
-    </td>
-    <td width="70" class="px-xs">
-        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:book}' }}"
-           title="{{ trans('entities.tags_assigned_books') }}"
-           class="pill text-book">@icon('book'){{ $tag->book_count }}</a>
-    </td>
-    <td width="70" class="px-xs">
-        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:bookshelf}' }}"
-           title="{{ trans('entities.tags_assigned_shelves') }}"
-           class="pill text-bookshelf">@icon('bookshelf'){{ $tag->shelf_count }}</a>
-    </td>
-    <td class="text-right text-muted">
-        @if($tag->values ?? false)
-            <a href="{{ url('/tags?name=' . urlencode($tag->name)) }}">{{ trans('entities.tags_x_unique_values', ['count' => $tag->values]) }}</a>
-        @elseif(empty($nameFilter))
-            <a href="{{ url('/tags?name=' . urlencode($tag->name)) }}">{{ trans('entities.tags_all_values') }}</a>
-        @endif
-    </td>
-</tr>
\ No newline at end of file
diff --git a/resources/views/tags/parts/tags-list-item.blade.php b/resources/views/tags/parts/tags-list-item.blade.php
new file mode 100644 (file)
index 0000000..3962db7
--- /dev/null
@@ -0,0 +1,31 @@
+<div class="item-list-row flex-container-row items-center wrap">
+    <div class="{{ isset($nameFilter) && $tag->value ? 'flex-2' : 'flex' }} py-s px-m min-width-m">
+        <span class="text-bigger mr-xl">@include('entities.tag', ['tag' => $tag])</span>
+    </div>
+    <div class="flex-2 flex-container-row justify-center items-center gap-m py-s px-m min-width-l wrap">
+        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() }}"
+           title="{{ trans('entities.tags_usages') }}"
+           class="flex fill-area min-width-xxs bold text-right text-muted"><span class="opacity-60">@icon('leaderboard')</span>{{ $tag->usages }}</a>
+        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:page}' }}"
+           title="{{ trans('entities.tags_assigned_pages') }}"
+           class="flex fill-area min-width-xxs bold text-right text-page"><span class="opacity-60">@icon('page')</span>{{ $tag->page_count }}</a>
+        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:chapter}' }}"
+           title="{{ trans('entities.tags_assigned_chapters') }}"
+           class="flex fill-area min-width-xxs bold text-right text-chapter"><span class="opacity-60">@icon('chapter')</span>{{ $tag->chapter_count }}</a>
+        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:book}' }}"
+           title="{{ trans('entities.tags_assigned_books') }}"
+           class="flex fill-area min-width-xxs bold text-right text-book"><span class="opacity-60">@icon('book')</span>{{ $tag->book_count }}</a>
+        <a href="{{ isset($tag->value) ? $tag->valueUrl() : $tag->nameUrl() . '+{type:bookshelf}' }}"
+           title="{{ trans('entities.tags_assigned_shelves') }}"
+           class="flex fill-area min-width-xxs bold text-right text-bookshelf"><span class="opacity-60">@icon('bookshelf')</span>{{ $tag->shelf_count }}</a>
+    </div>
+    @if($tag->values ?? false)
+        <div class="flex text-s-right text-muted py-s px-m min-width-s">
+            <a href="{{ url('/tags?name=' . urlencode($tag->name)) }}">{{ trans('entities.tags_x_unique_values', ['count' => $tag->values]) }}</a>
+        </div>
+    @elseif(empty($nameFilter))
+        <div class="flex text-s-right text-muted py-s px-m min-width-s">
+            <a href="{{ url('/tags?name=' . urlencode($tag->name)) }}">{{ trans('entities.tags_all_values') }}</a>
+        </div>
+    @endif
+</div>
\ No newline at end of file
index ea18933727b03272bf5bf2af710fefc56129b5c0..58617fb85d2f5871e8ce5f3205b69d5703624387 100644 (file)
@@ -1,6 +1,6 @@
 <section class="card content-wrap auto-height" id="api_tokens">
-    <div class="grid half mb-s">
-        <div><h2 class="list-heading">{{ trans('settings.users_api_tokens') }}</h2></div>
+    <div class="flex-container-row wrap justify-space-between items-center mb-s">
+        <h2 class="list-heading">{{ trans('settings.users_api_tokens') }}</h2>
         <div class="text-right pt-xs">
             @if(userCan('access-api'))
                 <a href="{{ url('/api/docs') }}" class="button outline">{{ trans('settings.users_api_tokens_docs') }}</a>
@@ -9,25 +9,25 @@
         </div>
     </div>
     @if (count($user->apiTokens) > 0)
-        <table class="table">
-            <tr>
-                <th>{{ trans('common.name') }}</th>
-                <th>{{ trans('settings.users_api_tokens_expires') }}</th>
-                <th></th>
-            </tr>
+        <div class="item-list my-m">
             @foreach($user->apiTokens as $token)
-                <tr>
-                    <td>
-                        {{ $token->name }} <br>
+                <div class="item-list-row flex-container-row items-center wrap py-xs gap-x-m">
+                    <div class="flex px-m py-xs min-width-m">
+                        <a href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}">{{ $token->name }}</a> <br>
                         <span class="small text-muted italic">{{ $token->token_id }}</span>
-                    </td>
-                    <td>{{ $token->expires_at->format('Y-m-d') ?? '' }}</td>
-                    <td class="text-right">
-                        <a class="button outline small" href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}">{{ trans('common.edit') }}</a>
-                    </td>
-                </tr>
+                    </div>
+                    <div class="flex flex-container-row items-center min-width-m">
+                        <div class="flex px-m py-xs text-muted">
+                            <strong class="text-small">{{ trans('settings.users_api_tokens_expires') }}</strong> <br>
+                            {{ $token->expires_at->format('Y-m-d') ?? '' }}
+                        </div>
+                        <div class="flex px-m py-xs text-right">
+                            <a class="button outline small" href="{{ $user->getEditUrl('/api-tokens/' . $token->id) }}">{{ trans('common.edit') }}</a>
+                        </div>
+                    </div>
+                </div>
             @endforeach
-        </table>
+        </div>
     @else
         <p class="text-muted italic py-m">{{ trans('settings.users_api_tokens_none') }}</p>
     @endif
index 03eae2c005a660e3e61edd6d1d236a7b5723df8b..0dd607f8c7de26f39608d113536df246afa3be22 100644 (file)
@@ -9,59 +9,34 @@
 
             <div class="flex-container-row wrap justify-space-between items-center">
                 <h1 class="list-heading">{{ trans('settings.users') }}</h1>
+                <div>
+                    <a href="{{ url("/settings/users/create") }}" class="outline button my-none">{{ trans('settings.users_add_new') }}</a>
+                </div>
+            </div>
 
+            <p class="text-muted">{{ trans('settings.users_index_desc') }}</p>
+
+            <div class="flex-container-row items-center justify-space-between gap-m mt-m mb-l wrap">
                 <div>
                     <div class="block inline mr-xs">
                         <form method="get" action="{{ url("/settings/users") }}">
-                            @foreach(collect($listDetails)->except('search') as $name => $val)
-                                <input type="hidden" name="{{ $name }}" value="{{ $val }}">
-                            @endforeach
-                            <input type="text" name="search" placeholder="{{ trans('settings.users_search') }}" @if($listDetails['search']) value="{{$listDetails['search']}}" @endif>
+                            <input type="text"
+                                   name="search"
+                                   placeholder="{{ trans('settings.users_search') }}"
+                                   value="{{ $listOptions->getSearch() }}">
                         </form>
                     </div>
-                    <a href="{{ url("/settings/users/create") }}" class="outline button mt-none">{{ trans('settings.users_add_new') }}</a>
+                </div>
+                <div class="justify-flex-end">
+                    @include('common.sort', $listOptions->getSortControlData())
                 </div>
             </div>
 
-            <table class="table">
-                <tr>
-                    <th width="9%"></th>
-                    <th width="36%">
-                        <a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'name']) }}">{{ trans('auth.name') }}</a>
-                        /
-                        <a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'email']) }}">{{ trans('auth.email') }}</a>
-                    </th>
-                    <th width="35%">{{ trans('settings.role_user_roles') }}</th>
-                    <th class="text-right" width="20%">
-                        <a href="{{ sortUrl('/settings/users', $listDetails, ['sort' => 'last_activity_at']) }}">{{ trans('settings.users_latest_activity') }}</a>
-                    </th>
-                </tr>
+            <div class="item-list">
                 @foreach($users as $user)
-                    <tr>
-                        <td class="text-center" style="line-height: 0;"><img class="avatar med" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}"></td>
-                        <td>
-                            <a href="{{ url("/settings/users/{$user->id}") }}">
-                                {{ $user->name }}
-                                <br>
-                                <span class="text-muted">{{ $user->email }}</span>
-                                @if($user->mfa_values_count > 0)
-                                    <span title="MFA Configured" class="text-pos">@icon('lock')</span>
-                                @endif
-                            </a>
-                        </td>
-                        <td>
-                            @foreach($user->roles as $index => $role)
-                                <small><a href="{{ url("/settings/roles/{$role->id}") }}">{{$role->display_name}}</a>@if($index !== count($user->roles) -1),@endif</small>
-                            @endforeach
-                        </td>
-                        <td class="text-right text-muted">
-                            @if($user->last_activity_at)
-                                <small title="{{ $user->last_activity_at->format('Y-m-d H:i:s') }}">{{ $user->last_activity_at->diffForHumans() }}</small>
-                            @endif
-                        </td>
-                    </tr>
+                    @include('users.parts.users-list-item', ['user' => $user])
                 @endforeach
-            </table>
+            </div>
 
             <div>
                 {{ $users->links() }}
index 2a5002c3b766c34cc97494336130268c448b499d..7ff48a83dd881b9a96f6c1c9f0438dddc844ba40 100644 (file)
@@ -48,7 +48,7 @@
 @endif
 
 @if($authMethod === 'standard')
-    <div new-user-password>
+    <div component="new-user-password">
         <label class="setting-list-label">{{ trans('settings.users_password') }}</label>
 
         @if(!isset($model))
                 'value' => old('send_invite', 'true') === 'true',
                 'label' => trans('settings.users_send_invite_option')
             ])
-
         @endif
 
-        <div id="password-input-container" @if(!isset($model)) style="display: none;" @endif>
+        <div refs="new-user-password@input-container" @if(!isset($model)) style="display: none;" @endif>
             <p class="small">{{ trans('settings.users_password_desc') }}</p>
             @if(isset($model))
                 <p class="small">
diff --git a/resources/views/users/parts/users-list-item.blade.php b/resources/views/users/parts/users-list-item.blade.php
new file mode 100644 (file)
index 0000000..dc7c9f2
--- /dev/null
@@ -0,0 +1,27 @@
+<div class="flex-container-row item-list-row items-center wrap py-xs">
+    <div class="px-m py-xs flex-container-row items-center flex-2 gap-l min-width-m">
+        <img class="avatar med" width="40" height="40" src="{{ $user->getAvatar(40)}}" alt="{{ $user->name }}">
+        <a href="{{ url("/settings/users/{$user->id}") }}">
+            {{ $user->name }}
+            <br>
+            <span class="text-muted">{{ $user->email }}</span>
+            @if($user->mfa_values_count > 0)
+                <span title="MFA Configured" class="text-pos">@icon('lock')</span>
+            @endif
+        </a>
+    </div>
+    <div class="flex-container-row items-center flex-3 min-width-m">
+        <div class="px-m py-xs flex">
+            @foreach($user->roles as $index => $role)
+                <small><a href="{{ url("/settings/roles/{$role->id}") }}">{{$role->display_name}}</a>@if($index !== count($user->roles) -1),@endif</small>
+            @endforeach
+        </div>
+        <div class="px-m py-xs flex text-right text-muted">
+            @if($user->last_activity_at)
+                <small>{{ trans('settings.users_latest_activity') }}</small>
+                <br>
+                <small title="{{ $user->last_activity_at->format('Y-m-d H:i:s') }}">{{ $user->last_activity_at->diffForHumans() }}</small>
+            @endif
+        </div>
+    </div>
+</div>
\ No newline at end of file
diff --git a/resources/views/users/preferences/parts/shortcut-control.blade.php b/resources/views/users/preferences/parts/shortcut-control.blade.php
new file mode 100644 (file)
index 0000000..b85813c
--- /dev/null
@@ -0,0 +1,12 @@
+<div class="flex-container-row justify-space-between items-center gap-m item-list-row">
+    <label for="shortcut-{{ $id }}" class="bold flex px-m py-xs">{{ $label }}</label>
+    <div class="px-m py-xs">
+        <input type="text"
+               component="shortcut-input"
+               class="small flex-none shortcut-input px-s py-xs"
+               id="shortcut-{{ $id }}"
+               name="shortcut[{{ $id }}]"
+               readonly
+               value="{{ $shortcuts->getShortcut($id) }}">
+    </div>
+</div>
\ No newline at end of file
diff --git a/resources/views/users/preferences/shortcuts.blade.php b/resources/views/users/preferences/shortcuts.blade.php
new file mode 100644 (file)
index 0000000..677892b
--- /dev/null
@@ -0,0 +1,75 @@
+@extends('layouts.simple')
+
+@section('body')
+    <div class="container small my-xl">
+
+        <section class="card content-wrap">
+            <form action="{{ url('/preferences/shortcuts') }}" method="post">
+                {{ method_field('put') }}
+                {{ csrf_field() }}
+
+                <h1 class="list-heading">{{ trans('preferences.shortcuts_interface') }}</h1>
+
+                <div class="flex-container-row items-center gap-m wrap mb-m">
+                    <p class="flex mb-none min-width-m text-small text-muted">
+                        {{ trans('preferences.shortcuts_toggle_desc') }}
+                        {{ trans('preferences.shortcuts_customize_desc') }}
+                    </p>
+                    <div class="flex min-width-m text-m-center">
+                        @include('form.toggle-switch', [
+                            'name' => 'enabled',
+                            'value' => $enabled,
+                            'label' => trans('preferences.shortcuts_toggle_label'),
+                        ])
+                    </div>
+                </div>
+
+                <hr>
+
+                <h2 class="list-heading mb-m">{{ trans('preferences.shortcuts_section_navigation') }}</h2>
+                <div class="flex-container-row wrap gap-m mb-xl">
+                    <div class="flex min-width-l item-list">
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.homepage'), 'id' => 'home_view'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.shelves'), 'id' => 'shelves_view'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.books'), 'id' => 'books_view'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('settings.settings'), 'id' => 'settings_view'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.my_favourites'), 'id' => 'favourites_view'])
+                    </div>
+                    <div class="flex min-width-l item-list">
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.view_profile'), 'id' => 'profile_view'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('auth.logout'), 'id' => 'logout'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.global_search'), 'id' => 'global_search'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.next'), 'id' => 'next'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.previous'), 'id' => 'previous'])
+                    </div>
+                </div>
+
+                <h2 class="list-heading mb-m">{{ trans('preferences.shortcuts_section_actions') }}</h2>
+                <div class="flex-container-row wrap gap-m mb-xl">
+                    <div class="flex min-width-l item-list">
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.new'), 'id' => 'new'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.edit'), 'id' => 'edit'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.copy'), 'id' => 'copy'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.delete'), 'id' => 'delete'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.favourite'), 'id' => 'favourite'])
+                    </div>
+                    <div class="flex min-width-l item-list">
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.export'), 'id' => 'export'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.sort'), 'id' => 'sort'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.permissions'), 'id' => 'permissions'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('common.move'), 'id' => 'move'])
+                        @include('users.preferences.parts.shortcut-control', ['label' => trans('entities.revisions'), 'id' => 'revisions'])
+                    </div>
+                </div>
+
+                <p class="text-small text-muted">{{ trans('preferences.shortcuts_overlay_desc') }}</p>
+
+                <div class="form-group text-right">
+                    <button class="button">{{ trans('preferences.shortcuts_save') }}</button>
+                </div>
+
+            </form>
+        </section>
+
+    </div>
+@stop
index 1cffbfd7d8d20c7d5779d30a69980e0fc760fe2f..95b4ae535236b879e9c9aa83ac2e486251a6d318 100644 (file)
@@ -29,6 +29,7 @@ use BookStack\Http\Controllers\StatusController;
 use BookStack\Http\Controllers\TagController;
 use BookStack\Http\Controllers\UserApiTokenController;
 use BookStack\Http\Controllers\UserController;
+use BookStack\Http\Controllers\UserPreferencesController;
 use BookStack\Http\Controllers\UserProfileController;
 use BookStack\Http\Controllers\UserSearchController;
 use BookStack\Http\Controllers\WebhookController;
@@ -183,8 +184,6 @@ Route::middleware('auth')->group(function () {
     Route::get('/ajax/tags/suggest/names', [TagController::class, 'getNameSuggestions']);
     Route::get('/ajax/tags/suggest/values', [TagController::class, 'getValueSuggestions']);
 
-    Route::get('/ajax/search/entities', [SearchController::class, 'searchEntitiesAjax']);
-
     // Comments
     Route::post('/comment/{pageId}', [CommentController::class, 'savePageComment']);
     Route::put('/comment/{id}', [CommentController::class, 'update']);
@@ -198,6 +197,8 @@ Route::middleware('auth')->group(function () {
     Route::get('/search/book/{bookId}', [SearchController::class, 'searchBook']);
     Route::get('/search/chapter/{bookId}', [SearchController::class, 'searchChapter']);
     Route::get('/search/entity/siblings', [SearchController::class, 'searchSiblings']);
+    Route::get('/search/entity-selector', [SearchController::class, 'searchForSelector']);
+    Route::get('/search/suggest', [SearchController::class, 'searchSuggestions']);
 
     // User Search
     Route::get('/search/users/select', [UserSearchController::class, 'forSelect']);
@@ -239,18 +240,22 @@ Route::middleware('auth')->group(function () {
     Route::get('/settings/users', [UserController::class, 'index']);
     Route::get('/settings/users/create', [UserController::class, 'create']);
     Route::get('/settings/users/{id}/delete', [UserController::class, 'delete']);
-    Route::patch('/settings/users/{id}/switch-books-view', [UserController::class, 'switchBooksView']);
-    Route::patch('/settings/users/{id}/switch-shelves-view', [UserController::class, 'switchShelvesView']);
-    Route::patch('/settings/users/{id}/switch-shelf-view', [UserController::class, 'switchShelfView']);
-    Route::patch('/settings/users/{id}/change-sort/{type}', [UserController::class, 'changeSort']);
-    Route::patch('/settings/users/{id}/update-expansion-preference/{key}', [UserController::class, 'updateExpansionPreference']);
-    Route::patch('/settings/users/toggle-dark-mode', [UserController::class, 'toggleDarkMode']);
-    Route::patch('/settings/users/update-code-language-favourite', [UserController::class, 'updateCodeLanguageFavourite']);
     Route::post('/settings/users/create', [UserController::class, 'store']);
     Route::get('/settings/users/{id}', [UserController::class, 'edit']);
     Route::put('/settings/users/{id}', [UserController::class, 'update']);
     Route::delete('/settings/users/{id}', [UserController::class, 'destroy']);
 
+    // User Preferences
+    Route::redirect('/preferences', '/');
+    Route::get('/preferences/shortcuts', [UserPreferencesController::class, 'showShortcuts']);
+    Route::put('/preferences/shortcuts', [UserPreferencesController::class, 'updateShortcuts']);
+    Route::patch('/preferences/change-view/{type}', [UserPreferencesController::class, 'changeView']);
+    Route::patch('/preferences/change-sort/{type}', [UserPreferencesController::class, 'changeSort']);
+    Route::patch('/preferences/change-expansion/{type}', [UserPreferencesController::class, 'changeExpansion']);
+    Route::patch('/preferences/toggle-dark-mode', [UserPreferencesController::class, 'toggleDarkMode']);
+    Route::patch('/preferences/update-code-language-favourite', [UserPreferencesController::class, 'updateCodeLanguageFavourite']);
+    Route::patch('/preferences/update-boolean', [UserPreferencesController::class, 'updateBooleanPreference']);
+
     // User API Tokens
     Route::get('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'create']);
     Route::post('/settings/users/{userId}/create-api-token', [UserApiTokenController::class, 'store']);
@@ -312,7 +317,8 @@ Route::get('/register', [Auth\RegisterController::class, 'getRegister']);
 Route::get('/register/confirm', [Auth\ConfirmEmailController::class, 'show']);
 Route::get('/register/confirm/awaiting', [Auth\ConfirmEmailController::class, 'showAwaiting']);
 Route::post('/register/confirm/resend', [Auth\ConfirmEmailController::class, 'resend']);
-Route::get('/register/confirm/{token}', [Auth\ConfirmEmailController::class, 'confirm']);
+Route::get('/register/confirm/{token}', [Auth\ConfirmEmailController::class, 'showAcceptForm']);
+Route::post('/register/confirm/accept', [Auth\ConfirmEmailController::class, 'confirm']);
 Route::post('/register', [Auth\RegisterController::class, 'postRegister']);
 
 // SAML routes
index 987e23a45bd42f917ceb2c741168021e6b5c5b51..25fa2b7963893a5cbb3689a1b8e7698c817f731a 100644 (file)
@@ -51,7 +51,7 @@ class AuditLogTest extends TestCase
         $resp->assertSeeText($page->name);
         $resp->assertSeeText('page_create');
         $resp->assertSeeText($activity->created_at->toDateTimeString());
-        $this->withHtml($resp)->assertElementContains('.table-user-item', $admin->name);
+        $this->withHtml($resp)->assertElementContains('a[href*="users/' . $admin->id . '"]', $admin->name);
     }
 
     public function test_shows_name_for_deleted_items()
index c015adb8619e410bb0adfc97d05e735dfcb278a4..db1f87bd5676a585ddb9867a7636cbc841211657 100644 (file)
@@ -360,6 +360,37 @@ class OidcTest extends TestCase
         $this->assertTrue(auth()->check());
     }
 
+    public function test_auth_login_with_autodiscovery_with_keys_that_do_not_have_use_property()
+    {
+        // Based on reading the OIDC discovery spec:
+        // > This contains the signing key(s) the RP uses to validate signatures from the OP. The JWK Set MAY also
+        // > contain the Server's encryption key(s), which are used by RPs to encrypt requests to the Server. When
+        // > both signing and encryption keys are made available, a use (Key Use) parameter value is REQUIRED for all
+        // > keys in the referenced JWK Set to indicate each key's intended usage.
+        // We can assume that keys without use are intended for signing.
+        $this->withAutodiscovery();
+
+        $keyArray = OidcJwtHelper::publicJwkKeyArray();
+        unset($keyArray['use']);
+
+        $this->mockHttpClient([
+            $this->getAutoDiscoveryResponse(),
+            new Response(200, [
+                'Content-Type'  => 'application/json',
+                'Cache-Control' => 'no-cache, no-store',
+                'Pragma'        => 'no-cache',
+            ], json_encode([
+                'keys' => [
+                    $keyArray,
+                ],
+            ])),
+        ]);
+
+        $this->assertFalse(auth()->check());
+        $this->runLogin();
+        $this->assertTrue(auth()->check());
+    }
+
     public function test_login_group_sync()
     {
         config()->set([
index 45d265b72bc72e7db6544de2319283b5a44faffb..5c3aab6a8ba2cdb0e7a9a33f09a18f4118043f24 100644 (file)
@@ -46,8 +46,18 @@ class RegistrationTest extends TestCase
             return $notification->token === $emailConfirmation->token;
         });
 
-        // Check confirmation email confirmation activation.
-        $this->get('/register/confirm/' . $emailConfirmation->token)->assertRedirect('/login');
+        // Check confirmation email confirmation accept page.
+        $resp = $this->get('/register/confirm/' . $emailConfirmation->token);
+        $acceptPage = $this->withHtml($resp);
+        $resp->assertOk();
+        $resp->assertSee('Thanks for confirming!');
+        $acceptPage->assertElementExists('form[method="post"][action$="/register/confirm/accept"][component="auto-submit"] button');
+        $acceptPage->assertFieldHasValue('token', $emailConfirmation->token);
+
+        // Check acceptance confirm
+        $this->post('/register/confirm/accept', ['token' => $emailConfirmation->token])->assertRedirect('/login');
+
+        // Check state on login redirect
         $this->get('/login')->assertSee('Your email has been confirmed! You should now be able to login using this email address.');
         $this->assertDatabaseMissing('email_confirmations', ['token' => $emailConfirmation->token]);
         $this->assertDatabaseHas('users', ['name' => $dbUser->name, 'email' => $dbUser->email, 'email_confirmed' => true]);
index cccff3a1f58da6edb792267e85ca4c0297ba0856..9e2750fd05e647cdb9fa1a350b05930e9c80b81d 100644 (file)
@@ -225,18 +225,18 @@ class BookTest extends TestCase
         setting()->putUser($editor, 'books_view_type', 'list');
 
         $resp = $this->actingAs($editor)->get('/books');
-        $this->withHtml($resp)->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'Grid View');
-        $this->withHtml($resp)->assertElementExists('input[name="view_type"][value="grid"]');
+        $this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'Grid View');
+        $this->withHtml($resp)->assertElementExists('button[name="view"][value="grid"]');
 
-        $resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'grid']);
+        $resp = $this->patch("/preferences/change-view/books", ['view' => 'grid']);
         $resp->assertRedirect();
         $this->assertEquals('grid', setting()->getUser($editor, 'books_view_type'));
 
         $resp = $this->actingAs($editor)->get('/books');
-        $this->withHtml($resp)->assertElementContains('form[action$="/settings/users/' . $editor->id . '/switch-books-view"]', 'List View');
-        $this->withHtml($resp)->assertElementExists('input[name="view_type"][value="list"]');
+        $this->withHtml($resp)->assertElementContains('form[action$="/preferences/change-view/books"]', 'List View');
+        $this->withHtml($resp)->assertElementExists('button[name="view"][value="list"]');
 
-        $resp = $this->patch("/settings/users/{$editor->id}/switch-books-view", ['view_type' => 'list']);
+        $resp = $this->patch("/preferences/change-view/books", ['view_type' => 'list']);
         $resp->assertRedirect();
         $this->assertEquals('list', setting()->getUser($editor, 'books_view_type'));
     }
index c309f2167954b3f99ce344c13fef926cb2280946..2650b6743cd015f5c85c027fdb6cffcf12de6670 100644 (file)
@@ -190,7 +190,7 @@ class EntitySearchTest extends TestCase
         $this->get('/search?term=' . urlencode('danzorbhsing {created_before:2037-01-01}'))->assertDontSee($page->name);
     }
 
-    public function test_ajax_entity_search()
+    public function test_entity_selector_search()
     {
         $page = $this->entities->newPage(['name' => 'my ajax search test', 'html' => 'ajax test']);
         $notVisitedPage = $this->entities->page();
@@ -198,38 +198,38 @@ class EntitySearchTest extends TestCase
         // Visit the page to make popular
         $this->asEditor()->get($page->getUrl());
 
-        $normalSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name));
+        $normalSearch = $this->get('/search/entity-selector?term=' . urlencode($page->name));
         $normalSearch->assertSee($page->name);
 
-        $bookSearch = $this->get('/ajax/search/entities?types=book&term=' . urlencode($page->name));
+        $bookSearch = $this->get('/search/entity-selector?types=book&term=' . urlencode($page->name));
         $bookSearch->assertDontSee($page->name);
 
-        $defaultListTest = $this->get('/ajax/search/entities');
+        $defaultListTest = $this->get('/search/entity-selector');
         $defaultListTest->assertSee($page->name);
         $defaultListTest->assertDontSee($notVisitedPage->name);
     }
 
-    public function test_ajax_entity_search_shows_breadcrumbs()
+    public function test_entity_selector_search_shows_breadcrumbs()
     {
         $chapter = $this->entities->chapter();
         $page = $chapter->pages->first();
         $this->asEditor();
 
-        $pageSearch = $this->get('/ajax/search/entities?term=' . urlencode($page->name));
+        $pageSearch = $this->get('/search/entity-selector?term=' . urlencode($page->name));
         $pageSearch->assertSee($page->name);
         $pageSearch->assertSee($chapter->getShortName(42));
         $pageSearch->assertSee($page->book->getShortName(42));
 
-        $chapterSearch = $this->get('/ajax/search/entities?term=' . urlencode($chapter->name));
+        $chapterSearch = $this->get('/search/entity-selector?term=' . urlencode($chapter->name));
         $chapterSearch->assertSee($chapter->name);
         $chapterSearch->assertSee($chapter->book->getShortName(42));
     }
 
-    public function test_ajax_entity_search_reflects_items_without_permission()
+    public function test_entity_selector_search_reflects_items_without_permission()
     {
         $page = $this->entities->page();
         $baseSelector = 'a[data-entity-type="page"][data-entity-id="' . $page->id . '"]';
-        $searchUrl = '/ajax/search/entities?permission=update&term=' . urlencode($page->name);
+        $searchUrl = '/search/entity-selector?permission=update&term=' . urlencode($page->name);
 
         $resp = $this->asEditor()->get($searchUrl);
         $this->withHtml($resp)->assertElementContains($baseSelector, $page->name);
@@ -457,4 +457,25 @@ class EntitySearchTest extends TestCase
         $this->withHtml($resp)->assertElementExists('form input[name="filters[updated_by]"][value="me"][checked="checked"]');
         $this->withHtml($resp)->assertElementExists('form input[name="filters[created_by]"][value="me"][checked="checked"]');
     }
+
+    public function test_search_suggestion_endpoint()
+    {
+        $this->entities->newPage(['name' => 'My suggestion page', 'html' => '<p>My supercool suggestion page</p>']);
+
+        // Test specific search
+        $resp = $this->asEditor()->get('/search/suggest?term="supercool+suggestion"');
+        $resp->assertSee('My suggestion page');
+        $resp->assertDontSee('My supercool suggestion page');
+        $resp->assertDontSee('No items available');
+        $this->withHtml($resp)->assertElementCount('a', 1);
+
+        // Test search limit
+        $resp = $this->asEditor()->get('/search/suggest?term=et');
+        $this->withHtml($resp)->assertElementCount('a', 5);
+
+        // Test empty state
+        $resp = $this->asEditor()->get('/search/suggest?term=spaghettisaurusrex');
+        $this->withHtml($resp)->assertElementCount('a', 0);
+        $resp->assertSee('No items available');
+    }
 }
index d00ec5ce59019e8962b49955732c2a4de919b28a..0749888c833d698ec7e3efe11ac8fb9c05007f32 100644 (file)
@@ -195,12 +195,12 @@ class PageRevisionTest extends TestCase
         $this->createRevisions($page, 1, ['html' => 'new page html']);
 
         $resp = $this->asAdmin()->get($page->refresh()->getUrl('/revisions'));
-        $this->withHtml($resp)->assertElementContains('td', '(WYSIWYG)');
-        $this->withHtml($resp)->assertElementNotContains('td', '(Markdown)');
+        $this->withHtml($resp)->assertElementContains('.item-list-row > div:nth-child(2)', 'WYSIWYG)');
+        $this->withHtml($resp)->assertElementNotContains('.item-list-row > div:nth-child(2)', 'Markdown)');
 
         $this->createRevisions($page, 1, ['markdown' => '# Some markdown content']);
         $resp = $this->get($page->refresh()->getUrl('/revisions'));
-        $this->withHtml($resp)->assertElementContains('td', '(Markdown)');
+        $this->withHtml($resp)->assertElementContains('.item-list-row > div:nth-child(2)', 'Markdown)');
     }
 
     public function test_revision_restore_action_only_visible_with_permission()
index ed5c798a5f614be01dbffc714f23ec2badf71dac..ab06686e0e6f3d5abb7718fe79efd614842667e8 100644 (file)
@@ -164,7 +164,7 @@ class TagTest extends TestCase
         $resp->assertSee('OtherTestContent');
         $resp->assertDontSee('OtherTagName');
         $resp->assertSee('Active Filter:');
-        $this->withHtml($resp)->assertElementCount('table .tag-item', 2);
+        $this->withHtml($resp)->assertElementCount('.item-list .tag-item', 2);
         $this->withHtml($resp)->assertElementContains('form[action$="/tags"]', 'Clear Filter');
     }
 
index 6b99ba365defa99e19bf49a50501dc31c371882a..4b613b49ce07b6313daf3fc1d1eb67fb8d37713a 100644 (file)
@@ -8,6 +8,7 @@ use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\Page;
+use Exception;
 use Illuminate\Support\Str;
 use Tests\TestCase;
 
@@ -653,4 +654,22 @@ class EntityPermissionsTest extends TestCase
         ]);
         $resp->assertRedirect($book->getUrl('/page/test-page'));
     }
+
+    public function test_book_permissions_can_be_generated_without_error_if_child_chapter_is_in_recycle_bin()
+    {
+        $book = $this->entities->bookHasChaptersAndPages();
+        /** @var Chapter $chapter */
+        $chapter = $book->chapters()->first();
+
+        $this->asAdmin()->delete($chapter->getUrl());
+
+        $error = null;
+        try {
+            $this->entities->setPermissions($book, ['view'], []);
+        } catch (Exception $e) {
+            $error = $e;
+        }
+
+        $this->assertNull($error);
+    }
 }
index 3d27e9c8d48c3cca7e41a3f1e87cf0b9e8609f86..990df607e1c14b9f5c37cb6c1f2a488ef77e950b 100644 (file)
@@ -62,11 +62,11 @@ class RecycleBinTest extends TestCase
 
         $viewReq = $this->asAdmin()->get('/settings/recycle-bin');
         $html = $this->withHtml($viewReq);
-        $html->assertElementContains('table.table', $page->name);
-        $html->assertElementContains('table.table', $editor->name);
-        $html->assertElementContains('table.table', $book->name);
-        $html->assertElementContains('table.table', $book->pages_count . ' Pages');
-        $html->assertElementContains('table.table', $book->chapters_count . ' Chapters');
+        $html->assertElementContains('.item-list-row', $page->name);
+        $html->assertElementContains('.item-list-row', $editor->name);
+        $html->assertElementContains('.item-list-row', $book->name);
+        $html->assertElementContains('.item-list-row', $book->pages_count . ' Pages');
+        $html->assertElementContains('.item-list-row', $book->chapters_count . ' Chapters');
     }
 
     public function test_recycle_bin_empty()
index 356fdaa37a24b14db956faab2ca0b09b262de25e..d0dd7d772084f9a6042e2902022854dfd0a7b21b 100644 (file)
@@ -330,7 +330,7 @@ abstract class TestCase extends BaseTestCase
 
     protected function assertNotificationContains(\Illuminate\Testing\TestResponse $resp, string $text)
     {
-        return $this->withHtml($resp)->assertElementContains('[notification]', $text);
+        return $this->withHtml($resp)->assertElementContains('.notification[role="alert"]', $text);
     }
 
     /**
index 4d612a870b2e393f6f468c719ec83fe256c3e9ab..efab53379f3d72a83d5af45d7fe4aa47de51b2d1 100644 (file)
@@ -338,6 +338,23 @@ class ThemeTest extends TestCase
         });
     }
 
+    public function test_login_and_register_message_template_files_can_be_used()
+    {
+        $loginMessage = 'Welcome to this instance, login below you scallywag';
+        $registerMessage = 'You want to register? Enter the deets below you numpty';
+
+        $this->usingThemeFolder(function (string $folder) use ($loginMessage, $registerMessage) {
+            $viewDir = theme_path('auth/parts');
+            mkdir($viewDir, 0777, true);
+            file_put_contents($viewDir . '/login-message.blade.php', $loginMessage);
+            file_put_contents($viewDir . '/register-message.blade.php', $registerMessage);
+            $this->setSettings(['registration-enabled' => 'true']);
+
+            $this->get('/login')->assertSee($loginMessage);
+            $this->get('/register')->assertSee($registerMessage);
+        });
+    }
+
     protected function usingThemeFolder(callable $callback)
     {
         // Create a folder and configure a theme
index e929d63ec60733e87846a8e7d43e2d5af93cf7f5..0e4065a827453c743188b16342f00d3bf5ddcdd2 100644 (file)
@@ -310,7 +310,7 @@ class ImageTest extends TestCase
         }
     }
 
-    public function test_system_images_remain_public()
+    public function test_system_images_remain_public_with_local_secure()
     {
         config()->set('filesystems.images', 'local_secure');
         $this->asAdmin();
@@ -327,6 +327,23 @@ class ImageTest extends TestCase
         }
     }
 
+    public function test_system_images_remain_public_with_local_secure_restricted()
+    {
+        config()->set('filesystems.images', 'local_secure_restricted');
+        $this->asAdmin();
+        $galleryFile = $this->getTestImage('my-system-test-restricted-upload.png');
+        $expectedPath = public_path('uploads/images/system/' . date('Y-m') . '/my-system-test-restricted-upload.png');
+
+        $upload = $this->call('POST', '/settings/customization', [], [], ['app_logo' => $galleryFile], []);
+        $upload->assertRedirect('/settings/customization');
+
+        $this->assertTrue(file_exists($expectedPath), 'Uploaded image not found at path: ' . $expectedPath);
+
+        if (file_exists($expectedPath)) {
+            unlink($expectedPath);
+        }
+    }
+
     public function test_secure_restricted_images_inaccessible_without_relation_permission()
     {
         config()->set('filesystems.images', 'local_secure_restricted');
index e295034ceefb208bc32ebba6280790ef926e69b6..4991e052a3bf121a969916bf2714e2ab99b0c8ce 100644 (file)
@@ -160,6 +160,23 @@ class UserManagementTest extends TestCase
         ]);
     }
 
+    public function test_delete_removes_user_preferences()
+    {
+        $editor = $this->getEditor();
+        setting()->putUser($editor, 'dark-mode-enabled', 'true');
+
+        $this->assertDatabaseHas('settings', [
+            'setting_key' => 'user:' . $editor->id . ':dark-mode-enabled',
+            'value' => 'true',
+        ]);
+
+        $this->asAdmin()->delete("settings/users/{$editor->id}");
+
+        $this->assertDatabaseMissing('settings', [
+            'setting_key' => 'user:' . $editor->id . ':dark-mode-enabled',
+        ]);
+    }
+
     public function test_guest_profile_shows_limited_form()
     {
         $guest = User::getDefault();
index c65b11d7d896a712a732dea8e6831c55f1b39ad2..03dad7990464f7228acf389818a8d2f6018a5462 100644 (file)
@@ -6,12 +6,51 @@ use Tests\TestCase;
 
 class UserPreferencesTest extends TestCase
 {
+    public function test_interface_shortcuts_updating()
+    {
+        $this->asEditor();
+
+        // View preferences with defaults
+        $resp = $this->get('/preferences/shortcuts');
+        $resp->assertSee('Interface Keyboard Shortcuts');
+
+        $html = $this->withHtml($resp);
+        $html->assertFieldHasValue('enabled', 'false');
+        $html->assertFieldHasValue('shortcut[home_view]', '1');
+
+        // Update preferences
+        $resp = $this->put('/preferences/shortcuts', [
+            'enabled' => 'true',
+            'shortcut' => ['home_view' => 'Ctrl + 1'],
+        ]);
+
+        $resp->assertRedirect('/preferences/shortcuts');
+        $resp->assertSessionHas('success', 'Shortcut preferences have been updated!');
+
+        // View updates to preferences page
+        $resp = $this->get('/preferences/shortcuts');
+        $html = $this->withHtml($resp);
+        $html->assertFieldHasValue('enabled', 'true');
+        $html->assertFieldHasValue('shortcut[home_view]', 'Ctrl + 1');
+    }
+
+    public function test_body_has_shortcuts_component_when_active()
+    {
+        $editor = $this->getEditor();
+        $this->actingAs($editor);
+
+        $this->withHtml($this->get('/'))->assertElementNotExists('body[component="shortcuts"]');
+
+        setting()->putUser($editor, 'ui-shortcuts-enabled', 'true');
+        $this->withHtml($this->get('/'))->assertElementExists('body[component="shortcuts"]');
+    }
+
     public function test_update_sort_preference()
     {
         $editor = $this->getEditor();
         $this->actingAs($editor);
 
-        $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/books', [
+        $updateRequest = $this->patch('/preferences/change-sort/books', [
             'sort'  => 'created_at',
             'order' => 'desc',
         ]);
@@ -29,27 +68,12 @@ class UserPreferencesTest extends TestCase
         $this->assertEquals('desc', setting()->getForCurrentUser('books_sort_order'));
     }
 
-    public function test_update_sort_preference_defaults()
-    {
-        $editor = $this->getEditor();
-        $this->actingAs($editor);
-
-        $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/bookshelves', [
-            'sort'  => 'cat',
-            'order' => 'dog',
-        ]);
-        $updateRequest->assertStatus(302);
-
-        $this->assertEquals('name', setting()->getForCurrentUser('bookshelves_sort'));
-        $this->assertEquals('asc', setting()->getForCurrentUser('bookshelves_sort_order'));
-    }
-
     public function test_update_sort_bad_entity_type_handled()
     {
         $editor = $this->getEditor();
         $this->actingAs($editor);
 
-        $updateRequest = $this->patch('/settings/users/' . $editor->id . '/change-sort/dogs', [
+        $updateRequest = $this->patch('/preferences/change-sort/dogs', [
             'sort'  => 'name',
             'order' => 'asc',
         ]);
@@ -64,7 +88,7 @@ class UserPreferencesTest extends TestCase
         $editor = $this->getEditor();
         $this->actingAs($editor);
 
-        $updateRequest = $this->patch('/settings/users/' . $editor->id . '/update-expansion-preference/home-details', ['expand' => 'true']);
+        $updateRequest = $this->patch('/preferences/change-expansion/home-details', ['expand' => 'true']);
         $updateRequest->assertStatus(204);
 
         $this->assertDatabaseHas('settings', [
@@ -73,7 +97,7 @@ class UserPreferencesTest extends TestCase
         ]);
         $this->assertEquals(true, setting()->getForCurrentUser('section_expansion#home-details'));
 
-        $invalidKeyRequest = $this->patch('/settings/users/' . $editor->id . '/update-expansion-preference/my-home-details', ['expand' => 'true']);
+        $invalidKeyRequest = $this->patch('/preferences/change-expansion/my-home-details', ['expand' => 'true']);
         $invalidKeyRequest->assertStatus(500);
     }
 
@@ -84,7 +108,7 @@ class UserPreferencesTest extends TestCase
         $this->withHtml($home)->assertElementNotExists('.dark-mode');
 
         $this->assertEquals(false, setting()->getForCurrentUser('dark-mode-enabled', false));
-        $prefChange = $this->patch('/settings/users/toggle-dark-mode');
+        $prefChange = $this->patch('/preferences/toggle-dark-mode');
         $prefChange->assertRedirect();
         $this->assertEquals(true, setting()->getForCurrentUser('dark-mode-enabled'));
 
@@ -138,7 +162,7 @@ class UserPreferencesTest extends TestCase
             ->assertElementNotExists('.featured-image-container')
             ->assertElementExists('.content-wrap .entity-list-item');
 
-        $req = $this->patch("/settings/users/{$editor->id}/switch-shelf-view", ['view_type' => 'grid']);
+        $req = $this->patch("/preferences/change-view/bookshelf", ['view' => 'grid']);
         $req->assertRedirect($shelf->getUrl());
 
         $resp = $this->actingAs($editor)->get($shelf->getUrl())
@@ -155,14 +179,14 @@ class UserPreferencesTest extends TestCase
         $page = $this->entities->page();
         $this->actingAs($editor);
 
-        $this->patch('/settings/users/update-code-language-favourite', ['language' => 'php', 'active' => true]);
-        $this->patch('/settings/users/update-code-language-favourite', ['language' => 'javascript', 'active' => true]);
+        $this->patch('/preferences/update-code-language-favourite', ['language' => 'php', 'active' => true]);
+        $this->patch('/preferences/update-code-language-favourite', ['language' => 'javascript', 'active' => true]);
 
         $resp = $this->get($page->getUrl('/edit'));
         $resp->assertSee('option:code-editor:favourites="php,javascript"', false);
 
-        $this->patch('/settings/users/update-code-language-favourite', ['language' => 'ruby', 'active' => true]);
-        $this->patch('/settings/users/update-code-language-favourite', ['language' => 'php', 'active' => false]);
+        $this->patch('/preferences/update-code-language-favourite', ['language' => 'ruby', 'active' => true]);
+        $this->patch('/preferences/update-code-language-favourite', ['language' => 'php', 'active' => false]);
 
         $resp = $this->get($page->getUrl('/edit'));
         $resp->assertSee('option:code-editor:favourites="javascript,ruby"', false);