diff --git a/app/Config/setting-defaults.php b/app/Config/setting-defaults.php index 5e1e4348a..88c4612ca 100644 --- a/app/Config/setting-defaults.php +++ b/app/Config/setting-defaults.php @@ -16,11 +16,20 @@ return [ 'app-editor' => 'wysiwyg', 'app-color' => '#206ea7', 'app-color-light' => 'rgba(32,110,167,0.15)', + 'link-color' => '#206ea7', 'bookshelf-color' => '#a94747', 'book-color' => '#077b70', 'chapter-color' => '#af4d0d', 'page-color' => '#206ea7', 'page-draft-color' => '#7e50b1', + 'app-color-dark' => '#195785', + 'app-color-light-dark' => 'rgba(32,110,167,0.15)', + 'link-color-dark' => '#429fe3', + 'bookshelf-color-dark' => '#ff5454', + 'book-color-dark' => '#389f60', + 'chapter-color-dark' => '#ee7a2d', + 'page-color-dark' => '#429fe3', + 'page-draft-color-dark' => '#a66ce8', 'app-custom-head' => false, 'registration-enabled' => false, diff --git a/database/migrations/2023_01_28_141230_copy_color_settings_for_dark_mode.php b/database/migrations/2023_01_28_141230_copy_color_settings_for_dark_mode.php new file mode 100644 index 000000000..eb779fc7b --- /dev/null +++ b/database/migrations/2023_01_28_141230_copy_color_settings_for_dark_mode.php @@ -0,0 +1,69 @@ +whereIn('setting_key', $colorSettings) + ->get()->toArray(); + + $newData = []; + foreach ($existing as $setting) { + $newSetting = (array) $setting; + $newSetting['setting_key'] .= '-dark'; + $newData[] = $newSetting; + + if ($newSetting['setting_key'] === 'app-color-dark') { + $newSetting['setting_key'] = 'link-color'; + $newData[] = $newSetting; + $newSetting['setting_key'] = 'link-color-dark'; + $newData[] = $newSetting; + } + } + + DB::table('settings')->insert($newData); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $colorSettings = [ + 'app-color-dark', + 'link-color', + 'link-color-dark', + 'app-color-light-dark', + 'bookshelf-color-dark', + 'book-color-dark', + 'chapter-color-dark', + 'page-color-dark', + 'page-draft-color-dark', + ]; + + DB::table('settings') + ->whereIn('setting_key', $colorSettings) + ->delete(); + } +} diff --git a/resources/js/components/attachments.js b/resources/js/components/attachments.js index b4e400aeb..d8a506270 100644 --- a/resources/js/components/attachments.js +++ b/resources/js/components/attachments.js @@ -45,7 +45,7 @@ export class Attachments extends Component { this.stopEdit(); /** @var {Tabs} */ const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs'); - tabs.show('items'); + tabs.show('attachment-panel-items'); window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => { this.list.innerHTML = resp.data; window.$components.init(this.list); diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js index a44fffc1b..418b7c98a 100644 --- a/resources/js/components/image-manager.js +++ b/resources/js/components/image-manager.js @@ -140,10 +140,9 @@ export class ImageManager extends Component { } setActiveFilterTab(filterName) { - this.filterTabs.forEach(t => t.classList.remove('selected')); - const activeTab = this.filterTabs.find(t => t.dataset.filter === filterName); - if (activeTab) { - activeTab.classList.add('selected'); + for (const tab of this.filterTabs) { + const selected = tab.dataset.filter === filterName; + tab.setAttribute('aria-selected', selected ? 'true' : 'false'); } } diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 27bce48db..82136184b 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -41,7 +41,7 @@ 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 {SettingAppColorScheme} from "./setting-app-color-scheme.js" export {SettingColorPicker} from "./setting-color-picker.js" export {SettingHomepageControl} from "./setting-homepage-control.js" export {ShelfSort} from "./shelf-sort.js" diff --git a/resources/js/components/setting-app-color-picker.js b/resources/js/components/setting-app-color-picker.js deleted file mode 100644 index 68e5abce5..000000000 --- a/resources/js/components/setting-app-color-picker.js +++ /dev/null @@ -1,49 +0,0 @@ -import {Component} from "./component"; - -export class SettingAppColorPicker extends Component { - - 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)); - } - - /** - * Update the app colors as a preview, and create a light version of the color. - */ - updateColor() { - const hexVal = this.colorInput.value; - const rgb = this.hexToRgb(hexVal); - const rgbLightVal = 'rgba('+ [rgb.r, rgb.g, rgb.b, '0.15'].join(',') +')'; - - this.lightColorInput.value = rgbLightVal; - - const customStyles = document.getElementById('custom-styles'); - const oldColor = customStyles.getAttribute('data-color'); - const oldColorLight = customStyles.getAttribute('data-color-light'); - - customStyles.innerHTML = customStyles.innerHTML.split(oldColor).join(hexVal); - customStyles.innerHTML = customStyles.innerHTML.split(oldColorLight).join(rgbLightVal); - - customStyles.setAttribute('data-color', hexVal); - customStyles.setAttribute('data-color-light', rgbLightVal); - } - - /** - * Covert a hex color code to rgb components. - * @attribution https://stackoverflow.com/a/5624139 - * @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); - return { - r: result ? parseInt(result[1], 16) : 0, - g: result ? parseInt(result[2], 16) : 0, - b: result ? parseInt(result[3], 16) : 0 - }; - } - -} diff --git a/resources/js/components/setting-app-color-scheme.js b/resources/js/components/setting-app-color-scheme.js new file mode 100644 index 000000000..71b14badc --- /dev/null +++ b/resources/js/components/setting-app-color-scheme.js @@ -0,0 +1,82 @@ +import {Component} from "./component"; + +export class SettingAppColorScheme extends Component { + + setup() { + this.container = this.$el; + this.mode = this.$opts.mode; + this.lightContainer = this.$refs.lightContainer; + this.darkContainer = this.$refs.darkContainer; + + this.container.addEventListener('tabs-change', event => { + const panel = event.detail.showing; + const newMode = (panel === 'color-scheme-panel-light') ? 'light' : 'dark'; + this.handleModeChange(newMode); + }); + + const onInputChange = (event) => { + this.updateAppColorsFromInputs(); + + if (event.target.name.startsWith('setting-app-color')) { + this.updateLightForInput(event.target); + } + }; + this.container.addEventListener('change', onInputChange); + this.container.addEventListener('input', onInputChange); + } + + handleModeChange(newMode) { + this.mode = newMode; + const isDark = (newMode === 'dark'); + + document.documentElement.classList.toggle('dark-mode', isDark); + this.updateAppColorsFromInputs(); + } + + updateAppColorsFromInputs() { + const inputContainer = this.mode === 'dark' ? this.darkContainer : this.lightContainer; + const inputs = inputContainer.querySelectorAll('input[type="color"]'); + for (const input of inputs) { + const splitName = input.name.split('-'); + const colorPos = splitName.indexOf('color'); + let cssId = splitName.slice(1, colorPos).join('-'); + if (cssId === 'app') { + cssId = 'primary'; + } + + const varName = '--color-' + cssId; + document.body.style.setProperty(varName, input.value); + } + } + + /** + * Update the 'light' app color variant for the given input. + * @param {HTMLInputElement} input + */ + updateLightForInput(input) { + const lightName = input.name.replace('-color', '-color-light'); + const hexVal = input.value; + const rgb = this.hexToRgb(hexVal); + const rgbLightVal = 'rgba('+ [rgb.r, rgb.g, rgb.b, '0.15'].join(',') +')'; + + console.log(input.name, lightName, hexVal, rgbLightVal) + const lightColorInput = this.container.querySelector(`input[name="${lightName}"][type="hidden"]`); + lightColorInput.value = rgbLightVal; + } + + /** + * Covert a hex color code to rgb components. + * @attribution https://stackoverflow.com/a/5624139 + * @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); + return { + r: result ? parseInt(result[1], 16) : 0, + g: result ? parseInt(result[2], 16) : 0, + b: result ? parseInt(result[3], 16) : 0 + }; + } + +} diff --git a/resources/js/components/setting-color-picker.js b/resources/js/components/setting-color-picker.js index 876e14f20..bfb2c93ce 100644 --- a/resources/js/components/setting-color-picker.js +++ b/resources/js/components/setting-color-picker.js @@ -15,6 +15,6 @@ export class SettingColorPicker extends Component { setValue(value) { this.colorInput.value = value; - this.colorInput.dispatchEvent(new Event('change')); + this.colorInput.dispatchEvent(new Event('change', {bubbles: true})); } } \ No newline at end of file diff --git a/resources/js/components/tabs.js b/resources/js/components/tabs.js index 46063d240..8d22e3e9b 100644 --- a/resources/js/components/tabs.js +++ b/resources/js/components/tabs.js @@ -1,49 +1,49 @@ -import {onSelect} from "../services/dom"; import {Component} from "./component"; /** * Tabs - * Works by matching 'tabToggle' with 'tabContent' sections. + * Uses accessible attributes to drive its functionality. + * On tab wrapping element: + * - role=tablist + * On tabs (Should be a button): + * - id + * - role=tab + * - aria-selected=true/false + * - aria-controls= + * On panels: + * - id + * - tabindex=0 + * - role=tabpanel + * - aria-labelledby= + * - hidden (If not shown by default). */ export class Tabs extends Component { setup() { - this.tabContentsByName = {}; - this.tabButtonsByName = {}; - this.allContents = []; - this.allButtons = []; + this.container = this.$el; + this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]')); + this.panels = Array.from(this.container.querySelectorAll('[role="tabpanel"]')); - for (const [key, elems] of Object.entries(this.$manyRefs || {})) { - if (key.startsWith('toggle')) { - const cleanKey = key.replace('toggle', '').toLowerCase(); - onSelect(elems, e => this.show(cleanKey)); - this.allButtons.push(...elems); - this.tabButtonsByName[cleanKey] = elems; + this.container.addEventListener('click', event => { + const button = event.target.closest('[role="tab"]'); + if (button) { + this.show(button.getAttribute('aria-controls')); } - if (key.startsWith('content')) { - const cleanKey = key.replace('content', '').toLowerCase(); - this.tabContentsByName[cleanKey] = elems; - this.allContents.push(...elems); - } - } + }); } - show(key) { - this.allContents.forEach(c => { - c.classList.add('hidden'); - c.classList.remove('selected'); - }); - this.allButtons.forEach(b => b.classList.remove('selected')); - - const contents = this.tabContentsByName[key] || []; - const buttons = this.tabButtonsByName[key] || []; - if (contents.length > 0) { - contents.forEach(c => { - c.classList.remove('hidden') - c.classList.add('selected') - }); - buttons.forEach(b => b.classList.add('selected')); + show(sectionId) { + for (const panel of this.panels) { + panel.toggleAttribute('hidden', panel.id !== sectionId); } + + for (const tab of this.tabs) { + const tabSection = tab.getAttribute('aria-controls'); + const selected = tabSection === sectionId; + tab.setAttribute('aria-selected', selected ? 'true' : 'false'); + } + + this.$emit('change', {showing: sectionId}); } } \ No newline at end of file diff --git a/resources/js/services/util.js b/resources/js/services/util.js index 1a56ebf6c..238f8b1d8 100644 --- a/resources/js/services/util.js +++ b/resources/js/services/util.js @@ -34,7 +34,7 @@ export function scrollAndHighlightElement(element) { if (!element) return; element.scrollIntoView({behavior: 'smooth'}); - const color = document.getElementById('custom-styles').getAttribute('data-color-light'); + const color = getComputedStyle(document.body).getPropertyValue('--color-primary-light'); const initColor = window.getComputedStyle(element).getPropertyValue('background-color'); element.style.backgroundColor = color; setTimeout(() => { diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 023cf1beb..6f4376d42 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -36,8 +36,6 @@ return [ 'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.', 'app_icon' => 'Application Icon', 'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.', - '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', @@ -51,8 +49,12 @@ return [ 'app_disable_comments_desc' => 'Disables comments across all pages in the application.
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.', + 'color_scheme' => 'Application Color Scheme', + 'color_scheme_desc' => 'Set the colors to use in the BookStack interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.', + 'ui_colors_desc' => 'Set the primary color and default link color for BookStack. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the Bookstack interface.', + 'app_color' => 'Primary Color', + 'link_color' => 'Default Link Color', + 'content_colors_desc' => 'Set 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', diff --git a/resources/sass/_blocks.scss b/resources/sass/_blocks.scss index 2794dd954..1d9bfc272 100644 --- a/resources/sass/_blocks.scss +++ b/resources/sass/_blocks.scss @@ -237,6 +237,13 @@ } } +.sub-card { + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + border: 1.5px solid; + @include lightDark(border-color, #E2E2E2, #444); + border-radius: 4px; +} + .outline-hover { border: 1px solid transparent !important; &:hover { diff --git a/resources/sass/_buttons.scss b/resources/sass/_buttons.scss index fb3af06e8..3c6775ad5 100644 --- a/resources/sass/_buttons.scss +++ b/resources/sass/_buttons.scss @@ -25,7 +25,6 @@ button { text-transform: uppercase; border: 1px solid var(--color-primary); vertical-align: top; - @include lightDark(filter, none, saturate(0.8) brightness(0.8)); &:hover, &:focus, &:active { background-color: var(--color-primary); text-decoration: none; @@ -85,10 +84,7 @@ button { user-select: none; font-size: 0.75rem; line-height: 1.4em; - color: var(--color-primary); - @include whenDark { - color: #AAA; - } + color: var(--color-link); &:active { outline: 0; } @@ -96,8 +92,8 @@ button { text-decoration: none; } &:hover, &:focus { - color: var(--color-primary); - fill: var(--color-primary); + color: var(--color-link); + fill: var(--color-link); } } .text-button.hover-underline:hover { diff --git a/resources/sass/_colors.scss b/resources/sass/_colors.scss index c51f01659..aff9ff6d0 100644 --- a/resources/sass/_colors.scss +++ b/resources/sass/_colors.scss @@ -9,11 +9,14 @@ background-color: var(--color-primary-light); @include whenDark { background: #000; - .text-primary { + .text-link { color: #AAA !important; } } } +.link-background { + background-color: var(--color-link) !important; +} /* * Status text colors @@ -41,6 +44,11 @@ fill: var(--color-primary) !important; } +.text-link, .text-link:hover, .text-link-hover:hover { + color: var(--color-link) !important; + fill: var(--color-link) !important; +} + .text-muted { @include lightDark(color, #575757, #888888, true); fill: currentColor !important; diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index ab1d506c7..2150f6d07 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -607,36 +607,37 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } -.tab-container .nav-tabs { +.tab-container [role="tablist"] { + display: flex; + align-items: end; + justify-items: start; text-align: start; border-bottom: 1px solid #DDD; @include lightDark(border-color, #ddd, #444); margin-bottom: $-m; - .tab-item { - padding: $-s; - @include lightDark(color, #666, #999); - &.selected { - border-bottom-width: 3px; - } - } } -.nav-tabs { - text-align: center; - a, .tab-item { - padding: $-m; - display: inline-block; - @include lightDark(color, #666, #999); - cursor: pointer; - border-right: 1px solid rgba(0, 0, 0, 0.1); - border-bottom: 2px solid transparent; - &.selected { - border-bottom: 2px solid var(--color-primary); - } - &:last-child { - border-right: 0; - } +.tab-container [role="tablist"] button[role="tab"], +.image-manager [role="tablist"] button[role="tab"] { + display: inline-block; + padding: $-s; + @include lightDark(color, rgba(0, 0, 0, .5), rgba(255, 255, 255, .5)); + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + &[aria-selected="true"] { + color: var(--color-link) !important; + border-bottom-color: var(--color-link) !important; } + &:hover, &:focus { + @include lightDark(color, rgba(0, 0, 0, .8), rgba(255, 255, 255, .8)); + @include lightDark(border-bottom-color, rgba(0, 0, 0, .2), rgba(255, 255, 255, .2)); + } +} +.tab-container [role="tablist"].controls-card { + margin-bottom: 0; + border-bottom: 0; + padding: 0 $-xs; } .image-picker .none { diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index ef14f6221..b7fc52f7d 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -258,7 +258,6 @@ input[type=color] { border-radius: 2px; display: inline-block; border: 2px solid currentColor; - opacity: 0.6; overflow: hidden; fill: currentColor; .svg-icon { diff --git a/resources/sass/_header.scss b/resources/sass/_header.scss index aa560e8e0..c1b6af4c6 100644 --- a/resources/sass/_header.scss +++ b/resources/sass/_header.scss @@ -22,9 +22,6 @@ header { border-bottom: 1px solid #DDD; box-shadow: $bs-card; @include lightDark(border-bottom-color, #DDD, #000); - @include whenDark { - filter: saturate(0.8) brightness(0.8); - } .header-links { display: flex; align-items: center; diff --git a/resources/sass/_layout.scss b/resources/sass/_layout.scss index 3fc419046..19333faf7 100644 --- a/resources/sass/_layout.scss +++ b/resources/sass/_layout.scss @@ -433,7 +433,7 @@ body.flexbox { display: none; } .tri-layout-left-contents > *, .tri-layout-right-contents > * { - @include lightDark(opacity, 0.6, 0.7); + @include lightDark(opacity, 0.6, 0.75); transition: opacity ease-in-out 120ms; &:hover, &:focus-within { opacity: 1 !important; @@ -442,7 +442,6 @@ body.flexbox { opacity: 1 !important; } } - } @include smaller-than($m) { diff --git a/resources/sass/_text.scss b/resources/sass/_text.scss index 6c68bd12b..edf8ce614 100644 --- a/resources/sass/_text.scss +++ b/resources/sass/_text.scss @@ -90,7 +90,7 @@ h2.list-heading { * Link styling */ a { - color: var(--color-primary); + color: var(--color-link); fill: currentColor; cursor: pointer; text-decoration: none; @@ -107,7 +107,7 @@ a { display: inline-block; } &:focus img:only-child { - outline: 2px dashed var(--color-primary); + outline: 2px dashed var(--color-link); outline-offset: 2px; } } diff --git a/resources/sass/_variables.scss b/resources/sass/_variables.scss index e1242bdda..aac9223f9 100644 --- a/resources/sass/_variables.scss +++ b/resources/sass/_variables.scss @@ -39,6 +39,7 @@ $fs-s: 12px; :root { --color-primary: #206ea7; --color-primary-light: rgba(32,110,167,0.15); + --color-link: #206ea7; --color-page: #206ea7; --color-page-draft: #7e50b1; diff --git a/resources/sass/styles.scss b/resources/sass/styles.scss index e50a2f96a..668cb5c85 100644 --- a/resources/sass/styles.scss +++ b/resources/sass/styles.scss @@ -113,7 +113,7 @@ $loadingSize: 10px; &:focus { top: $-xl; outline-offset: -10px; - outline: 2px dotted var(--color-primary); + outline: 2px dotted var(--color-link); } } diff --git a/resources/views/attachments/manager-list.blade.php b/resources/views/attachments/manager-list.blade.php index ebb1c24aa..f1dfe2b82 100644 --- a/resources/views/attachments/manager-list.blade.php +++ b/resources/views/attachments/manager-list.blade.php @@ -14,13 +14,13 @@ option:event-emit-select:name="insert" type="button" title="{{ trans('entities.attachments_insert_link') }}" - class="drag-card-action text-center text-primary">@icon('link') + class="drag-card-action text-center text-link">@icon('link') + class="drag-card-action text-center text-link">@icon('edit')
diff --git a/resources/views/attachments/manager.blade.php b/resources/views/attachments/manager.blade.php index 724ca9c8e..7d14d00e7 100644 --- a/resources/views/attachments/manager.blade.php +++ b/resources/views/attachments/manager.blade.php @@ -9,25 +9,54 @@
-

{{ trans('entities.attachments_explain') }} {{ trans('entities.attachments_explain_instant_save') }}

+

{{ trans('entities.attachments_explain') }} {{ trans('entities.attachments_explain_instant_save') }}

-