logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: eb27f1205b7bf077b81d698d24c2be656cc59023
parent daa39b6e8fd0a940615064436c510bedb205dbad
Author: Henry Jameson <me@hjkos.com>
Date:   Wed, 22 May 2024 15:20:42 +0300

Merge branch 'scrobbles-age' of ssh://git.pleroma.social:2222/pleroma/pleroma-fe into scrobbles-age

Diffstat:

Achangelog.d/mute-nsfw.add1+
Achangelog.d/notif-types.fix1+
Achangelog.d/poll-ended-notifications.fix1+
Achangelog.d/public-favorites.skip0
Achangelog.d/status-loading-indicator.add1+
Achangelog.d/themes3-fixes.fix1+
Achangelog.d/themes3.change1+
Msrc/App.scss423++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/App.vue1+
Dsrc/_variables.scss36------------------------------------
Msrc/boot/after_store.js18++++++++----------
Msrc/components/account_actions/account_actions.vue21+++++++--------------
Asrc/components/alert.style.js51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/announcement/announcement.vue8+++-----
Msrc/components/announcement_editor/announcement_editor.vue4+++-
Msrc/components/announcements_page/announcements_page.vue6++----
Msrc/components/attachment/attachment.scss31+++++++++++--------------------
Asrc/components/attachment/attachment.style.js24++++++++++++++++++++++++
Msrc/components/attachment/attachment.vue5++---
Msrc/components/autosuggest/autosuggest.vue18+++++++-----------
Msrc/components/avatar_list/avatar_list.vue5+----
Asrc/components/badge.style.js30++++++++++++++++++++++++++++++
Msrc/components/basic_user_card/basic_user_card.vue1-
Asrc/components/border.style.js13+++++++++++++
Asrc/components/button.style.js101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/button_unstyled.style.js96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/chat/chat.scss16+++-------------
Asrc/components/chat/chat.style.js19+++++++++++++++++++
Msrc/components/chat/chat.vue5++---
Msrc/components/chat_list/chat_list.vue5+----
Msrc/components/chat_list_item/chat_list_item.scss21++++-----------------
Msrc/components/chat_list_item/chat_list_item.vue3+--
Msrc/components/chat_message/chat_message.scss46++++++----------------------------------------
Asrc/components/chat_message/chat_message.style.js30++++++++++++++++++++++++++++++
Msrc/components/chat_message/chat_message.vue2+-
Msrc/components/chat_new/chat_new.scss5-----
Msrc/components/chat_new/chat_new.vue43++++++++++++++++++++++---------------------
Msrc/components/chat_title/chat_title.vue5+----
Msrc/components/checkbox/checkbox.vue29++++++++++++++---------------
Msrc/components/color_input/color_input.scss36++++++++++++++++++++++++++----------
Msrc/components/color_input/color_input.vue50+++++++++++++++++++++++++++++++-------------------
Msrc/components/conversation/conversation.js7++++++-
Msrc/components/conversation/conversation.vue91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/components/desktop_nav/desktop_nav.scss32++++----------------------------
Msrc/components/dialog_modal/dialog_modal.vue13++-----------
Msrc/components/emoji_input/emoji_input.vue57++++++++++++++++++++++++++-------------------------------
Msrc/components/emoji_picker/emoji_picker.scss21++-------------------
Msrc/components/emoji_picker/emoji_picker.vue10+++++-----
Msrc/components/emoji_reactions/emoji_reactions.vue17+++++------------
Msrc/components/extra_buttons/extra_buttons.vue30++++++++++++++----------------
Msrc/components/extra_notifications/extra_notifications.vue5+----
Msrc/components/favorite_button/favorite_button.vue4+---
Msrc/components/flash/flash.vue2--
Msrc/components/font_control/font_control.vue6++----
Asrc/components/fun_text.style.js40++++++++++++++++++++++++++++++++++++++++
Msrc/components/gallery/gallery.vue2--
Msrc/components/global_notice_list/global_notice_list.vue44+-------------------------------------------
Asrc/components/icon.style.js14++++++++++++++
Msrc/components/image_cropper/image_cropper.vue2+-
Msrc/components/importer/importer.vue1+
Asrc/components/input.style.js60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/interface_language_switcher/interface_language_switcher.vue2--
Msrc/components/link-preview/link-preview.vue14++++----------
Asrc/components/link.style.js24++++++++++++++++++++++++
Msrc/components/list/list.vue26+++++++++-----------------
Asrc/components/list/list_item.style.js48++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/lists_card/lists_card.vue16+---------------
Msrc/components/lists_edit/lists_edit.vue3+--
Msrc/components/lists_user_search/lists_user_search.vue3+--
Msrc/components/login_form/login_form.vue6++----
Msrc/components/media_upload/media_upload.vue2--
Msrc/components/mention_link/mention_link.scss13++++++-------
Msrc/components/mention_link/mention_link.vue2+-
Msrc/components/mentions_line/mentions_line.vue4++--
Asrc/components/menu_item.style.js90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/mfa_form/recovery_form.vue2+-
Msrc/components/mfa_form/totp_form.vue2+-
Asrc/components/mobile_drawer.style.js41+++++++++++++++++++++++++++++++++++++++++
Msrc/components/mobile_nav/mobile_nav.vue41+++++++++--------------------------------
Msrc/components/mobile_post_status_button/mobile_post_status_button.vue7+------
Asrc/components/modal/modals.style.js9+++++++++
Msrc/components/moderation_tools/moderation_tools.vue38++++++++++++++++++--------------------
Msrc/components/mrf_transparency_panel/mrf_transparency_panel.vue1-
Msrc/components/nav_panel/nav_panel.vue50++++++--------------------------------------------
Msrc/components/navigation/navigation_entry.vue79+++++++++++++++++++++++++++++--------------------------------------------------
Msrc/components/navigation/navigation_pins.vue27++++-----------------------
Msrc/components/notification/notification.scss26+++++++++++---------------
Asrc/components/notification/notification.style.js17+++++++++++++++++
Msrc/components/notification/notification.vue3+--
Msrc/components/notifications/notification_filters.vue28++++++++++++++--------------
Msrc/components/notifications/notifications.scss28+++++++++-------------------
Msrc/components/notifications/notifications.vue4++--
Msrc/components/opacity_input/opacity_input.vue2+-
Asrc/components/panel.style.js41+++++++++++++++++++++++++++++++++++++++++
Asrc/components/panel_header.style.js24++++++++++++++++++++++++
Msrc/components/panel_loading/panel_loading.vue8++------
Msrc/components/password_reset/password_reset.vue11++---------
Msrc/components/poll/poll.vue21++++++++++-----------
Msrc/components/poll/poll_form.vue6++----
Asrc/components/poll/poll_graph.style.js12++++++++++++
Asrc/components/popover.style.js36++++++++++++++++++++++++++++++++++++
Msrc/components/popover/popover.vue124++++++++++++++++---------------------------------------------------------------
Msrc/components/post_status_form/post_status_form.vue67++++++++++++++++---------------------------------------------------
Msrc/components/quick_filter_settings/quick_filter_settings.js7+++++++
Msrc/components/quick_filter_settings/quick_filter_settings.vue38+++++++++++++++++++++++++-------------
Msrc/components/quick_view_settings/quick_view_settings.js7+++++++
Msrc/components/quick_view_settings/quick_view_settings.vue22+++++++++++-----------
Msrc/components/range_input/range_input.vue6+++---
Msrc/components/react_button/react_button.vue8+-------
Msrc/components/registration/registration.vue26+++++++++++---------------
Msrc/components/reply_button/reply_button.vue4+---
Msrc/components/report/report.scss10++--------
Msrc/components/report/report.vue2+-
Msrc/components/retweet_button/retweet_button.vue4+---
Msrc/components/rich_content/rich_content.jsx8+++++++-
Msrc/components/rich_content/rich_content.scss30++++++++++++++++++++++++++----
Asrc/components/rich_content/rich_content.style.js18++++++++++++++++++
Asrc/components/root.style.js44++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/scope_selector/scope_selector.js8++++----
Msrc/components/scope_selector/scope_selector.vue7-------
Asrc/components/scrollbar.style.js11+++++++++++
Asrc/components/scrollbar_element.style.js101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/search/search.vue34++++++++++------------------------
Msrc/components/search_bar/search_bar.vue7++-----
Msrc/components/select/select.vue11+++--------
Msrc/components/selectable_list/selectable_list.vue29+++++++++++------------------
Msrc/components/settings_modal/admin_tabs/emoji_tab.scss6++----
Msrc/components/settings_modal/admin_tabs/emoji_tab.vue172++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/components/settings_modal/admin_tabs/frontends_tab.vue19++++++++++++++-----
Msrc/components/settings_modal/admin_tabs/instance_tab.vue10++++++++--
Msrc/components/settings_modal/helpers/attachment_setting.vue2+-
Msrc/components/settings_modal/helpers/emoji_editing_popover.vue97+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/components/settings_modal/helpers/number_setting.vue2+-
Msrc/components/settings_modal/helpers/string_setting.vue2+-
Msrc/components/settings_modal/helpers/unit_setting.vue2+-
Msrc/components/settings_modal/settings_modal.scss2--
Msrc/components/settings_modal/settings_modal.vue8++++----
Msrc/components/settings_modal/tabs/filtering_tab.vue25++++++++++++++++++++++++-
Msrc/components/settings_modal/tabs/notifications_tab.vue9+++++++--
Msrc/components/settings_modal/tabs/profile_tab.scss8++------
Msrc/components/settings_modal/tabs/profile_tab.vue10+++++++---
Msrc/components/settings_modal/tabs/security_tab/mfa.vue7+++----
Msrc/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue7++-----
Msrc/components/settings_modal/tabs/security_tab/mfa_totp.vue1+
Msrc/components/settings_modal/tabs/security_tab/security_tab.vue9+++++++++
Msrc/components/settings_modal/tabs/theme_tab/preview.vue6++++--
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.js22++++++++++++----------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.scss12+++---------
Msrc/components/shadow_control/shadow_control.js2+-
Msrc/components/shadow_control/shadow_control.vue34+++++++++++++---------------------
Msrc/components/shout_panel/shout_panel.vue16++++++----------
Msrc/components/side_drawer/side_drawer.vue96+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Asrc/components/status/post.style.js33+++++++++++++++++++++++++++++++++
Msrc/components/status/status.js12+++++++++++-
Msrc/components/status/status.scss56++++++++++++++++----------------------------------------
Msrc/components/status/status.vue10++++++++--
Msrc/components/status_body/status_body.scss14+-------------
Msrc/components/status_body/status_body.vue2++
Msrc/components/status_popover/status_popover.vue7+------
Msrc/components/sticker_picker/sticker_picker.vue4+---
Msrc/components/still-image/still-image.vue5+----
Asrc/components/tab_switcher/tab.style.js78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/tab_switcher/tab_switcher.jsx2+-
Msrc/components/tab_switcher/tab_switcher.scss30+++++++++++++++---------------
Asrc/components/text.style.js22++++++++++++++++++++++
Msrc/components/thread_tree/thread_tree.vue8+++-----
Msrc/components/timeline/timeline.js8++++----
Msrc/components/timeline/timeline.scss26++++++++++----------------
Msrc/components/timeline/timeline.vue2+-
Msrc/components/timeline_menu/timeline_menu.vue63---------------------------------------------------------------
Asrc/components/top_bar.style.js28++++++++++++++++++++++++++++
Asrc/components/underlay.style.js19+++++++++++++++++++
Msrc/components/update_notification/update_notification.scss4+---
Asrc/components/user_avatar/avatar.style.js22++++++++++++++++++++++
Msrc/components/user_avatar/user_avatar.vue22++++++++--------------
Msrc/components/user_card/user_card.scss72+++++++++++++++++++-----------------------------------------------------
Asrc/components/user_card/user_card.style.js41+++++++++++++++++++++++++++++++++++++++++
Msrc/components/user_card/user_card.vue15++++++---------
Msrc/components/user_list_menu/user_list_menu.vue6+++---
Msrc/components/user_list_popover/user_list_popover.vue2--
Msrc/components/user_note/user_note.vue6++----
Msrc/components/user_panel/user_panel.vue13++++++++++---
Msrc/components/user_popover/user_popover.vue2--
Msrc/components/user_profile/user_profile.vue114++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/components/user_reporting_modal/user_reporting_modal.vue10+++-------
Msrc/hocs/with_load_more/with_load_more.scss5+----
Msrc/i18n/cs.json194++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/i18n/en.json13++++++++-----
Msrc/i18n/fr.json314+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/i18n/ja_pedantic.json24+++++++++++++++++++++++-
Msrc/i18n/pt.json185++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/modules/config.js1+
Msrc/modules/instance.js2++
Msrc/modules/interface.js4++++
Msrc/panel.scss111++++++++++++++++++++++++++++---------------------------------------------------
Msrc/services/color_convert/color_convert.js2+-
Msrc/services/notifications_fetcher/notifications_fetcher.service.js3+++
Msrc/services/style_setter/style_setter.js473+++++++++++++++++++------------------------------------------------------------
Asrc/services/theme_data/css_utils.js163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/theme_data/iss_utils.js129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/theme_data/pleromafe.t3.js2++
Asrc/services/theme_data/theme2_keys.js177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/theme_data/theme2_to_theme3.js536+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/services/theme_data/theme3_slot_functions.js103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/services/theme_data/theme_data.service.js346++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Asrc/services/theme_data/theme_data_3.service.js468+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/unit/specs/services/theme_data/theme_data3.spec.js144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/check-changelog2+-
208 files changed, 5530 insertions(+), 2336 deletions(-)

diff --git a/changelog.d/mute-nsfw.add b/changelog.d/mute-nsfw.add @@ -0,0 +1 @@ +Added ability to mute sensitive posts (ported from eintei) diff --git a/changelog.d/notif-types.fix b/changelog.d/notif-types.fix @@ -0,0 +1 @@ +Synchronized requested notification types with backend, hopefully should fix missing notifications for polls and follow requests diff --git a/changelog.d/poll-ended-notifications.fix b/changelog.d/poll-ended-notifications.fix @@ -0,0 +1 @@ +Add poll end notifications to fetched types. diff --git a/changelog.d/public-favorites.skip b/changelog.d/public-favorites.skip diff --git a/changelog.d/status-loading-indicator.add b/changelog.d/status-loading-indicator.add @@ -0,0 +1 @@ +Display loading and error indicator for conversation page diff --git a/changelog.d/themes3-fixes.fix b/changelog.d/themes3-fixes.fix @@ -0,0 +1 @@ +fix color inputs and some in-development themes3 issues diff --git a/changelog.d/themes3.change b/changelog.d/themes3.change @@ -0,0 +1 @@ +Overhauled the way themes work, migrating to new Pleroma Interface Style Sheets system. diff --git a/src/App.scss b/src/App.scss @@ -1,9 +1,10 @@ // stylelint-disable rscss/class-format /* stylelint-disable no-descending-specificity */ -@import "./variables"; @import "./panel"; :root { + --font-size: 14px; + --status-margin: 0.75em; --navbar-height: 3.5rem; --post-line-height: 1.4; // Z-Index stuff @@ -13,19 +14,21 @@ --ZI_navbar_popovers: 7500; --ZI_navbar: 7000; --ZI_popovers: 6000; + + // Fallback for when stuff is loading + --background: var(--bg); } html { - font-size: 14px; + font-size: var(--font-size); // overflow-x: clip causes my browser's tab to crash with SIGILL lul } body { font-family: sans-serif; - font-family: var(--interfaceFont, sans-serif); + font-family: var(--font); margin: 0; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overscroll-behavior-y: none; @@ -42,17 +45,35 @@ body { // have a cursor/pointer to operate them @media (any-pointer: fine) { * { - scrollbar-color: var(--btn) transparent; + scrollbar-color: var(--fg) transparent; &::-webkit-scrollbar { background: transparent; } + &::-webkit-scrollbar-corner { + background: transparent; + } + + &::-webkit-resizer { + /* stylelint-disable-next-line declaration-no-important */ + background-color: transparent !important; + background-image: + linear-gradient( + 135deg, + transparent calc(50% - 1px), + var(--textFaint) 50%, + transparent calc(50% + 1px), + transparent calc(75% - 1px), + var(--textFaint) 75%, + transparent calc(75% + 1px), + ); + } + &::-webkit-scrollbar-button, &::-webkit-scrollbar-thumb { - background-color: var(--btn); - box-shadow: var(--buttonShadow); - border-radius: var(--btnRadius); + box-shadow: var(--shadow); + border-radius: var(--roundness); } // horizontal/vertical/increment/decrement are webkit-specific stuff @@ -61,7 +82,7 @@ body { &::-webkit-scrollbar-button { --___bgPadding: 2px; - color: var(--btnText); + color: var(--text); background-repeat: no-repeat, no-repeat; &:horizontal { @@ -69,15 +90,15 @@ body { &:increment { background-image: - linear-gradient(45deg, var(--btnText) 50%, transparent 51%), - linear-gradient(-45deg, transparent 50%, var(--btnText) 51%); + linear-gradient(45deg, var(--text) 50%, transparent 51%), + linear-gradient(-45deg, transparent 50%, var(--text) 51%); background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding); } &:decrement { background-image: - linear-gradient(45deg, transparent 50%, var(--btnText) 51%), - linear-gradient(-45deg, var(--btnText) 50%, transparent 51%); + linear-gradient(45deg, transparent 50%, var(--text) calc(50% + 1px)), + linear-gradient(-45deg, var(--text) 50%, transparent 51%); background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding); } } @@ -87,15 +108,15 @@ body { &:increment { background-image: - linear-gradient(-45deg, transparent 50%, var(--btnText) 51%), - linear-gradient(45deg, transparent 50%, var(--btnText) 51%); + linear-gradient(-45deg, transparent 50%, var(--text) 51%), + linear-gradient(45deg, transparent 50%, var(--text) 51%); background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%; } &:decrement { background-image: - linear-gradient(-45deg, var(--btnText) 50%, transparent 51%), - linear-gradient(45deg, var(--btnText) 50%, transparent 51%); + linear-gradient(-45deg, var(--text) 50%, transparent 51%), + linear-gradient(45deg, var(--text) 50%, transparent 51%); background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%; } } @@ -104,15 +125,14 @@ body { } // Body should have background to scrollbar otherwise it will use white (body color?) html { - scrollbar-color: var(--selectedMenu) var(--wallpaper); + scrollbar-color: var(--fg) var(--wallpaper); background: var(--wallpaper); } } a { text-decoration: none; - color: $fallback--link; - color: var(--link, $fallback--link); + color: var(--link); } h4 { @@ -128,27 +148,12 @@ h4 { i[class*="icon-"], .svg-inline--fa, .iconLetter { - color: $fallback--icon; - color: var(--icon, $fallback--icon); -} - -.button-unstyled:hover, -a:hover { - > i[class*="icon-"], - > .svg-inline--fa, - > .iconLetter { - color: var(--text); - } + color: var(--icon); } nav { z-index: var(--ZI_navbar); - background-color: $fallback--fg; - background-color: var(--topBar, $fallback--fg); - color: $fallback--faint; - color: var(--faint, $fallback--faint); - box-shadow: 0 0 4px rgb(0 0 0 / 60%); - box-shadow: var(--topBarShadow); + box-shadow: var(--shadow); box-sizing: border-box; height: var(--navbar-height); position: fixed; @@ -195,8 +200,7 @@ nav { grid-column: 1 / span 3; grid-row: 1 / 1; pointer-events: none; - background-color: rgb(0 0 0 / 15%); - background-color: var(--underlay, rgb(0 0 0 / 15%)); + background-color: var(--underlay); z-index: -1000; } @@ -204,7 +208,6 @@ nav { --miniColumn: 25rem; --maxiColumn: 45rem; --columnGap: 1em; - --status-margin: 0.75em; --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn))); @@ -366,106 +369,113 @@ nav { .button-default { user-select: none; - color: $fallback--text; - color: var(--btnText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); + color: var(--text); border: none; - border-radius: $fallback--btnRadius; - border-radius: var(--btnRadius, $fallback--btnRadius); + border-radius: var(--roundness); cursor: pointer; - box-shadow: $fallback--buttonShadow; - box-shadow: var(--buttonShadow); + background-color: var(--background); + box-shadow: var(--shadow); font-size: 1em; font-family: sans-serif; - font-family: var(--interfaceFont, sans-serif); + font-family: var(--font); - &.-sublime { - background: transparent; + &::-moz-focus-inner { + border: none; } - i[class*="icon-"], - .svg-inline--fa { - color: $fallback--text; - color: var(--btnText, $fallback--text); + &:disabled { + cursor: not-allowed; } +} - &::-moz-focus-inner { - border: none; +.menu-item, +.list-item { + display: block; + box-sizing: border-box; + border: none; + outline: none; + text-align: initial; + font-size: inherit; + font-family: inherit; + font-weight: 400; + cursor: pointer; + color: inherit; + clear: both; + position: relative; + white-space: nowrap; + border-color: var(--border); + border-style: solid; + border-width: 0; + border-top-width: 1px; + width: 100%; + line-height: var(--__line-height); + padding: var(--__vertical-gap) var(--__horizontal-gap); + background: transparent; + + --__line-height: 1.5em; + --__horizontal-gap: 0.75em; + --__vertical-gap: 0.5em; + + &.-non-interactive { + cursor: auto; } + &.-active, &:hover { - box-shadow: 0 0 4px rgb(255 255 255 / 30%); - box-shadow: var(--buttonHoverShadow); - } - - &:active { - box-shadow: - 0 0 4px 0 rgb(255 255 255 / 30%), - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset; - box-shadow: var(--buttonPressedShadow); - color: $fallback--text; - color: var(--btnPressedText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnPressed, $fallback--fg); - - svg, - i { - color: $fallback--text; - color: var(--btnPressedText, $fallback--text); - } + border-top-width: 1px; + border-bottom-width: 1px; } - &:disabled { - cursor: not-allowed; - color: $fallback--text; - color: var(--btnDisabledText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnDisabled, $fallback--fg); - - svg, - i { - color: $fallback--text; - color: var(--btnDisabledText, $fallback--text); - } + &.-active + &, + &:hover + & { + border-top-width: 0; } - &.toggled { - color: $fallback--text; - color: var(--btnToggledText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnToggled, $fallback--fg); - box-shadow: - 0 0 4px 0 rgb(255 255 255 / 30%), - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset; - box-shadow: var(--buttonPressedShadow); - - svg, - i { - color: $fallback--text; - color: var(--btnToggledText, $fallback--text); - } + &:hover + .menu-item-collapsible:not(.-expanded) + &, + &.-active + .menu-item-collapsible:not(.-expanded) + & { + border-top-width: 0; + } + + &[aria-expanded="true"] { + border-bottom-width: 1px; + } + + a, + button:not(.button-default) { + text-align: initial; + padding: 0; + background: none; + border: none; + outline: none; + display: inline; + font-size: 100%; + font-family: inherit; + line-height: unset; + color: var(--text); } - &.danger { - // TODO: add better color variable - color: $fallback--text; - color: var(--alertErrorPanelText, $fallback--text); - background-color: $fallback--alertError; - background-color: var(--alertError, $fallback--alertError); + &:first-child { + border-top-right-radius: var(--roundness); + border-top-left-radius: var(--roundness); + border-top-width: 0; + } + + &:last-child { + border-bottom-right-radius: var(--roundness); + border-bottom-left-radius: var(--roundness); + border-bottom-width: 0; } } .button-unstyled { - background: none; border: none; outline: none; display: inline; text-align: initial; font-size: 100%; font-family: inherit; + box-shadow: var(--shadow); + background-color: transparent; padding: 0; line-height: unset; cursor: pointer; @@ -473,28 +483,23 @@ nav { color: inherit; &.-link { - color: $fallback--link; - color: var(--link, $fallback--link); - } - - &.-fullwidth { - width: 100%; - } - - &.-hover-highlight { - &:hover svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } + /* stylelint-disable-next-line declaration-no-important */ + color: var(--link) !important; } } input, -textarea, +textarea { + border: none; + display: inline-block; + outline: none; +} + .input { &.unstyled { border-radius: 0; - background: none; + /* stylelint-disable-next-line declaration-no-important */ + background: none !important; box-shadow: none; height: unset; } @@ -502,19 +507,11 @@ textarea, --_padding: 0.5em; border: none; - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); - box-shadow: - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset, - 0 0 2px 0 rgb(0 0 0 / 100%) inset; - box-shadow: var(--inputShadow); - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); - color: $fallback--lightText; - color: var(--inputText, $fallback--lightText); - font-family: sans-serif; - font-family: var(--inputFont, sans-serif); + border-radius: var(--roundness); + background-color: var(--background); + color: var(--text); + box-shadow: var(--shadow); + font-family: var(--font); font-size: 1em; margin: 0; box-sizing: border-box; @@ -528,7 +525,6 @@ textarea, &[disabled="disabled"], &.disabled { cursor: not-allowed; - opacity: 0.5; } &[type="range"] { @@ -543,9 +539,9 @@ textarea, display: none; &:checked + label::before { - box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset; - box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset; - background-color: var(--accent, $fallback--link); + box-shadow: var(--shadow); + background-color: var(--background); + color: var(--text); } &:disabled { @@ -559,16 +555,14 @@ textarea, + label::before { flex-shrink: 0; display: inline-block; - content: ""; + content: "•"; transition: box-shadow 200ms; width: 1.1em; height: 1.1em; border-radius: 100%; // Radio buttons should always be circle - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); + background-color: var(--background); + box-shadow: var(--shadow); margin-right: 0.5em; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); vertical-align: top; text-align: center; line-height: 1.1; @@ -581,8 +575,9 @@ textarea, &[type="checkbox"] { &:checked + label::before { - color: $fallback--text; - color: var(--inputText, $fallback--text); + color: var(--text); + background-color: var(--background); + box-shadow: var(--shadow); } &:disabled { @@ -600,13 +595,9 @@ textarea, transition: color 200ms; width: 1.1em; height: 1.1em; - border-radius: $fallback--checkboxRadius; - border-radius: var(--checkboxRadius, $fallback--checkboxRadius); - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); + border-radius: var(--roundness); + box-shadow: var(--shadow); margin-right: 0.5em; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); vertical-align: top; text-align: center; line-height: 1.1; @@ -623,16 +614,14 @@ textarea, } // Textareas should have stock line-height + vertical padding instead of huge line-height -textarea { +textarea.input { padding: var(--_padding); line-height: var(--post-line-height); } option { - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); + color: var(--text); + background-color: var(--background); } .hide-number-spinner { @@ -653,7 +642,7 @@ option { li { border: 1px solid var(--border); - border-radius: var(--inputRadius); + border-radius: var(--roundness); padding: 0.5em; margin: 0.25em; } @@ -714,74 +703,58 @@ option { overflow: hidden; text-overflow: ellipsis; - &.badge-notification { - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); - color: white; - color: var(--badgeNotificationText, white); - } -} - -.alert { - margin: 0 0.35em; - padding: 0 0.25em; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - - &.error { - background-color: $fallback--alertError; - background-color: var(--alertError, $fallback--alertError); - color: $fallback--text; - color: var(--alertErrorText, $fallback--text); - - .panel-heading & { - color: $fallback--text; - color: var(--alertErrorPanelText, $fallback--text); - } + &.-dot, + &.-counter { + margin: 0; + position: absolute; } - &.warning { - background-color: $fallback--alertWarning; - background-color: var(--alertWarning, $fallback--alertWarning); - color: $fallback--text; - color: var(--alertWarningText, $fallback--text); - - .panel-heading & { - color: $fallback--text; - color: var(--alertWarningPanelText, $fallback--text); - } + &.-dot { + min-height: 8px; + max-height: 8px; + min-width: 8px; + max-width: 8px; + padding: 0; + line-height: 0; + font-size: 0; + left: calc(50% - 4px); + top: calc(50% - 4px); + margin-left: 6px; + margin-top: -6px; } - &.success { - background-color: var(--alertSuccess, $fallback--alertWarning); - color: var(--alertSuccessText, $fallback--text); - - .panel-heading & { - color: var(--alertSuccessPanelText, $fallback--text); - } + &.-counter { + border-radius: var(--roundness); + font-size: 0.75em; + line-height: 1; + text-align: right; + padding: 0.2em; + min-width: 0; + left: calc(50% - 0.5em); + top: calc(50% - 0.4em); + margin-left: 0.7em; + margin-top: -1em; } } -.faint { - color: $fallback--faint; - color: var(--faint, $fallback--faint); +.alert { + margin: 0 0.35em; + padding: 0 0.25em; + border-radius: var(--roundness); + border: 1px solid var(--border); } -.faint-link { - color: $fallback--faint; - color: var(--faint, $fallback--faint); +.faint { + --text: var(--textFaint); + --link: var(--linkFaint); - &:hover { - text-decoration: underline; - } + color: var(--text); } .visibility-notice { padding: 0.5em; - border: 1px solid $fallback--faint; - border: 1px solid var(--faint, $fallback--faint); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border: 1px solid var(--textFaint); + border-radius: var(--roundness); } .notice-dismissible { @@ -802,6 +775,10 @@ option { &.iconLetter { font-size: 1.1em; } + + &.svg-inline--fa { + vertical-align: -0.15em; + } } .fa-old-padding { @@ -816,6 +793,11 @@ option { opacity: 0.25; } +.timeago { + --link: var(--text); + --linkFaint: var(--textFaint); +} + .login-hint { text-align: center; @@ -914,3 +896,8 @@ option { padding: 0; position: absolute; } + +*::selection { + color: var(--selectionText); + background-color: var(--selectionBackground); +} diff --git a/src/App.vue b/src/App.vue @@ -1,5 +1,6 @@ <template> <div + v-show="$store.state.interface.themeApplied" id="app-loaded" :style="bgStyle" > diff --git a/src/_variables.scss b/src/_variables.scss @@ -1,36 +0,0 @@ -$main-color: #f58d2c; -$main-background: white; -$darkened-background: whitesmoke; - -$fallback--bg: #121a24; -$fallback--fg: #182230; -$fallback--faint: rgb(185 185 186 / 50%); -$fallback--text: #b9b9ba; -$fallback--link: #d8a070; -$fallback--icon: #666; -$fallback--lightBg: rgb(21 30 42); -$fallback--lightText: #b9b9ba; -$fallback--border: #222; -$fallback--cRed: #f00; -$fallback--cBlue: #0095ff; -$fallback--cGreen: #0fa00f; -$fallback--cOrange: orange; - -$fallback--alertError: rgb(211 16 20 / 50%); -$fallback--alertWarning: rgb(111 111 20 / 50%); - -$fallback--panelRadius: 10px; -$fallback--checkboxRadius: 2px; -$fallback--btnRadius: 4px; -$fallback--inputRadius: 4px; -$fallback--tooltipRadius: 5px; -$fallback--avatarRadius: 4px; -$fallback--avatarAltRadius: 10px; -$fallback--attachmentRadius: 10px; -$fallback--chatMessageRadius: 10px; - -$fallback--buttonShadow: 0 0 2px 0 rgb(0 0 0 / 100%), - 0 1px 0 0 rgb(255 255 255 / 20%) inset, - 0 -1px 0 0 rgb(0 0 0 / 20%) inset; - -$status-margin: 0.75em; diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -328,17 +328,14 @@ const setConfig = async ({ store }) => { } const checkOAuthToken = async ({ store }) => { - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - if (store.getters.getUserToken()) { - try { - await store.dispatch('loginUser', store.getters.getUserToken()) - } catch (e) { - console.error(e) - } + if (store.getters.getUserToken()) { + try { + await store.dispatch('loginUser', store.getters.getUserToken()) + } catch (e) { + console.error(e) } - resolve() - }) + } + return Promise.resolve() } const afterStoreSetup = async ({ store, i18n }) => { @@ -366,6 +363,7 @@ const afterStoreSetup = async ({ store, i18n }) => { } else { applyTheme(customTheme) } + store.commit('setThemeApplied') } else if (theme) { // do nothing, it will load asynchronously } else { diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue @@ -11,14 +11,14 @@ <template v-if="relationship.following"> <button v-if="relationship.showing_reblogs" - class="btn button-default dropdown-item" + class="dropdown-item menu-item" @click="hideRepeats" > {{ $t('user_card.hide_repeats') }} </button> <button v-if="!relationship.showing_reblogs" - class="btn button-default dropdown-item" + class="dropdown-item menu-item" @click="showRepeats" > {{ $t('user_card.show_repeats') }} @@ -31,34 +31,34 @@ <UserListMenu :user="user" /> <button v-if="relationship.followed_by" - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="removeUserFromFollowers" > {{ $t('user_card.remove_follower') }} </button> <button v-if="relationship.blocking" - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="unblockUser" > {{ $t('user_card.unblock') }} </button> <button v-else - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="blockUser" > {{ $t('user_card.block') }} </button> <button - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="reportUser" > {{ $t('user_card.report') }} </button> <button v-if="pleromaChatMessagesAvailable" - class="btn button-default btn-block dropdown-item" + class="dropdown-item menu-item" @click="openChat" > {{ $t('user_card.message') }} @@ -122,19 +122,12 @@ <script src="./account_actions.js"></script> <style lang="scss"> -@import "../../variables"; - .AccountActions { .ellipsis-button { width: 2.5em; margin: -0.5em 0; padding: 0.5em 0; text-align: center; - - &:not(:hover) .icon { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } } } </style> diff --git a/src/components/alert.style.js b/src/components/alert.style.js @@ -0,0 +1,51 @@ +export default { + name: 'Alert', + selector: '.alert', + validInnerComponents: [ + 'Text', + 'Icon', + 'Link', + 'Border', + 'ButtonUnstyled' + ], + variants: { + normal: '.neutral', + error: '.error', + warning: '.warning', + success: '.success' + }, + defaultRules: [ + { + directives: { + background: '--text', + opacity: 0.5, + blur: '9px' + } + }, + { + parent: { + component: 'Alert' + }, + component: 'Border', + textColor: '--parent' + }, + { + variant: 'error', + directives: { + background: '--cRed' + } + }, + { + variant: 'warning', + directives: { + background: '--cOrange' + } + }, + { + variant: 'success', + directives: { + background: '--cGreen' + } + } + ] +} diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue @@ -99,16 +99,14 @@ <script src="./announcement.js"></script> <style lang="scss"> -@import "../../variables"; - .announcement { - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); border-radius: 0; - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); .heading, .body { - margin-bottom: var(--status-margin, $status-margin); + margin-bottom: var(--status-margin); } .footer { diff --git a/src/components/announcement_editor/announcement_editor.vue b/src/components/announcement_editor/announcement_editor.vue @@ -3,7 +3,7 @@ <textarea ref="textarea" v-model="announcement.content" - class="post-textarea" + class="input post-textarea" rows="1" cols="1" :placeholder="$t('announcements.post_placeholder')" @@ -14,6 +14,7 @@ <input id="announcement-start-time" v-model="announcement.startsAt" + class="input" :type="announcement.allDay ? 'date' : 'datetime-local'" :disabled="disabled" > @@ -23,6 +24,7 @@ <input id="announcement-end-time" v-model="announcement.endsAt" + class="input" :type="announcement.allDay ? 'date' : 'datetime-local'" :disabled="disabled" > diff --git a/src/components/announcements_page/announcements_page.vue b/src/components/announcements_page/announcements_page.vue @@ -61,15 +61,13 @@ <script src="./announcements_page.js"></script> <style lang="scss"> -@import "../../variables"; - .announcements-page { .post-form { - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); .heading, .body { - margin-bottom: var(--status-margin, $status-margin); + margin-bottom: var(--status-margin); } .post-button { diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Attachment { display: inline-flex; flex-direction: column; @@ -9,10 +7,8 @@ height: 100%; border-style: solid; border-width: 1px; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-radius: var(--roundness); + border-color: var(--border); .attachment-wrapper { flex: 1 1 auto; @@ -84,6 +80,13 @@ } } + .video-container { + border: none; + outline: none; + color: inherit; + background: transparent; + } + .audio-container { display: flex; align-items: flex-end; @@ -126,23 +129,12 @@ .attachment-button { padding: 0; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); text-align: center; width: 2em; height: 2em; margin-left: 0.5em; font-size: 1.25em; - // TODO: theming? hard to theme with unknown background image color - background: rgb(230 230 230 / 70%); - - .svg-inline--fa { - color: rgb(0 0 0 / 60%); - } - - &:hover .svg-inline--fa { - color: rgb(0 0 0 / 90%); - } } } @@ -217,8 +209,7 @@ &.-placeholder { display: inline-block; - color: $fallback--link; - color: var(--postLink, $fallback--link); + color: var(--link); overflow: hidden; white-space: nowrap; height: auto; diff --git a/src/components/attachment/attachment.style.js b/src/components/attachment/attachment.style.js @@ -0,0 +1,24 @@ +export default { + name: 'Attachment', + selector: '.Attachment', + validInnerComponents: [ + 'Border', + 'ButtonUnstyled', + 'Input' + ], + defaultRules: [ + { + directives: { + roundness: 3 + } + }, + { + component: 'ButtonUnstyled', + parent: { component: 'Attachment' }, + directives: { + background: '#FFFFFF', + opacity: 0.5 + } + } + ] +} diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue @@ -38,7 +38,7 @@ v-if="edit" v-model="localDescription" type="text" - class="description-field" + class="input description-field" :placeholder="$t('post_status.media_description')" @keydown.enter.prevent="" > @@ -175,7 +175,6 @@ :is="videoTag" v-if="type === 'video' && !hidden" class="video-container" - :class="{ 'button-unstyled': 'isModal' }" :href="attachment.url" @click.stop.prevent="openModal" > @@ -253,7 +252,7 @@ v-if="edit" v-model="localDescription" type="text" - class="description-field" + class="input description-field" :placeholder="$t('post_status.media_description')" @keydown.enter.prevent="" > diff --git a/src/components/autosuggest/autosuggest.vue b/src/components/autosuggest/autosuggest.vue @@ -1,3 +1,4 @@ +<!-- FIXME THIS NEEDS TO BE REFACTORED TO USE POPOVER --> <template> <div v-click-outside="onClickOutside" @@ -6,12 +7,12 @@ <input v-model="term" :placeholder="placeholder" - class="autosuggest-input" + class="input autosuggest-input" @click="onInputClick" > <div v-if="resultsVisible && filtered.length > 0" - class="autosuggest-results" + class="panel autosuggest-results" > <slot v-for="item in filtered" @@ -24,8 +25,6 @@ <script src="./autosuggest.js"></script> <style lang="scss"> -@import "../../variables"; - .autosuggest { position: relative; @@ -40,18 +39,15 @@ top: 100%; right: 0; max-height: 400px; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); + background-color: var(--bg); border-style: solid; border-width: 1px; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border-color: var(--border); + border-radius: var(--roundness); border-top-left-radius: 0; border-top-right-radius: 0; box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); overflow-y: auto; z-index: 1; } diff --git a/src/components/avatar_list/avatar_list.vue b/src/components/avatar_list/avatar_list.vue @@ -17,8 +17,6 @@ <script src="./avatar_list.js"></script> <style lang="scss"> -@import "../../variables"; - .avatars { display: flex; margin: 0; @@ -36,8 +34,7 @@ } .avatar-small { - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); height: 24px; width: 24px; } diff --git a/src/components/badge.style.js b/src/components/badge.style.js @@ -0,0 +1,30 @@ +export default { + name: 'Badge', + selector: '.badge', + validInnerComponents: [ + 'Text', + 'Icon' + ], + variants: { + notification: '.-notification' + }, + defaultRules: [ + { + component: 'Root', + directives: { + '--badgeNotification': 'color | --cRed' + } + }, + { + directives: { + background: '--cGreen' + } + }, + { + variant: 'notification', + directives: { + background: '--cRed' + } + } + ] +} diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue @@ -47,7 +47,6 @@ display: flex; flex: 1 0; margin: 0; - padding: 0.6em 1em; --emoji-size: 14px; diff --git a/src/components/border.style.js b/src/components/border.style.js @@ -0,0 +1,13 @@ +export default { + name: 'Border', + selector: '/*border*/', + virtual: true, + defaultRules: [ + { + directives: { + textColor: '$mod(--parent, 10)', + textAuto: 'no-auto' + } + } + ] +} diff --git a/src/components/button.style.js b/src/components/button.style.js @@ -0,0 +1,101 @@ +export default { + name: 'Button', // Name of the component + selector: '.button-default', // CSS selector/prefix + // outOfTreeSelector: '' // out-of-tree selector is used when other components are laid over it but it's not part of the tree, see Underlay component + // States, system witll calculate ALL possible combinations of those and prepend "normal" to them + standalone "normal" state + states: { + // States are a bit expensive - the amount of combinations generated is about (1/6)n^3+n, so adding more state increased number of combination by an order of magnitude! + // All states inherit from "normal" state, there is no other inheirtance, i.e. hover+disabled only inherits from "normal", not from hover nor disabled. + // However, cascading still works, so resulting state will be result of merging of all relevant states/variants + // normal: '' // normal state is implicitly added, it is always included + toggled: '.toggled', + pressed: ':active', + hover: ':hover:not(:disabled)', + focused: ':focus-within', + disabled: ':disabled' + }, + // Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it. + variants: { + // Variants save on computation time since adding new variant just adds one more "set". + // normal: '', // you can override normal variant, it will be appenended to the main class + danger: '.danger' + // Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants. + // This (currently) is further multipled by number of places where component can exist. + }, + // This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever). + validInnerComponents: [ + 'Text', + 'Icon' + ], + // Default rules, used as "default theme", essentially. + defaultRules: [ + { + component: 'Root', + directives: { + '--defaultButtonHoverGlow': 'shadow | 0 0 4 --text', + '--defaultButtonShadow': 'shadow | 0 0 2 #000000', + '--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2) | $borderSide(#000000, bottom, 0.2)', + '--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)' + } + }, + { + // component: 'Button', // no need to specify components every time unless you're specifying how other component should look + // like within it + directives: { + background: '--fg', + shadow: ['--defaultButtonShadow', '--defaultButtonBevel'], + roundness: 3 + } + }, + { + state: ['hover'], + directives: { + shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel'] + } + }, + { + state: ['pressed'], + directives: { + shadow: ['--defaultButtonShadow', '--pressedButtonBevel'] + } + }, + { + state: ['hover', 'pressed'], + directives: { + shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel'] + } + }, + { + state: ['toggled'], + directives: { + background: '--inheritedBackground,-14.2', + shadow: ['--defaultButtonShadow', '--pressedButtonBevel'] + } + }, + { + state: ['toggled', 'hover'], + directives: { + background: '--inheritedBackground,-14.2', + shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel'] + } + }, + { + state: ['disabled'], + directives: { + background: '$blend(--inheritedBackground, 0.25, --parent)', + shadow: ['--defaultButtonBevel'] + } + }, + { + component: 'Text', + parent: { + component: 'Button', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + } + ] +} diff --git a/src/components/button_unstyled.style.js b/src/components/button_unstyled.style.js @@ -0,0 +1,96 @@ +export default { + name: 'ButtonUnstyled', + selector: '.button-unstyled', + states: { + toggled: '.toggled', + disabled: ':disabled', + hover: ':hover:not(:disabled)', + focused: ':focus-within' + }, + validInnerComponents: [ + 'Text', + 'Icon', + 'Badge' + ], + defaultRules: [ + { + directives: { + background: '#ffffff', + opacity: 0, + shadow: [] + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['hover'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled', 'hover'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled', 'focused'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['toggled', 'focused', 'hover'] + }, + directives: { + textColor: '--parent--text' + } + }, + { + component: 'Text', + parent: { + component: 'ButtonUnstyled', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + }, + { + component: 'Icon', + parent: { + component: 'ButtonUnstyled', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + } + ] +} diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss @@ -11,15 +11,15 @@ .chat-view-body { box-sizing: border-box; - background-color: var(--chatBg, $fallback--bg); display: flex; flex-direction: column; width: 100%; overflow: visible; min-height: calc(100vh - var(--navbar-height)); margin: 0; - border-radius: 10px 10px 0 0; - border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0; + border-radius: var(--roundness); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; &::after { border-radius: 0; @@ -37,8 +37,6 @@ .footer { position: sticky; bottom: 0; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); z-index: 1; } @@ -61,8 +59,6 @@ position: absolute; right: 1.3em; top: -3.2em; - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); display: flex; justify-content: center; align-items: center; @@ -79,12 +75,6 @@ visibility: visible; } - i { - font-size: 1em; - color: $fallback--text; - color: var(--text, $fallback--text); - } - .unread-message-count { font-size: 0.8em; left: 50%; diff --git a/src/components/chat/chat.style.js b/src/components/chat/chat.style.js @@ -0,0 +1,19 @@ +export default { + name: 'Chat', + selector: '.chat-message-list', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Avatar', + 'ChatMessage' + ], + defaultRules: [ + { + directives: { + background: '--bg', + blur: '5px' + } + } + ] +} diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue @@ -26,7 +26,7 @@ </div> </div> <div - class="message-list" + class="chat-message-list message-list" :style="{ height: scrollableContainerHeight }" > <template v-if="!errorLoadingChat"> @@ -61,7 +61,7 @@ <FAIcon icon="chevron-down" /> <div v-if="newMessageCount" - class="badge badge-notification unread-chat-count unread-message-count" + class="badge -notification unread-chat-count unread-message-count" > {{ newMessageCount }} </div> @@ -95,6 +95,5 @@ <script src="./chat.js"></script> <style lang="scss"> -@import "../../variables"; @import "./chat"; </style> diff --git a/src/components/chat_list/chat_list.vue b/src/components/chat_list/chat_list.vue @@ -45,8 +45,6 @@ <script src="./chat_list.js"></script> <style lang="scss"> -@import "../../variables"; - .chat-list { min-height: 25em; margin-bottom: 0; @@ -57,8 +55,7 @@ font-size: 1.2em; display: flex; justify-content: center; - color: $fallback--text; - color: var(--faint, $fallback--text); + color: var(--textFaint); } </style> diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss @@ -1,8 +1,6 @@ .chat-list-item { display: flex; flex-direction: row; - padding: 0.75em; - height: 5em; overflow: hidden; box-sizing: border-box; cursor: pointer; @@ -11,11 +9,6 @@ outline: none; } - &:hover { - background-color: var(--selectedPost, $fallback--lightBg); - box-shadow: 0 0 3px 1px rgb(0 0 0 / 10%); - } - .chat-list-item-left { margin-right: 1em; } @@ -29,7 +22,7 @@ .heading { width: 100%; - display: inline-flex; + display: flex; justify-content: space-between; line-height: 1em; } @@ -47,18 +40,17 @@ } .chat-preview { - display: inline-flex; + display: flex; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin: 0.35em 0; - color: $fallback--text; - color: var(--faint, $fallback--text); + color: var(--textFaint); width: 100%; } a { - color: var(--faintLink, $fallback--link); + color: var(--linkFaint); text-decoration: none; pointer-events: none; } @@ -73,11 +65,6 @@ } } - .Avatar { - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - } - .chat-preview-body { --emoji-size: 1.4em; diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue @@ -36,7 +36,7 @@ /> <div v-if="chat.unread > 0" - class="badge badge-notification unread-chat-count" + class="badge -notification unread-chat-count" > {{ chat.unread }} </div> @@ -48,6 +48,5 @@ <script src="./chat_list_item.js"></script> <style lang="scss"> -@import "../../variables"; @import "./chat_list_item"; </style> diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .chat-message-wrapper { &.hovered-message-chain { .animated.Avatar { @@ -27,12 +25,6 @@ .menu-icon { cursor: pointer; - - &:hover, - .extra-button-popover.open & { - color: $fallback--text; - color: var(--text, $fallback--text); - } } .popover { @@ -61,10 +53,12 @@ } .status { - border-radius: $fallback--chatMessageRadius; - border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius); + background-color: var(--background); + color: var(--text); + border-radius: var(--roundness); display: flex; padding: 0.75em; + border: 1px solid var(--border); } .created-at { @@ -97,8 +91,7 @@ .error { .status-content.media-body, .created-at { - color: $fallback--cRed; - color: var(--badgeNotification, $fallback--cRed); + color: var(--badgeNotification); } } @@ -117,16 +110,6 @@ align-content: end; justify-content: flex-end; - a { - color: var(--chatMessageOutgoingLink, $fallback--link); - } - - .status { - color: var(--chatMessageOutgoingText, $fallback--text); - background-color: var(--chatMessageOutgoingBg, $fallback--lightBg); - border: 1px solid var(--chatMessageOutgoingBorder, --lightBg); - } - .chat-message-inner { align-items: flex-end; } @@ -137,22 +120,6 @@ } .incoming { - a { - color: var(--chatMessageIncomingLink, $fallback--link); - } - - .status { - color: var(--chatMessageIncomingText, $fallback--text); - background-color: var(--chatMessageIncomingBg, $fallback--bg); - border: 1px solid var(--chatMessageIncomingBorder, --border); - } - - .created-at { - a { - color: var(--chatMessageIncomingText, $fallback--text); - } - } - .chat-message-menu { left: 0.4rem; } @@ -176,6 +143,5 @@ margin: 1.4em 0; font-size: 0.9em; user-select: none; - color: $fallback--text; - color: var(--faintedText, $fallback--text); + color: var(--textFaint); } diff --git a/src/components/chat_message/chat_message.style.js b/src/components/chat_message/chat_message.style.js @@ -0,0 +1,30 @@ +export default { + name: 'ChatMessage', + selector: '.chat-message', + variants: { + outgoing: '.outgoing' + }, + validInnerComponents: [ + 'Text', + 'Icon', + 'Border', + 'Button', + 'RichContent', + 'Attachment', + 'PollGraph' + ], + defaultRules: [ + { + directives: { + background: '--bg, 2', + backgroundNoCssColor: 'yes' + } + }, + { + variant: 'outgoing', + directives: { + background: '--bg, 5' + } + } + ] +} diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue @@ -53,7 +53,7 @@ <template #content> <div class="dropdown-menu"> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click="deleteMessage" > <FAIcon icon="times" /> {{ $t("chats.delete") }} diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss @@ -16,11 +16,6 @@ padding-bottom: 0.7rem; } - .basic-user-card:hover { - cursor: pointer; - background-color: var(--selectedPost, $fallback--lightBg); - } - .go-back-button { text-align: center; line-height: 1; diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue @@ -16,27 +16,29 @@ /> </button> </div> - <div class="input-wrap"> - <div class="input-search"> - <FAIcon - class="search-icon fa-scale-110 fa-old-padding" - icon="search" - /> + <div class="panel-body"> + <div class="input-wrap"> + <div class="input-search"> + <FAIcon + class="search-icon fa-scale-110 fa-old-padding" + icon="search" + /> + </div> + <input + ref="search" + v-model="query" + class="input" + placeholder="Search people" + @input="onInput" + > </div> - <input - ref="search" - v-model="query" - placeholder="Search people" - @input="onInput" - > - </div> - <div class="member-list"> - <div - v-for="user in availableUsers" - :key="user.id" - class="member" - > - <div @click.capture.prevent="goToChat(user)"> + <div class="member-list"> + <div + v-for="user in availableUsers" + :key="user.id" + class="list-item" + @click.capture.prevent="goToChat(user)" + > <BasicUserCard :user="user" /> </div> </div> @@ -46,6 +48,5 @@ <script src="./chat_new.js"></script> <style lang="scss"> -@import "../../variables"; @import "./chat_new"; </style> diff --git a/src/components/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue @@ -26,8 +26,6 @@ <script src="./chat_title.js"></script> <style lang="scss"> -@import "../../variables"; - .chat-title { display: flex; overflow: hidden; @@ -54,8 +52,7 @@ margin-right: 0.5em; height: 1.5em; width: 1.5em; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); &.animated::before { display: none; diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue @@ -12,7 +12,7 @@ @change="$emit('update:modelValue', $event.target.checked)" > <i - class="checkbox-indicator" + class="input -checkbox checkbox-indicator" :aria-hidden="true" @transitionend.capture="onTransitionEnd" /> @@ -54,7 +54,6 @@ export default { </script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .checkbox { @@ -62,9 +61,15 @@ export default { display: inline-block; min-height: 1.2em; - &-indicator { + & > &-indicator { + /* Reset .input stuff */ + padding: 0; + margin: 0; position: relative; + line-height: inherit; + display: inline; padding-left: 1.2em; + box-shadow: none; } &-indicator::before { @@ -76,12 +81,9 @@ export default { transition: color 200ms; width: 1.1em; height: 1.1em; - border-radius: $fallback--checkboxRadius; - border-radius: var(--checkboxRadius, $fallback--checkboxRadius); - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); + border-radius: var(--roundness); + box-shadow: var(--shadow); + background-color: var(--background); vertical-align: top; text-align: center; line-height: 1.1em; @@ -98,21 +100,18 @@ export default { } .label { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--text); } } input[type="checkbox"] { &:checked + .checkbox-indicator::before { - color: $fallback--text; - color: var(--inputText, $fallback--text); + color: var(--text); } &:indeterminate + .checkbox-indicator::before { content: "–"; - color: $fallback--text; - color: var(--inputText, $fallback--text); + color: var(--text); } } diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .color-input { display: inline-flex; @@ -11,9 +9,8 @@ padding: 0.2em 8px; input { + color: var(--text); background: none; - color: $fallback--lightText; - color: var(--inputText, $fallback--lightText); border: none; padding: 0; margin: 0; @@ -23,21 +20,38 @@ min-width: 3em; padding: 0; } + } + + .nativeColor { + cursor: pointer; + flex: 0 0 auto; - &.nativeColor { - flex: 0 0 2em; - min-width: 2em; - align-self: stretch; - min-height: 100%; + input { + appearance: none; + max-width: 0; + min-width: 0; + max-height: 0; + /* stylelint-disable-next-line declaration-no-important */ + opacity: 0 !important; } } .computedIndicator, + .validIndicator, + .invalidIndicator, .transparentIndicator { flex: 0 0 2em; + margin: 0 0.5em; min-width: 2em; align-self: stretch; - min-height: 100%; + min-height: 1.5em; + border-radius: var(--roundness); + } + + .invalidIndicator { + background: transparent; + box-sizing: border-box; + border: 2px solid var(--cRed); } .transparentIndicator { @@ -58,11 +72,13 @@ &::after { top: 0; left: 0; + border-top-left-radius: var(--roundness); } &::before { bottom: 0; right: 0; + border-bottom-right-radius: var(--roundness); } } } diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue @@ -25,30 +25,51 @@ :disabled="!present || disabled" @input="$emit('update:modelValue', $event.target.value)" > - <input + <div v-if="validColor" - :id="name" - class="nativeColor unstyled" - type="color" - :value="modelValue || fallback" - :disabled="!present || disabled" - @input="$emit('update:modelValue', $event.target.value)" - > + class="validIndicator" + :style="{backgroundColor: modelValue || fallback}" + /> <div - v-if="transparentColor" + v-else-if="transparentColor" class="transparentIndicator" /> <div - v-if="computedColor" + v-else-if="computedColor" class="computedIndicator" :style="{backgroundColor: fallback}" /> + <div + v-else + class="invalidIndicator" + /> + <label class="nativeColor"> + <FAIcon icon="eye-dropper" /> + <input + :id="name" + class="unstyled" + type="color" + :value="modelValue || fallback" + :disabled="!present || disabled" + @input="$emit('update:modelValue', $event.target.value)" + > + </label> </div> </div> </template> <script> import Checkbox from '../checkbox/checkbox.vue' import { hex2rgb } from '../../services/color_convert/color_convert.js' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEyeDropper +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEyeDropper +) + export default { components: { Checkbox @@ -108,12 +129,3 @@ export default { } </script> <style lang="scss" src="./color_input.scss"></style> - -<style lang="scss"> -.color-control { - input.text-input { - max-width: 7em; - flex: 1; - } -} -</style> diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js @@ -56,7 +56,8 @@ const conversation = { expanded: false, threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' statusContentPropertiesObject: {}, - inlineDivePosition: null + inlineDivePosition: null, + loadStatusError: null } }, props: [ @@ -392,11 +393,15 @@ const conversation = { this.setHighlight(this.originalStatusId) }) } else { + this.loadStatusError = null this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId }) .then((status) => { this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.fetchConversation() }) + .catch((error) => { + this.loadStatusError = error + }) } }, getReplies (id) { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue @@ -28,7 +28,27 @@ class="rightside-button" /> </div> - <div class="conversation-body panel-body"> + <div + v-if="isPage && !status" + class="conversation-body" + :class="{ 'panel-body': isExpanded }" + > + <p v-if="!loadStatusError"> + <FAIcon + spin + icon="circle-notch" + /> + {{ $t('status.loading') }} + </p> + <p v-else> + {{ $t('status.load_error', { error: loadStatusError }) }} + </p> + </div> + <div + v-else + class="conversation-body" + :class="{ 'panel-body': isExpanded }" + > <div v-if="isTreeView" class="thread-body" @@ -203,6 +223,7 @@ </div> <div v-else + class="Conversation -hidden" :style="hiddenStyle" /> </template> @@ -210,14 +231,17 @@ <script src="./conversation.js"></script> <style lang="scss"> -@import "../../variables"; - .Conversation { z-index: 1; + &.-hidden { + background: var(--__panel-background); + backdrop-filter: var(--__panel-backdrop-filter); + } + .conversation-dive-to-top-level-box { - padding: var(--status-margin, $status-margin); - border-bottom: 1px solid var(--border, $fallback--border); + padding: var(--status-margin); + border-bottom: 1px solid var(--border); border-radius: 0; /* Make the button stretch along the whole row */ @@ -227,20 +251,22 @@ } .thread-ancestors { - margin-left: var(--status-margin, $status-margin); - border-left: 2px solid var(--border, $fallback--border); + margin-left: var(--status-margin); + border-left: 2px solid var(--border); } - .thread-ancestor.-faded .StatusContent { - --link: var(--faintLink); - --text: var(--faint); - - color: var(--text); + .thread-ancestor.-faded .RichContent { + /* stylelint-disable declaration-no-important */ + --text: var(--textFaint) !important; + --link: var(--linkFaint) !important; + --funtextGreentext: var(--funtextGreentextFaint) !important; + --funtextCyantext: var(--funtextCyantextFaint) !important; + /* stylelint-enable declaration-no-important */ } .thread-ancestor-dive-box { - padding-left: var(--status-margin, $status-margin); - border-bottom: 1px solid var(--border, $fallback--border); + padding-left: var(--status-margin); + border-bottom: 1px solid var(--border); border-radius: 0; /* Make the button stretch along the whole row */ @@ -253,16 +279,17 @@ } .thread-ancestor-dive-box-inner { - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); } .conversation-status { - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); border-radius: 0; } .thread-ancestor-has-other-replies .conversation-status, - &:last-child .conversation-status, + &:last-child:not(.-expanded) .conversation-status, + &.-expanded .conversation-status:last-child, .thread-ancestor:last-child .conversation-status, .thread-ancestor:last-child .thread-ancestor-dive-box, &.-expanded .thread-tree .conversation-status { @@ -270,20 +297,36 @@ } .thread-ancestors + .thread-tree > .conversation-status { - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); } /* expanded conversation in timeline */ &.status-fadein.-expanded .thread-body { - border-left: 4px solid $fallback--cRed; - border-left-color: var(--cRed, $fallback--cRed); - border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; - border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); - border-bottom: 1px solid var(--border, $fallback--border); + border-left: 4px solid var(--cRed); + border-radius: var(--roundness); + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom: 1px solid var(--border); } &.-expanded.status-fadein { - margin: calc(var(--status-margin, $status-margin) / 2); + --___margin: calc(var(--status-margin) / 2); + + background: var(--background); + margin: var(--___margin); + + &::before { + z-index: -1; + content: ""; + display: block; + position: absolute; + top: calc(var(--___margin) * -1); + bottom: calc(var(--___margin) * -1); + left: calc(var(--___margin) * -1); + right: calc(var(--___margin) * -1); + background: var(--background); + backdrop-filter: var(--__panel-backdrop-filter); + } } } </style> diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .DesktopNav { width: 100%; z-index: var(--ZI_navbar); @@ -9,7 +7,7 @@ } a { - color: var(--topBarLink, $fallback--link); + color: var(--link); } .inner-nav { @@ -54,27 +52,7 @@ .button-default { &, svg { - color: $fallback--text; - color: var(--btnTopBarText, $fallback--text); - } - - &:active { - background-color: $fallback--fg; - background-color: var(--btnPressedTopBar, $fallback--fg); - color: $fallback--text; - color: var(--btnPressedTopBarText, $fallback--text); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledTopBarText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledTopBarText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnToggledTopBar, $fallback--fg); + color: var(--text); } } @@ -94,8 +72,7 @@ mask-repeat: no-repeat; mask-position: center; mask-size: contain; - background-color: $fallback--fg; - background-color: var(--topBarText, $fallback--fg); + background-color: var(--text); position: absolute; top: 0; bottom: 0; @@ -116,8 +93,7 @@ text-align: center; .svg-inline--fa { - color: $fallback--link; - color: var(--topBarLink, $fallback--link); + color: var(--link); } } diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue @@ -12,7 +12,7 @@ <slot name="header" /> </div> </div> - <div class="dialog-modal-content"> + <div class="panel-body dialog-modal-content"> <slot name="default" /> </div> <div class="dialog-modal-footer user-interactions panel-footer"> @@ -25,8 +25,6 @@ <script src="./dialog_modal.js"></script> <style lang="scss"> -@import "../../variables"; - // TODO: unify with other modals. .dark-overlay { &::before { @@ -54,8 +52,6 @@ z-index: 2001; cursor: default; display: block; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); .dialog-modal-heading { .title { @@ -66,18 +62,13 @@ .dialog-modal-content { margin: 0; padding: 1rem; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); white-space: normal; } .dialog-modal-footer { margin: 0; padding: 0.5em; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-top: 1px solid $fallback--border; - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); display: flex; justify-content: flex-end; diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue @@ -1,7 +1,7 @@ <template> <div ref="root" - class="emoji-input" + class="input emoji-input" :class="{ 'with-picker': !hideEmojiButton }" > <slot @@ -68,9 +68,9 @@ v-for="(suggestion, index) in suggestions" :id="suggestionItemId(index)" :key="index" - class="autocomplete-item" + class="menu-item autocomplete-item" role="option" - :class="{ highlighted: index === highlighted }" + :class="{ '-active': index === highlighted }" :aria-label="autoCompleteItemLabel(suggestion)" :aria-selected="index === highlighted" @click.stop.prevent="onClick($event, suggestion)" @@ -110,9 +110,8 @@ <script src="./emoji_input.js"></script> <style lang="scss"> -@import "../../variables"; - -.emoji-input { +.input.emoji-input { + padding: 0; display: flex; flex-direction: column; position: relative; @@ -127,8 +126,7 @@ line-height: 24px; &:hover i { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } @@ -145,6 +143,12 @@ input, textarea { flex: 1 0 auto; + color: inherit; + /* stylelint-disable-next-line declaration-no-important */ + background: none !important; + box-shadow: none; + border: none; + outline: none; } &.with-picker input { @@ -179,26 +183,28 @@ position: absolute; } - &-item { + &-item.menu-item { display: flex; - cursor: pointer; - padding: 0.2em 0.4em; - border-bottom: 1px solid rgb(0 0 0 / 40%); - height: 32px; + padding-top: 0; + padding-bottom: 0; .image { - width: 32px; - height: 32px; - line-height: 32px; + width: calc(var(--__line-height) + var(--__vertical-gap) * 2); + height: calc(var(--__line-height) + var(--__vertical-gap) * 2); + line-height: var(--__line-height); text-align: center; - font-size: 32px; - margin-right: 4px; + margin-right: var(--__horizontal-gap); img { - width: 32px; - height: 32px; + width: calc(var(--__line-height) + var(--__vertical-gap) * 2); + height: calc(var(--__line-height) + var(--__vertical-gap) * 2); object-fit: contain; } + + span { + font-size: var(--__line-height); + line-height: var(--__line-height); + } } .label { @@ -216,17 +222,6 @@ line-height: 9px; } } - - &.highlighted { - background-color: $fallback--fg; - background-color: var(--selectedMenuPopover, $fallback--fg); - color: var(--selectedMenuPopoverText, $fallback--text); - - --faint: var(--selectedMenuPopoverFaintText, $fallback--faint); - --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); - --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - } } } </style> diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - $emoji-picker-header-height: 36px; $emoji-picker-header-picture-width: 32px; $emoji-picker-header-picture-height: 32px; @@ -10,15 +8,6 @@ $emoji-picker-emoji-size: 32px; max-width: calc(100vw - 20px); // popover gives 10px margin from window edge display: flex; flex-direction: column; - background-color: $fallback--bg; - background-color: var(--popover, $fallback--bg); - color: $fallback--link; - color: var(--popoverText, $fallback--link); - - --faint: var(--popoverFaintText, $fallback--faint); - --faintLink: var(--popoverFaintLink, $fallback--faint); - --lightText: var(--popoverLightText, $fallback--lightText); - --icon: var(--popoverIcon, $fallback--icon); &-header-image { display: inline-flex; @@ -81,8 +70,7 @@ $emoji-picker-emoji-size: 32px; .additional-tabs { display: flex; border-left: 1px solid; - border-left-color: $fallback--icon; - border-left-color: var(--icon, $fallback--icon); + border-left-color: var(--border); padding-left: 7px; flex: 0 0 auto; } @@ -109,13 +97,8 @@ $emoji-picker-emoji-size: 32px; pointer-events: none; } - &.active { + &.toggled { border-bottom: 4px solid; - - svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } } } } diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue @@ -23,9 +23,9 @@ v-for="group in filteredEmojiGroups" :ref="setGroupRef('group-header-' + group.id)" :key="group.id" - class="emoji-tabs-item" + class="button-unstyled emoji-tabs-item" :class="{ - active: activeGroupView === group.id + toggled: activeGroupView === group.id }" :title="group.text" role="button" @@ -52,8 +52,8 @@ class="additional-tabs" > <span - class="stickers-tab-icon additional-tabs-item" - :class="{active: showingStickers}" + class="button-unstyled stickers-tab-icon additional-tabs-item" + :class="{toggled: showingStickers}" :title="$t('emoji.stickers')" @click.prevent="toggleStickers" > @@ -77,7 +77,7 @@ ref="search" v-model="keyword" type="text" - class="form-control" + class="input form-control" :placeholder="$t('emoji.search_emoji')" @input="$event.target.composing = false" > diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue @@ -72,7 +72,6 @@ <script src="./emoji_reactions.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .EmojiReactions { @@ -92,7 +91,6 @@ padding: 0; .emoji-reaction-count-button { - background-color: var(--btn); margin: 0; height: 100%; border-top-left-radius: 0; @@ -102,11 +100,9 @@ display: inline-flex; justify-content: center; align-items: center; - color: $fallback--text; - color: var(--btnText, $fallback--text); &.-picked-reaction { - border: 1px solid var(--accent, $fallback--link); + border: 1px solid var(--accent); margin-right: -1px; } } @@ -149,18 +145,16 @@ } .svg-inline--fa { - color: $fallback--text; - color: var(--btnText, $fallback--text); + color: var(--text); } &.-picked-reaction { - border: 1px solid var(--accent, $fallback--link); + border: 1px solid var(--accent); margin-left: -1px; // offset the border, can't use inset shadows either margin-right: -1px; .svg-inline--fa { - color: $fallback--link; - color: var(--accent, $fallback--link); + color: var(--accent); } } @@ -176,8 +170,7 @@ @include focused-style { .svg-inline--fa { - color: $fallback--link; - color: var(--accent, $fallback--link); + color: var(--accent); } .focus-marker { diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue @@ -12,13 +12,13 @@ > <template #content="{close}"> <div + :id="`popup-menu-${randomSeed}`" class="dropdown-menu" role="menu" - :id="`popup-menu-${randomSeed}`" > <button v-if="canMute && !status.thread_muted" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="muteConversation" > @@ -29,7 +29,7 @@ </button> <button v-if="canMute && status.thread_muted" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="unmuteConversation" > @@ -40,7 +40,7 @@ </button> <button v-if="!status.pinned && canPin" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="pinStatus" @click="close" @@ -52,7 +52,7 @@ </button> <button v-if="status.pinned && canPin" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="unpinStatus" @click="close" @@ -65,7 +65,7 @@ <template v-if="canBookmark"> <button v-if="!status.bookmarked" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="bookmarkStatus" @click="close" @@ -77,7 +77,7 @@ </button> <button v-if="status.bookmarked" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="unbookmarkStatus" @click="close" @@ -90,7 +90,7 @@ </template> <button v-if="ownStatus && editingAvailable" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="editStatus" @click="close" @@ -102,7 +102,7 @@ </button> <button v-if="isEdited && editingAvailable" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="showStatusHistory" @click="close" @@ -114,7 +114,7 @@ </button> <button v-if="canDelete" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="deleteStatus" @click="close" @@ -125,7 +125,7 @@ /><span>{{ $t("status.delete") }}</span> </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="copyLink" @click="close" @@ -137,7 +137,7 @@ </button> <a v-if="!status.is_local" - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" title="Source" :href="status.external_url" @@ -149,7 +149,7 @@ /><span>{{ $t("status.external_source") }}</span> </a> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click.prevent="reportStatus" @click="close" @@ -201,7 +201,6 @@ <script src="./extra_buttons.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .ExtraButtons { @@ -211,8 +210,7 @@ margin: -10px; &:hover .svg-inline--fa { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } diff --git a/src/components/extra_notifications/extra_notifications.vue b/src/components/extra_notifications/extra_notifications.vue @@ -80,8 +80,6 @@ <script src="./extra_notifications.js" /> <style lang="scss"> -@import "../../variables"; - .ExtraNotifications { width: 100%; display: flex; @@ -91,8 +89,7 @@ .notification { width: 100%; border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); display: flex; flex-direction: column; align-items: stretch; diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue @@ -65,7 +65,6 @@ <script src="./favorite_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .FavoriteButton { @@ -88,8 +87,7 @@ &:hover .svg-inline--fa, &.-favorited .svg-inline--fa { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } @include unfocused-style { diff --git a/src/components/flash/flash.vue b/src/components/flash/flash.vue @@ -42,8 +42,6 @@ <script src="./flash.js"></script> <style lang="scss"> -@import "../../variables"; - .Flash { display: inline-block; width: 100%; diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue @@ -14,7 +14,7 @@ v-if="typeof fallback !== 'undefined'" :id="name + '-o'" :aria-labelledby="name + '-label'" - class="opt exlcude-disabled visible-for-screenreader-only" + class="input -checkbox opt exlcude-disabled visible-for-screenreader-only" type="checkbox" :checked="present" @change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)" @@ -44,7 +44,7 @@ v-if="isCustom" :id="name" v-model="family" - class="custom-font" + class="input custom-font" type="text" > </div> @@ -53,8 +53,6 @@ <script src="./font_control.js"></script> <style lang="scss"> -@import "../../variables"; - .font-control { input.custom-font { min-width: 10em; diff --git a/src/components/fun_text.style.js b/src/components/fun_text.style.js @@ -0,0 +1,40 @@ +export default { + name: 'FunText', + selector: '/*fun-text*/', + virtual: true, + variants: { + greentext: '.greentext', + cyantext: '.cyantext' + }, + states: { + faint: '.faint' + }, + defaultRules: [ + { + directives: { + textColor: '--text', + textAuto: 'preserve' + } + }, + { + state: ['faint'], + directives: { + textOpacity: 0.5 + } + }, + { + variant: 'greentext', + directives: { + textColor: '--cGreen', + textAuto: 'preserve' + } + }, + { + variant: 'cyantext', + directives: { + textColor: '--cBlue', + textAuto: 'preserve' + } + } + ] +} diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue @@ -87,8 +87,6 @@ <script src='./gallery.js'></script> <style lang="scss"> -@import "../../variables"; - .Gallery { .gallery-rows { display: flex; diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue @@ -4,7 +4,7 @@ v-for="(notice, index) in notices" :key="index" class="alert global-notice" - :class="{ ['global-' + notice.level]: true }" + :class="{ [notice.level]: true }" > <div class="notice-message"> {{ $t(notice.messageKey, notice.messageArgs) }} @@ -25,8 +25,6 @@ <script src="./global_notice_list.js"></script> <style lang="scss"> -@import "../../variables"; - .global-notice-list { position: fixed; top: calc(var(--navbar-height) + 0.5em); @@ -52,48 +50,8 @@ } } - .global-error { - background-color: var(--alertPopupError, $fallback--cRed); - color: var(--alertPopupErrorText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupErrorText, $fallback--text); - } - } - - .global-warning { - background-color: var(--alertPopupWarning, $fallback--cOrange); - color: var(--alertPopupWarningText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupWarningText, $fallback--text); - } - } - - .global-success { - background-color: var(--alertPopupSuccess, $fallback--cGreen); - color: var(--alertPopupSuccessText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupSuccessText, $fallback--text); - } - } - - .global-info { - background-color: var(--alertPopupNeutral, $fallback--fg); - color: var(--alertPopupNeutralText, $fallback--text); - - .svg-inline--fa { - color: var(--alertPopupNeutralText, $fallback--text); - } - } - .close-notice { padding-right: 0.2em; - - .svg-inline--fa:hover { - opacity: 0.6; - } } } </style> diff --git a/src/components/icon.style.js b/src/components/icon.style.js @@ -0,0 +1,14 @@ +export default { + name: 'Icon', + virtual: true, + selector: '.svg-inline--fa', + defaultRules: [ + { + component: 'Icon', + directives: { + textColor: '$blend(--stack, 0.5, --parent--text)', + textAuto: 'no-auto' + } + } + ] +} diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue @@ -41,7 +41,7 @@ <input ref="input" type="file" - class="image-cropper-img-input" + class="input image-cropper-img-input" :accept="mimes" > </div> diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue @@ -3,6 +3,7 @@ <form> <input ref="input" + class="input" type="file" @change="change" > diff --git a/src/components/input.style.js b/src/components/input.style.js @@ -0,0 +1,60 @@ +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--text', + alpha: 1 +} + +export default { + name: 'Input', + selector: '.input', + variant: { + checkbox: '.-checkbox', + radio: '.-radio' + }, + states: { + disabled: ':disabled', + hover: ':hover:not(:disabled)', + focused: ':focus-within' + }, + validInnerComponents: [ + 'Text' + ], + defaultRules: [ + { + component: 'Root', + directives: { + '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2)| $borderSide(#000000, top, 0.2)' + } + }, + { + variant: 'checkbox', + directives: { + roundness: 1 + } + }, + { + directives: { + '--font': 'generic | inherit', + background: '--fg, -5', + roundness: 3, + shadow: [{ + x: 0, + y: 0, + blur: 2, + spread: 0, + color: '#000000', + alpha: 1 + }, '--defaultInputBevel'] + } + }, + { + state: ['hover'], + directives: { + shadow: [hoverGlow, '--defaultInputBevel'] + } + } + ] +} diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -104,8 +104,6 @@ export default { </script> <style lang="scss"> -@import "../../variables"; - .interface-language-switcher { .language-select { margin-right: 1em; diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue @@ -33,8 +33,6 @@ <script src="./link-preview.js"></script> <style lang="scss"> -@import "../../variables"; - .link-preview-card { display: flex; flex-direction: row; @@ -51,8 +49,7 @@ width: 100%; height: 100%; object-fit: cover; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); + border-radius: var(--roundness); } } @@ -82,13 +79,10 @@ margin: 2em 0; } - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); border-style: solid; border-width: 1px; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-radius: var(--roundness); + border-color: var(--border); } </style> diff --git a/src/components/link.style.js b/src/components/link.style.js @@ -0,0 +1,24 @@ +export default { + name: 'Link', + selector: 'a', + virtual: true, + states: { + faint: '.faint' + }, + defaultRules: [ + { + component: 'Link', + directives: { + textColor: '--link' + } + }, + { + component: 'Link', + state: ['faint'], + directives: { + textOpacity: 0.5, + textOpacityMode: 'fake' + } + } + ] +} diff --git a/src/components/list/list.vue b/src/components/list/list.vue @@ -7,6 +7,7 @@ v-for="item in items" :key="getKey(item)" class="list-item" + :class="[getClass(item), nonInteractive ? '-non-interactive' : '']" role="listitem" > <slot @@ -33,24 +34,15 @@ export default { getKey: { type: Function, default: item => item.id + }, + getClass: { + type: Function, + default: item => '' + }, + nonInteractive: { + type: Boolean, + default: false } } } </script> - -<style lang="scss"> -@import "../../variables"; - -.list { - &-item:not(:last-child) { - border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); - } - - &-empty-content { - text-align: center; - padding: 10px; - } -} -</style> diff --git a/src/components/list/list_item.style.js b/src/components/list/list_item.style.js @@ -0,0 +1,48 @@ +export default { + name: 'ListItem', + selector: '.list-item', + states: { + active: '.-active', + hover: ':hover:not(.-non-interactive)' + }, + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', + 'Avatar' + ], + defaultRules: [ + { + directives: { + background: '--bg', + opacity: 0 + } + }, + { + state: ['active'], + directives: { + background: '--inheritedBackground, 10', + opacity: 1 + } + }, + { + state: ['hover'], + directives: { + background: '--inheritedBackground, 10', + opacity: 1 + } + }, + { + state: ['hover', 'active'], + directives: { + background: '--inheritedBackground, 20', + opacity: 1 + } + } + ] +} diff --git a/src/components/lists_card/lists_card.vue b/src/components/lists_card/lists_card.vue @@ -21,8 +21,6 @@ <script src="./lists_card.js"></script> <style lang="scss"> -@import "../../variables"; - .list-card { display: flex; } @@ -35,18 +33,6 @@ .button-list-edit { margin: 0; padding: 1em; - color: $fallback--link; - color: var(--link, $fallback--link); - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - } + color: var(--link); } </style> diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue @@ -36,6 +36,7 @@ id="list-edit-title" ref="title" v-model="titleDraft" + class="input" > <button v-if="id" @@ -164,8 +165,6 @@ <script src="./lists_edit.js"></script> <style lang="scss"> -@import "../../variables"; - .ListEdit { --panel-body-padding: 0.5em; diff --git a/src/components/lists_user_search/lists_user_search.vue b/src/components/lists_user_search/lists_user_search.vue @@ -10,6 +10,7 @@ <input ref="search" v-model="query" + class="input" :placeholder="$t('lists.search')" @input="onInput" > @@ -27,8 +28,6 @@ <script src="./lists_user_search.js"></script> <style lang="scss"> -@import "../../variables"; - .ListsUserSearch { .input-wrap { display: flex; diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue @@ -18,7 +18,7 @@ id="username" v-model="user.username" :disabled="loggingIn" - class="form-control" + class="input form-control" :placeholder="$t('login.placeholder')" > </div> @@ -29,7 +29,7 @@ ref="passwordInput" v-model="user.password" :disabled="loggingIn" - class="form-control" + class="input form-control" type="password" > </div> @@ -93,8 +93,6 @@ <script src="./login_form.js"></script> <style lang="scss"> -@import "../../variables"; - .login-form { display: flex; flex-direction: column; diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue @@ -36,8 +36,6 @@ <script src="./media_upload.js"></script> <style lang="scss"> -@import "../../variables"; - .media-upload { .hidden-input-file { display: none; diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss @@ -1,10 +1,7 @@ -@import "../../variables"; - .MentionLink { position: relative; white-space: normal; display: inline; - color: var(--link); word-break: normal; & .new, @@ -14,7 +11,7 @@ } .mention-avatar { - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); width: 1.5em; height: 1.5em; vertical-align: middle; @@ -61,8 +58,10 @@ } &.-has-selection { - color: var(--alertNeutralText, $fallback--text); - background-color: var(--alertNeutral, $fallback--fg); + --color: var(--selectionText); + --link: var(--selectionText); + + background-color: var(--selectionBackground); } .at { @@ -102,7 +101,7 @@ } .serverName.-faded { - color: var(--faintLink, $fallback--link); + color: var(--linkFaint); } } diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue @@ -22,7 +22,7 @@ :class="classnames" > <a - class="short button-unstyled" + class="short" :class="{ '-with-tooltip': shouldShowTooltip }" :href="url" @click.prevent="onClick" diff --git a/src/components/mentions_line/mentions_line.vue b/src/components/mentions_line/mentions_line.vue @@ -22,13 +22,13 @@ /> </span><button v-if="!expanded" - class="button-unstyled showMoreLess" + class="button-unstyled -link showMoreLess" @click="toggleShowMore" > {{ $t('status.plus_more', { number: extraMentions.length }) }} </button><button v-if="expanded" - class="button-unstyled showMoreLess" + class="button-unstyled -link showMoreLess" @click="toggleShowMore" > {{ $t('general.show_less') }} diff --git a/src/components/menu_item.style.js b/src/components/menu_item.style.js @@ -0,0 +1,90 @@ +export default { + name: 'MenuItem', + selector: '.menu-item', + validInnerComponents: [ + 'Text', + 'Icon', + 'Input', + 'Border', + 'ButtonUnstyled', + 'Badge', + 'Avatar' + ], + states: { + hover: ':hover', + active: '.-active' + }, + defaultRules: [ + { + directives: { + background: '--bg', + opacity: 0 + } + }, + { + state: ['hover'], + directives: { + background: '$mod(--bg, 5)', + opacity: 1 + } + }, + { + state: ['active'], + directives: { + background: '$mod(--bg, 10)', + opacity: 1 + } + }, + { + state: ['active', 'hover'], + directives: { + background: '$mod(--bg, 15)', + opacity: 1 + } + }, + { + component: 'Text', + parent: { + component: 'MenuItem', + state: ['hover'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + }, + { + component: 'Text', + parent: { + component: 'MenuItem', + state: ['active'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + }, + { + component: 'Icon', + parent: { + component: 'MenuItem', + state: ['active'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + }, + { + component: 'Icon', + parent: { + component: 'MenuItem', + state: ['hover'] + }, + directives: { + textColor: '--link', + textAuto: 'no-preserve' + } + } + ] +} diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue @@ -16,7 +16,7 @@ <input id="code" v-model="code" - class="form-control" + class="input form-control" > </div> diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue @@ -18,7 +18,7 @@ <input id="code" v-model="code" - class="form-control" + class="input form-control" > </div> diff --git a/src/components/mobile_drawer.style.js b/src/components/mobile_drawer.style.js @@ -0,0 +1,41 @@ +export default { + name: 'MobileDrawer', + selector: '.mobile-drawer', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'Input', + 'PanelHeader', + 'MenuItem', + 'Notification', + 'Alert', + 'UserCard' + ], + defaultRules: [ + { + directives: { + background: '--bg', + backgroundNoCssColor: 'yes' + } + }, + { + component: 'PanelHeader', + parent: { component: 'MobileDrawer' }, + directives: { + background: '--fg', + shadow: [{ + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }] + } + } + ] +} diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue @@ -20,7 +20,7 @@ /> <div v-if="(unreadChatCount && !chatsPinned) || unreadAnnouncementCount" - class="alert-dot" + class="badge -dot -notification" /> </button> <NavigationPins class="pins" /> @@ -37,24 +37,24 @@ /> <div v-if="unseenNotificationsCount" - class="alert-dot" + class="badge -dot -notification" /> </button> </div> </nav> <aside v-if="currentUser" - class="mobile-notifications-drawer" + class="mobile-notifications-drawer mobile-drawer" :class="{ '-closed': !notificationsOpen }" @touchstart.stop="notificationsTouchStart" @touchmove.stop="notificationsTouchMove" > - <div class="mobile-notifications-header"> + <div class="panel-heading mobile-notifications-header"> <span class="title"> {{ $t('notifications.notifications') }} <span v-if="unseenCountBadgeText" - class="badge badge-notification unseen-count" + class="badge -notification unseen-count" >{{ unseenCountBadgeText }}</span> </span> <span class="spacer" /> @@ -123,8 +123,6 @@ <script src="./mobile_nav.js"></script> <style lang="scss"> -@import "../../variables"; - .MobileNav { z-index: var(--ZI_navbar); @@ -137,7 +135,7 @@ box-sizing: border-box; a { - color: var(--topBarLink, $fallback--link); + color: var(--link); } } @@ -165,19 +163,6 @@ display: flex; } - .alert-dot { - border-radius: 100%; - height: 8px; - width: 8px; - position: absolute; - left: calc(50% - 4px); - top: calc(50% - 4px); - margin-left: 6px; - margin-top: -6px; - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); - } - .mobile-notifications-drawer { width: 100%; height: 100vh; @@ -185,13 +170,13 @@ position: fixed; top: 0; left: 0; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); transition-property: transform; transition-duration: 0.25s; transform: translateX(0); z-index: var(--ZI_navbar); -webkit-overflow-scrolling: touch; + background: var(--background); &.-closed { transform: translateX(100%); @@ -208,11 +193,7 @@ height: 50px; line-height: 50px; position: absolute; - color: var(--topBarText); - background-color: $fallback--fg; - background-color: var(--topBar, $fallback--fg); - box-shadow: 0 0 4px rgb(0 0 0 / 60%); - box-shadow: var(--topBarShadow); + box-shadow: var(--shadow); .spacer { flex: 1; @@ -238,10 +219,6 @@ height: calc(100vh - var(--navbar-height)); overflow-x: hidden; overflow-y: scroll; - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); .notifications { padding: 0; diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue @@ -13,8 +13,6 @@ <script src="./mobile_post_status_button.js"></script> <style lang="scss"> -@import "../../variables"; - .MobilePostButton { &.button-default { width: 5em; @@ -25,8 +23,6 @@ right: 1.5em; // TODO: this needs its own color, it has to stand out enough and link color // is not very optimal for this particular use. - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); display: flex; justify-content: center; align-items: center; @@ -42,8 +38,7 @@ svg { font-size: 1.5em; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } diff --git a/src/components/modal/modals.style.js b/src/components/modal/modals.style.js @@ -0,0 +1,9 @@ +export default { + name: 'Modals', + selector: '.modal-view', + lazy: true, + validInnerComponents: [ + 'Panel' + ], + defaultRules: [] +} diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue @@ -12,13 +12,13 @@ <div class="dropdown-menu"> <span v-if="canGrantRole"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleRight(&quot;admin&quot;)" > {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleRight(&quot;moderator&quot;)" > {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }} @@ -31,14 +31,14 @@ </span> <button v-if="canChangeActivationState" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleActivationStatus()" > {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }} </button> <button v-if="canDeleteAccount" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="deleteUserDialog(true)" > {{ $t('user_card.admin_menu.delete_account') }} @@ -50,74 +50,74 @@ /> <span v-if="canUseTagPolicy"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.FORCE_NSFW)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }" /> {{ $t('user_card.admin_menu.force_nsfw') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.STRIP_MEDIA)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }" /> {{ $t('user_card.admin_menu.strip_media') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.FORCE_UNLISTED)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }" /> {{ $t('user_card.admin_menu.force_unlisted') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.SANDBOX)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }" /> {{ $t('user_card.admin_menu.sandbox') }} </button> <button v-if="user.is_local" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }" /> {{ $t('user_card.admin_menu.disable_remote_subscription') }} </button> <button v-if="user.is_local" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }" /> {{ $t('user_card.admin_menu.disable_any_subscription') }} </button> <button v-if="user.is_local" - class="button-default dropdown-item" + class="menu-item dropdown-item menu-item" @click="toggleTag(tags.QUARANTINE)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }" /> {{ $t('user_card.admin_menu.quarantine') }} @@ -166,8 +166,6 @@ <script src="./moderation_tools.js"></script> <style lang="scss"> -@import "../../variables"; - .moderation-tools-popover { height: 100%; diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue @@ -227,6 +227,5 @@ <script src="./mrf_transparency_panel.js"></script> <style lang="scss"> -@import "../../variables"; @import "./mrf_transparency_panel"; </style> diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue @@ -37,7 +37,8 @@ </NavigationEntry> <div v-show="showTimelines" - class="timelines-background" + class="timelines-background menu-item-collapsible" + :class="{ '-expanded': showTimelines }" > <div class="timelines"> <NavigationEntry @@ -57,12 +58,11 @@ > <router-link :title="$t('lists.manage_lists')" - class="extra-button" + class="button-unstyled extra-button" :to="{ name: 'lists' }" @click.stop > <FAIcon - class="extra-button" fixed-width icon="wrench" /> @@ -75,7 +75,8 @@ </NavigationEntry> <div v-show="showLists" - class="timelines-background" + class="timelines-background menu-item-collapsible" + :class="{ '-expanded': showLists }" > <ListsMenuContent :show-pin="editMode || forceEditMode" @@ -102,12 +103,10 @@ <script src="./nav_panel.js"></script> <style lang="scss"> -@import "../../variables"; - .NavPanel { .panel { overflow: hidden; - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); } ul { @@ -116,33 +115,6 @@ padding: 0; } - li { - position: relative; - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - } - - > li { - &:first-child .menu-item { - border-top-right-radius: $fallback--panelRadius; - border-top-right-radius: var(--panelRadius, $fallback--panelRadius); - border-top-left-radius: $fallback--panelRadius; - border-top-left-radius: var(--panelRadius, $fallback--panelRadius); - } - - &:last-child .menu-item { - border-bottom-right-radius: $fallback--panelRadius; - border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); - border-bottom-left-radius: $fallback--panelRadius; - border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); - } - } - - li:last-child { - border: none; - } - .navigation-chevron { margin-left: 0.8em; margin-right: 0.8em; @@ -156,16 +128,6 @@ .timelines-background { padding: 0 0 0 0.6em; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - } - - .timelines { - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); } .nav-panel-heading { diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue @@ -1,7 +1,6 @@ <template> <OptionalRouterLink v-slot="{ isActive, href, navigate } = {}" - ass="ass" :to="routeTo" > <li @@ -11,7 +10,7 @@ > <component :is="routeTo ? 'a' : 'button'" - class="main-link button-unstyled" + class="main-link" :href="href" @click="navigate" > @@ -35,7 +34,7 @@ <slot /> <div v-if="item.badgeGetter && getters[item.badgeGetter]" - class="badge badge-notification" + class="badge -notification" > {{ getters[item.badgeGetter] }} </div> @@ -63,73 +62,53 @@ <script src="./navigation_entry.js"></script> <style lang="scss"> -@import "../../variables"; +.NavigationEntry.menu-item { + --__line-height: 2.5em; + --__horizontal-gap: 0.5em; + --__vertical-gap: 0.4em; -.NavigationEntry { + padding: 0; display: flex; - box-sizing: border-box; align-items: baseline; - height: 3.5em; - line-height: 3.5em; - padding: 0 1em; - width: 100%; - color: $fallback--link; - color: var(--link, $fallback--link); - .timelines-chevron { - margin-right: 0; + &[aria-expanded] { + padding-right: var(--__horizontal-gap); } .main-link { + line-height: var(--__line-height); + box-sizing: border-box; flex: 1; + padding: var(--__vertical-gap) var(--__horizontal-gap); } .menu-icon { - margin-right: 0.8em; + line-height: var(--__line-height); + padding: 0; + width: var(--__line-height); + margin-right: var(--__horizontal-gap); + } + + .timelines-chevron { + line-height: var(--__line-height); + padding: 0; + width: var(--__line-height); + margin-right: 0; } .extra-button { - width: 3em; + line-height: var(--__line-height); + padding: 0; + width: var(--__line-height); text-align: center; &:last-child { - margin-right: -0.8em; - } - } - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - - .menu-icon { - --icon: var(--text, $fallback--icon); + margin-right: calc(-1 * var(--__horizontal-gap)); } } - &.-active { - font-weight: bolder; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - - .menu-icon { - --icon: var(--text, $fallback--icon); - } - - &:hover { - text-decoration: underline; - } + .badge { + margin: 0 var(--__horizontal-gap); } } </style> diff --git a/src/components/navigation/navigation_pins.vue b/src/components/navigation/navigation_pins.vue @@ -3,7 +3,8 @@ <router-link v-for="item in pinnedList" :key="item.name" - class="pinned-item" + class="button-unstyled pinned-item" + active-class="toggled" :to="getRouteTo(item)" :title="item.labelRaw || $t(item.label)" > @@ -18,7 +19,7 @@ >{{ item.iconLetter }}</span> <div v-if="item.badgeGetter && getters[item.badgeGetter]" - class="alert-dot" + class="badge -dot -notification" /> </router-link> </span> @@ -27,25 +28,12 @@ <script src="./navigation_pins.js"></script> <style lang="scss"> -@import "../../variables"; - .NavigationPins { display: flex; flex-wrap: wrap; overflow: hidden; height: 100%; - .alert-dot { - border-radius: 100%; - height: 0.5em; - width: 0.5em; - position: absolute; - right: calc(50% - 0.75em); - top: calc(50% - 0.5em); - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); - } - .pinned-item { position: relative; flex: 1 0 3em; @@ -60,15 +48,8 @@ margin: 0; } - &.router-link-active { - color: $fallback--text; - color: var(--panelText, $fallback--text); + &.toggled { border-bottom: 4px solid; - - & .svg-inline--fa, - & .iconLetter { - color: inherit; - } } } } diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss @@ -1,13 +1,15 @@ -@import "../../variables"; - // TODO Copypaste from Status, should unify it somehow .Notification { border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); word-wrap: break-word; word-break: break-word; + &.Status { + /* stylelint-disable-next-line declaration-no-important */ + background-color: transparent !important; + } + --emoji-size: 14px; &:hover { @@ -71,28 +73,22 @@ } &.-type--repeat .type-icon { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--cGreen); } &.-type--follow .type-icon { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } &.-type--follow-request .type-icon { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } &.-type--like .type-icon { - color: orange; - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } &.-type--move .type-icon { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } } diff --git a/src/components/notification/notification.style.js b/src/components/notification/notification.style.js @@ -0,0 +1,17 @@ +export default { + name: 'Notification', + selector: '.Notification', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', + 'Avatar', + 'Attachment' + ], + defaultRules: [] +} diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -155,7 +155,7 @@ <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" - class="timeago-link faint-link" + class="timeago-link faint" > <Timeago :time="notification.created_at" @@ -247,7 +247,6 @@ /> <template v-else> <StatusContent - :class="{ faint: !statusExpanded }" :compact="!statusExpanded" :status="notification.status" /> diff --git a/src/components/notifications/notification_filters.vue b/src/components/notifications/notification_filters.vue @@ -8,65 +8,65 @@ <template #content> <div class="dropdown-menu"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('likes')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.likes }" />{{ $t('settings.notification_visibility_likes') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('repeats')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.repeats }" />{{ $t('settings.notification_visibility_repeats') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('follows')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.follows }" />{{ $t('settings.notification_visibility_follows') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('mentions')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.mentions }" />{{ $t('settings.notification_visibility_mentions') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('emojiReactions')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.emojiReactions }" />{{ $t('settings.notification_visibility_emoji_reactions') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('moves')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.moves }" />{{ $t('settings.notification_visibility_moves') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleNotificationFilter('polls')" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': filters.polls }" />{{ $t('settings.notification_visibility_polls') }} </button> diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Notifications { &:not(.minimal) { // a bit of a hack to allow scrolling below notifications @@ -7,8 +5,7 @@ } .loadmore-error { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } .notification { @@ -25,7 +22,7 @@ &.unseen { .notification-overlay { - background-image: linear-gradient(135deg, var(--badgeNotification, $fallback--cRed) 4px, transparent 10px); + background-image: linear-gradient(135deg, var(--badgeNotification) 4px, transparent 10px); } } } @@ -35,6 +32,11 @@ .notification { box-sizing: border-box; + /* TODO cleanup this */ + .Status { + flex: 1; + } + &:hover .animated.Avatar { canvas { display: none; @@ -60,24 +62,17 @@ width: 32px; height: 32px; } - - .faint { - --link: var(--faintLink); - --text: var(--faint); - } } .follow-request-accept { &:hover { - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } .follow-request-reject { &:hover { - color: $fallback--cRed; - color: var(--cRed, $fallback--cRed); + color: var(--cRed); } } @@ -97,11 +92,6 @@ } } - /* TODO cleanup this */ - .Status { - flex: 1; - } - time { white-space: nowrap; } diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue @@ -18,7 +18,7 @@ {{ $t('notifications.notifications') }} <span v-if="unseenCountBadgeText" - class="badge badge-notification unseen-count" + class="badge -notification unseen-count" >{{ unseenCountBadgeText }}</span> </div> <div @@ -85,7 +85,7 @@ </div> <button v-else-if="!loading" - class="button-unstyled -link -fullwidth" + class="button-unstyled -link text-center" @click.prevent="fetchOlderNotifications()" > <div class="new-status-notification text-center"> diff --git a/src/components/opacity_input/opacity_input.vue b/src/components/opacity_input/opacity_input.vue @@ -18,7 +18,7 @@ /> <input :id="name" - class="input-number" + class="input input-number" type="number" :value="modelValue || fallback" :disabled="!present || disabled" diff --git a/src/components/panel.style.js b/src/components/panel.style.js @@ -0,0 +1,41 @@ +export default { + name: 'Panel', + selector: '.panel', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'Input', + 'PanelHeader', + 'MenuItem', + 'Post', + 'Notification', + 'Alert', + 'UserCard', + 'Chat', + 'Attachment', + 'Tab', + 'ListItem' + ], + defaultRules: [ + { + directives: { + backgroundNoCssColor: 'yes', + background: '--bg', + roundness: 3, + blur: '5px', + shadow: [{ + x: 1, + y: 1, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }] + } + } + ] +} diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js @@ -0,0 +1,24 @@ +export default { + name: 'PanelHeader', + selector: '.panel-heading', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Button', + 'ButtonUnstyled', + 'Badge', + 'Alert', + 'Avatar' + ], + defaultRules: [ + { + component: 'PanelHeader', + directives: { + backgroundNoCssColor: 'yes', + background: '--fg', + shadow: [] + } + } + ] +} diff --git a/src/components/panel_loading/panel_loading.vue b/src/components/panel_loading/panel_loading.vue @@ -23,22 +23,18 @@ export default {} </script> <style lang="scss"> -@import "src/variables"; - .panel-loading { display: flex; height: 100%; align-items: center; justify-content: center; font-size: 2em; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); .loading-text svg { line-height: 0; vertical-align: middle; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } </style> diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue @@ -30,7 +30,7 @@ <div v-else> <p v-if="passwordResetRequested" - class="password-reset-required error" + class="alert password-reset-required error" > {{ $t('password_reset.password_reset_required') }} </p> @@ -43,7 +43,7 @@ v-model="user.email" :disabled="isPending" :placeholder="$t('password_reset.placeholder')" - class="form-control" + class="input form-control" type="input" > </div> @@ -77,8 +77,6 @@ <script src="./password_reset.js"></script> <style lang="scss"> -@import "../../variables"; - .password-reset-form { display: flex; flex-direction: column; @@ -117,11 +115,6 @@ margin: 0.3em 0 1em; } - .password-reset-required { - background-color: var(--alertError, $fallback--alertError); - padding: 10px 0; - } - .notice-dismissible { padding-right: 2rem; } diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue @@ -37,12 +37,14 @@ :role="poll.multiple ? 'checkbox' : 'radio'" :aria-labelledby="`option-vote-${randomSeed}-${index}`" :aria-checked="choices[index]" + class="input unstyled" @click="activateOption(index)" > + <!-- TODO: USE CHECKBOX --> <input v-if="poll.multiple" type="checkbox" - class="poll-checkbox" + class="input -checkbox poll-checkbox" :disabled="loading" :value="index" > @@ -51,6 +53,7 @@ type="radio" :disabled="loading" :value="index" + class="input -radio" > <label class="option-vote"> <RichContent @@ -103,8 +106,6 @@ <script src="./poll.js"></script> <style lang="scss"> -@import "../../variables"; - .poll { .votes { display: flex; @@ -114,6 +115,10 @@ .poll-option { margin: 0.75em 0.5em; + + .input { + line-height: inherit; + } } .option-result { @@ -121,8 +126,7 @@ display: flex; flex-direction: row; position: relative; - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); + color: var(--textLight); } .option-result-label { @@ -141,12 +145,7 @@ .result-fill { height: 100%; position: absolute; - color: $fallback--text; - color: var(--pollText, $fallback--text); - background-color: $fallback--lightBg; - background-color: var(--poll, $fallback--lightBg); - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); top: 0; left: 0; transition: width 0.5s; diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue @@ -13,7 +13,7 @@ :id="`poll-${index}`" v-model="options[index]" size="1" - class="poll-option-input" + class="input poll-option-input" type="text" :placeholder="$t('polls.option')" :maxlength="maxLength" @@ -67,7 +67,7 @@ <input v-model="expiryAmount" type="number" - class="expiry-amount hide-number-spinner" + class="input expiry-amount hide-number-spinner" :min="minExpirationInCurrentUnit" :max="maxExpirationInCurrentUnit" @change="expiryAmountChange" @@ -95,8 +95,6 @@ <script src="./poll_form.js"></script> <style lang="scss"> -@import "../../variables"; - .poll-form { display: flex; flex-direction: column; diff --git a/src/components/poll/poll_graph.style.js b/src/components/poll/poll_graph.style.js @@ -0,0 +1,12 @@ +export default { + name: 'PollGraph', + selector: '.result-fill', + defaultRules: [ + { + directives: { + background: '--accent', + opacity: 0.5 + } + } + ] +} diff --git a/src/components/popover.style.js b/src/components/popover.style.js @@ -0,0 +1,36 @@ +export default { + name: 'Popover', + selector: '.popover', + lazy: true, + variants: { + modal: '.modal' + }, + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'Input', + 'MenuItem', + 'Post', + 'UserCard' + ], + defaultRules: [ + { + directives: { + background: '--bg', + blur: '10px', + shadow: [{ + x: 2, + y: 2, + blur: 3, + spread: 0, + color: '#000000', + alpha: 0.5 + }] + } + } + ] +} diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue @@ -42,8 +42,6 @@ <script src="./popover.js" /> <style lang="scss"> -@import "../../variables"; - .popover-trigger-button { display: inline-block; } @@ -53,81 +51,54 @@ position: fixed; min-width: 0; max-width: calc(100vw - 20px); - box-shadow: 2px 2px 3px rgb(0 0 0 / 50%); - box-shadow: var(--popupShadow); + box-shadow: var(--shadow); } .popover-default { &::after { content: ""; position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 3; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + top: -1px; + bottom: -1px; + left: -1px; + right: -1px; + z-index: -1px; + box-shadow: var(--shadow); pointer-events: none; } - border-radius: $fallback--btnRadius; - border-radius: var(--btnRadius, $fallback--btnRadius); - background-color: $fallback--bg; - background-color: var(--popover, $fallback--bg); - color: $fallback--text; - color: var(--popoverText, $fallback--text); - - --faint: var(--popoverFaintText, $fallback--faint); - --faintLink: var(--popoverFaintLink, $fallback--faint); - --lightText: var(--popoverLightText, $fallback--lightText); - --postLink: var(--popoverPostLink, $fallback--link); - --postFaintLink: var(--popoverPostFaintLink, $fallback--link); - --icon: var(--popoverIcon, $fallback--icon); + border-radius: var(--roundness); + border-color: var(--border); + border-style: solid; + border-width: 1px; + background-color: var(--background); } .dropdown-menu { display: block; - padding: 0.5rem 0; + padding: 0; font-size: 1em; text-align: left; list-style: none; max-width: 100vw; z-index: var(--ZI_popover_override, var(--ZI_popovers)); white-space: nowrap; + background-color: var(--background); .dropdown-divider { height: 0; margin: 0.5rem 0; overflow: hidden; - border-top: 1px solid $fallback--border; - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); } .dropdown-item { - line-height: 21px; - overflow: hidden; - display: block; - padding: 0.5em 0.75em; - clear: both; - font-weight: 400; - text-align: inherit; - white-space: nowrap; border: none; - border-radius: 0; - background-color: transparent; - box-shadow: none; - width: 100%; - height: 100%; - box-sizing: border-box; - - --btnText: var(--popoverText, $fallback--text); &-icon { svg { - width: 22px; - margin-right: 0.75rem; - color: var(--menuPopoverIcon, $fallback--icon); + width: var(--__line-height); + margin-right: var(--__horizontal-gap); } } @@ -138,40 +109,18 @@ } } - &:active, - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - box-shadow: none; - - --btnText: var(--selectedMenuPopoverText, $fallback--link); - --faint: var(--selectedMenuPopoverFaintText, $fallback--faint); - --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); - --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - - svg { - color: var(--selectedMenuPopoverIcon, $fallback--icon); - - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - } - } - .menu-checkbox { display: inline-block; vertical-align: middle; - min-width: 22px; - max-width: 22px; - min-height: 22px; - max-height: 22px; - line-height: 22px; + min-width: calc(var(--__line-height) + 1px); + max-width: calc(var(--__line-height) + 1px); + min-height: calc(var(--__line-height) + 1px); + max-height: calc(var(--__line-height) + 1px); + line-height: var(--__line-height); text-align: center; border-radius: 0; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); - margin-right: 0.75em; + box-shadow: var(--shadow); + margin-right: var(--__horizontal-gap); &.menu-checkbox-checked::after { font-size: 1.25em; @@ -188,30 +137,5 @@ } } } - - .button-default.dropdown-item { - &, - i[class*="icon-"] { - color: $fallback--text; - color: var(--btnText, $fallback--text); - } - - &:active { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuPopoverText, $fallback--link); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledText, $fallback--text); - } - } } </style> diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue @@ -161,7 +161,7 @@ v-model="newStatus.spoilerText" enable-emoji-picker :suggest="emojiSuggestor" - class="form-control" + class="input form-control" > <template #default="inputProps"> <input @@ -171,7 +171,7 @@ :disabled="posting && !optimisticPosting" v-bind="propsToNative(inputProps)" size="1" - class="form-post-subject" + class="input form-post-subject" > </template> </EmojiInput> @@ -180,7 +180,7 @@ v-model="newStatus.status" :suggest="emojiUserSuggestor" :placement="emojiPickerPlacement" - class="form-control main-input" + class="input form-control main-input" enable-emoji-picker hide-emoji-button :newline-on-ctrl-enter="submitOnEnter" @@ -198,7 +198,7 @@ rows="1" cols="1" :disabled="posting && !optimisticPosting" - class="form-post-body" + class="input form-post-body" :class="{ 'scrollable-form': !!maxHeight }" v-bind="propsToNative(inputProps)" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" @@ -237,7 +237,7 @@ <Select id="post-content-type" v-model="newStatus.contentType" - class="form-control" + class="input form-control" :attrs="{ 'aria-label': $t('post_status.content_type_selection') }" > <option @@ -375,8 +375,6 @@ <script src="./post_status_form.js"></script> <style lang="scss"> -@import "../../variables"; - .post-status-form { position: relative; @@ -437,15 +435,12 @@ .preview-error { font-style: italic; - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } .preview-status { - border: 1px solid $fallback--border; - border: 1px solid var(--border, $fallback--border); - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border: 1px solid var(--border); + border-radius: var(--roundness); padding: 0.5em; margin: 0; } @@ -456,8 +451,7 @@ .text-format { .only-format { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } } @@ -503,31 +497,6 @@ padding: 0 0.1em; display: flex; align-items: center; - - &.selected, - &:hover { - // needs to be specific to override icon default color - svg, - i, - label { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } - } - - &.disabled { - svg, - i { - cursor: not-allowed; - color: $fallback--icon; - color: var(--btnDisabledText, $fallback--icon); - - &:hover { - color: $fallback--icon; - color: var(--btnDisabledText, $fallback--icon); - } - } - } } .error { @@ -580,7 +549,7 @@ line-height: 1.85; } - .form-post-body { + .input.form-post-body { // TODO: make a resizable textarea component? box-sizing: content-box; // needed for easier computation of dynamic size overflow: hidden; @@ -591,6 +560,7 @@ height: calc(var(--post-line-height) * 1em); min-height: calc(var(--post-line-height) * 1em); resize: none; + background: transparent; &.scrollable-form { overflow-y: auto; @@ -609,8 +579,7 @@ margin: 0 0.5em; &.error { - color: $fallback--cRed; - color: var(--cRed, $fallback--cRed); + color: var(--cRed); } } @@ -633,14 +602,10 @@ align-items: center; justify-content: center; opacity: 0.6; - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - border: 2px dashed $fallback--text; - border: 2px dashed var(--text, $fallback--text); + color: var(--text); + background-color: var(--bg); + border-radius: var(--roundness); + border: 2px dashed var(--text); } } </style> diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js @@ -63,6 +63,13 @@ const QuickFilterSettings = { const value = !this.muteBotStatuses this.$store.dispatch('setOption', { name: 'muteBotStatuses', value }) } + }, + muteSensitiveStatuses: { + get () { return this.mergedConfig.muteSensitiveStatuses }, + set () { + const value = !this.muteSensitiveStatuses + this.$store.dispatch('setOption', { name: 'muteSensitiveStatuses', value }) + } } } } diff --git a/src/components/quick_filter_settings/quick_filter_settings.vue b/src/components/quick_filter_settings/quick_filter_settings.vue @@ -16,39 +16,39 @@ > <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilityAll" role="menuitemradio" @click="replyVisibilityAll = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilityAll }" :aria-hidden="true" />{{ $t('settings.reply_visibility_all') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilityFollowing" role="menuitemradio" @click="replyVisibilityFollowing = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilityFollowing }" :aria-hidden="true" />{{ $t('settings.reply_visibility_following_short') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilitySelf" role="menuitemradio" @click="replyVisibilitySelf = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilitySelf }" :aria-hidden="true" />{{ $t('settings.reply_visibility_self_short') }} @@ -60,43 +60,55 @@ /> </div> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="muteBotStatuses" @click="muteBotStatuses = !muteBotStatuses" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': muteBotStatuses }" :aria-hidden="true" />{{ $t('settings.mute_bot_posts') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" + role="menuitemcheckbox" + :aria-checked="muteSensitiveStatuses" + @click="muteSensitiveStatuses = !muteSensitiveStatuses" + > + <span + class="input menu-checkbox" + :class="{ 'menu-checkbox-checked': muteSensitiveStatuses }" + :aria-hidden="true" + />{{ $t('settings.mute_sensitive_posts') }} + </button> + <button + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="hideMedia" @click="hideMedia = !hideMedia" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hideMedia }" :aria-hidden="true" />{{ $t('settings.hide_media_previews') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="hideMutedPosts" @click="hideMutedPosts = !hideMutedPosts" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hideMutedPosts }" :aria-hidden="true" />{{ $t('settings.hide_all_muted_posts') }} </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click="openTab('filtering')" > diff --git a/src/components/quick_view_settings/quick_view_settings.js b/src/components/quick_view_settings/quick_view_settings.js @@ -61,6 +61,13 @@ const QuickViewSettings = { const value = !this.muteBotStatuses this.$store.dispatch('setOption', { name: 'muteBotStatuses', value }) } + }, + muteSensitiveStatuses: { + get () { return this.mergedConfig.muteSensitiveStatuses }, + set () { + const value = !this.muteSensitiveStatuses + this.$store.dispatch('setOption', { name: 'muteSensitiveStatuses', value }) + } } } } diff --git a/src/components/quick_view_settings/quick_view_settings.vue b/src/components/quick_view_settings/quick_view_settings.vue @@ -12,13 +12,13 @@ > <div role="group"> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="conversationDisplay === 'tree'" role="menuitemradio" @click="conversationDisplay = 'tree'" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :aria-hidden="true" :class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }" /><FAIcon @@ -27,13 +27,13 @@ /> {{ $t('settings.conversation_display_tree_quick') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="conversationDisplay === 'linear'" role="menuitemradio" @click="conversationDisplay = 'linear'" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }" :aria-hidden="true" /><FAIcon @@ -47,45 +47,45 @@ class="dropdown-divider" /> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="showUserAvatars" @click="showUserAvatars = !showUserAvatars" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': showUserAvatars }" :aria-hidden="true" />{{ $t('settings.mention_link_show_avatar_quick') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="autoUpdate" @click="autoUpdate = !autoUpdate" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': autoUpdate }" :aria-hidden="true" />{{ $t('settings.auto_update') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="collapseWithSubjects" @click="collapseWithSubjects = !collapseWithSubjects" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': collapseWithSubjects }" :aria-hidden="true" />{{ $t('settings.collapse_subject') }} </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click="openTab('general')" > diff --git a/src/components/range_input/range_input.vue b/src/components/range_input/range_input.vue @@ -14,7 +14,7 @@ v-if="typeof fallback !== 'undefined'" :id="name + '-o'" :aria-labelledby="name + '-label'" - class="opt visible-for-screenreader-only" + class="input -checkbox opt visible-for-screenreader-only" type="checkbox" :checked="present" @change="$emit('update:modelValue', !present ? fallback : undefined)" @@ -27,7 +27,7 @@ /> <input :id="name" - class="input-number" + class="input input-number" type="range" :value="modelValue || fallback" :disabled="!present || disabled" @@ -38,7 +38,7 @@ > <input :id="name + '-numeric'" - class="input-number" + class="input input-number" type="number" :aria-labelledby="name + '-label'" :value="modelValue || fallback" diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue @@ -41,7 +41,6 @@ <script src="./react_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .ReactButton { @@ -58,7 +57,7 @@ height: 1px; width: 100%; margin: 0.5em; - background-color: var(--border, $fallback--border); + background-color: var(--border); } .reaction-picker { @@ -99,11 +98,6 @@ padding: 10px; margin: -10px; - &:hover .svg-inline--fa { - color: $fallback--text; - color: var(--text, $fallback--text); - } - @include unfocused-style { .focus-marker { visibility: hidden; diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue @@ -25,7 +25,7 @@ id="sign-up-username" v-model.trim="v$.user.username.$model" :disabled="isPending" - class="form-control" + class="input form-control" :aria-required="true" :placeholder="$t('registration.username_placeholder')" > @@ -53,7 +53,7 @@ id="sign-up-fullname" v-model.trim="v$.user.fullname.$model" :disabled="isPending" - class="form-control" + class="input form-control" :aria-required="true" :placeholder="$t('registration.fullname_placeholder')" > @@ -81,7 +81,7 @@ id="email" v-model="v$.user.email.$model" :disabled="isPending" - class="form-control" + class="input form-control" type="email" :aria-required="accountActivationRequired" > @@ -106,7 +106,7 @@ id="bio" v-model="user.bio" :disabled="isPending" - class="form-control" + class="input form-control" :placeholder="bioPlaceholder" /> </div> @@ -123,7 +123,7 @@ id="sign-up-password" v-model="user.password" :disabled="isPending" - class="form-control" + class="input form-control" type="password" :aria-required="true" > @@ -151,7 +151,7 @@ id="sign-up-password-confirmation" v-model="user.confirm" :disabled="isPending" - class="form-control" + class="input form-control" type="password" :aria-required="true" > @@ -184,7 +184,7 @@ id="sign-up-birthday" v-model="user.birthday" :disabled="isPending" - class="form-control" + class="input form-control" type="date" :max="birthdayRequired ? birthdayMinAttr : undefined" :aria-required="birthdayRequired" @@ -229,7 +229,7 @@ id="reason" v-model="user.reason" :disabled="isPending" - class="form-control" + class="input form-control" :placeholder="reasonPlaceholder" /> </div> @@ -256,7 +256,7 @@ id="captcha-answer" v-model="captcha.solution" :disabled="isPending" - class="form-control" + class="input form-control" type="text" autocomplete="off" autocorrect="off" @@ -275,7 +275,7 @@ id="token" v-model="token" disabled="true" - class="form-control" + class="input form-control" type="text" > </div> @@ -320,9 +320,6 @@ <script src="./registration.js"></script> <style lang="scss"> -@import "../../variables"; -$validations-cRed: #f04124; - .registration-form { display: flex; flex-direction: column; @@ -369,8 +366,7 @@ $validations-cRed: #f04124; } .form-group--error .form--label { - color: $validations-cRed; - color: var(--cRed, $validations-cRed); + color: var(--cRed); } .form-error { diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue @@ -59,7 +59,6 @@ <script src="./reply_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .ReplyButton { @@ -78,8 +77,7 @@ .interactive { &:hover .svg-inline--fa, &.-active .svg-inline--fa { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + color: var(--cBlue); } @include unfocused-style { diff --git a/src/components/report/report.scss b/src/components/report/report.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Report { .report-content { margin: 0.5em 0 1em; @@ -10,12 +8,8 @@ } .reported-status { - border: 1px solid $fallback--faint; - border-color: var(--faint, $fallback--faint); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); - color: $fallback--text; - color: var(--text, $fallback--text); + border: 1px solid var(--border); + border-radius: var(--roundness); display: block; padding: 0.5em; margin: 0.5em 0; diff --git a/src/components/report/report.vue b/src/components/report/report.vue @@ -17,7 +17,7 @@ <Select :id="report-state" v-model="state" - class="form-control" + class="input form-control" > <option v-for="state in ['open', 'closed', 'resolved']" diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue @@ -84,7 +84,6 @@ <script src="./retweet_button.js"></script> <style lang="scss"> -@import "../../variables"; @import "../../mixins"; .RetweetButton { @@ -107,8 +106,7 @@ &:hover .svg-inline--fa, &.-repeated .svg-inline--fa { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--cGreen); } @include unfocused-style { diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx @@ -79,6 +79,12 @@ export default { required: false, type: Boolean, default: false + }, + // Faint style (for notifs) + faint: { + required: false, + type: Boolean, + default: false } }, // NEVER EVER TOUCH DATA INSIDE RENDER @@ -277,7 +283,7 @@ export default { // DO NOT USE SLOTS they cause a re-render feedback loop here. // slots updated -> rerender -> emit -> update up the tree -> rerender -> ... // at least until vue3? - const result = <span class="RichContent"> + const result = <span class={['RichContent', this.faint ? '-faint' : '']}> { pass2 } </span> diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss @@ -1,10 +1,19 @@ -@import "../../variables"; - .RichContent { + font-family: var(--font); + + &.-faint { + /* stylelint-disable declaration-no-important */ + --text: var(--textFaint) !important; + --link: var(--linkFaint) !important; + --funtextGreentext: var(--funtextGreentextFaint) !important; + --funtextCyantext: var(--funtextCyantextFaint) !important; + /* stylelint-enable declaration-no-important */ + } + blockquote { margin: 0.2em 0 0.2em 0.2em; font-style: italic; - border-left: 0.2em solid var(--faint, $fallback--faint); + border-left: 0.2em solid var(--textFaint); padding-left: 1em; } @@ -17,7 +26,7 @@ kbd, var, pre { - font-family: var(--postCodeFont, monospace); + font-family: var(--monoFont); } p { @@ -65,4 +74,17 @@ vertical-align: middle; object-fit: contain; } + + .greentext { + color: var(--funtextGreentext); + } + + .cyantext { + color: var(--funtextCyantext); + } +} + +a .RichContent { + /* stylelint-disable-next-line declaration-no-important */ + color: var(--link) !important; } diff --git a/src/components/rich_content/rich_content.style.js b/src/components/rich_content/rich_content.style.js @@ -0,0 +1,18 @@ +export default { + name: 'RichContent', + selector: '.RichContent', + validInnerComponents: [ + 'Text', + 'FunText', + 'Link' + ], + defaultRules: [ + { + directives: { + '--font': 'generic | inherit', + '--monoFont': 'generic | monospace', + textNoCssColor: 'yes' + } + } + ] +} diff --git a/src/components/root.style.js b/src/components/root.style.js @@ -0,0 +1,44 @@ +export default { + name: 'Root', + selector: ':root', + validInnerComponents: [ + 'Underlay', + 'Modals', + 'Popover', + 'TopBar', + 'Scrollbar', + 'ScrollbarElement', + 'MobileDrawer', + 'Alert', + 'Button' // mobile post button + ], + defaultRules: [ + { + directives: { + // These are here just to establish order, + // themes should override those + '--bg': 'color | #121a24', + '--fg': 'color | #182230', + '--text': 'color | #b9b9ba', + '--link': 'color | #d8a070', + '--accent': 'color | #d8a070', + '--cRed': 'color | #FF0000', + '--cBlue': 'color | #0095ff', + '--cGreen': 'color | #0fa00f', + '--cOrange': 'color | #ffa500', + + // Fonts + '--font': 'generic | sans-serif', + '--monoFont': 'generic | monospace', + + // Fallback no-background-image color + // (also useful in some other places like scrollbars) + '--wallpaper': 'color | --bg, -2', + + // Selection colors + '--selectionBackground': 'color | --accent', + '--selectionText': 'color | $textColor(--accent, --text)' + } + } + ] +} diff --git a/src/components/scope_selector/scope_selector.js b/src/components/scope_selector/scope_selector.js @@ -44,10 +44,10 @@ const ScopeSelector = { }, css () { return { - public: { selected: this.currentScope === 'public' }, - unlisted: { selected: this.currentScope === 'unlisted' }, - private: { selected: this.currentScope === 'private' }, - direct: { selected: this.currentScope === 'direct' } + public: { toggled: this.currentScope === 'public' }, + unlisted: { toggled: this.currentScope === 'unlisted' }, + private: { toggled: this.currentScope === 'private' }, + direct: { toggled: this.currentScope === 'direct' } } } }, diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue @@ -64,8 +64,6 @@ <script src="./scope_selector.js"></script> <style lang="scss"> -@import "../../variables"; - .ScopeSelector { .scope { display: inline-block; @@ -73,11 +71,6 @@ min-width: 1.3em; min-height: 1.3em; text-align: center; - - &.selected svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } } } </style> diff --git a/src/components/scrollbar.style.js b/src/components/scrollbar.style.js @@ -0,0 +1,11 @@ +export default { + name: 'Scrollbar', + selector: '::-webkit-scrollbar', + defaultRules: [ + { + directives: { + background: '--wallpaper' + } + } + ] +} diff --git a/src/components/scrollbar_element.style.js b/src/components/scrollbar_element.style.js @@ -0,0 +1,101 @@ +const border = (top, shadow) => ({ + x: 0, + y: top ? 1 : -1, + blur: 0, + spread: 0, + color: shadow ? '#000000' : '#FFFFFF', + alpha: 0.2, + inset: true +}) + +const buttonInsetFakeBorders = [border(true, false), border(false, true)] +const inputInsetFakeBorders = [border(true, true), border(false, false)] +const buttonOuterShadow = { + x: 0, + y: 0, + blur: 2, + spread: 0, + color: '#000000', + alpha: 1 +} + +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--text', + alpha: 1 +} + +export default { + name: 'ScrollbarElement', + selector: '::-webkit-scrollbar-button', + states: { + pressed: ':active', + hover: ':hover:not(:disabled)', + disabled: ':disabled' + }, + validInnerComponents: [ + 'Text' + ], + defaultRules: [ + { + directives: { + background: '--fg', + shadow: [buttonOuterShadow, ...buttonInsetFakeBorders], + roundness: 3 + } + }, + { + state: ['hover'], + directives: { + shadow: [hoverGlow, ...buttonInsetFakeBorders] + } + }, + { + state: ['pressed'], + directives: { + shadow: [buttonOuterShadow, ...inputInsetFakeBorders] + } + }, + { + state: ['hover', 'pressed'], + directives: { + shadow: [hoverGlow, ...inputInsetFakeBorders] + } + }, + { + state: ['toggled'], + directives: { + background: '--accent,-24.2', + shadow: [buttonOuterShadow, ...inputInsetFakeBorders] + } + }, + { + state: ['toggled', 'hover'], + directives: { + background: '--accent,-24.2', + shadow: [hoverGlow, ...inputInsetFakeBorders] + } + }, + { + state: ['disabled'], + directives: { + background: '$blend(--inheritedBackground, 0.25, --parent)', + shadow: [...buttonInsetFakeBorders] + } + }, + { + component: 'Text', + parent: { + component: 'Button', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + } + ] +} diff --git a/src/components/search/search.vue b/src/components/search/search.vue @@ -1,15 +1,15 @@ <template> - <div class="panel panel-default"> + <div class="Search panel panel-default"> <div class="panel-heading"> <div class="title"> {{ $t('nav.search') }} </div> </div> - <div class="search-input-container"> + <div class="panel-body search-input-container"> <input ref="searchInput" v-model="searchTerm" - class="search-input" + class="input search-input" :placeholder="$t('nav.search')" @keyup.enter="newQuery(searchTerm)" > @@ -23,7 +23,7 @@ </div> <div v-if="loading && statusesOffset == 0" - class="text-center loading-icon" + class="panel-body text-center loading-icon" > <FAIcon icon="circle-notch" @@ -67,7 +67,7 @@ /> <button v-if="!loading && loaded && lastStatusFetchCount > 0" - class="more-statuses-button button-unstyled -link -fullwidth" + class="more-statuses-button button-unstyled -link" @click.prevent="search(searchTerm, 'statuses')" > <div class="new-status-notification text-center"> @@ -148,11 +148,8 @@ <script src="./search.js"></script> <style lang="scss"> -@import "../../variables"; - .search-result-heading { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--faint); padding: 0.75rem; text-align: center; } @@ -171,17 +168,7 @@ .search-result { box-sizing: border-box; border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); -} - -.search-result-footer { - border-width: 1px 0 0; - border-style: solid; - border-color: var(--border, $fallback--border); - padding: 10px; - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); + border-color: var(--border); } .search-input-container { @@ -212,8 +199,7 @@ .hashtag { flex: 1 1 auto; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -226,14 +212,14 @@ line-height: 2.25rem; font-weight: 500; text-align: center; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } } .more-statuses-button { height: 3.5em; line-height: 3.5em; + width: 100%; } </style> diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue @@ -22,7 +22,7 @@ id="search-bar-input" ref="searchInput" v-model="searchTerm" - class="search-bar-input" + class="input search-bar-input" :placeholder="$t('nav.search')" type="text" @keyup.enter="find(searchTerm)" @@ -60,8 +60,6 @@ <script src="./search_bar.js"></script> <style lang="scss"> -@import "../../variables"; - .SearchBar { display: inline-flex; align-items: baseline; @@ -86,8 +84,7 @@ } .cancel-icon { - color: $fallback--text; - color: var(--btnTopBarText, $fallback--text); + color: var(--text); } } diff --git a/src/components/select/select.vue b/src/components/select/select.vue @@ -22,8 +22,6 @@ <script src="./select.js"> </script> <style lang="scss"> -@import "../../variables"; - /* TODO fix order of styles */ label.Select { padding: 0; @@ -32,12 +30,10 @@ label.Select { appearance: none; background: transparent; border: none; - color: $fallback--text; - color: var(--inputText, --text, $fallback--text); + color: var(--text); margin: 0; padding: 0 2em 0 0.2em; - font-family: sans-serif; - font-family: var(--inputFont, sans-serif); + font-family: var(--font); font-size: 1em; width: 100%; z-index: 1; @@ -52,8 +48,7 @@ label.Select { right: 5px; height: 100%; width: 0.875em; - color: $fallback--text; - color: var(--inputText, $fallback--text); + font-family: var(--font); line-height: 2; z-index: 0; pointer-events: none; diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue @@ -23,16 +23,19 @@ <List :items="items" :get-key="getKey" + :get-class="item => isSelected(item) ? '-active' : ''" > <template #item="{item}"> <div class="selectable-list-item-inner" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }" + @click.stop="toggle(!isSelected(item), item)" > <div class="selectable-list-checkbox-wrapper"> <Checkbox :model-value="isSelected(item)" @update:model-value="checked => toggle(checked, item)" + @click.stop /> </div> <slot @@ -51,9 +54,11 @@ <script src="./selectable_list.js"></script> <style lang="scss"> -@import "../../variables"; - .selectable-list { + --__line-height: 1.5em; + --__horizontal-gap: 0.75em; + --__vertical-gap: 0.5em; + &-item-inner { display: flex; align-items: center; @@ -63,24 +68,12 @@ } } - &-item-selected-inner { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: var(--selectedMenuText, $fallback--text); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - } - &-header { display: flex; align-items: center; - padding: 0.6em 0; - border-bottom: 2px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + padding: var(--__vertical-gap) var(--__horizontal-gap); + border-bottom: 1px solid; + border-bottom-color: var(--border); &-actions { flex: 1; @@ -88,7 +81,7 @@ } &-checkbox-wrapper { - padding: 0 10px; + padding-right: var(--__horizontal-gap); flex: none; } } diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.scss b/src/components/settings_modal/admin_tabs/emoji_tab.scss @@ -1,5 +1,3 @@ -@import "src/variables"; - .emoji-tab { .btn-group .btn:not(:first-child) { margin-left: 0.5em; @@ -25,7 +23,7 @@ } .emoji-unsaved { - box-shadow: 0 3px 5px var(--cBlue, $fallback--cBlue); + box-shadow: 0 3px 5px var(--cBlue); } .emoji-list { @@ -56,6 +54,6 @@ } .warning { - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } } diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.vue b/src/components/settings_modal/admin_tabs/emoji_tab.vue @@ -13,13 +13,15 @@ <button class="button button-default btn" type="button" - @click="reloadEmoji"> + @click="reloadEmoji" + > {{ $t('admin_dash.emoji.reload') }} </button> <button class="button button-default btn" type="button" - @click="importFromFS"> + @click="importFromFS" + > {{ $t('admin_dash.emoji.importFS') }} </button> </li> @@ -28,7 +30,8 @@ <button class="button button-default btn" type="button" - @click="$refs.remotePackPopover.showPopover"> + @click="$refs.remotePackPopover.showPopover" + > {{ $t('admin_dash.emoji.remote_packs') }} <Popover @@ -43,11 +46,16 @@ <template #content> <div class="emoji-tab-popover-input"> <h3>{{ $t('admin_dash.emoji.remote_pack_instance') }}</h3> - <input v-model="remotePackInstance" :placeholder="$t('admin_dash.emoji.remote_pack_instance')"> + <input + v-model="remotePackInstance" + class="input" + :placeholder="$t('admin_dash.emoji.remote_pack_instance')" + > <button class="button button-default btn emoji-tab-popover-button" type="button" - @click="listRemotePacks"> + @click="listRemotePacks" + > {{ $t('admin_dash.emoji.do_list') }} </button> </div> @@ -61,9 +69,22 @@ <li> <h4>{{ $t('admin_dash.emoji.edit_pack') }}</h4> - <Select class="form-control" v-model="packName"> - <option value="" disabled hidden>{{ $t('admin_dash.emoji.emoji_pack') }}</option> - <option v-for="(pack, listPackName) in knownPacks" :label="listPackName" :key="listPackName"> + <Select + v-model="packName" + class="form-control" + > + <option + value="" + disabled + hidden + > + {{ $t('admin_dash.emoji.emoji_pack') }} + </option> + <option + v-for="(pack, listPackName) in knownPacks" + :key="listPackName" + :label="listPackName" + > {{ listPackName }} </option> </Select> @@ -71,7 +92,8 @@ <button class="button button-default btn emoji-tab-popover-button" type="button" - @click="$refs.createPackPopover.showPopover"> + @click="$refs.createPackPopover.showPopover" + > {{ $t('admin_dash.emoji.create_pack') }} </button> <Popover @@ -86,11 +108,16 @@ <template #content> <div class="emoji-tab-popover-input"> <h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3> - <input v-model="newPackName" :placeholder="$t('admin_dash.emoji.new_pack_name')"> + <input + v-model="newPackName" + :placeholder="$t('admin_dash.emoji.new_pack_name')" + class="input" + > <button class="button button-default btn emoji-tab-popover-button" type="button" - @click="createEmojiPack"> + @click="createEmojiPack" + > {{ $t('admin_dash.emoji.create') }} </button> </div> @@ -105,67 +132,96 @@ <li> <label> {{ $t('admin_dash.emoji.description') }} - <ModifiedIndicator :changed="metaEdited('description')" message-key="admin_dash.emoji.metadata_changed" /> + <ModifiedIndicator + :changed="metaEdited('description')" + message-key="admin_dash.emoji.metadata_changed" + /> <textarea v-model="packMeta.description" :disabled="pack.remote !== undefined" - class="bio resize-height" /> + class="bio resize-height input" + /> </label> </li> <li> <label> {{ $t('admin_dash.emoji.homepage') }} - <ModifiedIndicator :changed="metaEdited('homepage')" message-key="admin_dash.emoji.metadata_changed" /> + <ModifiedIndicator + :changed="metaEdited('homepage')" + message-key="admin_dash.emoji.metadata_changed" + /> - <input - class="emoji-info-input" v-model="packMeta.homepage" - :disabled="pack.remote !== undefined"> + <input + v-model="packMeta.homepage" + class="emoji-info-input input" + :disabled="pack.remote !== undefined" + > </label> </li> <li> <label> {{ $t('admin_dash.emoji.fallback_src') }} - <ModifiedIndicator :changed="metaEdited('fallback-src')" message-key="admin_dash.emoji.metadata_changed" /> + <ModifiedIndicator + :changed="metaEdited('fallback-src')" + message-key="admin_dash.emoji.metadata_changed" + /> - <input class="emoji-info-input" v-model="packMeta['fallback-src']" :disabled="pack.remote !== undefined"> + <input + v-model="packMeta['fallback-src']" + class="emoji-info-input input" + :disabled="pack.remote !== undefined" + > </label> </li> <li> <label> {{ $t('admin_dash.emoji.fallback_sha256') }} - <input :disabled="true" class="emoji-info-input" v-model="packMeta['fallback-src-sha256']"> + <input + v-model="packMeta['fallback-src-sha256']" + :disabled="true" + class="emoji-info-input input" + > </label> </li> <li> - <Checkbox :disabled="pack.remote !== undefined" v-model="packMeta['share-files']"> + <Checkbox + v-model="packMeta['share-files']" + :disabled="pack.remote !== undefined" + > {{ $t('admin_dash.emoji.share') }} </Checkbox> - <ModifiedIndicator :changed="metaEdited('share-files')" message-key="admin_dash.emoji.metadata_changed" /> + <ModifiedIndicator + :changed="metaEdited('share-files')" + message-key="admin_dash.emoji.metadata_changed" + /> </li> <li class="btn-group"> <button + v-if="pack.remote === undefined" class="button button-default btn" type="button" - v-if="pack.remote === undefined" - @click="savePackMetadata"> + @click="savePackMetadata" + > {{ $t('admin_dash.emoji.save_meta') }} </button> <button + v-if="pack.remote === undefined" class="button button-default btn" type="button" - v-if="pack.remote === undefined" - @click="savePackMetadata"> + @click="savePackMetadata" + > {{ $t('admin_dash.emoji.revert_meta') }} </button> <button - class="button button-default btn" v-if="pack.remote === undefined" + class="button button-default btn" type="button" - @click="deleteModalVisible = true"> + @click="deleteModalVisible = true" + > {{ $t('admin_dash.emoji.delete_pack') }} <ConfirmModal @@ -174,16 +230,18 @@ :cancel-text="$t('status.delete_confirm_cancel_button')" :confirm-text="$t('status.delete_confirm_accept_button')" @cancelled="deleteModalVisible = false" - @accepted="deleteEmojiPack" > + @accepted="deleteEmojiPack" + > {{ $t('admin_dash.emoji.delete_confirm', [packName]) }} </ConfirmModal> </button> <button + v-if="pack.remote !== undefined" class="button button-default btn" type="button" - v-if="pack.remote !== undefined" - @click="$refs.dlPackPopover.showPopover"> + @click="$refs.dlPackPopover.showPopover" + > {{ $t('admin_dash.emoji.download_pack') }} <Popover @@ -202,12 +260,17 @@ <div class="emoji-tab-popover-input"> <label> {{ $t('admin_dash.emoji.download_as_name') }} - <input class="emoji-data-input" + <input v-model="remotePackDownloadAs" - :placeholder="$t('admin_dash.emoji.download_as_name_full')"> + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.download_as_name_full')" + > </label> - <div v-if="downloadWillReplaceLocal" class="warning"> + <div + v-if="downloadWillReplaceLocal" + class="warning" + > <em>{{ $t('admin_dash.emoji.replace_warning') }}</em> </div> </div> @@ -215,7 +278,8 @@ <button class="button button-default btn" type="button" - @click="downloadRemotePack"> + @click="downloadRemotePack" + > {{ $t('admin_dash.emoji.download') }} </button> </div> @@ -231,31 +295,47 @@ <h4> {{ $t('admin_dash.emoji.files') }} - <ModifiedIndicator v-if="pack" + <ModifiedIndicator + v-if="pack" :changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)" - message-key="admin_dash.emoji.emoji_changed"/> + message-key="admin_dash.emoji.emoji_changed" + /> </h4> - <div class="emoji-list" v-if="pack"> + <div + v-if="pack" + class="emoji-list" + > <EmojiEditingPopover v-if="pack.remote === undefined" - placement="bottom" new-upload + placement="bottom" + new-upload :title="$t('admin_dash.emoji.adding_new')" - :packName="packName" - @updatePackFiles="updatePackFiles" @displayError="displayError" + :pack-name="packName" + @updatePackFiles="updatePackFiles" + @displayError="displayError" > <template #trigger> - <FAIcon icon="plus" size="2x" :title="$t('admin_dash.emoji.add_file')" /> + <FAIcon + icon="plus" + size="2x" + :title="$t('admin_dash.emoji.add_file')" + /> </template> </EmojiEditingPopover> <EmojiEditingPopover - placement="top" ref="emojiPopovers" - v-for="(file, shortcode) in pack.files" :key="shortcode" + v-for="(file, shortcode) in pack.files" + ref="emojiPopovers" + :key="shortcode" + placement="top" :title="$t('admin_dash.emoji.editing', [shortcode])" :disabled="pack.remote !== undefined" - :shortcode="shortcode" :file="file" :packName="packName" - @updatePackFiles="updatePackFiles" @displayError="displayError" + :shortcode="shortcode" + :file="file" + :pack-name="packName" + @updatePackFiles="updatePackFiles" + @displayError="displayError" > <template #trigger> <StillImage diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue @@ -6,7 +6,10 @@ <div class="setting-item"> <h2>{{ $t('admin_dash.tabs.frontends') }}</h2> <p>{{ $t('admin_dash.frontend.wip_notice') }}</p> - <ul class="setting-list" v-if="adminDraft"> + <ul + v-if="adminDraft" + class="setting-list" + > <li> <h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3> <p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p> @@ -23,12 +26,18 @@ </ul> </li> </ul> - <div v-else class="setting-list"> + <div + v-else + class="setting-list" + > {{ $t('admin_dash.frontend.default_frontend_unavail') }} </div> <div class="setting-list relative"> - <PanelLoading class="overlay" v-if="working"/> + <PanelLoading + v-if="working" + class="overlay" + /> <h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3> <ul class="cards-list"> <li @@ -107,7 +116,7 @@ <button v-for="ref in frontend.refs" :key="ref" - class="button-default dropdown-item" + class="menu-item dropdown-item" @click.prevent="update(frontend, ref)" @click="close" > @@ -164,7 +173,7 @@ <button v-for="ref in frontend.installedRefs || frontend.refs" :key="ref" - class="button-default dropdown-item" + class="menu-item dropdown-item" @click.prevent="setDefault(frontend, ref)" @click="close" > diff --git a/src/components/settings_modal/admin_tabs/instance_tab.vue b/src/components/settings_modal/admin_tabs/instance_tab.vue @@ -8,7 +8,10 @@ </li> <!-- See https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3963 --> <li v-if="adminDraft[':pleroma'][':instance'][':favicon'] !== undefined"> - <AttachmentSetting compact path=":pleroma.:instance.:favicon" /> + <AttachmentSetting + compact + path=":pleroma.:instance.:favicon" + /> </li> <li> <StringSetting path=":pleroma.:instance.:email" /> @@ -20,7 +23,10 @@ <StringSetting path=":pleroma.:instance.:short_description" /> </li> <li> - <AttachmentSetting compact path=":pleroma.:instance.:instance_thumbnail" /> + <AttachmentSetting + compact + path=":pleroma.:instance.:instance_thumbnail" + /> </li> <li> <AttachmentSetting path=":pleroma.:instance.:background_image" /> diff --git a/src/components/settings_modal/helpers/attachment_setting.vue b/src/components/settings_modal/helpers/attachment_setting.vue @@ -29,7 +29,7 @@ <label for="path">{{ $t('settings.url') }}</label> <input :id="path" - class="string-input" + class="input string-input" :disabled="shouldBeDisabled" :value="realDraftMode ? draft : state" @change="update" diff --git a/src/components/settings_modal/helpers/emoji_editing_popover.vue b/src/components/settings_modal/helpers/emoji_editing_popover.vue @@ -1,10 +1,10 @@ <template> <Popover + ref="emojiPopover" trigger="click" :placement="placement" bound-to-selector=".emoji-list" popover-class="emoji-tab-edit-popover popover-default" - ref="emojiPopover" :bound-to="{ x: 'container' }" :offset="{ y: 5 }" :disabled="disabled" @@ -18,23 +18,36 @@ {{ title }} </h3> - <StillImage class="emoji" v-if="emojiPreview" :src="emojiPreview" /> - <div v-else class="emoji"></div> - - <div class="emoji-tab-popover-input" v-if="newUpload"> + <StillImage + v-if="emojiPreview" + class="emoji" + :src="emojiPreview" + /> + <div + v-else + class="emoji" + /> + + <div + v-if="newUpload" + class="emoji-tab-popover-input" + > <input type="file" accept="image/*" - class="emoji-tab-popover-file" - @change="uploadFile = $event.target.files"> + class="emoji-tab-popover-file input" + @change="uploadFile = $event.target.files" + > </div> <div> <div class="emoji-tab-popover-input"> <label> {{ $t('admin_dash.emoji.shortcode') }} - <input class="emoji-data-input" + <input v-model="editedShortcode" - :placeholder="$t('admin_dash.emoji.new_shortcode')"> + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.new_shortcode')" + > </label> </div> @@ -42,9 +55,11 @@ <label> {{ $t('admin_dash.emoji.filename') }} - <input class="emoji-data-input" + <input v-model="editedFile" - :placeholder="$t('admin_dash.emoji.new_filename')"> + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.new_filename')" + > </label> </div> @@ -52,7 +67,8 @@ class="button button-default btn" type="button" :disabled="newUpload ? uploadFile.length == 0 : !isEdited" - @click="newUpload ? uploadEmoji() : saveEditedEmoji()"> + @click="newUpload ? uploadEmoji() : saveEditedEmoji()" + > {{ $t('admin_dash.emoji.save') }} </button> @@ -60,13 +76,15 @@ <button class="button button-default btn emoji-tab-popover-button" type="button" - @click="deleteModalVisible = true"> + @click="deleteModalVisible = true" + > {{ $t('admin_dash.emoji.delete') }} </button> <button class="button button-default btn emoji-tab-popover-button" type="button" - @click="revertEmoji"> + @click="revertEmoji" + > {{ $t('admin_dash.emoji.revert') }} </button> <ConfirmModal @@ -75,7 +93,8 @@ :cancel-text="$t('status.delete_confirm_cancel_button')" :confirm-text="$t('status.delete_confirm_accept_button')" @cancelled="deleteModalVisible = false" - @accepted="deleteEmoji" > + @accepted="deleteEmoji" + > {{ $t('admin_dash.emoji.delete_confirm', [shortcode]) }} </ConfirmModal> </template> @@ -91,6 +110,30 @@ import StillImage from 'components/still-image/still-image.vue' export default { components: { Popover, ConfirmModal, StillImage }, + inject: ['emojiAddr'], + props: { + placement: String, + disabled: { + type: Boolean, + default: false + }, + + newUpload: Boolean, + + title: String, + packName: String, + shortcode: { + type: String, + // Only exists when this is not a new upload + default: '' + }, + file: { + type: String, + // Only exists when this is not a new upload + default: '' + } + }, + emits: ['updatePackFiles', 'displayError'], data () { return { uploadFile: [], @@ -113,7 +156,6 @@ export default { return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file) } }, - inject: ['emojiAddr'], methods: { saveEditedEmoji () { if (!this.isEdited) return @@ -167,29 +209,6 @@ export default { this.$emit('updatePackFiles', resp) }) } - }, - emits: ['updatePackFiles', 'displayError'], - props: { - placement: String, - disabled: { - type: Boolean, - default: false - }, - - newUpload: Boolean, - - title: String, - packName: String, - shortcode: { - type: String, - // Only exists when this is not a new upload - default: '' - }, - file: { - type: String, - // Only exists when this is not a new upload - default: '' - } } } </script> diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue @@ -17,7 +17,7 @@ </label> <input :id="path" - class="number-input" + class="input number-input" type="number" :step="step || 1" :disabled="shouldBeDisabled" diff --git a/src/components/settings_modal/helpers/string_setting.vue b/src/components/settings_modal/helpers/string_setting.vue @@ -17,7 +17,7 @@ </label> <input :id="path" - class="string-input" + class="input string-input" :disabled="shouldBeDisabled" :value="realDraftMode ? draft : state" @change="update" diff --git a/src/components/settings_modal/helpers/unit_setting.vue b/src/components/settings_modal/helpers/unit_setting.vue @@ -11,7 +11,7 @@ </label> <input :id="path" - class="number-input" + class="input number-input" type="number" step="1" :disabled="disabled" diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss @@ -1,5 +1,3 @@ -@import "src/variables"; - .settings-modal { overflow: hidden; diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue @@ -14,7 +14,7 @@ <div v-if="currentSaveStateNotice" class="alert" - :class="{ transparent: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}" + :class="{ success: !currentSaveStateNotice.error, error: currentSaveStateNotice.error}" @click.prevent > {{ currentSaveStateNotice.error ? $t('settings.saving_err') : $t('settings.saving_ok') }} @@ -70,7 +70,7 @@ <template #content="{close}"> <div class="dropdown-menu"> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click.prevent="backup" @click="close" > @@ -80,7 +80,7 @@ /><span>{{ $t("settings.file_export_import.backup_settings") }}</span> </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click.prevent="backupWithTheme" @click="close" > @@ -90,7 +90,7 @@ /><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span> </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" @click.prevent="restore" @click="close" > diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue @@ -45,6 +45,29 @@ </BooleanSetting> </li> <li> + <BooleanSetting path="muteSensitiveStatuses"> + {{ $t('settings.mute_sensitive_posts') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="hideMutedFederationRestrictions"> + {{ $t('settings.hide_muted_federation_restrictions') }} + </BooleanSetting> + <ul + class="setting-list suboptions" + :class="[{disabled: !streaming}]" + > + <li + v-for="item in muteFederationRestrictionsLevels" + :key="'mute_' + item + '_federation_restriction'" + > + <BooleanSetting :path="'muteFederationRestrictions.' + item"> + {{ $t('settings.mute_' + item + '_federation_restriction') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> <BooleanSetting path="hidePostStats"> {{ $t('settings.hide_post_stats') }} </BooleanSetting> @@ -67,7 +90,7 @@ <textarea id="muteWords" v-model="muteWordsString" - class="resize-height" + class="input resize-height" /> <div>{{ $t('settings.filtering_explanation') }}</div> </li> diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue @@ -19,7 +19,10 @@ </div> </li> <li> - <BooleanSetting path="unseenAtTop" expert="1"> + <BooleanSetting + path="unseenAtTop" + expert="1" + > {{ $t('settings.notification_setting_unseen_at_top') }} </BooleanSetting> </li> @@ -38,7 +41,9 @@ </li> <li> <h3> {{ $t('settings.notification_visibility') }}</h3> - <p v-if="expertLevel > 0">{{ $t('settings.notification_setting_filters_chrome_push') }}</p> + <p v-if="expertLevel > 0"> + {{ $t('settings.notification_setting_filters_chrome_push') }} + </p> <ul class="setting-list two-column"> <li> <h4> {{ $t('settings.notification_visibility_mentions') }}</h4> diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss @@ -1,5 +1,3 @@ -@import "../../../variables"; - .profile-tab { .bio { margin: 0; @@ -43,16 +41,14 @@ display: block; width: 100%; height: 100%; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); } .reset-button { position: absolute; top: 0.2em; right: 0.2em; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); background-color: rgb(0 0 0 / 60%); opacity: 0.7; width: 1.5em; diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue @@ -12,7 +12,7 @@ <input id="username" v-model="newName" - class="name-changer" + class="input name-changer" v-bind="propsToNative(inputProps)" > </template> @@ -26,7 +26,7 @@ <template #default="inputProps"> <textarea v-model="newBio" - class="bio resize-height" + class="input bio resize-height" v-bind="propsToNative(inputProps)" /> </template> @@ -47,7 +47,7 @@ id="birthday" v-model="newBirthday" type="date" - class="birthday-input" + class="input birthday-input" > <Checkbox v-model="showBirthday"> {{ $t('settings.birthday.show_birthday') }} @@ -71,6 +71,7 @@ v-model="newFields[i].name" :placeholder="$t('settings.profile_fields.name')" v-bind="propsToNative(inputProps)" + class="input" > </template> </EmojiInput> @@ -85,6 +86,7 @@ v-model="newFields[i].value" :placeholder="$t('settings.profile_fields.value')" v-bind="propsToNative(inputProps)" + class="input" > </template> </EmojiInput> @@ -205,6 +207,7 @@ <div> <input type="file" + class="input" @change="uploadFile('banner', $event)" > </div> @@ -247,6 +250,7 @@ <div> <input type="file" + class="input" @change="uploadFile('background', $event)" > </div> diff --git a/src/components/settings_modal/tabs/security_tab/mfa.vue b/src/components/settings_modal/tabs/security_tab/mfa.vue @@ -99,12 +99,14 @@ <input v-model="otpConfirmToken" type="text" + class="input" > <p>{{ $t('settings.enter_current_password_to_confirm') }}:</p> <input v-model="currentPassword" type="password" + class="input" > <div class="confirm-otp-actions"> <button @@ -137,8 +139,6 @@ <script src="./mfa.js"></script> <style lang="scss"> -@import "../../../../variables"; - .mfa-settings { .mfa-heading, .method-item { @@ -149,8 +149,7 @@ } .warning { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } .setup-otp { diff --git a/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue b/src/components/settings_modal/tabs/security_tab/mfa_backup_codes.vue @@ -21,16 +21,13 @@ </template> <script src="./mfa_backup_codes.js"></script> <style lang="scss"> -@import "../../../../variables"; - .mfa-backup-codes { .warning { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); + color: var(--cOrange); } .backup-codes { - font-family: var(--postCodeFont, monospace); + font-family: var(--monoFont); } } </style> diff --git a/src/components/settings_modal/tabs/security_tab/mfa_totp.vue b/src/components/settings_modal/tabs/security_tab/mfa_totp.vue @@ -30,6 +30,7 @@ <input v-model="currentPassword" type="password" + class="input" > </confirm> <div diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -8,6 +8,7 @@ v-model="newEmail" type="email" autocomplete="email" + class="input" > </div> <div> @@ -16,6 +17,7 @@ v-model="changeEmailPassword" type="password" autocomplete="current-password" + class="input" > </div> <button @@ -40,6 +42,7 @@ <input v-model="changePasswordInputs[0]" type="password" + class="input" > </div> <div> @@ -47,6 +50,7 @@ <input v-model="changePasswordInputs[1]" type="password" + class="input" > </div> <div> @@ -54,6 +58,7 @@ <input v-model="changePasswordInputs[2]" type="password" + class="input" > </div> <button @@ -155,6 +160,7 @@ </i18n-t> <input v-model="addAliasTarget" + class="input" > </div> <button @@ -187,6 +193,7 @@ </i18n-t> <input v-model="moveAccountTarget" + class="input" > </div> <div> @@ -195,6 +202,7 @@ v-model="moveAccountPassword" type="password" autocomplete="current-password" + class="input" > </div> <button @@ -222,6 +230,7 @@ <input v-model="deleteAccountConfirmPasswordInput" type="password" + class="input" > <button class="btn button-default" diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue @@ -5,7 +5,7 @@ <div class="panel-heading"> <div class="title"> {{ $t('settings.style.preview.header') }} - <span class="badge badge-notification"> + <span class="badge -notification"> 99 </span> </div> @@ -81,7 +81,7 @@ class="faint" scope="global" > - <a style="color: var(--faintLink);"> + <a style="color: var(--linkFaint);"> {{ $t('settings.style.preview.faint_link') }} </a> </i18n-t> @@ -95,6 +95,7 @@ <input :value="$t('settings.style.preview.input')" type="text" + class="input" > <div class="actions"> @@ -103,6 +104,7 @@ id="preview_checkbox" checked="very yes" type="checkbox" + class="input" > <label for="preview_checkbox">{{ $t('settings.style.preview.checkbox') }}</label> </span> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -4,15 +4,7 @@ import { getContrastRatioLayers } from 'src/services/color_convert/color_convert.js' import { - DEFAULT_SHADOWS, - generateColors, - generateShadows, - generateRadii, - generateFonts, - composePreset, - getThemes, - shadows2to3, - colors2to3 + getThemes } from 'src/services/style_setter/style_setter.js' import { newImporter, @@ -25,7 +17,15 @@ import { CURRENT_VERSION, OPACITIES, getLayers, - getOpacitySlot + getOpacitySlot, + DEFAULT_SHADOWS, + generateColors, + generateShadows, + generateRadii, + generateFonts, + composePreset, + shadows2to3, + colors2to3 } from 'src/services/theme_data/theme_data.service.js' import ColorInput from 'src/components/color_input/color_input.vue' import RangeInput from 'src/components/range_input/range_input.vue' @@ -514,6 +514,7 @@ export default { this.$store.dispatch('setOption', { name: 'customTheme', value: { + themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, ...this.previewTheme } @@ -521,6 +522,7 @@ export default { this.$store.dispatch('setOption', { name: 'customThemeSource', value: { + themeFileVersion: this.selectedVersion, themeEngineVersion: CURRENT_VERSION, shadows: this.shadowsLocal, fonts: this.fontsLocal, diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -1,5 +1,3 @@ -@import "src/variables"; - .theme-tab { padding-bottom: 2em; @@ -162,8 +160,7 @@ .preview-container { border-top: 1px dashed; border-bottom: 1px dashed; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); margin: 1em 0; padding: 1em; background-color: var(--wallpaper); @@ -227,8 +224,6 @@ min-width: 20px; min-height: 20px; line-height: 20px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); } .avatar { @@ -254,8 +249,7 @@ .separator { margin: 1em; border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); } .btn { @@ -296,7 +290,7 @@ border: 0; box-shadow: none; background: transparent; - color: var(--faint, $fallback--faint); + color: var(--textFaint); align-self: stretch; } diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js @@ -1,7 +1,7 @@ import ColorInput from '../color_input/color_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' import Select from '../select/select.vue' -import { getCssShadow } from '../../services/style_setter/style_setter.js' +import { getCssShadow } from '../../services/theme_data/theme_data.service.js' import { hex2rgb } from '../../services/color_convert/color_convert.js' import { library } from '@fortawesome/fontawesome-svg-core' import { diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue @@ -11,14 +11,14 @@ <input v-model="selected.y" :disabled="!present" - class="input-number" + class="input input-number" type="number" > <div class="wrap"> <input v-model="selected.y" :disabled="!present" - class="input-range" + class="input input-range" type="range" max="20" min="-20" @@ -38,14 +38,14 @@ <input v-model="selected.x" :disabled="!present" - class="input-number" + class="input input-number" type="number" > <div class="wrap"> <input v-model="selected.x" :disabled="!present" - class="input-range" + class="input input-range" type="range" max="20" min="-20" @@ -129,7 +129,7 @@ v-model="selected.inset" :disabled="!present" name="inset" - class="input-inset visible-for-screenreader-only" + class="input -checkbox input-inset visible-for-screenreader-only" type="checkbox" > <label @@ -153,7 +153,7 @@ v-model="selected.blur" :disabled="!present" name="blur" - class="input-range" + class="input input-range" type="range" max="20" min="0" @@ -161,7 +161,7 @@ <input v-model="selected.blur" :disabled="!present" - class="input-number" + class="input input-number" type="number" min="0" > @@ -181,7 +181,7 @@ v-model="selected.spread" :disabled="!present" name="spread" - class="input-range" + class="input input-range" type="range" max="20" min="-20" @@ -189,7 +189,7 @@ <input v-model="selected.spread" :disabled="!present" - class="input-number" + class="input input-number" type="number" > </div> @@ -219,8 +219,6 @@ <script src="./shadow_control.js"></script> <style lang="scss"> -@import "../../variables"; - .shadow-control { display: flex; flex-wrap: wrap; @@ -237,8 +235,6 @@ display: flex; flex-wrap: wrap; - $side: 15em; - input[type="number"] { width: 5em; min-width: 2em; @@ -261,7 +257,7 @@ .x-shift-control .wrap, input[type="range"] { margin: 0; - width: $side; + width: 15em; height: 2em; } @@ -271,7 +267,7 @@ .wrap { width: 2em; - height: $side; + height: 15em; } input[type="range"] { @@ -293,16 +289,12 @@ linear-gradient(-45deg, transparent 75%, #666 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0; - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border-radius: var(--roundness); .preview-block { width: 33%; height: 33%; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); } } } diff --git a/src/components/shout_panel/shout_panel.vue b/src/components/shout_panel/shout_panel.vue @@ -5,7 +5,7 @@ > <div class="panel panel-default"> <div - class="panel-heading timeline-heading" + class="panel-heading" :class="{ 'shout-heading': floating }" @click.stop.prevent="togglePanel" > @@ -18,7 +18,7 @@ /> </div> </div> - <div class="shout-window"> + <div class="panel-body shout-window"> <div v-for="message in messages" :key="message.id" @@ -41,10 +41,10 @@ </div> </div> </div> - <div class="shout-input"> + <div class="panel-body shout-input"> <textarea v-model="currentMessage" - class="shout-input-textarea" + class="shout-input-textarea input" rows="1" @keyup.enter="submit(currentMessage)" /> @@ -75,8 +75,6 @@ <script src="./shout_panel.js"></script> <style lang="scss"> -@import "../../variables"; - .floating-shout { position: fixed; bottom: 0.5em; @@ -97,8 +95,7 @@ cursor: pointer; .icon { - color: $fallback--text; - color: var(--panelText, $fallback--text); + color: var(--text); margin-right: 0.5em; } @@ -128,8 +125,7 @@ img { height: 24px; width: 24px; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); margin-right: 0.5em; margin-top: 0.25em; } diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue @@ -1,6 +1,6 @@ <template> <div - class="side-drawer-container" + class="side-drawer-container mobile-drawer" :class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }" > <div @@ -35,7 +35,10 @@ v-if="!currentUser" @click="toggleDrawer" > - <router-link :to="{ name: 'login' }"> + <router-link + :to="{ name: 'login' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -47,7 +50,10 @@ v-if="currentUser || !privateMode" @click="toggleDrawer" > - <router-link :to="timelinesRoute"> + <router-link + :to="timelinesRoute" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -59,7 +65,10 @@ v-if="currentUser" @click="toggleDrawer" > - <router-link :to="{ name: 'lists' }"> + <router-link + :to="{ name: 'lists' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -74,6 +83,7 @@ <router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }" style="position: relative;" + class="menu-item" > <FAIcon fixed-width @@ -82,7 +92,7 @@ /> {{ $t("nav.chats") }} <span v-if="unreadChatCount" - class="badge badge-notification" + class="badge -notification" > {{ unreadChatCount }} </span> @@ -91,7 +101,10 @@ </ul> <ul v-if="currentUser"> <li @click="toggleDrawer"> - <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> + <router-link + :to="{ name: 'interactions', params: { username: currentUser.screen_name } }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -103,7 +116,10 @@ v-if="currentUser.locked" @click="toggleDrawer" > - <router-link to="/friend-requests"> + <router-link + to="/friend-requests" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -111,7 +127,7 @@ /> {{ $t("nav.friend_requests") }} <span v-if="followRequestCount > 0" - class="badge badge-notification" + class="badge -notification" > {{ followRequestCount }} </span> @@ -121,7 +137,10 @@ v-if="shout" @click="toggleDrawer" > - <router-link :to="{ name: 'shout-panel' }"> + <router-link + :to="{ name: 'shout-panel' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -135,7 +154,10 @@ v-if="currentUser || !privateMode" @click="toggleDrawer" > - <router-link :to="{ name: 'search' }"> + <router-link + :to="{ name: 'search' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -147,7 +169,10 @@ v-if="currentUser && suggestionsEnabled" @click="toggleDrawer" > - <router-link :to="{ name: 'who-to-follow' }"> + <router-link + :to="{ name: 'who-to-follow' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -157,7 +182,7 @@ </li> <li @click="toggleDrawer"> <button - class="button-unstyled -link -fullwidth" + class="menu-item" @click="openSettingsModal" > <FAIcon @@ -168,7 +193,10 @@ </button> </li> <li @click="toggleDrawer"> - <router-link :to="{ name: 'about'}"> + <router-link + :to="{ name: 'about'}" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -181,7 +209,7 @@ @click="toggleDrawer" > <button - class="button-unstyled -link -fullwidth" + class="menu-item" @click.stop="openAdminModal" > <FAIcon @@ -197,6 +225,7 @@ > <router-link :to="{ name: 'announcements' }" + class="menu-item" > <FAIcon fixed-width @@ -205,7 +234,7 @@ /> {{ $t("nav.announcements") }} <span v-if="unreadAnnouncementCount" - class="badge badge-notification" + class="badge -notification" > {{ unreadAnnouncementCount }} </span> @@ -215,7 +244,10 @@ v-if="currentUser" @click="toggleDrawer" > - <router-link :to="{ name: 'edit-navigation' }"> + <router-link + :to="{ name: 'edit-navigation' }" + class="menu-item" + > <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -228,7 +260,7 @@ @click="toggleDrawer" > <button - class="button-unstyled -link -fullwidth" + class="menu-item" @click="doLogout" > <FAIcon @@ -251,8 +283,6 @@ <script src="./side_drawer.js"></script> <style lang="scss"> -@import "../../variables"; - .side-drawer-container { position: fixed; z-index: var(--ZI_navbar); @@ -305,17 +335,8 @@ width: 80%; max-width: 20em; flex: 0 0 80%; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); - background-color: $fallback--bg; - background-color: var(--popover, $fallback--bg); - color: $fallback--link; - color: var(--popoverText, $fallback--link); - - --faint: var(--popoverFaintText, $fallback--faint); - --faintLink: var(--popoverFaintLink, $fallback--faint); - --lightText: var(--popoverLightText, $fallback--lightText); - --icon: var(--popoverIcon, $fallback--icon); + box-shadow: var(--shadow); + background-color: var(--background); .badge { margin-left: 10px; @@ -362,8 +383,7 @@ margin: 0; padding: 0; border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); } .side-drawer ul:last-child { @@ -380,18 +400,6 @@ height: 3em; line-height: 3em; padding: 0 0.7em; - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuPopoverText, $fallback--text); - - --faint: var(--selectedMenuPopoverFaintText, $fallback--faint); - --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); - --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - } } } </style> diff --git a/src/components/status/post.style.js b/src/components/status/post.style.js @@ -0,0 +1,33 @@ +export default { + name: 'Post', + selector: '.Status', + states: { + selected: '.-focused' + }, + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', + 'Avatar', + 'Attachment', + 'PollGraph' + ], + defaultRules: [ + { + directives: { + background: '--bg' + } + }, + { + state: ['selected'], + directives: { + background: '--inheritedBackground, 10' + } + } + ] +} diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -238,6 +238,9 @@ const Status = { showActorTypeIndicator () { return !this.hideBotIndication }, + sensitiveStatus () { + return this.status.nsfw + }, mentionsLine () { if (!this.headTailLinks) return [] const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url)) @@ -265,7 +268,9 @@ const Status = { // Wordfiltered this.muteWordHits.length > 0 || // bot status - (this.muteBotStatuses && this.botStatus && !this.compact) + (this.muteBotStatuses && this.botStatus && !this.compact) || + // sensitive status + (this.muteSensitiveStatuses && this.sensitiveStatus && !this.compact) return !this.unmuted && !this.shouldNotMute && reasonsToMute }, userIsMuted () { @@ -371,6 +376,9 @@ const Status = { muteBotStatuses () { return this.mergedConfig.muteBotStatuses }, + muteSensitiveStatuses () { + return this.mergedConfig.muteSensitiveStatuses + }, hideBotIndication () { return this.mergedConfig.hideBotIndication }, @@ -421,6 +429,8 @@ const Status = { let multiplier = 60 * 1000 // minutes is smallest unit switch (unit) { case 'm': + break + case 'h': multiplier *= 60 // hour break case 'd': diff --git a/src/components/status/status.scss b/src/components/status/status.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .Status { min-width: 0; white-space: normal; @@ -12,24 +10,8 @@ --_still-image-label-visibility: hidden; } - &.-focused { - background-color: $fallback--lightBg; - background-color: var(--selectedPost, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedPostText, $fallback--text); - - --lightText: var(--selectedPostLightText, $fallback--light); - --faint: var(--selectedPostFaintText, $fallback--faint); - --faintLink: var(--selectedPostFaintLink, $fallback--faint); - --postLink: var(--selectedPostPostLink, $fallback--faint); - --postFaintLink: var(--selectedPostFaintPostLink, $fallback--faint); - --icon: var(--selectedPostIcon, $fallback--icon); - } - .gravestone { - padding: var(--status-margin, $status-margin); - color: $fallback--faint; - color: var(--faint, $fallback--faint); + padding: var(--status-margin); display: flex; .deleted-text { @@ -40,7 +22,7 @@ .status-container { display: flex; - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); > * { min-width: 0; @@ -52,7 +34,7 @@ } .pin { - padding: var(--status-margin, $status-margin) var(--status-margin, $status-margin) 0; + padding: var(--status-margin) var(--status-margin) 0; display: flex; align-items: center; justify-content: flex-end; @@ -68,7 +50,7 @@ } .left-side { - margin-right: var(--status-margin, $status-margin); + margin-right: var(--status-margin); } .right-side { @@ -77,7 +59,7 @@ } .usercard { - margin-bottom: var(--status-margin, $status-margin); + margin-bottom: var(--status-margin); } .status-username { @@ -135,11 +117,6 @@ .button-unstyled { padding: 5px; margin: -5px; - - &:hover svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } } .svg-inline--fa { @@ -243,16 +220,15 @@ } .repeat-info { - padding: 0.4em var(--status-margin, $status-margin); + padding: 0.4em var(--status-margin); .repeat-icon { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + color: var(--cGreen); } } .repeater-avatar { - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); margin-left: 28px; width: 20px; height: 20px; @@ -289,7 +265,7 @@ position: relative; width: 100%; display: flex; - margin-top: var(--status-margin, $status-margin); + margin-top: var(--status-margin); > * { max-width: 4em; @@ -357,7 +333,7 @@ } .favs-repeated-users { - margin-top: var(--status-margin, $status-margin); + margin-top: var(--status-margin); } .stats { @@ -368,10 +344,10 @@ .avatar-row { flex: 1; - overflow: hidden; position: relative; display: flex; align-items: center; + overflow: hidden; &::before { content: ""; @@ -379,16 +355,16 @@ height: 100%; width: 1px; left: 0; - background-color: var(--faint, $fallback--faint); + background-color: var(--textFaint); } } .stat-count { - margin-right: var(--status-margin, $status-margin); + margin-right: var(--status-margin); user-select: none; .stat-title { - color: var(--faint, $fallback--faint); + color: var(--textFaint); font-size: 0.85em; text-transform: uppercase; position: relative; @@ -425,8 +401,8 @@ .quoted-status { margin-top: 0.5em; - border: 1px solid var(--border, $fallback--border); - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); + border: 1px solid var(--border); + border-radius: var(--roundness); &.-unavailable-prompt { padding: 0.5em; diff --git a/src/components/status/status.vue b/src/components/status/status.vue @@ -31,6 +31,12 @@ /> </small> <small + v-if="muteSensitiveStatuses && status.nsfw" + class="mute-thread" + > + {{ $t('status.sensitive_muted') }} + </small> + <small v-if="showReasonMutedThread" class="mute-thread" > @@ -180,7 +186,7 @@ <span class="heading-right"> <router-link - class="timeago faint-link" + class="timeago faint" :to="{ name: 'conversation', params: { id: status.id } }" > <Timeago @@ -450,7 +456,7 @@ > <button v-if="showOtherRepliesAsButton && replies.length > 1" - class="button-unstyled -link faint" + class="button-unstyled -link" :title="$tc('status.ancestor_follow', replies.length - 1, { numReplies: replies.length - 1 })" @click.prevent="dive" > diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .StatusBody { display: flex; flex-direction: column; @@ -14,7 +12,6 @@ & .text, & .summary { - font-family: var(--postFont, sans-serif); white-space: pre-wrap; overflow-wrap: break-word; word-wrap: break-word; @@ -41,7 +38,7 @@ margin-bottom: 0.5em; border-style: solid; border-width: 0 0 1px; - border-color: var(--border, $fallback--border); + border-color: var(--border); flex-grow: 0; &.-tall { @@ -112,15 +109,6 @@ } } - .greentext { - color: $fallback--cGreen; - color: var(--postGreentext, $fallback--cGreen); - } - - .cyantext { - color: var(--postCyantext, $fallback--cBlue); - } - &.-compact { align-items: top; flex-direction: row; diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue @@ -11,6 +11,7 @@ > <RichContent class="media-body summary" + :faint="compact" :html="status.summary_raw_html" :emoji="status.emojis" /> @@ -48,6 +49,7 @@ :html="status.raw_html" :emoji="status.emojis" :handle-links="true" + :faint="compact" :greentext="mergedConfig.greentext" :attentions="status.attentions" @parseReady="onParseReady" diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue @@ -40,19 +40,14 @@ <script src="./status_popover.js"></script> <style lang="scss"> -@import "../../variables"; - /* popover styles load on-demand, so we need to override */ .status-popover.popover { font-size: 1rem; min-width: 15em; max-width: 95%; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); border-style: solid; border-width: 1px; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); /* TODO cleanup this */ .Status.Status { diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue @@ -32,8 +32,6 @@ <script src="./sticker_picker.js"></script> <style lang="scss"> -@import "../../variables"; - .sticker-picker { width: 100%; @@ -56,7 +54,7 @@ height: 100%; &:hover { - filter: drop-shadow(0 0 5px var(--accent, $fallback--link)); + filter: drop-shadow(0 0 5px var(--accent)); } } } diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue @@ -28,8 +28,6 @@ <script src="./still-image.js"></script> <style lang="scss"> -@import "../../variables"; - .still-image { position: relative; line-height: 0; @@ -68,8 +66,7 @@ color: #fff; display: block; padding: 2px 4px; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); z-index: 2; visibility: var(--_still-image-label-visibility, visible); } diff --git a/src/components/tab_switcher/tab.style.js b/src/components/tab_switcher/tab.style.js @@ -0,0 +1,78 @@ +export default { + name: 'Tab', // Name of the component + selector: '.tab', // CSS selector/prefix + states: { + active: '.active', + hover: ':hover:not(.disabled)', + disabled: '.disabled' + }, + validInnerComponents: [ + 'Text', + 'Icon' + ], + defaultRules: [ + { + directives: { + background: '--fg', + shadow: ['--defaultButtonShadow', '--defaultButtonBevel'], + roundness: 3 + } + }, + { + state: ['hover'], + directives: { + shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel'] + } + }, + { + state: ['active'], + directives: { + opacity: 0 + } + }, + { + state: ['hover', 'active'], + directives: { + shadow: ['--defaultButtonShadow', '--defaultButtonBevel'] + } + }, + { + state: ['disabled'], + directives: { + background: '$blend(--inheritedBackground, 0.25, --parent)', + shadow: ['--defaultButtonBevel'] + } + }, + { + component: 'Text', + parent: { + component: 'Tab', + state: ['disabled'] + }, + directives: { + textOpacity: 0.25, + textOpacityMode: 'blend' + } + }, + { + component: 'Icon', + parent: { + component: 'Tab', + state: ['active'] + }, + directives: { + textColor: '--text' + } + }, + { + component: 'Icon', + parent: { + component: 'Tab', + state: ['active', 'hover'] + }, + directives: { + textColor: '--text' + } + } + ] +} diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx @@ -97,7 +97,7 @@ export default { .map((slot, index) => { const props = slot.props if (!props) return - const classesTab = ['tab', 'button-default'] + const classesTab = ['tab'] const classesWrapper = ['tab-wrapper'] if (this.activeIndex === index) { classesTab.push('active') diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - /* stylelint-disable no-descending-specificity */ .tab-switcher { display: flex; @@ -25,8 +23,7 @@ content: ""; flex: 1 1 auto; border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + border-bottom-color: var(--border); } .tab-wrapper { @@ -37,8 +34,7 @@ right: 0; bottom: 0; border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + border-bottom-color: var(--border); } } @@ -80,8 +76,7 @@ flex-basis: 0.5em; content: ""; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &::after { @@ -106,16 +101,14 @@ right: 0; bottom: 0; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &::before { flex: 0 0 6px; content: ""; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &:last-child .tab { @@ -173,6 +166,15 @@ } .tab { + user-select: none; + color: var(--text); + border: none; + cursor: pointer; + box-shadow: var(--shadow); + font-size: 1em; + font-family: var(--font); + border-radius: var(--roundness); + background-color: var(--background); position: relative; white-space: nowrap; padding: 6px 1em; @@ -188,8 +190,6 @@ &.active { background: transparent; z-index: 5; - color: $fallback--text; - color: var(--tabActiveText, $fallback--text); } img { @@ -231,7 +231,7 @@ margin-top: 0.5em; margin-left: 0.2em; margin-bottom: 0.25em; - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); @media all and (min-width: 800px) { display: none; diff --git a/src/components/text.style.js b/src/components/text.style.js @@ -0,0 +1,22 @@ +export default { + name: 'Text', + selector: '/*text*/', + virtual: true, + states: { + faint: '.faint' + }, + defaultRules: [ + { + directives: { + textColor: '--text', + textAuto: 'no-preserve' + } + }, + { + state: ['faint'], + directives: { + textOpacity: 0.5 + } + } + ] +} diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue @@ -119,15 +119,13 @@ <script src="./thread_tree.js"></script> <style lang="scss"> -@import "../../variables"; - .thread-tree-replies { - margin-left: var(--status-margin, $status-margin); - border-left: 2px solid var(--border, $fallback--border); + margin-left: var(--status-margin); + border-left: 2px solid var(--border); } .thread-tree-replies-hidden { - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); /* Make the button stretch along the whole row */ display: flex; diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js @@ -77,13 +77,13 @@ const Timeline = { } }, classes () { - let rootClasses = !this.embedded ? ['panel', 'panel-default'] : ['-nonpanel'] + let rootClasses = !this.embedded ? ['panel', 'panel-default'] : ['-embedded'] if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention']) return { root: rootClasses, - header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : []), - body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []), - footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : []) + header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading', '-sticky'] : ['panel-body']), + body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : ['panel-body']), + footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : ['panel-body']) } }, // id map of statuses which need to be hidden in the main list due to pinning logic diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss @@ -1,31 +1,20 @@ -@import "../../variables"; - .Timeline { - .alert-dot { - border-radius: 100%; - height: 8px; - width: 8px; - position: absolute; - left: calc(50% - 4px); - top: calc(50% - 4px); - margin-left: 6px; - margin-top: -6px; - background-color: var(--badgeNeutral); + .timeline-body { + background: none; + backdrop-filter: none; } .alert-badge { font-size: 0.75em; line-height: 1; text-align: right; - border-radius: var(--tooltipRadius); + border-radius: var(--roundness); position: absolute; left: calc(50% - 0.5em); top: calc(50% - 0.4em); padding: 0.2em; margin-left: 0.7em; margin-top: -1em; - background-color: var(--badgeNeutral); - color: var(--badgeNeutralText); } .loadmore-button { @@ -41,12 +30,17 @@ z-index: 2; } - &.-nonpanel { + &.-embedded { .timeline-heading { text-align: center; line-height: 2.75em; padding: 0 0.5em; + // Override the shrug empty filler + &:empty::before { + content: initial; + } + .button-default, .alert { line-height: 2em; diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue @@ -38,7 +38,7 @@ fixed-width icon="circle-plus" /> - <div class="alert-badge"> + <div class="badge -counter"> {{ mobileLoadButtonString }} </div> </button> diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue @@ -45,8 +45,6 @@ <script src="./timeline_menu.js"></script> <style lang="scss"> -@import "../../variables"; - .timeline-menu-popover { min-width: 24rem; max-width: 100vw; @@ -60,65 +58,6 @@ margin: 0; padding: 0; } - - a { - display: block; - padding: 0 0.65em; - height: 3.5em; - line-height: 3.5em; - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - } - - &.router-link-active { - font-weight: bolder; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - - &:hover { - text-decoration: underline; - } - } - - svg { - margin-right: 0.4em; - margin-left: -0.2em; - } - } - - li { - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - padding: 0; - - &:last-child a { - border-bottom-right-radius: $fallback--panelRadius; - border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); - border-bottom-left-radius: $fallback--panelRadius; - border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); - } - - &:last-child { - border: none; - } - } } .TimelineMenu { @@ -159,8 +98,6 @@ } &.open .timeline-menu-title svg { - color: $fallback--text; - color: var(--panelText, $fallback--text); transform: rotate(180deg); } diff --git a/src/components/top_bar.style.js b/src/components/top_bar.style.js @@ -0,0 +1,28 @@ +export default { + name: 'TopBar', + selector: 'nav', + validInnerComponents: [ + 'Link', + 'Text', + 'Icon', + 'Button', + 'ButtonUnstyled', + 'Input', + 'Badge' + ], + defaultRules: [ + { + directives: { + background: '--fg', + shadow: [{ + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }] + } + } + ] +} diff --git a/src/components/underlay.style.js b/src/components/underlay.style.js @@ -0,0 +1,19 @@ +export default { + name: 'Underlay', + selector: '#content', + // Out of tree selector: Most components are laid over underlay, but underlay itself is not part of the DOM tree, + // i.e. it's a separate absolutely-positioned component, so we need to treat it differently depending on whether + // we are searching for underlay specifically or for whatever is laid on top of it. + outOfTreeSelector: '.underlay', + validInnerComponents: [ + 'Panel' + ], + defaultRules: [ + { + directives: { + background: '#000000', + opacity: 0.2 + } + } + ] +} diff --git a/src/components/update_notification/update_notification.scss b/src/components/update_notification/update_notification.scss @@ -1,5 +1,3 @@ -@import "src/variables"; - .UpdateNotification { overflow: hidden; } @@ -48,7 +46,7 @@ .panel-body { border-width: 0 0 1px; border-style: solid; - border-color: var(--border, $fallback--border); + border-color: var(--border); } .panel-footer { diff --git a/src/components/user_avatar/avatar.style.js b/src/components/user_avatar/avatar.style.js @@ -0,0 +1,22 @@ +export default { + name: 'Avatar', + selector: '.Avatar', + variants: { + compact: '.-compact' + }, + defaultRules: [ + { + directives: { + roundness: 3, + shadow: [{ + x: 0, + y: 1, + blur: 8, + spread: 0, + color: '#000000', + alpha: 0.7 + }] + } + } + ] +} diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue @@ -32,12 +32,10 @@ <script src="./user_avatar.js"></script> <style lang="scss"> -@import "../../variables"; - .Avatar { - --_avatarShadowBox: var(--avatarStatusShadow); - --_avatarShadowFilter: var(--avatarStatusShadowFilter); - --_avatarShadowInset: var(--avatarStatusShadowInset); + --_avatarShadowBox: var(--shadow); + --_avatarShadowFilter: var(--shadowFilter); + --_avatarShadowInset: var(--shadowInset); --_still-image-label-visibility: hidden; display: inline-block; @@ -48,16 +46,14 @@ &.-compact { width: 32px; height: 32px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); } .avatar { width: 100%; height: 100%; box-shadow: var(--_avatarShadowBox); - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); &.-better-shadow { box-shadow: var(--_avatarShadowInset); @@ -69,13 +65,11 @@ } &.-compact { - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + border-radius: var(--roundness); } &.-placeholder { - background-color: $fallback--fg; - background-color: var(--fg, $fallback--fg); + background-color: var(--background); } } @@ -92,7 +86,7 @@ padding: 0.2em; background: rgb(127 127 127 / 50%); color: #fff; - border-radius: var(--tooltipRadius); + border-radius: var(--roundness); } } </style> diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .user-card { position: relative; z-index: 1; @@ -21,14 +19,6 @@ position: relative; } - .panel-body { - word-wrap: break-word; - border-bottom-right-radius: inherit; - border-bottom-left-radius: inherit; - // create new stacking context - position: relative; - } - .background-image { position: absolute; top: 0; @@ -62,11 +52,6 @@ padding: 1em; margin: 0; - a { - color: $fallback--link; - color: var(--postLink, $fallback--link); - } - img { object-fit: contain; vertical-align: middle; @@ -76,53 +61,37 @@ } &.-rounded-t { - border-top-left-radius: $fallback--panelRadius; - border-top-left-radius: var(--panelRadius, $fallback--panelRadius); - border-top-right-radius: $fallback--panelRadius; - border-top-right-radius: var(--panelRadius, $fallback--panelRadius); + border-top-left-radius: var(--roundness); + border-top-right-radius: var(--roundness); - --__roundnessTop: var(--panelRadius); + --__roundnessTop: var(--roundness); --__roundnessBottom: 0; } &.-rounded { - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); - --__roundnessTop: var(--panelRadius); - --__roundnessBottom: var(--panelRadius); + --__roundnessTop: var(--roundness); + --__roundnessBottom: var(--roundness); } &.-popover { - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); - --__roundnessTop: var(--tooltipRadius); - --__roundnessBottom: var(--tooltipRadius); + --__roundnessTop: var(--roundness); + --__roundnessBottom: var(--roundness); } &.-bordered { border-width: 1px; border-style: solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); } } .user-info { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); padding: 0 26px; - a { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - - &:hover { - color: var(--icon); - } - } - .container { min-width: 0; padding: 16px 0 6px; @@ -164,8 +133,7 @@ display: flex; justify-content: center; align-items: center; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); opacity: 0; transition: opacity 0.2s ease; @@ -188,8 +156,7 @@ padding: 0.5em 0; &:not(:hover) .icon { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); + color: var(--lightText); } } @@ -203,6 +170,7 @@ } .user-screen-name { + color: var(--text); min-width: 1px; flex: 0 1 auto; text-overflow: ellipsis; @@ -214,16 +182,11 @@ flex: 0 0 auto; margin-left: 1em; font-size: 0.7em; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } .user-role { flex: none; - color: $fallback--text; - color: var(--alertNeutralText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--alertNeutral, $fallback--fg); } } @@ -241,6 +204,11 @@ --emoji-size: 1.7em; + .RichContent { + /* stylelint-disable-next-line declaration-no-important */ + --link: var(--text) !important; + } + .top-line, .bottom-line { display: flex; @@ -334,8 +302,6 @@ padding: 0.5em 1.5em 0; text-align: center; justify-content: space-between; - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); flex-wrap: wrap; } diff --git a/src/components/user_card/user_card.style.js b/src/components/user_card/user_card.style.js @@ -0,0 +1,41 @@ +export default { + name: 'UserCard', + selector: '.user-card', + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Button', + 'ButtonUnstyled', + 'Input', + 'RichContent', + 'Alert' + ], + defaultRules: [ + { + directives: { + background: '--bg', + opacity: 0, + roundness: 3, + shadow: [{ + x: 1, + y: 1, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }], + '--profileTint': 'color | $alpha(--background, 0.5)' + } + }, + { + parent: { + component: 'UserCard' + }, + component: 'RichContent', + directives: { + opacity: 0 + } + } + ] +} diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -113,19 +113,19 @@ <template v-if="!hideBio"> <span v-if="user.deactivated" - class="alert user-role" + class="alert neutral user-role" > {{ $t('user_card.deactivated') }} </span> <span v-if="!!visibleRole" - class="alert user-role" + class="alert neutral user-role" > {{ $t(`general.role.${visibleRole}`) }} </span> <span v-if="user.actor_type === 'Service'" - class="alert user-role" + class="alert neutral user-role" > {{ $t('user_card.bot') }} </span> @@ -166,14 +166,14 @@ v-if="userHighlightType !== 'disabled'" :id="'userHighlightColorTx'+user.id" v-model="userHighlightColor" - class="userHighlightText" + class="input userHighlightText" type="text" > <input v-if="userHighlightType !== 'disabled'" :id="'userHighlightColor'+user.id" v-model="userHighlightColor" - class="userHighlightCl" + class="input userHighlightCl" type="color" > {{ ' ' }} @@ -282,10 +282,7 @@ /> </div> </div> - <div - v-if="!hideBio" - class="panel-body" - > + <div v-if="!hideBio"> <div v-if="!mergedConfig.hideUserStats && switcher" class="user-counts" diff --git a/src/components/user_list_menu/user_list_menu.vue b/src/components/user_list_menu/user_list_menu.vue @@ -10,11 +10,11 @@ <button v-for="list in lists" :key="list.id" - class="button-default dropdown-item" + class="menu-item dropdown-item" @click="toggleList(list.id)" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': list.inList }" /> {{ list.title }} @@ -22,7 +22,7 @@ </div> </template> <template #trigger> - <button class="btn button-default dropdown-item -has-submenu"> + <button class="menu-item dropdown-item -has-submenu"> {{ $t('lists.manage_lists') }} <FAIcon class="chevron-icon" diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue @@ -48,8 +48,6 @@ <script src="./user_list_popover.js"></script> <style lang="scss"> -@import "../../variables"; - .user-list-popover { padding: 0.5em; diff --git a/src/components/user_note/user_note.vue b/src/components/user_note/user_note.vue @@ -33,7 +33,7 @@ <textarea v-show="editing" v-model="localNote" - class="note-text" + class="input note-text" /> <span v-show="!editing" @@ -48,8 +48,6 @@ <script src="./user_note.js"></script> <style lang="scss"> -@import "../../variables"; - .user-note { display: flex; flex-direction: column; @@ -82,7 +80,7 @@ .note-text.-blank { font-style: italic; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } } </style> diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue @@ -22,8 +22,15 @@ <script src="./user_panel.js"></script> <style lang="scss"> -.user-panel .signed-in { - overflow: visible; - z-index: 10; +.user-panel { + .panel { + background: var(--background); + backdrop-filter: var(--backdrop-filter); + } + + .signed-in { + overflow: visible; + z-index: 10; + } } </style> diff --git a/src/components/user_popover/user_popover.vue b/src/components/user_popover/user_popover.vue @@ -24,8 +24,6 @@ <script src="./user_popover.js"></script> <style lang="scss"> -@import "../../variables"; - /* popover styles load on-demand, so we need to override */ /* stylelint-disable block-no-empty */ .user-popover.popover { diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue @@ -4,52 +4,54 @@ v-if="user" class="user-profile panel panel-default" > - <UserCard - :user-id="userId" - :switcher="true" - :selected="timeline.viewing" - avatar-action="zoom" - rounded="top" - :has-note-editor="true" - /> - <span - v-if="!!user.birthday" - class="user-birthday" - > - <FAIcon - class="fa-old-padding" - icon="birthday-cake" + <div class="panel-body"> + <UserCard + :user-id="userId" + :switcher="true" + :selected="timeline.viewing" + avatar-action="zoom" + rounded="top" + :has-note-editor="true" /> - {{ $t('user_card.birthday', { birthday: formattedBirthday }) }} - </span> - <div - v-if="user.fields_html && user.fields_html.length > 0" - class="user-profile-fields" - > - <dl - v-for="(field, index) in user.fields_html" - :key="index" - class="user-profile-field" + <span + v-if="!!user.birthday" + class="user-birthday" > - <dt - :title="user.fields_text[index].name" - class="user-profile-field-name" - > - <RichContent - :html="field.name" - :emoji="user.emoji" - /> - </dt> - <dd - :title="user.fields_text[index].value" - class="user-profile-field-value" + <FAIcon + class="fa-old-padding" + icon="birthday-cake" + /> + {{ $t('user_card.birthday', { birthday: formattedBirthday }) }} + </span> + <div + v-if="user.fields_html && user.fields_html.length > 0" + class="user-profile-fields" + > + <dl + v-for="(field, index) in user.fields_html" + :key="index" + class="user-profile-field" > - <RichContent - :html="field.value" - :emoji="user.emoji" - /> - </dd> - </dl> + <dt + :title="user.fields_text[index].name" + class="user-profile-field-name" + > + <RichContent + :html="field.name" + :emoji="user.emoji" + /> + </dt> + <dd + :title="user.fields_text[index].value" + class="user-profile-field-value" + > + <RichContent + :html="field.value" + :emoji="user.emoji" + /> + </dd> + </dl> + </div> </div> <tab-switcher :active-tab="tab" @@ -72,10 +74,14 @@ <div v-if="followsTabVisible" key="followees" + class="panel-body" :label="$t('user_card.followees')" :disabled="!user.friends_count" > - <FriendList :user-id="userId"> + <FriendList + :user-id="userId" + :non-interactive="true" + > <template #item="{item}"> <FollowCard :user="item" /> </template> @@ -84,10 +90,14 @@ <div v-if="followersTabVisible" key="followers" + class="panel-body" :label="$t('user_card.followers')" :disabled="!user.followers_count" > - <FollowerList :user-id="userId"> + <FollowerList + :user-id="userId" + :non-interactive="true" + > <template #item="{item}"> <FollowCard :user="item" @@ -117,7 +127,7 @@ :title="$t('user_card.favorites')" timeline-name="favorites" :timeline="favorites" - :user-id="userId" + :user-id="isUs ? undefined : userId" :in-profile="true" :footer-slipgate="footerRef" /> @@ -136,7 +146,7 @@ {{ $t('settings.profile_tab') }} </div> </div> - <div class="panel-body"> + <div> <span v-if="error">{{ error }}</span> <FAIcon v-else @@ -151,8 +161,6 @@ <script src="./user_profile.js"></script> <style lang="scss"> -@import "../../variables"; - .user-profile { flex: 2; flex-basis: 500px; @@ -182,9 +190,8 @@ .user-profile-field { display: flex; margin: 0.25em; - border: 1px solid var(--border, $fallback--border); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border: 1px solid var(--border); + border-radius: var(--roundness); .user-profile-field-name { flex: 0 1 30%; @@ -192,7 +199,7 @@ text-align: right; color: var(--lightText); min-width: 120px; - border-right: 1px solid var(--border, $fallback--border); + border-right: 1px solid var(--border); } .user-profile-field-value { @@ -229,4 +236,5 @@ padding: 7em; } } + </style> diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -19,7 +19,7 @@ <p>{{ $t('user_reporting.add_comment_description') }}</p> <textarea v-model="comment" - class="form-control" + class="input form-control" :placeholder="$t('user_reporting.additional_comments')" rows="1" @input="resize" @@ -72,8 +72,6 @@ <script src="./user_reporting_modal.js"></script> <style lang="scss"> -@import "../../variables"; - .user-reporting-panel { width: 90vw; max-width: 700px; @@ -84,8 +82,7 @@ display: flex; flex-direction: column-reverse; border-top: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); overflow: hidden; } @@ -155,8 +152,7 @@ width: 50%; max-width: 320px; border-right: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); padding: 1.1em; > div { diff --git a/src/hocs/with_load_more/with_load_more.scss b/src/hocs/with_load_more/with_load_more.scss @@ -1,12 +1,9 @@ -@import "../../variables"; - .with-load-more { &-footer { padding: 10px; text-align: center; border-top: 1px solid; - border-top-color: $fallback--border; - border-top-color: var(--border, $fallback--border); + border-top-color: var(--border); .error { font-size: 1rem; diff --git a/src/i18n/cs.json b/src/i18n/cs.json @@ -737,7 +737,8 @@ "frontend_version": "Frontend verze" }, "commit_value_tooltip": "Hodnota není uložena, stiskněte toto tlačítko pro potvrzení změn", - "hard_reset_value_tooltip": "Odstranit nastavení z úložiště a vynutit výchozí hodnotu" + "hard_reset_value_tooltip": "Odstranit nastavení z úložiště a vynutit výchozí hodnotu", + "accent": "Akcentní barva" }, "time": { "day": "{0} day", @@ -748,7 +749,7 @@ "hours": "{0} hours", "hour_short": "{0}h", "hours_short": "{0}h", - "in_future": "in {0}", + "in_future": "za {0}", "in_past": "před {0}", "minute": "{0} minute", "minutes": "{0} minutes", @@ -758,8 +759,8 @@ "months": "{0} měs", "month_short": "{0} měs", "months_short": "{0} měs", - "now": "teď", - "now_short": "teď", + "now": "právě teď", + "now_short": "nyní", "second": "{0} second", "seconds": "{0} seconds", "second_short": "{0}s", @@ -771,7 +772,23 @@ "year": "{0} r", "years": "{0} l", "year_short": "{0}r", - "years_short": "{0}l" + "years_short": "{0}l", + "unit": { + "seconds_short": "{0}s", + "days": "{0} den | {0} dnů", + "days_short": "{0}d", + "hours": "{0} hodina | {0} hodin", + "hours_short": "{0}h", + "minutes": "{0} minuta | {0} minut", + "months": "{0} měsíc | {0} měsíců", + "months_short": "{0}mo", + "minutes_short": "{0}min", + "seconds": "{0} sekunda | {0} sekund", + "weeks": "{0} týden | {0} týdnů", + "weeks_short": "{0}w", + "years": "{0} rok | {0} roky", + "years_short": "{0}y" + } }, "timeline": { "collapse": "Zabalit", @@ -783,11 +800,60 @@ "show_new": "Zobrazit nové", "up_to_date": "Aktuální", "no_more_statuses": "Žádné další příspěvky", - "no_statuses": "Žádné příspěvky" + "no_statuses": "Žádné příspěvky", + "socket_reconnected": "Navázáno spojení v reálném čase", + "error": "Chyba při načítání časové osy: {0}", + "reload": "Načíst znovu", + "socket_broke": "Spojení v reálném čase ztraceno: CloseEvent code {0}" }, "status": { "reply_to": "Odpověď uživateli", - "replies_list": "Odpovědi:" + "replies_list": "Odpovědi:", + "many_attachments": "Příspěvek má {number} příloh(u)", + "collapse_attachments": "Sbalit přílohy", + "unpin": "Odepnout z profilu", + "thread_muted": "Vlákno ztlumeno", + "show_attachment_description": "Popis náhledu (otevřete přílohu pro celý popis)", + "move_down": "Posunout přílohu doprava", + "thread_show": "Zobrazit toto vlákno", + "pin": "Připnout na profil", + "mute_conversation": "Ztlumit konverzaci", + "thread_hide": "Skrýt toto vlákno", + "show_full_subject": "Zobrazit celý předmět", + "edited_at": "(naposledy upraveno {time})", + "repeat_confirm_accept_button": "Zopakovat", + "repeat_confirm_title": "Potvrzení zopakování", + "delete_error": "Chyba při mazání příspěvku: {0}", + "delete_confirm": "Opravdu chcete smazat tento příspěvek?", + "delete_confirm_title": "Potvrzení smazání", + "delete_confirm_accept_button": "Smazat", + "delete_confirm_cancel_button": "Ponechat", + "you": "(Vy)", + "hide_attachment": "Skrýt přílohu", + "remove_attachment": "Odstranit přílohu", + "attachment_stop_flash": "Zastavit Flash player", + "nsfw": "NSFW", + "repeat_confirm_cancel_button": "Neopakovat", + "favorites": "Oblíbené", + "repeats": "Opakovaní", + "repeat_confirm": "Opravdu chcete zopakovat tento příspěvek?", + "delete": "Smazat příspěvek", + "copy_link": "Kopírovat odkaz k příspěvku", + "external_source": "Externí zdroj", + "edit": "Upravit příspěvek", + "bookmark": "Přidat do záložek", + "unbookmark": "Odebrat ze záložek", + "mentions": "Zmínky", + "hide_full_subject": "Skrýt celý předmět", + "show_content": "Zobrazit obsah", + "hide_content": "Skrýt obsah", + "unmute_conversation": "Zrušit ztlumení konverzace", + "status_unavailable": "Příspěvek je nedostupný", + "status_deleted": "Tento příspěvek byl smazán", + "expand": "Rozbalit", + "show_all_attachments": "Zobrazit všechny přílohy", + "move_up": "Posunout přílohu doleva", + "open_gallery": "Otevřít galerii" }, "user_card": { "approve": "Schválit", @@ -995,13 +1061,121 @@ "nodb": "Žádné nastavení v databázi", "frontends": "Frontendy", "instance": "Instance", - "limits": "Limity" + "limits": "Limity", + "emoji": "Emoji" }, "nodb": { - "heading": "Nastavení v databázi je vypnuto" + "heading": "Nastavení v databázi je vypnuto", + "documentation": "dokumentace", + "text2": "Většina konfiguračních možností nebude dostupná." }, "wip_notice": "Tento administrační panel je experimentální a v aktivní vývoji, {adminFeLink}.", "old_ui_link": "staré administrační rozhraní je dostupné zde", - "reset_all": "Resetovat vše" + "reset_all": "Resetovat vše", + "frontend": { + "failure_installing_frontend": "Nepodařilo se nainstalovat frontend {version}: {reason}", + "reinstall": "Přeinstalovat", + "available_frontends": "Dostupné k instalaci", + "is_default": "(Výchozí)", + "versions": "Dostupné verze", + "build_url": "URL sestavení", + "install": "Instalovat", + "install_version": "Instalovat verzi {version}", + "more_install_options": "Více instalačních možností", + "more_default_options": "Více výchozích nastavení pro možnosti", + "set_default": "Nastavit výchozí", + "default_frontend": "Výchozí frontend", + "set_default_version": "Nastavit verzi {version} jako výchozí", + "repository": "Odkaz k repozitáři", + "is_default_custom": "(Výchozí, verze: {version})", + "success_installing_frontend": "Frontend {version} byl úspěšně nainstalován" + }, + "captcha": { + "native": "Nativní", + "kocaptcha": "KoCaptcha" + }, + "instance": { + "instance": "Informace o instanci", + "captcha_header": "CAPTCHA", + "restrict": { + "activities": "Přístup k příspěvkům a aktivitám", + "timelines": "Přístup k časovým osám", + "profiles": "Přístup k uživatelským profilům", + "header": "Omezit přístup pro anonymní návštěvníky" + }, + "registrations": "Registrace uživatelů", + "kocaptcha": "KoCaptcha nastavení" + }, + "limits": { + "posts": "Limity příspěvků", + "uploads": "Limity příloh", + "users": "Limity uživatelských profilů", + "arbitrary_limits": "Libovolné limity", + "profile_fields": "Limity profilových polí", + "user_uploads": "Limity médií profilů" + }, + "emoji": { + "global_actions": "Globální akce", + "reload": "Znovu načíst emoji", + "importFS": "Importovat emoji ze souborového systému", + "error": "Chyba: {0}", + "create_pack": "Vytvořit balíček", + "delete_pack": "Smazat balíček", + "new_pack_name": "Nový název balíčku", + "create": "Vytvořit", + "emoji_packs": "Emoji balíčky", + "remote_packs": "Vzdálené balíčky", + "do_list": "List", + "emoji_pack": "Emoji balíček", + "edit_pack": "Upravit balíček", + "description": "Popis", + "homepage": "Domovská stránka", + "fallback_src": "Záložní zdroj", + "fallback_sha256": "Záložní SHA256", + "share": "Sdílet", + "save": "Uložit", + "save_meta": "Uložit metadata", + "revert_meta": "Vrátit zpět metadata", + "delete": "Smazat", + "add_file": "Přidat soubor", + "adding_new": "Přidávání nových emoji", + "shortcode": "Zkratka", + "filename": "Jméno souboru", + "new_shortcode": "Zkrat, ponechte prázdné pro odvození", + "delete_confirm": "Opravdu chcete smazat {0}?", + "download_pack": "Stáhnout balíček", + "downloading_pack": "Stahování {0}", + "download": "Stáhnout", + "download_as_name": "Nové jméno", + "download_as_name_full": "Nové jméno, pro opakované použití ponechte prázdné", + "files": "Soubory", + "editing": "Upravování {0}", + "delete_title": "Smazat?", + "emoji_changed": "Neuložené změny emoji souborů, zkontrolujte zvýrazněné emoji", + "replace_warning": "Tímto se NAHRADÍ místní balíček se stejným jménem", + "metadata_changed": "Metadata jsou rozdílné od uložených", + "revert": "Vrátit zpět", + "new_filename": "Jméno souboru, ponechte prázdné pro odvození" + }, + "temp_overrides": { + ":pleroma": { + ":instance": { + ":background_image": { + "label": "Obrázek na pozadí", + "description": "Obrázek na pozadí (především používáno PleromaFE)" + }, + ":description_limit": { + "label": "Limit", + "description": "Limit počtu znaků pro popisy příloh" + }, + ":public": { + "label": "Instance je veřejná" + }, + ":limit_to_local_content": { + "label": "Limitovat vyhledávání pouze na místní obsah" + } + } + } + } } } diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -186,7 +186,6 @@ "edit_pinned": "Edit pinned items", "edit_finish": "Done editing", "mobile_sidebar": "Toggle mobile sidebar", - "mobile_notifications": "Open notifications", "mobile_notifications": "Open notifications (there are unread ones)", "mobile_notifications_close": "Close notifications", "mobile_notifications_mark_as_seen": "Mark all as seen", @@ -511,6 +510,7 @@ "hide_actor_type_indication": "Hide actor type (bots, groups, etc.) indication in posts", "hide_scrobbles": "Hide scrobbles", "hide_scrobbles_after": "Hide scrobbles older than", + "mute_sensitive_posts": "Mute sensitive posts", "hide_all_muted_posts": "Hide muted posts", "max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)", "hide_isp": "Hide instance-specific panel", @@ -915,7 +915,7 @@ "description": "Detailed setting for allowing/disallowing access to certain aspects of API. By default (indeterminate state) it will disallow if instance is not public, ticked checkbox means disallow access even if instance is public, unticked means allow access even if instance is private. Please note that unexpected behavior might happen if some settings are set, i.e. if profile access is disabled posts will show without profile information.", "timelines": "Timelines access", "profiles": "User profiles access", - "activities": "Statues/activities access" + "activities": "Statuses/activities access" } }, "limits": { @@ -940,8 +940,8 @@ "set_default": "Set default", "set_default_version": "Set version {version} as default", "wip_notice": "Please note that this section is a WIP and lacks certain features as backend implementation of front-end management is incomplete.", - "default_frontend": "Default front-end", - "default_frontend_tip": "Default front-end will be shown to all users. Currently there's no way to for a user to select personal front-end. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.", + "default_frontend": "Default frontend", + "default_frontend_tip": "Default frontend will be shown to all users. Currently there's no way to for a user to select personal frontend. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.", "default_frontend_unavail": "Default frontend settings are not available, as this requires configuration in the database", "available_frontends": "Available for install", "failure_installing_frontend": "Failed to install frontend {version}: {reason}", @@ -1084,6 +1084,7 @@ "external_source": "External source", "thread_muted": "Thread muted", "thread_muted_and_words": ", has words:", + "sensitive_muted": "Muting sensitive content", "show_full_subject": "Show full subject", "hide_full_subject": "Hide full subject", "show_content": "Show content", @@ -1120,7 +1121,9 @@ "hide_quote": "Hide the quoted status", "display_quote": "Display the quoted status", "invisible_quote": "Quoted status unavailable: {link}", - "more_actions": "More actions on this status" + "more_actions": "More actions on this status", + "loading": "Loading...", + "load_error": "Unable to load status: {error}" }, "user_card": { "approve": "Approve", diff --git a/src/i18n/fr.json b/src/i18n/fr.json @@ -90,7 +90,11 @@ "heading": { "totp": "Authentification à double-facteur", "recovery": "Récupération de l'authentification à double-facteur" - } + }, + "logout_confirm_title": "Confirmation de déconnexion", + "logout_confirm": "Souhaitez-vous vous déconnecter ?", + "logout_confirm_accept_button": "Déconnexion", + "logout_confirm_cancel_button": "Ne pas se déconnecter" }, "media_modal": { "previous": "Précédent", @@ -110,7 +114,7 @@ "timeline": "Flux personnel", "twkn": "Réseau connu", "user_search": "Recherche de comptes", - "who_to_follow": "Suggestion de suivit", + "who_to_follow": "Suggestion de suivi", "preferences": "Préférences", "search": "Recherche", "administration": "Administration", @@ -124,7 +128,10 @@ "edit_pinned": "Éditer les éléments agrafés", "edit_finish": "Édition terminée", "mobile_sidebar": "(Dés)activer le panneau latéral", - "mobile_notifications_close": "Fermer les notifications" + "mobile_notifications_close": "Fermer les notifications", + "search_close": "Fermer la barre de recherche", + "announcements": "Annonces", + "mobile_notifications_mark_as_seen": "Marquer tout comme vu" }, "notifications": { "broken_favorite": "Message inconnu, recherche en cours…", @@ -140,7 +147,13 @@ "follow_request": "veut vous suivre", "error": "Erreur de chargement des notifications : {0}", "poll_ended": "Sondage terminé", - "submitted_report": "Rapport envoyé" + "submitted_report": "Rapport envoyé", + "unread_announcements": "{num} annonce non lue | {num} annonces non lues", + "unread_chats": "{num} message non lu | {num} messages non lus", + "configuration_tip_settings": "les préférences", + "unread_follow_requests": "{num} nouvelle demande de suivi | {num} nouvelles demandes de suivi", + "configuration_tip": "Vous pouvez personnaliser ce qui est affiché ici dans {theSettings}. {dismiss}", + "configuration_tip_dismiss": "Ne plus montrer" }, "interactions": { "favs_repeats": "Partages et favoris", @@ -154,7 +167,7 @@ "new_status": "Poster un nouveau statut", "account_not_locked_warning": "Votre compte n'est pas {0}. N'importe qui peut vous suivre pour voir vos billets en Abonné·e·s uniquement.", "account_not_locked_warning_link": "verrouillé", - "attachments_sensitive": "Marquer les pièce-jointes comme sensible", + "attachments_sensitive": "Marquer les pièces jointes comme sensible", "content_type": { "text/plain": "Texte brut", "text/html": "HTML", @@ -183,9 +196,13 @@ "preview": "Prévisualisation", "media_description": "Description de la pièce-jointe", "post": "Post", - "edit_status": "Éditer le status", + "edit_status": "Éditer le statut", "edit_remote_warning": "Des instances distantes pourraient ne pas supporter l'édition et seront incapables de recevoir la nouvelle version de votre post.", - "edit_unsupported_warning": "Pleroma ne supporte pas l'édition de mentions ni de sondages." + "edit_unsupported_warning": "Pleroma ne supporte pas l'édition de mentions ni de sondages.", + "reply_option": "Répondre à ce statut", + "quote_option": "Citer ce statut", + "scope_notice_dismiss": "Fermer ce message", + "content_type_selection": "Format du statut" }, "registration": { "bio": "Biographie", @@ -205,14 +222,18 @@ "email_required": "ne peut pas être laissé vide", "password_required": "ne peut pas être laissé vide", "password_confirmation_required": "ne peut pas être laissé vide", - "password_confirmation_match": "doit être identique au mot de passe" + "password_confirmation_match": "doit être identique au mot de passe", + "birthday_min_age": "doit être le ou avant le {date}", + "birthday_required": "ne peut pas être vide" }, "reason_placeholder": "Cette instance modère les inscriptions manuellement.\nExpliquer ce qui motive votre inscription à l'administration.", "reason": "Motivation d'inscription", "register": "Enregistrer", "email_language": "Dans quelle langue voulez-vous recevoir les emails du server ?", "bio_optional": "Biographie (optionnelle)", - "email_optional": "Courriel (optionnel)" + "email_optional": "Courriel (optionnel)", + "birthday": "Anniversaire :", + "birthday_optional": "Anniversaire (optionnel) :" }, "selectable_list": { "select_all": "Tout selectionner" @@ -684,7 +705,64 @@ "use_websockets": "Utiliser les websockets (mises à jour en temps réel)", "user_popover_avatar_action_zoom": "Zoomer sur l'avatar", "user_popover_avatar_action_open": "Ouvrir le profil", - "conversation_display_tree_quick": "Vue arborescente" + "conversation_display_tree_quick": "Vue arborescente", + "emoji_reactions_scale": "Taille des réactions", + "backup_running": "Cette sauvegarde est en cours, {number} enregistrement effectué. | Cette sauvegarde est en cours, {number} enregistrements effectués.", + "backup_failed": "Cette sauvegarde a échoué.", + "autocomplete_select_first": "Sélectionner automatiquement la première occurrence lorsque les résultats de l'autocomplétion sont disponibles", + "confirm_dialogs_unfollow": "arrête de suivre un utilisateur", + "confirm_dialogs_repeat": "reposte un statut", + "actor_type": "Ce compte est :", + "actor_type_Person": "un utilisateur normal", + "actor_type_Service": "un robot", + "actor_type_Group": "un groupe", + "confirm_dialogs_logout": "à la déconnexion", + "confirm_dialogs_approve_follow": "accepte un nouvel abonné", + "confirm_dialogs_deny_follow": "refuse un nouvel abonné", + "confirm_dialogs_remove_follower": "supprime un abonné", + "actor_type_description": "En marquant votre compte comme un groupe, vous répétez automatiquement les statuts qui le mentionnent.", + "add_language": "Ajouter une langue de remplacement", + "remove_language": "Supprimer", + "primary_language": "Langue principale :", + "fallback_language": "Langue de remplacement {index} :", + "confirm_dialogs": "Demande de confirmation quand", + "confirm_dialogs_block": "bloque un utilisateur", + "confirm_dialogs_mute": "mute un utilisateur", + "confirm_dialogs_delete": "supprime un statut", + "url": "URL", + "preview": "Aperçu", + "reset_value": "Réinitialiser", + "hard_reset_value_tooltip": "Supprime le réglage du stockage, force l'utilisation de la valeur par défaut", + "reset_value_tooltip": "Réinitialiser le brouillon", + "hard_reset_value": "Remise à zéro", + "hide_actor_type_indication": "Cacher le type (robots, groupes, etc.) dans les status", + "notification_extra_follow_requests": "Afficher les nouvelles demandes de suivi", + "user_popover_avatar_action": "Action du clic sur l'avatar", + "user_popover_avatar_action_close": "Fermer la fenêtre contextuelle", + "notification_setting_ignore_inactionable_seen": "Ignorer les status de lecture des notifications non actionnables (favoris, répétitions, etc)", + "notification_setting_ignore_inactionable_seen_tip": "Ceci ne marquera pas ces notifications comme lues, et vous recevrez encore les notifications de bureau si vous le décidez", + "notification_setting_unseen_at_top": "Afficher les notifications non lues au-dessus des autres", + "notification_setting_filters_chrome_push": "Sur certains navigateurs (chrome), il peut être impossible de filtrer complètement les notifications par type lorsqu'elles arrivent", + "enable_web_push_always_show": "Toujours afficher les notifications web", + "commit_value": "Sauvegarder", + "hide_scrobbles": "Masquer les scrobbles", + "notification_setting_annoyance": "Agacement", + "notification_setting_drawer_marks_as_seen": "Fermer le tiroir marque toutes les notifications comme lues (mobile)", + "commit_value_tooltip": "Les valeurs ne sont pas sauvegardées, appuyez sur ce bouton pour soumettre vos changements", + "birthday": { + "show_birthday": "Afficher mon anniversaire", + "label": "Anniversaire" + }, + "notification_visibility_native_notifications": "Afficher une notification native", + "notification_visibility_follow_requests": "Demandes de suivi", + "notification_visibility_reports": "Rapports", + "notification_extra_chats": "Afficher les discussions non lues", + "notification_extra_announcements": "Afficher les annonces non lues", + "notification_extra_tip": "Afficher les astuces de personnalisation pour les notifications extras", + "enable_web_push_always_show_tip": "Certains navigateurs (Chromium, Chrome) exigent que les messages push donnent toujours lieu à une notification, sinon le message générique \"Le site web a été mis à jour en arrière-plan\" s'affiche ; activez cette option pour empêcher l'affichage de cette notification, car Chrome semble masquer les notifications push si l'onglet est au centre de l'attention. Cela peut entraîner l'affichage de notifications en double sur d'autres navigateurs.", + "user_popover_avatar_overlay": "Afficher la fenêtre contextuelle sur l'avatar de l'utilisateur", + "notification_visibility_in_column": "Afficher la colonne / le tiroir de notifications", + "notification_show_extra": "Afficher les extras dans la colonne de notifications" }, "timeline": { "collapse": "Fermer", @@ -758,7 +836,20 @@ "show_all_conversation": "Montrer tout le fil ({numStatus} autre message) | Montrer tout le fil ({numStatus} autre messages)", "edit": "Éditer le status", "edited_at": "(dernière édition {time})", - "status_history": "Historique du status" + "status_history": "Historique du status", + "delete_error": "Erreur de suppression du statut : {0}", + "repeat_confirm": "Voulez-vous réellement reposter ce statut ?", + "reaction_count_label": "{num} personne a réagi | {num} personnes ont réagi", + "repeat_confirm_cancel_button": "Ne pas reposter", + "hide_quote": "Masquer les status cités", + "display_quote": "Afficher les status cités", + "invisible_quote": "Citation de statut non disponible : {link}", + "delete_confirm_title": "Confirmer la suppression", + "more_actions": "Plus d'action sur ce statut", + "delete_confirm_cancel_button": "Conserver", + "repeat_confirm_title": "Confirmer reposte", + "repeat_confirm_accept_button": "Reposter", + "delete_confirm_accept_button": "Supprimer" }, "user_card": { "approve": "Accepter", @@ -828,7 +919,39 @@ "edit_profile": "Éditer le profil", "deactivated": "Désactivé", "follow_cancel": "Annuler la requête", - "remove_follower": "Retirer l'abonné·e" + "remove_follower": "Retirer l'abonné·e", + "remove_follower_confirm_accept_button": "Supprimer", + "approve_confirm_cancel_button": "Ne pas approuver", + "block_confirm_accept_button": "Bloquer", + "mute_confirm_title": "Confirmation de mise en sourdine", + "block_confirm_cancel_button": "Ne pas bloquer", + "unfollow_confirm": "Voulez-vous vraiment arrêter de suivre {user} ?", + "unfollow_confirm_accept_button": "Ne plus suivre", + "birthday": "Né(e) le {birthday}", + "edit_note": "Éditer note", + "edit_note_apply": "Appliquer", + "edit_note_cancel": "Abandonner", + "note": "Note", + "group": "Groupe", + "unfollow_confirm_title": "Confirmer l'arrêt de suivi", + "block_confirm_title": "Confirmer le blocage", + "deny_confirm_accept_button": "Refuser", + "deny_confirm_cancel_button": "Ne pas refuser", + "deny_confirm": "Voulez-vous refuser la demande de suivi de {user} ?", + "deny_confirm_title": "Refuser la confirmation", + "remove_follower_confirm_cancel_button": "Conserver", + "mute_duration_prompt": "Mettre cet utilisateur en sourdine pour (0 pour une durée indéterminée) :", + "remove_follower_confirm_title": "Confirmation de suppression d'utilisateur", + "note_blank": "(Aucun)", + "mute_confirm": "Voulez-vous vraiment mettre {user} en sourdine ?", + "mute_confirm_accept_button": "Mettre en sourdine", + "mute_confirm_cancel_button": "Ne pas mettre en sourdine", + "remove_follower_confirm": "Voulez-vous vraiment supprimer {user} de vos abonnés ?", + "approve_confirm_accept_button": "Approuver", + "approve_confirm": "Voulez-vous approuver la demande de suivi de {user} ?", + "block_confirm": "Voulez-vous vraiment bloquer {user} ?", + "approve_confirm_title": "Approuver confirmation", + "unfollow_confirm_cancel_button": "Ne pas arrêter le suivi" }, "user_profile": { "timeline_title": "Flux du compte", @@ -857,7 +980,10 @@ "add_reaction": "Ajouter une réaction", "accept_follow_request": "Accepter la demande de suivit", "reject_follow_request": "Rejeter la demande de suivit", - "bookmark": "Favori" + "bookmark": "Favori", + "autocomplete_available": "{number} résultat est disponible. Utilisez les touches haut et bas pour naviguer à l'intérieur. | {number} résultats sont disponibles. Utilisez les touches haut et bas pour naviguer à l'intérieur.", + "toggle_expand": "Développer ou réduire la notification pour afficher le message dans son intégralité", + "toggle_mute": "Développer ou réduire la notification pour révéler le contenu en sourdine" }, "upload": { "error": { @@ -950,7 +1076,9 @@ "symbols": "Symboles", "travel-and-places": "Voyages & lieux" }, - "regional_indicator": "Indicateur régional {letter}" + "regional_indicator": "Indicateur régional {letter}", + "unpacked": "Émojis non catégorisés", + "hide_custom_emoji": "Masquer les émojis personnalisés" }, "remote_user_resolver": { "error": "Non trouvé.", @@ -1012,7 +1140,7 @@ "person_talking": "{count} personnes discutant", "hashtags": "Mot-dièses", "people_talking": "{count} personnes discutant", - "no_results": "Aucun résultats", + "no_results": "Aucun résultat", "no_more_results": "Pas de résultats supplémentaires", "load_more": "Charger plus de résultats" }, @@ -1083,7 +1211,8 @@ "update_changelog_here": "Liste compète des changements", "art_by": "Œuvre par {linkToArtist}", "big_update_content": "Nous n'avons pas fait de nouvelle version depuis un moment, les choses peuvent vous paraitre différentes de vos habitudes.", - "update_bugs": "Veuillez rapporter les problèmes sur {pleromaGitlab}, comme beaucoup de changements on été fait, même si nous testons entièrement et utilisons la version de dévelopement nous-même, nous avons pu en louper. Les retours et suggestions sont bienvenues sur ce que vous avez pu rencontrer, ou sur comment améliorer Pleroma (BE) et Pleroma-FE." + "update_bugs": "Veuillez rapporter les problèmes sur {pleromaGitlab}, comme beaucoup de changements on été fait, même si nous testons entièrement et utilisons la version de dévelopement nous-même, nous avons pu en louper. Les retours et suggestions sont bienvenues sur ce que vous avez pu rencontrer, ou sur comment améliorer Pleroma (BE) et Pleroma-FE.", + "big_update_title": "Soyez indulgent avec nous" }, "unicode_domain_indicator": { "tooltip": "Ce domaine contient des caractères non ascii." @@ -1097,5 +1226,158 @@ "state_open": "Ouvert", "state_closed": "Fermé", "state_resolved": "Résolut" + }, + "announcements": { + "page_header": "Annonces", + "title": "Annonce", + "mark_as_read_action": "Marquer comme lu", + "post_form_header": "Faire une annonce", + "post_placeholder": "Écrivez le contenu de l'annonce ici...", + "post_action": "Envoyer", + "post_error": "Erreur : {error}", + "close_error": "Fermer", + "delete_action": "Supprimer", + "start_time_prompt": "Heure de début : ", + "end_time_prompt": "Heure de fin : ", + "all_day_prompt": "L'événement dure toute la journée", + "inactive_message": "Cette annonce n'est pas active", + "published_time_display": "Publié le {time}", + "start_time_display": "Démarre à {time}", + "end_time_display": "Se termine à {time}", + "edit_action": "Modifier", + "submit_edit_action": "Envoyer", + "cancel_edit_action": "Annuler" + }, + "admin_dash": { + "frontend": { + "success_installing_frontend": "Installation réussie de l'interface {version}", + "failure_installing_frontend": "Échec de l'installation de l'interface {version} : {reason}", + "default_frontend_unavail": "Les paramètres de l'interface ne sont pas disponibles, ils doivent être configurés dans la base de données", + "build_url": "Construction URL", + "reinstall": "Réinstaller", + "repository": "Lien du dépôt", + "versions": "Versions disponibles", + "default_frontend_tip": "L'interface par défaut sera affichée à tous les utilisateurs. Si vous décidez de quitter PleromaFE, vous devrez utiliser l'ancienne AdminFE buguée pour configurer votre instance jusqu'à ce que nous la remplacions.", + "is_default": "(Défaut)", + "is_default_custom": "(Défaut, version : {version})", + "install": "Installation", + "install_version": "Installation de la version {version}", + "more_install_options": "Plus d'options d'installation", + "more_default_options": "Plus d'options de paramétrages par défaut", + "set_default": "Définir la valeur par défaut", + "set_default_version": "Définir la version {version} comme version par défaut", + "wip_notice": "Veuillez noter que cette section est en cours de développement et que certaines fonctionnalités de l'interface ne sont pas implémentées côté serveur.", + "default_frontend": "Interface par défaut", + "available_frontends": "Disponible pour installation" + }, + "temp_overrides": { + ":pleroma": { + ":instance": { + ":public": { + "label": "Cette instance est publique", + "description": "En désactivant cette option, toutes les API ne seront accessibles qu'aux utilisateurs connectés, ce qui rendra les chronologies publiques et fédérées inaccessibles aux visiteurs anonymes." + }, + ":limit_to_local_content": { + "label": "Limitez la recherche au contenu local", + "description": "Désactive la recherche globale sur le réseau pour les utilisateurs non authentifiés (par défaut), tous les utilisateurs ou aucun" + }, + ":description_limit": { + "label": "Limite", + "description": "Limite de nombre de caractères pour la description des fichiers joints" + }, + ":background_image": { + "description": "Image de fond (principalement utilisé par PleromaFE)", + "label": "Image de fond d'écran" + } + } + } + }, + "tabs": { + "emoji": "Émoji", + "limits": "Limites", + "frontends": "Interfaces", + "instance": "Instance", + "nodb": "Pas de configuration de base de données" + }, + "instance": { + "kocaptcha": "Réglages KoCaptcha", + "access": "Accès à l'instance", + "restrict": { + "header": "Restreindre l'accès aux visiteurs anonymes", + "profiles": "Accès aux profils d'utilisateur", + "activities": "Accès aux status/activités", + "description": "Paramètre détaillé permettant d'autoriser/interdire l'accès à certains aspects de l'API. Par défaut (état indéterminé), l'accès est interdit si l'instance n'est pas publique ; si la case est cochée, l'accès est interdit même si l'instance est publique ; si la case n'est pas cochée, l'accès est autorisé même si l'instance est privée. Veuillez noter qu'un comportement inattendu peut se produire si certains paramètres sont définis, par exemple si l'accès au profil est désactivé, les messages s'afficheront sans les informations relatives au profil.", + "timelines": "Accès aux flux" + }, + "registrations": "Inscription des utilisateurs", + "captcha_header": "CAPTCHA", + "instance": "Informations sur l'instance" + }, + "emoji": { + "global_actions": "Actions globales", + "reload": "Recharger les émojis", + "importFS": "Importer les émojis depuis le système de fichiers", + "error": "Erreur : {0}", + "create_pack": "Créer un pack", + "delete_pack": "Supprimer un paquet", + "new_pack_name": "Renommer le pack", + "create": "Créer", + "emoji_packs": "Pack d'émojis", + "remote_packs": "Packs d'autres instances", + "do_list": "Liste", + "remote_pack_instance": "Instance du pack", + "emoji_pack": "Pack d'émoji", + "edit_pack": "Modifier le pack", + "description": "Description", + "homepage": "Page d'accueil", + "fallback_src": "Source de remplacement", + "fallback_sha256": "Remplacement SHA256", + "share": "Partager", + "save": "Sauvegarder", + "save_meta": "Sauvegarder les métadonnées", + "revert_meta": "Annuler métadonnées", + "delete": "Supprimer", + "revert": "Revenir en arrière", + "add_file": "Ajouter un fichier", + "adding_new": "Ajouter un nouvel émoji", + "shortcode": "Shortcode", + "filename": "Nom du fichier", + "new_filename": "Nom de fichier, laisser blanc pour inférer", + "delete_confirm": "Êtes-vous sûr de vouloir supprimer {0} ?", + "download_pack": "Télécharger pack", + "downloading_pack": "Télécharge {0}", + "download": "Téléchargement", + "download_as_name": "Nouveau nom", + "download_as_name_full": "Nouveau nom, laissez blanc pour réutiliser le précédent", + "files": "Fichiers", + "editing": "Édition de {0}", + "delete_title": "Supprimer ?", + "metadata_changed": "Métadonnées différentes de celles sauvegardées", + "emoji_changed": "Modifications du fichier émoji non sauvegardées, vérifier l'émoji surligné", + "replace_warning": "Vous allez REMPLACER le pack local qui porte ce nom" + }, + "window_title": "Administration", + "nodb": { + "heading": "La configuration de base de données est désactivée", + "documentation": "documentation", + "text2": "La majorité des options de configuration ne seront pas disponibles.", + "text": "Vous devez modifier les fichiers de configuration du serveur pour que {property} soit définie à {value}, plus de détails dans la {documentation}." + }, + "limits": { + "arbitrary_limits": "Limites arbitraires", + "posts": "Limites des statuts", + "uploads": "Limites des pièces jointes", + "users": "Limites du profil d'utilisateur", + "profile_fields": "Limites des champs du profile", + "user_uploads": "Limites des médias du profil" + }, + "captcha": { + "native": "Natif", + "kocaptcha": "KoCaptcha" + }, + "wip_notice": "Ce tableau de bord d'administration est expérimental et en cours de développement, {adminFeLink}.", + "old_ui_link": "L'ancien espace d'administration est disponible ici", + "reset_all": "Tout réinitialiser", + "commit_all": "Tout sauvegarder" } } diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json @@ -1243,7 +1243,8 @@ "tabs": { "limits": "制限", "instance": "インスタンス", - "frontends": "フロントエンド" + "frontends": "フロントエンド", + "emoji": "絵文字" }, "limits": { "arbitrary_limits": "変更可能な制限", @@ -1252,6 +1253,27 @@ "profile_fields": "追加情報欄の制限", "user_uploads": "プロフィール画像の制限", "users": "ユーザープロフィールの設定" + }, + "emoji": { + "create_pack": "パックを作成", + "delete_pack": "パックを削除", + "create": "作成", + "emoji_packs": "絵文字パック", + "remote_packs": "リモートのパック", + "emoji_pack": "絵文字パック", + "edit_pack": "パックを編集", + "homepage": "ホームページ", + "save": "保存", + "save_meta": "メタデータを保存", + "shortcode": "ショートコード", + "filename": "ファイル名", + "delete_confirm": "{0}を削除してもよろしいですか?", + "download_pack": "パックをダウンロード", + "downloading_pack": "{0}をダウンロード中", + "download": "ダウンロード", + "editing": "{0}を編集中", + "error": "エラー: {0}", + "delete": "削除" } }, "lists": { diff --git a/src/i18n/pt.json b/src/i18n/pt.json @@ -11,7 +11,8 @@ "title": "Características", "who_to_follow": "Quem seguir", "upload_limit": "Limite de carregamento", - "pleroma_chat_messages": "Chat do Pleroma" + "pleroma_chat_messages": "Chat do Pleroma", + "shout": "Shoutbox" }, "finder": { "error_fetching_user": "Erro ao pesquisar utilizador", @@ -36,11 +37,27 @@ "error_retry": "Por favor, tenta novamente", "loading": "A carregar…", "dismiss": "Ignorar", - "role": - { + "role": { "moderator": "Moderador", "admin": "Admin" - } + }, + "undo": "Refazer", + "yes": "Sim", + "no": "Não", + "unpin": "Desafixar o item", + "scroll_to_top": "Rolar para o topo", + "flash_content": "Clique para mostrar conteúdo Flash usando o Ruffle (Experimental, talvez não funcione).", + "flash_security": "Note que isso pode ser potencialmente perigoso dado que o conteúdo Flash ainda é código arbitrário.", + "flash_fail": "Falha ao carregar conteúdo flash, veja o console para detalhes.", + "scope_in_timeline": { + "direct": "Direct", + "private": "Apenas-seguidores", + "public": "Público", + "unlisted": "Não-listado" + }, + "pin": "Fixar o item", + "generic_error_message": "Um erro ocorreu: {0}", + "never_show_again": "Não mostrar mais" }, "image_cropper": { "crop_picture": "Cortar imagem", @@ -64,11 +81,17 @@ "recovery_code": "Código de recuperação", "authentication_code": "Código de autenticação", "enter_two_factor_code": "Introduza o código de dois fatores", - "enter_recovery_code": "Introduza um código de recuperação" + "enter_recovery_code": "Introduza um código de recuperação", + "logout_confirm_title": "Confirmação de logoff", + "logout_confirm": "Você realmente quer sair?", + "logout_confirm_accept_button": "Sair", + "logout_confirm_cancel_button": "Não sair" }, "media_modal": { "previous": "Anterior", - "next": "Próximo" + "next": "Próximo", + "counter": "{current} / {total}", + "hide": "Fechar visualizador de mídia" }, "nav": { "about": "Sobre", @@ -88,7 +111,18 @@ "administration": "Administração", "chats": "Salas de Chat", "timelines": "Cronologias", - "bookmarks": "Itens Guardados" + "bookmarks": "Itens Guardados", + "home_timeline": "Timeline da home", + "lists": "Listas", + "edit_pinned": "Editar itens fixados", + "edit_nav_mobile": "Customizar barra de navegação", + "mobile_notifications_mark_as_seen": "Marcar todas como vistas", + "search_close": "Fechar barra de busca", + "mobile_notifications_close": "Fechar notificações", + "announcements": "Anúncios", + "edit_finish": "Edição finalizada", + "mobile_sidebar": "Alternar barra lateral móvel", + "mobile_notifications": "Abrir notificações (há notificações não lidas)" }, "notifications": { "broken_favorite": "Publicação desconhecida, a procurar…", @@ -102,7 +136,15 @@ "reacted_with": "reagiu com {0}", "migrated_to": "migrou para", "follow_request": "quer seguir-te", - "error": "Erro ao obter notificações: {0}" + "error": "Erro ao obter notificações: {0}", + "unread_announcements": "{num} anúncio não lido | {num} anúncios não lidos", + "unread_chats": "{num} mensagem não lida | {num} mensagens não lidas", + "configuration_tip": "Você pode customizar o que você deseja mostrar aqui em {theSettings}. {dismiss}", + "unread_follow_requests": "{num} novo pedido de seguidor | {num} novos pedidos de seguidores", + "configuration_tip_settings": "as configurações", + "configuration_tip_dismiss": "Não mostrar novamente", + "poll_ended": "enquete finalizada", + "submitted_report": "enviado um relatório" }, "post_status": { "new_status": "Publicar nova publicação", @@ -136,7 +178,14 @@ "media_description": "Descrição da multimédia", "media_description_error": "Falha ao atualizar ficheiro, tente novamente", "direct_warning_to_first_only": "Esta publicação só será visível para os utilizadores mencionados no início da mensagem.", - "direct_warning_to_all": "Esta publicação será visível para todos os utilizadores mencionados." + "direct_warning_to_all": "Esta publicação será visível para todos os utilizadores mencionados.", + "edit_status": "Editar status", + "reply_option": "Responder a esse status", + "quote_option": "Citar esse status", + "edit_remote_warning": "Outras instâncias remotas talvez não suportem edição e sejam incapazes de receber a última versão do seu post.", + "content_type_selection": "Formato do post", + "scope_notice_dismiss": "Fechar essa notificação", + "edit_unsupported_warning": "Pleroma não suporta editar menções ou enquetes." }, "registration": { "bio": "Biografia", @@ -156,8 +205,18 @@ "email_required": "não pode ser deixado em branco", "password_required": "não pode ser deixado em branco", "password_confirmation_required": "não pode ser deixado em branco", - "password_confirmation_match": "deve corresponder à palavra-passe" - } + "password_confirmation_match": "deve corresponder à palavra-passe", + "birthday_required": "não pode ser deixado em branco", + "birthday_min_age": "deve ser em ou antes de {date}" + }, + "birthday": "Data de nascimento:", + "reason": "Razão para registrar", + "register": "Registrar", + "reason_placeholder": "Essa instância aprova os registros manualmente.\nPermita ao administrador saber o porquê do seu registro.", + "birthday_optional": "Data de nascimento (opcional):", + "bio_optional": "Bio (opcional)", + "email_optional": "Email (opcional)", + "email_language": "Em qual linguagem você deseja receber emails do servidor?" }, "settings": { "app_name": "Nome da aplicação", @@ -523,7 +582,56 @@ "autohide_floating_post_button": "Automaticamente ocultar o botão 'Nova Publicação' (telemóvel)", "notification_visibility_moves": "Utilizador Migrado", "accent": "Destaque", - "pad_emoji": "Preencher espaços ao adicionar emojis do seletor" + "pad_emoji": "Preencher espaços ao adicionar emojis do seletor", + "confirm_dialogs_logout": "saindo", + "move_account_error": "Erro ao mover conta: {error}", + "confirm_dialogs_delete": "excluindo um status", + "save": "Salvar mudanças", + "lists_navigation": "Mostrar listas na navegação", + "email_language": "Linguagem para receber emails do servidor", + "account_backup_description": "Isso permite a você baixar um arquivo das informações da sua conta e os seus posts, mas eles ainda não podem ser importados para uma conta do Pleroma.", + "add_backup_error": "Erro ao adicionar um novo backup: {error}", + "confirm_dialogs": "Pedir por confirmação quando", + "confirm_dialogs_repeat": "repetindo um status", + "account_alias": "Apelidos de conta", + "account_alias_table_head": "Apelido", + "list_aliases_error": "Erro ao buscar por apelidos: {error}", + "hide_list_aliases_error_action": "Fechar", + "confirm_dialogs_deny_follow": "negando um seguidor", + "confirm_dialogs_approve_follow": "aprovando um seguidor", + "backup_running": "Esse backup está em andamento, {number} registro processado. | Esse backup está em progresso, {number} registros processados.", + "add_backup": "Criar um novo backup", + "added_backup": "Adicionado um novo backup.", + "backup_failed": "Esse backup falhou.", + "list_backups_error": "Erro ao buscar a lista de backup: {error}", + "move_account_notes": "Se você deseja mover a conta para outro lugar, você deve ir para sua conta de destino e adicionar um apelido apontando para cá.", + "add_alias_error": "Erro ao adicionar apelido: {error}", + "move_account": "Mover conta", + "actor_type": "Essa conta é:", + "actor_type_description": "Marcando a sua conta como um grupo irá fazer com que ela automaticamente repita os status que a mencionam.", + "actor_type_Person": "um usuário normal", + "actor_type_Service": "um bot", + "actor_type_Group": "um grupo", + "account_backup": "Backup da conta", + "confirm_dialogs_unfollow": "deixando de seguir usuário", + "confirm_dialogs_block": "bloqueando um usuário", + "confirm_dialogs_remove_follower": "removendo um seguidor", + "remove_alias": "Remover esse apelido", + "new_alias_target": "Adicionar um novo apelido (e.g. {example})", + "added_alias": "Apelido adicionado.", + "move_account_target": "Conta de destino (e.g. {example})", + "moved_account": "Conta movida.", + "remove_language": "Remover", + "primary_language": "Linguagem primária:", + "fallback_language": "Linguagem de reserva {index}:", + "add_language": "Adicionar linguagem de reserva", + "expert_mode": "Mostrar avançados", + "setting_changed": "As configurações são diferentes do padrão", + "setting_server_side": "Essas configurações estão atreladas ao seu perfil e afetarão todas as sessões e clientes", + "mention_links": "Links de menção", + "confirm_dialogs_mute": "mutando um usuário", + "backup_not_ready": "Esse backup não está pronto ainda.", + "remove_backup": "Remover" }, "timeline": { "collapse": "Esconder", @@ -699,7 +807,20 @@ "load_all": "A carregar todos os {emojiAmount} emojis", "load_all_hint": "Carregado o primeiro emoji {saneAmount}, carregar todos os emojis pode causar problemas de desempenho.", "keep_open": "Manter o seletor aberto", - "stickers": "Autocolantes" + "stickers": "Autocolantes", + "hide_custom_emoji": "Ocultar emojis customizados", + "unicode_groups": { + "symbols": "Símbolos", + "activities": "Atividades", + "animals-and-nature": "Animais & Natureza", + "people-and-body": "Pessoas & Corpo", + "smileys-and-emotion": "Sorriso & Emoção", + "travel-and-places": "Viagem & Lugares", + "food-and-drink": "Comida & Bebidas", + "objects": "Objetos" + }, + "regional_indicator": "Indicador regional {letter}", + "unpacked": "Emoji desempacotado" }, "polls": { "single_choice": "Escolha única", @@ -713,7 +834,9 @@ "expiry": "Tempo para finalizar sondagem", "multiple_choices": "Escolha múltipla", "type": "Tipo de sondagem", - "add_poll": "Adicionar Sondagem" + "add_poll": "Adicionar Sondagem", + "votes_count": "{count} voto | {count} votos", + "people_voted_count": "{count} pessoa votou | {count} pessoas votaram" }, "importer": { "error": "Ocorreu um erro ao importar este ficheiro.", @@ -737,7 +860,9 @@ "load_older": "Carregar interações mais antigas", "follows": "Novos seguidores", "favs_repeats": "Gostos e Partilhas", - "moves": "O utilizador migra" + "moves": "O utilizador migra", + "emoji_reactions": "Reações de Emoji", + "reports": "Relatórios" }, "errors": { "storage_unavailable": "O Pleroma não conseguiu aceder ao armazenamento do navegador. A sua sessão ou definições locais não serão armazenadas e poderá encontrar problemas inesperados. Tente ativar as cookies." @@ -828,5 +953,35 @@ "day_short": "{0}d", "days": "{0} dias", "day": "{0} dia" + }, + "report": { + "state_closed": "Fechar", + "reported_statuses": "Estado das denúncias:", + "reported_user": "Usuário denunciado:", + "state_resolved": "Resolvido", + "state": "Estado:", + "state_open": "Abrir", + "notes": "Notas:" + }, + "announcements": { + "start_time_display": "Inicia às {time}", + "post_form_header": "Enviar anúncio", + "post_placeholder": "Digite o conteúdo do seu anúncio aqui...", + "page_header": "Anúncios", + "title": "Anúncio", + "mark_as_read_action": "Marcar como lido", + "post_action": "Postar", + "post_error": "Erro: {error}", + "close_error": "Fechar", + "delete_action": "Apagar", + "start_time_prompt": "Tempo de início: ", + "end_time_prompt": "Tempo de término: ", + "all_day_prompt": "Esse é um evento para o dia todo", + "published_time_display": "Publicado às {time}", + "end_time_display": "Finaliza às {time}", + "edit_action": "Editar", + "submit_edit_action": "Enviar", + "cancel_edit_action": "Cancelar", + "inactive_message": "Esse anúncio está inativo" } } diff --git a/src/modules/config.js b/src/modules/config.js @@ -36,6 +36,7 @@ export const defaultState = { hideMutedThreads: undefined, // instance default hideWordFilteredPosts: undefined, // instance default muteBotStatuses: undefined, // instance default + muteSensitiveStatuses: undefined, // instance default collapseMessageWithSubject: undefined, // instance default padEmoji: true, hideAttachments: false, diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -71,6 +71,7 @@ const defaultState = { hideSitename: false, hideUserStats: false, muteBotStatuses: false, + muteSensitiveStatuses: false, modalOnRepeat: false, modalOnUnfollow: false, modalOnBlock: true, @@ -386,6 +387,7 @@ const instance = { } else { applyTheme(themeData.theme) } + commit('setThemeApplied') }) }, fetchEmoji ({ dispatch, state }) { diff --git a/src/modules/interface.js b/src/modules/interface.js @@ -1,4 +1,5 @@ const defaultState = { + themeApplied: false, settingsModalState: 'hidden', settingsModalLoadedUser: false, settingsModalLoadedAdmin: false, @@ -35,6 +36,9 @@ const interfaceMod = { state.settings.currentSaveStateNotice = { error: true, errorData: error } } }, + setThemeApplied (state) { + state.themeApplied = true + }, setNotificationPermission (state, permission) { state.notificationPermission = permission }, diff --git a/src/panel.scss b/src/panel.scss @@ -1,15 +1,24 @@ /* stylelint-disable no-descending-specificity */ .panel { + --__panel-background: var(--background); + --__panel-backdrop-filter: var(--backdrop-filter); + + .tab-switcher .tabs { + background: var(--__panel-background); + backdrop-filter: var(--__panel-backdrop-filter); + } + position: relative; display: flex; flex-direction: column; - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); + + .panel-heading { + background-color: inherit; + } &::after, & { - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); } &::after { @@ -20,19 +29,25 @@ left: 0; right: 0; z-index: 5; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + box-shadow: var(--shadow); pointer-events: none; } } .panel-body { padding: var(--panel-body-padding, 0); + background: var(--background); + backdrop-filter: var(--__panel-backdrop-filter); + + .tab-switcher .tabs { + background: none; + backdrop-filter: none; + } &:empty::before { content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations display: block; - margin: 1em; + padding: 1em; text-align: center; } @@ -50,6 +65,7 @@ --__panel-heading-height: 3.2em; --__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0)); + backdrop-filter: var(--__panel-backdrop-filter); position: relative; box-sizing: border-box; display: grid; @@ -76,8 +92,7 @@ &.-stub { &, &::after { - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); } } @@ -119,82 +134,33 @@ padding-bottom: 0; align-self: stretch; } + + > .alert { + line-height: calc(var(--__panel-heading-height-inner) - 2px); + } } } // TODO Should refactor panels into separate component and utilize slots .panel-heading { - border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; - border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; + border-radius: var(--roundness) var(--roundness) 0 0; border-width: 0 0 1px; align-items: start; - // panel theme - color: var(--panelText); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); + background-image: + linear-gradient(to bottom, var(--background), var(--background)), + linear-gradient(to bottom, var(--__panel-background), var(--__panel-background)); &::after { - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); + background-color: var(--background); z-index: -2; - border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; - border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; - box-shadow: var(--panelHeaderShadow); - } - - a, - .-link { - color: $fallback--link; - color: var(--panelLink, $fallback--link); - } - - .button-unstyled:hover, - a:hover { - i[class*="icon-"], - .svg-inline--fa, - .iconLetter { - color: var(--panelText); - } - } - - .faint { - background-color: transparent; - color: $fallback--faint; - color: var(--panelFaint, $fallback--faint); - } - - .faint-link { - color: $fallback--faint; - color: var(--faintLink, $fallback--faint); + border-radius: var(--roundness) var(--roundness) 0 0; + box-shadow: var(--shadow); } &:not(.-flexible-height) { > .button-default { flex-shrink: 0; - - &, - i[class*="icon-"] { - color: $fallback--text; - color: var(--btnPanelText, $fallback--text); - } - - &:active { - background-color: $fallback--fg; - background-color: var(--btnPressedPanel, $fallback--fg); - color: $fallback--text; - color: var(--btnPressedPanelText, $fallback--text); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledPanelText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledPanelText, $fallback--text); - } } } @@ -232,11 +198,12 @@ } .panel-footer { - border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; - border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + border-top-left-radius: 0; + border-top-right-radius: 0; align-items: center; border-width: 1px 0 0; border-style: solid; - border-color: var(--border, $fallback--border); + border-color: var(--border); + background-color: var(--__panel-background); } /* stylelint-enable no-descending-specificity */ diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js @@ -173,7 +173,7 @@ export const mixrgb = (a, b) => { * @returns {String} CSS rgba() color */ export const rgba2css = function (rgba) { - return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a})` + return `rgba(${Math.floor(rgba.r)}, ${Math.floor(rgba.g)}, ${Math.floor(rgba.b)}, ${rgba.a ?? 1})` } /** diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -8,8 +8,11 @@ const mastoApiNotificationTypes = [ 'favourite', 'reblog', 'follow', + 'follow_request', 'move', + 'poll', 'pleroma:emoji_reaction', + 'pleroma:chat_mention', 'pleroma:report' ] diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -1,24 +1,118 @@ -import { convert } from 'chromatism' -import { rgb2hex, hex2rgb, rgba2css, getCssColor, relativeLuminance } from '../color_convert/color_convert.js' -import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/theme_data.service.js' +import { hex2rgb } from '../color_convert/color_convert.js' +import { generatePreset } from '../theme_data/theme_data.service.js' +import { init } from '../theme_data/theme_data_3.service.js' +import { convertTheme2To3 } from '../theme_data/theme2_to_theme3.js' +import { getCssRules } from '../theme_data/css_utils.js' import { defaultState } from '../../modules/config.js' +import { chunk } from 'lodash' + +export const generateTheme = async (input, callbacks) => { + const { + onNewRule = (rule, isLazy) => {}, + onLazyFinished = () => {}, + onEagerFinished = () => {} + } = callbacks + + let extraRules + if (input.themeFileVersion === 1) { + extraRules = convertTheme2To3(input) + } else { + const { theme } = generatePreset(input) + extraRules = convertTheme2To3(theme) + } -export const applyTheme = (input) => { - const { rules } = generatePreset(input) - const head = document.head - const body = document.body - body.classList.add('hidden') + // Assuming that "worst case scenario background" is panel background since it's the most likely one + const themes3 = init(extraRules, extraRules[0].directives['--bg'].split('|')[1].trim()) + + getCssRules(themes3.eager, themes3.staticVars).forEach(rule => { + // Hacks to support multiple selectors on same component + if (rule.match(/::-webkit-scrollbar-button/)) { + const parts = rule.split(/[{}]/g) + const newRule = [ + parts[0], + ', ', + parts[0].replace(/button/, 'thumb'), + ', ', + parts[0].replace(/scrollbar-button/, 'resizer'), + ' {', + parts[1], + '}' + ].join('') + onNewRule(newRule, false) + } else { + onNewRule(rule, false) + } + }) + onEagerFinished() + + // Optimization - instead of processing all lazy rules in one go, process them in small chunks + // so that UI can do other things and be somewhat responsive while less important rules are being + // processed + let counter = 0 + const chunks = chunk(themes3.lazy, 200) + // let t0 = performance.now() + const processChunk = () => { + const chunk = chunks[counter] + Promise.all(chunk.map(x => x())).then(result => { + getCssRules(result.filter(x => x), themes3.staticVars).forEach(rule => { + if (rule.match(/\.modal-view/)) { + const parts = rule.split(/[{}]/g) + const newRule = [ + parts[0], + ', ', + parts[0].replace(/\.modal-view/, '#modal'), + ', ', + parts[0].replace(/\.modal-view/, '.shout-panel'), + ' {', + parts[1], + '}' + ].join('') + onNewRule(newRule, true) + } else { + onNewRule(rule, true) + } + }) + // const t1 = performance.now() + // console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms') + // t0 = t1 + counter += 1 + if (counter < chunks.length) { + setTimeout(processChunk, 0) + } else { + onLazyFinished() + } + }) + } - const styleEl = document.createElement('style') - head.appendChild(styleEl) - const styleSheet = styleEl.sheet + return { lazyProcessFunc: processChunk } +} - styleSheet.toString() - styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max') - body.classList.remove('hidden') +export const applyTheme = async (input) => { + const styleSheet = new CSSStyleSheet() + const lazyStyleSheet = new CSSStyleSheet() + + const { lazyProcessFunc } = await generateTheme( + input, + { + onNewRule (rule, isLazy) { + if (isLazy) { + lazyStyleSheet.insertRule(rule, 'index-max') + } else { + styleSheet.insertRule(rule, 'index-max') + } + }, + onEagerFinished () { + document.adoptedStyleSheets = [styleSheet] + }, + onLazyFinished () { + document.adoptedStyleSheets = [styleSheet, lazyStyleSheet] + } + } + ) + + setTimeout(lazyProcessFunc, 0) + + return Promise.resolve() } const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) => @@ -51,308 +145,6 @@ export const applyConfig = (config) => { body.classList.remove('hidden') } -export const getCssShadow = (input, usesDropShadow) => { - if (input.length === 0) { - return 'none' - } - - return input - .filter(_ => usesDropShadow ? _.inset : _) - .map((shad) => [ - shad.x, - shad.y, - shad.blur, - shad.spread - ].map(_ => _ + 'px').concat([ - getCssColor(shad.color, shad.alpha), - shad.inset ? 'inset' : '' - ]).join(' ')).join(', ') -} - -const getCssShadowFilter = (input) => { - if (input.length === 0) { - return 'none' - } - - return input - // drop-shadow doesn't support inset or spread - .filter((shad) => !shad.inset && Number(shad.spread) === 0) - .map((shad) => [ - shad.x, - shad.y, - // drop-shadow's blur is twice as strong compared to box-shadow - shad.blur / 2 - ].map(_ => _ + 'px').concat([ - getCssColor(shad.color, shad.alpha) - ]).join(' ')) - .map(_ => `drop-shadow(${_})`) - .join(' ') -} - -export const generateColors = (themeData) => { - const sourceColors = !themeData.themeEngineVersion - ? colors2to3(themeData.colors || themeData) - : themeData.colors || themeData - - const { colors, opacity } = getColors(sourceColors, themeData.opacity || {}) - - const htmlColors = Object.entries(colors) - .reduce((acc, [k, v]) => { - if (!v) return acc - acc.solid[k] = rgb2hex(v) - acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v) - return acc - }, { complete: {}, solid: {} }) - return { - rules: { - colors: Object.entries(htmlColors.complete) - .filter(([k, v]) => v) - .map(([k, v]) => `--${k}: ${v}`) - .join(';') - }, - theme: { - colors: htmlColors.solid, - opacity - } - } -} - -export const generateRadii = (input) => { - let inputRadii = input.radii || {} - // v1 -> v2 - if (typeof input.btnRadius !== 'undefined') { - inputRadii = Object - .entries(input) - .filter(([k, v]) => k.endsWith('Radius')) - .reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {}) - } - const radii = Object.entries(inputRadii).filter(([k, v]) => v).reduce((acc, [k, v]) => { - acc[k] = v - return acc - }, { - btn: 4, - input: 4, - checkbox: 2, - panel: 10, - avatar: 5, - avatarAlt: 50, - tooltip: 2, - attachment: 5, - chatMessage: inputRadii.panel - }) - - return { - rules: { - radii: Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';') - }, - theme: { - radii - } - } -} - -export const generateFonts = (input) => { - const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => { - acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => { - acc[k] = v - return acc - }, acc[k]) - return acc - }, { - interface: { - family: 'sans-serif' - }, - input: { - family: 'inherit' - }, - post: { - family: 'inherit' - }, - postCode: { - family: 'monospace' - } - }) - - return { - rules: { - fonts: Object - .entries(fonts) - .filter(([k, v]) => v) - .map(([k, v]) => `--${k}Font: ${v.family}`).join(';') - }, - theme: { - fonts - } - } -} - -const border = (top, shadow) => ({ - x: 0, - y: top ? 1 : -1, - blur: 0, - spread: 0, - color: shadow ? '#000000' : '#FFFFFF', - alpha: 0.2, - inset: true -}) -const buttonInsetFakeBorders = [border(true, false), border(false, true)] -const inputInsetFakeBorders = [border(true, true), border(false, false)] -const hoverGlow = { - x: 0, - y: 0, - blur: 4, - spread: 0, - color: '--faint', - alpha: 1 -} - -export const DEFAULT_SHADOWS = { - panel: [{ - x: 1, - y: 1, - blur: 4, - spread: 0, - color: '#000000', - alpha: 0.6 - }], - topBar: [{ - x: 0, - y: 0, - blur: 4, - spread: 0, - color: '#000000', - alpha: 0.6 - }], - popup: [{ - x: 2, - y: 2, - blur: 3, - spread: 0, - color: '#000000', - alpha: 0.5 - }], - avatar: [{ - x: 0, - y: 1, - blur: 8, - spread: 0, - color: '#000000', - alpha: 0.7 - }], - avatarStatus: [], - panelHeader: [], - button: [{ - x: 0, - y: 0, - blur: 2, - spread: 0, - color: '#000000', - alpha: 1 - }, ...buttonInsetFakeBorders], - buttonHover: [hoverGlow, ...buttonInsetFakeBorders], - buttonPressed: [hoverGlow, ...inputInsetFakeBorders], - input: [...inputInsetFakeBorders, { - x: 0, - y: 0, - blur: 2, - inset: true, - spread: 0, - color: '#000000', - alpha: 1 - }] -} -export const generateShadows = (input, colors) => { - // TODO this is a small hack for `mod` to work with shadows - // this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element - const hackContextDict = { - button: 'btn', - panel: 'bg', - top: 'topBar', - popup: 'popover', - avatar: 'bg', - panelHeader: 'panel', - input: 'input' - } - - const cleanInputShadows = Object.fromEntries( - Object.entries(input.shadows || {}) - .map(([name, shadowSlot]) => [ - name, - // defaulting color to black to avoid potential problems - shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef })) - ]) - ) - const inputShadows = cleanInputShadows && !input.themeEngineVersion - ? shadows2to3(cleanInputShadows, input.opacity) - : cleanInputShadows || {} - const shadows = Object.entries({ - ...DEFAULT_SHADOWS, - ...inputShadows - }).reduce((shadowsAcc, [slotName, shadowDefs]) => { - const slotFirstWord = slotName.replace(/[A-Z].*$/, '') - const colorSlotName = hackContextDict[slotFirstWord] - const isLightOnDark = relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5 - const mod = isLightOnDark ? 1 : -1 - const newShadow = shadowDefs.reduce((shadowAcc, def) => [ - ...shadowAcc, - { - ...def, - color: rgb2hex(computeDynamicColor( - def.color, - (variableSlot) => convert(colors[variableSlot]).rgb, - mod - )) - } - ], []) - return { ...shadowsAcc, [slotName]: newShadow } - }, {}) - - return { - rules: { - shadows: Object - .entries(shadows) - // TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally - // convert all non-inset shadows into filter: drop-shadow() to boost performance - .map(([k, v]) => [ - `--${k}Shadow: ${getCssShadow(v)}`, - `--${k}ShadowFilter: ${getCssShadowFilter(v)}`, - `--${k}ShadowInset: ${getCssShadow(v, true)}` - ].join(';')) - .join(';') - }, - theme: { - shadows - } - } -} - -export const composePreset = (colors, radii, shadows, fonts) => { - return { - rules: { - ...shadows.rules, - ...colors.rules, - ...radii.rules, - ...fonts.rules - }, - theme: { - ...shadows.theme, - ...colors.theme, - ...radii.theme, - ...fonts.theme - } - } -} - -export const generatePreset = (input) => { - const colors = generateColors(input) - return composePreset( - colors, - generateRadii(input), - generateShadows(input, colors.theme.colors, colors.mod), - generateFonts(input) - ) -} - export const getThemes = () => { const cache = 'no-store' @@ -382,47 +174,6 @@ export const getThemes = () => { }, {}) }) } -export const colors2to3 = (colors) => { - return Object.entries(colors).reduce((acc, [slotName, color]) => { - const btnPositions = ['', 'Panel', 'TopBar'] - switch (slotName) { - case 'lightBg': - return { ...acc, highlight: color } - case 'btnText': - return { - ...acc, - ...btnPositions - .reduce( - (statePositionAcc, position) => - ({ ...statePositionAcc, ['btn' + position + 'Text']: color }) - , {} - ) - } - default: - return { ...acc, [slotName]: color } - } - }, {}) -} - -/** - * This handles compatibility issues when importing v2 theme's shadows to current format - * - * Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables - */ -export const shadows2to3 = (shadows, opacity) => { - return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => { - const isDynamic = ({ color = '#000000' }) => color.startsWith('--') - const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])] - const newShadow = shadowDefs.reduce((shadowAcc, def) => [ - ...shadowAcc, - { - ...def, - alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha - } - ], []) - return { ...shadowsAcc, [slotName]: newShadow } - }, {}) -} export const getPreset = (val) => { return getThemes() @@ -449,4 +200,4 @@ export const getPreset = (val) => { }) } -export const setPreset = (val) => getPreset(val).then(data => applyTheme(data.theme)) +export const setPreset = (val) => getPreset(val).then(data => applyTheme(data)) diff --git a/src/services/theme_data/css_utils.js b/src/services/theme_data/css_utils.js @@ -0,0 +1,163 @@ +import { convert } from 'chromatism' + +import { hex2rgb, rgba2css } from '../color_convert/color_convert.js' + +// This changes what backgrounds are used to "stacked" solid colors so you can see +// what theme engine "thinks" is actual background color is for purposes of text color +// generation and for when --stacked variable is used +const DEBUG = false + +export const parseCssShadow = (text) => { + const dimensions = /(\d[a-z]*\s?){2,4}/.exec(text)?.[0] + const inset = /inset/.exec(text)?.[0] + const color = text.replace(dimensions, '').replace(inset, '') + + const [x, y, blur = 0, spread = 0] = dimensions.split(/ /).filter(x => x).map(x => x.trim()) + const isInset = inset?.trim() === 'inset' + const colorString = color.split(/ /).filter(x => x).map(x => x.trim())[0] + + return { + x, + y, + blur, + spread, + inset: isInset, + color: colorString + } +} + +export const getCssColorString = (color, alpha = 1) => rgba2css({ ...convert(color).rgb, a: alpha }) + +export const getCssShadow = (input, usesDropShadow) => { + if (input.length === 0) { + return 'none' + } + + return input + .filter(_ => usesDropShadow ? _.inset : _) + .map((shad) => [ + shad.x, + shad.y, + shad.blur, + shad.spread + ].map(_ => _ + 'px ').concat([ + getCssColorString(shad.color, shad.alpha), + shad.inset ? 'inset' : '' + ]).join(' ')).join(', ') +} + +export const getCssShadowFilter = (input) => { + if (input.length === 0) { + return 'none' + } + + return input + // drop-shadow doesn't support inset or spread + .filter((shad) => !shad.inset && Number(shad.spread) === 0) + .map((shad) => [ + shad.x, + shad.y, + // drop-shadow's blur is twice as strong compared to box-shadow + shad.blur / 2 + ].map(_ => _ + 'px').concat([ + getCssColorString(shad.color, shad.alpha) + ]).join(' ')) + .map(_ => `drop-shadow(${_})`) + .join(' ') +} + +export const getCssRules = (rules) => rules.map(rule => { + let selector = rule.selector + if (!selector) { + selector = 'html' + } + const header = selector + ' {' + const footer = '}' + + const virtualDirectives = Object.entries(rule.virtualDirectives || {}).map(([k, v]) => { + return ' ' + k + ': ' + v + }).join(';\n') + + const directives = Object.entries(rule.directives).map(([k, v]) => { + switch (k) { + case 'roundness': { + return ' ' + [ + '--roundness: ' + v + 'px' + ].join(';\n ') + } + case 'shadow': { + return ' ' + [ + '--shadow: ' + getCssShadow(rule.dynamicVars.shadow), + '--shadowFilter: ' + getCssShadowFilter(rule.dynamicVars.shadow), + '--shadowInset: ' + getCssShadow(rule.dynamicVars.shadow, true) + ].join(';\n ') + } + case 'background': { + if (DEBUG) { + return ` + --background: ${getCssColorString(rule.dynamicVars.stacked)}; + background-color: ${getCssColorString(rule.dynamicVars.stacked)}; + ` + } + if (v === 'transparent') { + if (rule.component === 'Root') return [] + return [ + rule.directives.backgroundNoCssColor !== 'yes' ? ('background-color: ' + v) : '', + ' --background: ' + v + ].filter(x => x).join(';\n') + } + const color = getCssColorString(rule.dynamicVars.background, rule.directives.opacity) + const cssDirectives = ['--background: ' + color] + if (rule.directives.backgroundNoCssColor !== 'yes') { + cssDirectives.push('background-color: ' + color) + } + return cssDirectives.filter(x => x).join(';\n') + } + case 'blur': { + const cssDirectives = [] + if (rule.directives.opacity < 1) { + cssDirectives.push(`--backdrop-filter: blur(${v}) `) + if (rule.directives.backgroundNoCssColor !== 'yes') { + cssDirectives.push(`backdrop-filter: blur(${v}) `) + } + } + return cssDirectives.join(';\n') + } + case 'font': { + return 'font-family: ' + v + } + case 'textColor': { + if (rule.directives.textNoCssColor === 'yes') { return '' } + return 'color: ' + v + } + default: + if (k.startsWith('--')) { + const [type, value] = v.split('|').map(x => x.trim()) // woah, Extreme! + switch (type) { + case 'color': { + const color = rule.dynamicVars[k] + if (typeof color === 'string') { + return k + ': ' + rgba2css(hex2rgb(color)) + } else { + return k + ': ' + rgba2css(color) + } + } + case 'generic': + return k + ': ' + value + default: + return '' + } + } + return '' + } + }).filter(x => x).map(x => ' ' + x).join(';\n') + + return [ + header, + directives + ';', + (rule.component === 'Text' && rule.state.indexOf('faint') < 0 && rule.directives.textNoCssColor !== 'yes') ? ' color: var(--text);' : '', + '', + virtualDirectives, + footer + ].join('\n') +}).filter(x => x) diff --git a/src/services/theme_data/iss_utils.js b/src/services/theme_data/iss_utils.js @@ -0,0 +1,129 @@ +// "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }} +// into an array [item2, item3] for iterating +export const unroll = (item) => { + const out = [] + let currentParent = item + while (currentParent) { + out.push(currentParent) + currentParent = currentParent.parent + } + return out +} + +// This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations +// Can only accept primitives. Duplicates are not supported and can cause unexpected behavior +export const getAllPossibleCombinations = (array) => { + const combos = [array.map(x => [x])] + for (let comboSize = 2; comboSize <= array.length; comboSize++) { + const previous = combos[combos.length - 1] + const newCombos = previous.map(self => { + const selfSet = new Set() + self.forEach(x => selfSet.add(x)) + const nonSelf = array.filter(x => !selfSet.has(x)) + return nonSelf.map(x => [...self, x]) + }) + const flatCombos = newCombos.reduce((acc, x) => [...acc, ...x], []) + const uniqueComboStrings = new Set() + const uniqueCombos = flatCombos.map(x => x.toSorted()).filter(x => { + if (uniqueComboStrings.has(x.join())) { + return false + } else { + uniqueComboStrings.add(x.join()) + return true + } + }) + combos.push(uniqueCombos) + } + return combos.reduce((acc, x) => [...acc, ...x], []) +} + +// Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) selector +export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => { + if (!rule && !isParent) return null + const component = components[rule.component] + const { states = {}, variants = {}, selector, outOfTreeSelector } = component + + const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state]) + + const applicableVariantName = (rule.variant || 'normal') + let applicableVariant = '' + if (applicableVariantName !== 'normal') { + applicableVariant = variants[applicableVariantName] + } else { + applicableVariant = variants?.normal ?? '' + } + + let realSelector + if (selector === ':root') { + realSelector = '' + } else if (isParent) { + realSelector = selector + } else { + if (outOfTreeSelector && !ignoreOutOfTreeSelector) realSelector = outOfTreeSelector + else realSelector = selector + } + + const selectors = [realSelector, applicableVariant, ...applicableStates] + .toSorted((a, b) => { + if (a.startsWith(':')) return 1 + if (/^[a-z]/.exec(a)) return -1 + else return 0 + }) + .join('') + + if (rule.parent) { + return (genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, true) + ' ' + selectors).trim() + } + return selectors.trim() +} + +export const combinationsMatch = (criteria, subject, strict) => { + if (criteria.component !== subject.component) return false + + // All variants inherit from normal + if (subject.variant !== 'normal' || strict) { + if (criteria.variant !== subject.variant) return false + } + + // Subject states > 1 essentially means state is "normal" and therefore matches + if (subject.state.length > 1 || strict) { + const subjectStatesSet = new Set(subject.state) + const criteriaStatesSet = new Set(criteria.state) + + const setsAreEqual = + [...criteriaStatesSet].every(state => subjectStatesSet.has(state)) && + [...subjectStatesSet].every(state => criteriaStatesSet.has(state)) + + if (!setsAreEqual) return false + } + return true +} + +export const findRules = (criteria, strict) => subject => { + // If we searching for "general" rules - ignore "specific" ones + if (criteria.parent === null && !!subject.parent) return false + if (!combinationsMatch(criteria, subject, strict)) return false + + if (criteria.parent !== undefined && criteria.parent !== null) { + if (!subject.parent && !strict) return true + const pathCriteria = unroll(criteria) + const pathSubject = unroll(subject) + if (pathCriteria.length < pathSubject.length) return false + + // Search: .a .b .c + // Matches: .a .b .c; .b .c; .c; .z .a .b .c + // Does not match .a .b .c .d, .a .b .e + for (let i = 0; i < pathCriteria.length; i++) { + const criteriaParent = pathCriteria[i] + const subjectParent = pathSubject[i] + if (!subjectParent) return true + if (!combinationsMatch(criteriaParent, subjectParent, strict)) return false + } + } + return true +} + +export const normalizeCombination = rule => { + rule.variant = rule.variant ?? 'normal' + rule.state = [...new Set(['normal', ...(rule.state || [])])] +} diff --git a/src/services/theme_data/pleromafe.t3.js b/src/services/theme_data/pleromafe.t3.js @@ -0,0 +1,2 @@ +export const sampleRules = [ +] diff --git a/src/services/theme_data/theme2_keys.js b/src/services/theme_data/theme2_keys.js @@ -0,0 +1,177 @@ +export default [ + 'bg', + 'wallpaper', + 'fg', + 'text', + 'underlay', + 'link', + 'accent', + 'faint', + 'faintLink', + 'postFaintLink', + + 'cBlue', + 'cRed', + 'cGreen', + 'cOrange', + + 'profileBg', + 'profileTint', + + 'highlight', + 'highlightLightText', + 'highlightPostLink', + 'highlightFaintText', + 'highlightFaintLink', + 'highlightPostFaintLink', + 'highlightText', + 'highlightLink', + 'highlightIcon', + + 'popover', + 'popoverLightText', + 'popoverPostLink', + 'popoverFaintText', + 'popoverFaintLink', + 'popoverPostFaintLink', + 'popoverText', + 'popoverLink', + 'popoverIcon', + + 'selectedPost', + 'selectedPostFaintText', + 'selectedPostLightText', + 'selectedPostPostLink', + 'selectedPostFaintLink', + 'selectedPostText', + 'selectedPostLink', + 'selectedPostIcon', + + 'selectedMenu', + 'selectedMenuLightText', + 'selectedMenuFaintText', + 'selectedMenuFaintLink', + 'selectedMenuText', + 'selectedMenuLink', + 'selectedMenuIcon', + + 'selectedMenuPopover', + 'selectedMenuPopoverLightText', + 'selectedMenuPopoverFaintText', + 'selectedMenuPopoverFaintLink', + 'selectedMenuPopoverText', + 'selectedMenuPopoverLink', + 'selectedMenuPopoverIcon', + + 'lightText', + + 'postLink', + + 'postGreentext', + + 'postCyantext', + + 'border', + + 'poll', + 'pollText', + + 'icon', + + // Foreground, + 'fgText', + 'fgLink', + + // Panel header, + 'panel', + 'panelText', + 'panelFaint', + 'panelLink', + + // Top bar, + 'topBar', + 'topBarText', + 'topBarLink', + + // Tabs, + 'tab', + 'tabText', + 'tabActiveText', + + // Buttons, + 'btn', + 'btnText', + 'btnPanelText', + 'btnTopBarText', + + // Buttons: pressed, + 'btnPressed', + 'btnPressedText', + 'btnPressedPanel', + 'btnPressedPanelText', + 'btnPressedTopBar', + 'btnPressedTopBarText', + + // Buttons: toggled, + 'btnToggled', + 'btnToggledText', + 'btnToggledPanelText', + 'btnToggledTopBarText', + + // Buttons: disabled, + 'btnDisabled', + 'btnDisabledText', + 'btnDisabledPanelText', + 'btnDisabledTopBarText', + + // Input fields, + 'input', + 'inputText', + 'inputPanelText', + 'inputTopbarText', + + 'alertError', + 'alertErrorText', + 'alertErrorPanelText', + + 'alertWarning', + 'alertWarningText', + 'alertWarningPanelText', + + 'alertSuccess', + 'alertSuccessText', + 'alertSuccessPanelText', + + 'alertNeutral', + 'alertNeutralText', + 'alertNeutralPanelText', + + 'alertPopupError', + 'alertPopupErrorText', + + 'alertPopupWarning', + 'alertPopupWarningText', + + 'alertPopupSuccess', + 'alertPopupSuccessText', + + 'alertPopupNeutral', + 'alertPopupNeutralText', + + 'badgeNeutral', + 'badgeNeutralText', + + 'badgeNotification', + 'badgeNotificationText', + + 'chatBg', + + 'chatMessageIncomingBg', + 'chatMessageIncomingText', + 'chatMessageIncomingLink', + 'chatMessageIncomingBorder', + 'chatMessageOutgoingBg', + 'chatMessageOutgoingText', + 'chatMessageOutgoingLink', + 'chatMessageOutgoingBorder' +] diff --git a/src/services/theme_data/theme2_to_theme3.js b/src/services/theme_data/theme2_to_theme3.js @@ -0,0 +1,536 @@ +import { convert } from 'chromatism' +import allKeys from './theme2_keys' + +// keys that are meant to be used globally, i.e. what's the rest of the theme is based upon. +export const basePaletteKeys = new Set([ + 'bg', + 'fg', + 'text', + 'link', + 'accent', + + 'cBlue', + 'cRed', + 'cGreen', + 'cOrange' +]) + +export const fontsKeys = new Set([ + 'interface', + 'input', + 'post', + 'postCode' +]) + +export const opacityKeys = new Set([ + 'alert', + 'alertPopup', + 'bg', + 'border', + 'btn', + 'faint', + 'input', + 'panel', + 'popover', + 'profileTint', + 'underlay' +]) + +export const shadowsKeys = new Set([ + 'panel', + 'topBar', + 'popup', + 'avatar', + 'avatarStatus', + 'panelHeader', + 'button', + 'buttonHover', + 'buttonPressed', + 'input' +]) + +export const radiiKeys = new Set([ + 'btn', + 'input', + 'checkbox', + 'panel', + 'avatar', + 'avatarAlt', + 'tooltip', + 'attachment', + 'chatMessage' +]) + +// Keys that are not available in editor and never meant to be edited +export const hiddenKeys = new Set([ + 'profileBg', + 'profileTint' +]) + +export const extendedBasePrefixes = [ + 'border', + 'icon', + 'highlight', + 'lightText', + + 'popover', + + 'panel', + 'topBar', + 'tab', + 'btn', + 'input', + 'selectedMenu', + + 'alert', + 'alertPopup', + 'badge', + + 'post', + 'selectedPost', // wrong nomenclature + 'poll', + + 'chatBg', + 'chatMessage' +] +export const nonComponentPrefixes = new Set([ + 'border', + 'icon', + 'highlight', + 'lightText', + 'chatBg' +]) + +export const extendedBaseKeys = Object.fromEntries( + extendedBasePrefixes.map(prefix => [ + prefix, + allKeys.filter(k => { + if (prefix === 'alert') { + return k.startsWith(prefix) && !k.startsWith('alertPopup') + } + return k.startsWith(prefix) + }) + ]) +) + +// Keysets that are only really used intermideately, i.e. to generate other colors +export const temporary = new Set([ + '', + 'highlight' +]) + +export const temporaryColors = {} + +export const convertTheme2To3 = (data) => { + data.colors.accent = data.colors.accent || data.colors.link + data.colors.link = data.colors.link || data.colors.accent + const generateRoot = () => { + const directives = {} + basePaletteKeys.forEach(key => { directives['--' + key] = 'color | ' + convert(data.colors[key]).hex }) + return { + component: 'Root', + directives + } + } + + const convertOpacity = () => { + const newRules = [] + Object.keys(data.opacity || {}).forEach(key => { + if (!opacityKeys.has(key) || data.opacity[key] === undefined) return null + const originalOpacity = data.opacity[key] + const rule = {} + + switch (key) { + case 'alert': + rule.component = 'Alert' + break + case 'alertPopup': + rule.component = 'Alert' + rule.parent = { component: 'Popover' } + break + case 'bg': + rule.component = 'Panel' + break + case 'border': + rule.component = 'Border' + break + case 'btn': + rule.component = 'Button' + break + case 'faint': + rule.component = 'Text' + rule.state = ['faint'] + break + case 'input': + rule.component = 'Input' + break + case 'panel': + rule.component = 'PanelHeader' + break + case 'popover': + rule.component = 'Popover' + break + case 'profileTint': + return null + case 'underlay': + rule.component = 'Underlay' + break + } + + switch (key) { + case 'alert': + case 'alertPopup': + case 'bg': + case 'btn': + case 'input': + case 'panel': + case 'popover': + case 'underlay': + rule.directives = { opacity: originalOpacity } + break + case 'faint': + case 'border': + rule.directives = { textOpacity: originalOpacity } + break + } + + newRules.push(rule) + + if (rule.component === 'Button') { + newRules.push({ ...rule, component: 'ScrollbarElement' }) + newRules.push({ ...rule, component: 'Tab' }) + newRules.push({ ...rule, component: 'Tab', state: ['active'], directives: { opacity: 0 } }) + } + if (rule.component === 'Panel') { + newRules.push({ ...rule, component: 'Post' }) + } + }) + return newRules + } + + const convertRadii = () => { + const newRules = [] + Object.keys(data.radii || {}).forEach(key => { + if (!radiiKeys.has(key) || data.radii[key] === undefined) return null + const originalRadius = data.radii[key] + const rule = {} + + switch (key) { + case 'btn': + rule.component = 'Button' + break + case 'tab': + rule.component = 'Tab' + break + case 'input': + rule.component = 'Input' + break + case 'checkbox': + rule.component = 'Input' + rule.variant = 'checkbox' + break + case 'panel': + rule.component = 'Panel' + break + case 'avatar': + rule.component = 'Avatar' + break + case 'avatarAlt': + rule.component = 'Avatar' + rule.variant = 'compact' + break + case 'tooltip': + rule.component = 'Popover' + break + case 'attachment': + rule.component = 'Attachment' + break + case 'ChatMessage': + rule.component = 'Button' + break + } + rule.directives = { + roundness: originalRadius + } + newRules.push(rule) + if (rule.component === 'Button') { + newRules.push({ ...rule, component: 'ScrollbarElement' }) + newRules.push({ ...rule, component: 'Tab' }) + } + }) + return newRules + } + + const convertFonts = () => { + const newRules = [] + Object.keys(data.fonts || {}).forEach(key => { + if (!fontsKeys.has(key)) return + const originalFont = data.fonts[key].family + const rule = {} + + switch (key) { + case 'interface': + case 'postCode': + rule.component = 'Root' + break + case 'input': + rule.component = 'Input' + break + case 'post': + rule.component = 'RichContent' + break + } + switch (key) { + case 'interface': + case 'input': + case 'post': + rule.directives = { '--font': 'generic | ' + originalFont } + break + case 'postCode': + rule.directives = { '--monoFont': 'generic | ' + originalFont } + newRules.push({ ...rule, component: 'RichContent' }) + break + } + newRules.push(rule) + }) + return newRules + } + const convertShadows = () => { + const newRules = [] + Object.keys(data.shadows || {}).forEach(key => { + if (!shadowsKeys.has(key)) return + const originalShadow = data.shadows[key] + const rule = {} + + switch (key) { + case 'panel': + rule.component = 'Panel' + break + case 'topBar': + rule.component = 'TopBar' + break + case 'popup': + rule.component = 'Popover' + break + case 'avatar': + rule.component = 'Avatar' + break + case 'avatarStatus': + rule.component = 'Avatar' + rule.parent = { component: 'Post' } + break + case 'panelHeader': + rule.component = 'PanelHeader' + break + case 'button': + rule.component = 'Button' + break + case 'buttonHover': + rule.component = 'Button' + rule.state = ['hover'] + break + case 'buttonPressed': + rule.component = 'Button' + rule.state = ['pressed'] + break + case 'input': + rule.component = 'Input' + break + } + rule.directives = { + shadow: originalShadow + } + newRules.push(rule) + if (key === 'topBar') { + newRules.push({ ...rule, component: 'PanelHeader', parent: { component: 'MobileDrawer' } }) + } + if (key === 'avatarStatus') { + newRules.push({ ...rule, parent: { component: 'Notification' } }) + } + if (key === 'buttonPressed') { + newRules.push({ ...rule, state: ['toggled'] }) + newRules.push({ ...rule, state: ['toggled', 'focus'] }) + newRules.push({ ...rule, state: ['pressed', 'focus'] }) + } + if (key === 'buttonHover') { + newRules.push({ ...rule, state: ['toggled', 'hover'] }) + newRules.push({ ...rule, state: ['pressed', 'hover'] }) + newRules.push({ ...rule, state: ['toggled', 'focus', 'hover'] }) + newRules.push({ ...rule, state: ['pressed', 'focus', 'hover'] }) + } + + if (rule.component === 'Button') { + newRules.push({ ...rule, component: 'ScrollbarElement' }) + newRules.push({ ...rule, component: 'Tab' }) + } + }) + return newRules + } + + const extendedRules = Object.entries(extendedBaseKeys).map(([prefix, keys]) => { + if (nonComponentPrefixes.has(prefix)) return null + const rule = {} + if (prefix === 'alertPopup') { + rule.component = 'Alert' + rule.parent = { component: 'Popover' } + } else if (prefix === 'selectedPost') { + rule.component = 'Post' + rule.state = ['selected'] + } else if (prefix === 'selectedMenu') { + rule.component = 'MenuItem' + rule.state = ['hover'] + } else if (prefix === 'chatMessageIncoming') { + rule.component = 'ChatMessage' + } else if (prefix === 'chatMessageOutgoing') { + rule.component = 'ChatMessage' + rule.variant = 'outgoing' + } else if (prefix === 'panel') { + rule.component = 'PanelHeader' + } else if (prefix === 'topBar') { + rule.component = 'TopBar' + } else if (prefix === 'chatMessage') { + rule.component = 'ChatMessage' + } else if (prefix === 'poll') { + rule.component = 'PollGraph' + } else if (prefix === 'btn') { + rule.component = 'Button' + } else { + rule.component = prefix[0].toUpperCase() + prefix.slice(1).toLowerCase() + } + return keys.map((key) => { + if (!data.colors[key]) return null + const leftoverKey = key.replace(prefix, '') + const parts = (leftoverKey || 'Bg').match(/[A-Z][a-z]*/g) + const last = parts.slice(-1)[0] + let newRule = { directives: {} } + let variantArray = [] + + switch (last) { + case 'Text': + case 'Faint': // typo + case 'Link': + case 'Icon': + case 'Greentext': + case 'Cyantext': + case 'Border': + newRule.parent = rule + newRule.directives.textColor = data.colors[key] + newRule.directives.textAuto = 'no-auto' + variantArray = parts.slice(0, -1) + break + default: + newRule = { ...rule, directives: {} } + newRule.directives.background = data.colors[key] + variantArray = parts + break + } + + if (last === 'Text' || last === 'Link') { + const secondLast = parts.slice(-2)[0] + if (secondLast === 'Light') { + return null // unsupported + } else if (secondLast === 'Faint') { + newRule.state = ['faint'] + variantArray = parts.slice(0, -2) + } + } + + switch (last) { + case 'Text': + case 'Link': + case 'Icon': + case 'Border': + newRule.component = last + break + case 'Greentext': + case 'Cyantext': + newRule.component = 'FunText' + newRule.variant = last.toLowerCase() + break + case 'Faint': + newRule.component = 'Text' + newRule.state = ['faint'] + break + } + + variantArray = variantArray.filter(x => x !== 'Bg') + + if (last === 'Link' && prefix === 'selectedPost') { + // selectedPost has typo - duplicate 'Post' + variantArray = variantArray.filter(x => x !== 'Post') + } + + if (prefix === 'popover' && variantArray[0] === 'Post') { + newRule.component = 'Post' + newRule.parent = { component: 'Popover' } + variantArray = variantArray.filter(x => x !== 'Post') + } + + if (prefix === 'selectedMenu' && variantArray[0] === 'Popover') { + newRule.parent = { component: 'Popover' } + variantArray = variantArray.filter(x => x !== 'Popover') + } + + switch (prefix) { + case 'btn': + case 'input': + case 'alert': { + const hasPanel = variantArray.find(x => x === 'Panel') + if (hasPanel) { + newRule.parent = { component: 'PanelHeader' } + variantArray = variantArray.filter(x => x !== 'Panel') + } + const hasTop = variantArray.find(x => x === 'Top') // TopBar + if (hasTop) { + newRule.parent = { component: 'TopBar' } + variantArray = variantArray.filter(x => x !== 'Top' && x !== 'Bar') + } + break + } + } + + if (variantArray.length > 0) { + if (prefix === 'btn') { + newRule.state = variantArray.map(x => x.toLowerCase()) + } else { + newRule.variant = variantArray[0].toLowerCase() + } + } + + if (newRule.component === 'Panel') { + return [newRule, { ...newRule, component: 'MobileDrawer' }] + } else if (newRule.component === 'Button') { + const rules = [ + newRule, + { ...newRule, component: 'Tab' }, + { ...newRule, component: 'ScrollbarElement' } + ] + if (newRule.state?.indexOf('toggled') >= 0) { + rules.push({ ...newRule, state: [...newRule.state, 'focused'] }) + rules.push({ ...newRule, state: [...newRule.state, 'hover'] }) + rules.push({ ...newRule, state: [...newRule.state, 'hover', 'focused'] }) + } + if (newRule.state?.indexOf('hover') >= 0) { + rules.push({ ...newRule, state: [...newRule.state, 'focused'] }) + } + return rules + } else if (newRule.component === 'Badge') { + if (newRule.variant === 'notification') { + return [newRule, { component: 'Root', directives: { '--badgeNotification': 'color | ' + newRule.directives.background } }] + } else if (newRule.variant === 'neutral') { + return [{ ...newRule, variant: 'normal' }] + } else { + return [newRule] + } + } else if (newRule.component === 'TopBar') { + return [newRule, { ...newRule, parent: { component: 'MobileDrawer' }, component: 'PanelHeader' }] + } else { + return [newRule] + } + }) + }) + + const flatExtRules = extendedRules.filter(x => x).reduce((acc, x) => [...acc, ...x], []).filter(x => x).reduce((acc, x) => [...acc, ...x], []) + + return [generateRoot(), ...convertShadows(), ...convertRadii(), ...convertOpacity(), ...convertFonts(), ...flatExtRules] +} diff --git a/src/services/theme_data/theme3_slot_functions.js b/src/services/theme_data/theme3_slot_functions.js @@ -0,0 +1,103 @@ +import { convert, brightness } from 'chromatism' +import { alphaBlend, getTextColor, relativeLuminance } from '../color_convert/color_convert.js' + +export const process = (text, functions, { findColor, findShadow }, { dynamicVars, staticVars }) => { + const { funcName, argsString } = /\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-,.'"\s]*)\)/.exec(text).groups + const args = argsString.split(/,/g).map(a => a.trim()) + + const func = functions[funcName] + if (args.length < func.argsNeeded) { + throw new Error(`$${funcName} requires at least ${func.argsNeeded} arguments, but ${args.length} were provided`) + } + return func.exec(args, { findColor, findShadow }, { dynamicVars, staticVars }) +} + +export const colorFunctions = { + alpha: { + argsNeeded: 2, + exec: (args, { findColor }, { dynamicVars, staticVars }) => { + const [color, amountArg] = args + + const colorArg = convert(findColor(color, { dynamicVars, staticVars })).rgb + const amount = Number(amountArg) + return { ...colorArg, a: amount } + } + }, + textColor: { + argsNeeded: 2, + exec: (args, { findColor }, { dynamicVars, staticVars }) => { + const [backgroundArg, foregroundArg, preserve = 'preserve'] = args + + const background = convert(findColor(backgroundArg, { dynamicVars, staticVars })).rgb + const foreground = convert(findColor(foregroundArg, { dynamicVars, staticVars })).rgb + + return getTextColor(background, foreground, preserve === 'preserve') + } + }, + blend: { + argsNeeded: 3, + exec: (args, { findColor }, { dynamicVars, staticVars }) => { + const [backgroundArg, amountArg, foregroundArg] = args + + const background = convert(findColor(backgroundArg, { dynamicVars, staticVars })).rgb + const foreground = convert(findColor(foregroundArg, { dynamicVars, staticVars })).rgb + const amount = Number(amountArg) + + return alphaBlend(background, amount, foreground) + } + }, + mod: { + argsNeeded: 2, + exec: (args, { findColor }, { dynamicVars, staticVars }) => { + const [colorArg, amountArg] = args + + const color = convert(findColor(colorArg, { dynamicVars, staticVars })).rgb + const amount = Number(amountArg) + + const effectiveBackground = dynamicVars.lowerLevelBackground + const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 + const mod = isLightOnDark ? 1 : -1 + return brightness(amount * mod, color).rgb + } + } +} + +export const shadowFunctions = { + borderSide: { + argsNeeded: 3, + exec: (args, { findColor }) => { + const [color, side, alpha = '1', widthArg = '1', inset = 'inset'] = args + + const width = Number(widthArg) + const isInset = inset === 'inset' + + const targetShadow = { + x: 0, + y: 0, + blur: 0, + spread: 0, + color, + alpha: Number(alpha), + inset: isInset + } + + side.split('-').forEach((position) => { + switch (position) { + case 'left': + targetShadow.x = width * (inset ? 1 : -1) + break + case 'right': + targetShadow.x = -1 * width * (inset ? 1 : -1) + break + case 'top': + targetShadow.y = width * (inset ? 1 : -1) + break + case 'bottom': + targetShadow.y = -1 * width * (inset ? 1 : -1) + break + } + }) + return [targetShadow] + } + } +} diff --git a/src/services/theme_data/theme_data.service.js b/src/services/theme_data/theme_data.service.js @@ -1,5 +1,5 @@ import { convert, brightness, contrastRatio } from 'chromatism' -import { alphaBlendLayers, getTextColor, relativeLuminance } from '../color_convert/color_convert.js' +import { rgb2hex, rgba2css, alphaBlendLayers, getTextColor, relativeLuminance, getCssColor } from '../color_convert/color_convert.js' import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js' /* @@ -407,3 +407,347 @@ export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({ } } }, { colors: {}, opacity: {} }) + +export const composePreset = (colors, radii, shadows, fonts) => { + return { + rules: { + ...shadows.rules, + ...colors.rules, + ...radii.rules, + ...fonts.rules + }, + theme: { + ...shadows.theme, + ...colors.theme, + ...radii.theme, + ...fonts.theme + } + } +} + +export const generatePreset = (input) => { + const colors = generateColors(input) + return composePreset( + colors, + generateRadii(input), + generateShadows(input, colors.theme.colors, colors.mod), + generateFonts(input) + ) +} + +export const getCssShadow = (input, usesDropShadow) => { + if (input.length === 0) { + return 'none' + } + + return input + .filter(_ => usesDropShadow ? _.inset : _) + .map((shad) => [ + shad.x, + shad.y, + shad.blur, + shad.spread + ].map(_ => _ + 'px').concat([ + getCssColor(shad.color, shad.alpha), + shad.inset ? 'inset' : '' + ]).join(' ')).join(', ') +} + +const getCssShadowFilter = (input) => { + if (input.length === 0) { + return 'none' + } + + return input + // drop-shadow doesn't support inset or spread + .filter((shad) => !shad.inset && Number(shad.spread) === 0) + .map((shad) => [ + shad.x, + shad.y, + // drop-shadow's blur is twice as strong compared to box-shadow + shad.blur / 2 + ].map(_ => _ + 'px').concat([ + getCssColor(shad.color, shad.alpha) + ]).join(' ')) + .map(_ => `drop-shadow(${_})`) + .join(' ') +} + +export const generateColors = (themeData) => { + const sourceColors = !themeData.themeEngineVersion + ? colors2to3(themeData.colors || themeData) + : themeData.colors || themeData + + const { colors, opacity } = getColors(sourceColors, themeData.opacity || {}) + + const htmlColors = Object.entries(colors) + .reduce((acc, [k, v]) => { + if (!v) return acc + acc.solid[k] = rgb2hex(v) + acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v) + return acc + }, { complete: {}, solid: {} }) + return { + rules: { + colors: Object.entries(htmlColors.complete) + .filter(([k, v]) => v) + .map(([k, v]) => `--${k}: ${v}`) + .join(';') + }, + theme: { + colors: htmlColors.solid, + opacity + } + } +} + +export const generateRadii = (input) => { + let inputRadii = input.radii || {} + // v1 -> v2 + if (typeof input.btnRadius !== 'undefined') { + inputRadii = Object + .entries(input) + .filter(([k, v]) => k.endsWith('Radius')) + .reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {}) + } + const radii = Object.entries(inputRadii).filter(([k, v]) => v).reduce((acc, [k, v]) => { + acc[k] = v + return acc + }, { + btn: 4, + input: 4, + checkbox: 2, + panel: 10, + avatar: 5, + avatarAlt: 50, + tooltip: 2, + attachment: 5, + chatMessage: inputRadii.panel + }) + + return { + rules: { + radii: Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';') + }, + theme: { + radii + } + } +} + +export const generateFonts = (input) => { + const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => { + acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => { + acc[k] = v + return acc + }, acc[k]) + return acc + }, { + interface: { + family: 'sans-serif' + }, + input: { + family: 'inherit' + }, + post: { + family: 'inherit' + }, + postCode: { + family: 'monospace' + } + }) + + return { + rules: { + fonts: Object + .entries(fonts) + .filter(([k, v]) => v) + .map(([k, v]) => `--${k}Font: ${v.family}`).join(';') + }, + theme: { + fonts + } + } +} + +const border = (top, shadow) => ({ + x: 0, + y: top ? 1 : -1, + blur: 0, + spread: 0, + color: shadow ? '#000000' : '#FFFFFF', + alpha: 0.2, + inset: true +}) +const buttonInsetFakeBorders = [border(true, false), border(false, true)] +const inputInsetFakeBorders = [border(true, true), border(false, false)] +const hoverGlow = { + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '--faint', + alpha: 1 +} + +export const DEFAULT_SHADOWS = { + panel: [{ + x: 1, + y: 1, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }], + topBar: [{ + x: 0, + y: 0, + blur: 4, + spread: 0, + color: '#000000', + alpha: 0.6 + }], + popup: [{ + x: 2, + y: 2, + blur: 3, + spread: 0, + color: '#000000', + alpha: 0.5 + }], + avatar: [{ + x: 0, + y: 1, + blur: 8, + spread: 0, + color: '#000000', + alpha: 0.7 + }], + avatarStatus: [], + panelHeader: [], + button: [{ + x: 0, + y: 0, + blur: 2, + spread: 0, + color: '#000000', + alpha: 1 + }, ...buttonInsetFakeBorders], + buttonHover: [hoverGlow, ...buttonInsetFakeBorders], + buttonPressed: [hoverGlow, ...inputInsetFakeBorders], + input: [...inputInsetFakeBorders, { + x: 0, + y: 0, + blur: 2, + inset: true, + spread: 0, + color: '#000000', + alpha: 1 + }] +} +export const generateShadows = (input, colors) => { + // TODO this is a small hack for `mod` to work with shadows + // this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element + const hackContextDict = { + button: 'btn', + panel: 'bg', + top: 'topBar', + popup: 'popover', + avatar: 'bg', + panelHeader: 'panel', + input: 'input' + } + + const cleanInputShadows = Object.fromEntries( + Object.entries(input.shadows || {}) + .map(([name, shadowSlot]) => [ + name, + // defaulting color to black to avoid potential problems + shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef })) + ]) + ) + const inputShadows = cleanInputShadows && !input.themeEngineVersion + ? shadows2to3(cleanInputShadows, input.opacity) + : cleanInputShadows || {} + const shadows = Object.entries({ + ...DEFAULT_SHADOWS, + ...inputShadows + }).reduce((shadowsAcc, [slotName, shadowDefs]) => { + const slotFirstWord = slotName.replace(/[A-Z].*$/, '') + const colorSlotName = hackContextDict[slotFirstWord] + const isLightOnDark = relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5 + const mod = isLightOnDark ? 1 : -1 + const newShadow = shadowDefs.reduce((shadowAcc, def) => [ + ...shadowAcc, + { + ...def, + color: rgb2hex(computeDynamicColor( + def.color, + (variableSlot) => convert(colors[variableSlot]).rgb, + mod + )) + } + ], []) + return { ...shadowsAcc, [slotName]: newShadow } + }, {}) + + return { + rules: { + shadows: Object + .entries(shadows) + // TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally + // convert all non-inset shadows into filter: drop-shadow() to boost performance + .map(([k, v]) => [ + `--${k}Shadow: ${getCssShadow(v)}`, + `--${k}ShadowFilter: ${getCssShadowFilter(v)}`, + `--${k}ShadowInset: ${getCssShadow(v, true)}` + ].join(';')) + .join(';') + }, + theme: { + shadows + } + } +} + +/** + * This handles compatibility issues when importing v2 theme's shadows to current format + * + * Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables + */ +export const shadows2to3 = (shadows, opacity) => { + return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => { + const isDynamic = ({ color = '#000000' }) => color.startsWith('--') + const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])] + const newShadow = shadowDefs.reduce((shadowAcc, def) => [ + ...shadowAcc, + { + ...def, + alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha + } + ], []) + return { ...shadowsAcc, [slotName]: newShadow } + }, {}) +} + +export const colors2to3 = (colors) => { + return Object.entries(colors).reduce((acc, [slotName, color]) => { + const btnPositions = ['', 'Panel', 'TopBar'] + switch (slotName) { + case 'lightBg': + return { ...acc, highlight: color } + case 'btnText': + return { + ...acc, + ...btnPositions + .reduce( + (statePositionAcc, position) => + ({ ...statePositionAcc, ['btn' + position + 'Text']: color }) + , {} + ) + } + default: + return { ...acc, [slotName]: color } + } + }, {}) +} diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js @@ -0,0 +1,468 @@ +import { convert, brightness } from 'chromatism' +import { flattenDeep } from 'lodash' +import { + alphaBlend, + getTextColor, + rgba2css, + mixrgb, + relativeLuminance +} from '../color_convert/color_convert.js' + +import { + colorFunctions, + shadowFunctions, + process +} from './theme3_slot_functions.js' + +import { + unroll, + getAllPossibleCombinations, + genericRuleToSelector, + normalizeCombination, + findRules +} from './iss_utils.js' +import { parseCssShadow } from './css_utils.js' + +// Ensuring the order of components +const components = { + Root: null, + Text: null, + FunText: null, + Link: null, + Icon: null, + Border: null, + Panel: null, + Chat: null, + ChatMessage: null +} + +const findShadow = (shadows, { dynamicVars, staticVars }) => { + return (shadows || []).map(shadow => { + let targetShadow + if (typeof shadow === 'string') { + if (shadow.startsWith('$')) { + targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars }) + } else if (shadow.startsWith('--')) { + const [variable] = shadow.split(/,/g).map(str => str.trim()) // discarding modifier since it's not supported + const variableSlot = variable.substring(2) + return findShadow(staticVars[variableSlot], { dynamicVars, staticVars }) + } else { + targetShadow = parseCssShadow(shadow) + } + } else { + targetShadow = shadow + } + + const shadowArray = Array.isArray(targetShadow) ? targetShadow : [targetShadow] + return shadowArray.map(s => ({ + ...s, + color: findColor(s.color, { dynamicVars, staticVars }) + })) + }) +} + +const findColor = (color, { dynamicVars, staticVars }) => { + if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color + let targetColor = null + if (color.startsWith('--')) { + const [variable, modifier] = color.split(/,/g).map(str => str.trim()) + const variableSlot = variable.substring(2) + if (variableSlot === 'stack') { + const { r, g, b } = dynamicVars.stacked + targetColor = { r, g, b } + } else if (variableSlot.startsWith('parent')) { + if (variableSlot === 'parent') { + const { r, g, b } = dynamicVars.lowerLevelBackground + targetColor = { r, g, b } + } else { + const virtualSlot = variableSlot.replace(/^parent/, '') + targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb + } + } else { + switch (variableSlot) { + case 'inheritedBackground': + targetColor = convert(dynamicVars.inheritedBackground).rgb + break + case 'background': + targetColor = convert(dynamicVars.background).rgb + break + default: + targetColor = convert(staticVars[variableSlot]).rgb + } + } + + if (modifier) { + const effectiveBackground = dynamicVars.lowerLevelBackground ?? targetColor + const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 + const mod = isLightOnDark ? 1 : -1 + targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb + } + } + + if (color.startsWith('$')) { + try { + targetColor = process(color, colorFunctions, { findColor }, { dynamicVars, staticVars }) + } catch (e) { + console.error('Failure executing color function', e) + targetColor = '#FF00FF' + } + } + // Color references other color + return targetColor +} + +const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVars) => { + const opacity = directives.textOpacity + const backgroundColor = convert(dynamicVars.lowerLevelBackground).rgb + const textColor = convert(findColor(intendedTextColor, { dynamicVars, staticVars })).rgb + if (opacity === null || opacity === undefined || opacity >= 1) { + return convert(textColor).hex + } + if (opacity === 0) { + return convert(backgroundColor).hex + } + const opacityMode = directives.textOpacityMode + switch (opacityMode) { + case 'fake': + return convert(alphaBlend(textColor, opacity, backgroundColor)).hex + case 'mixrgb': + return convert(mixrgb(backgroundColor, textColor)).hex + default: + return rgba2css({ a: opacity, ...textColor }) + } +} + +// Loading all style.js[on] files dynamically +const componentsContext = require.context('src', true, /\.style.js(on)?$/) +componentsContext.keys().forEach(key => { + const component = componentsContext(key).default + if (components[component.name] != null) { + console.warn(`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`) + } + components[component.name] = component +}) + +const ruleToSelector = genericRuleToSelector(components) + +export const init = (extraRuleset, ultimateBackgroundColor) => { + const staticVars = {} + const stacked = {} + const computed = {} + + const rulesetUnsorted = [ + ...Object.values(components) + .map(c => (c.defaultRules || []).map(r => ({ component: c.name, ...r }))) + .reduce((acc, arr) => [...acc, ...arr], []), + ...extraRuleset + ].map(rule => { + normalizeCombination(rule) + let currentParent = rule.parent + while (currentParent) { + normalizeCombination(currentParent) + currentParent = currentParent.parent + } + + return rule + }) + + const ruleset = rulesetUnsorted + .map((data, index) => ({ data, index })) + .sort(({ data: a, index: ai }, { data: b, index: bi }) => { + const parentsA = unroll(a).length + const parentsB = unroll(b).length + + if (parentsA === parentsB) { + if (a.component === 'Text') return -1 + if (b.component === 'Text') return 1 + return ai - bi + } + if (parentsA === 0 && parentsB !== 0) return -1 + if (parentsB === 0 && parentsA !== 0) return 1 + return parentsA - parentsB + }) + .map(({ data }) => data) + + const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name)) + + const processCombination = (combination) => { + const selector = ruleToSelector(combination, true) + const cssSelector = ruleToSelector(combination) + + const parentSelector = selector.split(/ /g).slice(0, -1).join(' ') + const soloSelector = selector.split(/ /g).slice(-1)[0] + + const lowerLevelSelector = parentSelector + const lowerLevelBackground = computed[lowerLevelSelector]?.background + const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives + const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw + + const dynamicVars = computed[selector] || { + lowerLevelBackground, + lowerLevelVirtualDirectives, + lowerLevelVirtualDirectivesRaw + } + + // Inheriting all of the applicable rules + const existingRules = ruleset.filter(findRules(combination)) + const computedDirectives = existingRules.map(r => r.directives).reduce((acc, directives) => ({ ...acc, ...directives }), {}) + const computedRule = { + ...combination, + directives: computedDirectives + } + + computed[selector] = computed[selector] || {} + computed[selector].computedRule = computedRule + computed[selector].dynamicVars = dynamicVars + + if (virtualComponents.has(combination.component)) { + const virtualName = [ + '--', + combination.component.toLowerCase(), + combination.variant === 'normal' + ? '' + : combination.variant[0].toUpperCase() + combination.variant.slice(1).toLowerCase(), + ...combination.state.filter(x => x !== 'normal').toSorted().map(state => state[0].toUpperCase() + state.slice(1).toLowerCase()) + ].join('') + + let inheritedTextColor = computedDirectives.textColor + let inheritedTextAuto = computedDirectives.textAuto + let inheritedTextOpacity = computedDirectives.textOpacity + let inheritedTextOpacityMode = computedDirectives.textOpacityMode + const lowerLevelTextSelector = [...selector.split(/ /g).slice(0, -1), soloSelector].join(' ') + const lowerLevelTextRule = computed[lowerLevelTextSelector] + + if (inheritedTextColor == null || inheritedTextOpacity == null || inheritedTextOpacityMode == null) { + inheritedTextColor = computedDirectives.textColor ?? lowerLevelTextRule.textColor + inheritedTextAuto = computedDirectives.textAuto ?? lowerLevelTextRule.textAuto + inheritedTextOpacity = computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity + inheritedTextOpacityMode = computedDirectives.textOpacityMode ?? lowerLevelTextRule.textOpacityMode + } + + const newTextRule = { + ...computedRule, + directives: { + ...computedRule.directives, + textColor: inheritedTextColor, + textAuto: inheritedTextAuto ?? 'preserve', + textOpacity: inheritedTextOpacity, + textOpacityMode: inheritedTextOpacityMode + } + } + + dynamicVars.inheritedBackground = lowerLevelBackground + dynamicVars.stacked = convert(stacked[lowerLevelSelector]).rgb + + const intendedTextColor = convert(findColor(inheritedTextColor, { dynamicVars, staticVars })).rgb + const textColor = newTextRule.directives.textAuto === 'no-auto' + ? intendedTextColor + : getTextColor( + convert(stacked[lowerLevelSelector]).rgb, + intendedTextColor, + newTextRule.directives.textAuto === 'preserve' + ) + const virtualDirectives = computed[lowerLevelSelector].virtualDirectives || {} + const virtualDirectivesRaw = computed[lowerLevelSelector].virtualDirectivesRaw || {} + + // Storing color data in lower layer to use as custom css properties + virtualDirectives[virtualName] = getTextColorAlpha(newTextRule.directives, textColor, dynamicVars) + virtualDirectivesRaw[virtualName] = textColor + + computed[lowerLevelSelector].virtualDirectives = virtualDirectives + computed[lowerLevelSelector].virtualDirectivesRaw = virtualDirectivesRaw + + return { + dynamicVars, + selector: cssSelector.split(/ /g).slice(0, -1).join(' '), + ...combination, + directives: {}, + virtualDirectives: { + [virtualName]: getTextColorAlpha(newTextRule.directives, textColor, dynamicVars) + }, + virtualDirectivesRaw: { + [virtualName]: textColor + } + } + } else { + computed[selector] = computed[selector] || {} + + // TODO: DEFAULT TEXT COLOR + const lowerLevelStackedBackground = stacked[lowerLevelSelector] || convert(ultimateBackgroundColor).rgb + + if (computedDirectives.background) { + let inheritRule = null + const variantRules = ruleset.filter( + findRules({ + component: combination.component, + variant: combination.variant, + parent: combination.parent + }) + ) + const lastVariantRule = variantRules[variantRules.length - 1] + if (lastVariantRule) { + inheritRule = lastVariantRule + } else { + const normalRules = ruleset.filter(findRules({ + component: combination.component, + parent: combination.parent + })) + const lastNormalRule = normalRules[normalRules.length - 1] + inheritRule = lastNormalRule + } + + const inheritSelector = ruleToSelector({ ...inheritRule, parent: combination.parent }, true) + const inheritedBackground = computed[inheritSelector].background + + dynamicVars.inheritedBackground = inheritedBackground + + const rgb = convert(findColor(computedDirectives.background, { dynamicVars, staticVars })).rgb + + if (!stacked[selector]) { + let blend + const alpha = computedDirectives.opacity ?? 1 + if (alpha >= 1) { + blend = rgb + } else if (alpha <= 0) { + blend = lowerLevelStackedBackground + } else { + blend = alphaBlend(rgb, computedDirectives.opacity, lowerLevelStackedBackground) + } + stacked[selector] = blend + computed[selector].background = { ...rgb, a: computedDirectives.opacity ?? 1 } + } + } + + if (computedDirectives.shadow) { + dynamicVars.shadow = flattenDeep(findShadow(flattenDeep(computedDirectives.shadow), { dynamicVars, staticVars })) + } + + if (!stacked[selector]) { + computedDirectives.background = 'transparent' + computedDirectives.opacity = 0 + stacked[selector] = lowerLevelStackedBackground + computed[selector].background = { ...lowerLevelStackedBackground, a: 0 } + } + + dynamicVars.stacked = stacked[selector] + dynamicVars.background = computed[selector].background + + const dynamicSlots = Object.entries(computedDirectives).filter(([k, v]) => k.startsWith('--')) + + dynamicSlots.forEach(([k, v]) => { + const [type, ...value] = v.split('|').map(x => x.trim()) // woah, Extreme! + switch (type) { + case 'color': { + const color = findColor(value[0], { dynamicVars, staticVars }) + dynamicVars[k] = color + if (combination.component === 'Root') { + staticVars[k.substring(2)] = color + } + break + } + case 'shadow': { + const shadow = value + dynamicVars[k] = shadow + if (combination.component === 'Root') { + staticVars[k.substring(2)] = shadow + } + break + } + case 'generic': { + dynamicVars[k] = value + if (combination.component === 'Root') { + staticVars[k.substring(2)] = value + } + break + } + } + }) + + const rule = { + dynamicVars, + selector: cssSelector, + ...combination, + directives: computedDirectives + } + + return rule + } + } + + const processInnerComponent = (component, parent) => { + const combinations = [] + const { + validInnerComponents = [], + states: originalStates = {}, + variants: originalVariants = {} + } = component + + // Normalizing states and variants to always include "normal" + const states = { normal: '', ...originalStates } + const variants = { normal: '', ...originalVariants } + const innerComponents = (validInnerComponents).map(name => { + const result = components[name] + if (result === undefined) console.error(`Component ${component.name} references a component ${name} which does not exist!`) + return result + }) + + // Optimization: we only really need combinations without "normal" because all states implicitly have it + const permutationStateKeys = Object.keys(states).filter(s => s !== 'normal') + const stateCombinations = [ + ['normal'], + ...getAllPossibleCombinations(permutationStateKeys) + .map(combination => ['normal', ...combination]) + .filter(combo => { + // Optimization: filter out some hard-coded combinations that don't make sense + if (combo.indexOf('disabled') >= 0) { + return !( + combo.indexOf('hover') >= 0 || + combo.indexOf('focused') >= 0 || + combo.indexOf('pressed') >= 0 + ) + } + return true + }) + ] + + const stateVariantCombination = Object.keys(variants).map(variant => { + return stateCombinations.map(state => ({ variant, state })) + }).reduce((acc, x) => [...acc, ...x], []) + + stateVariantCombination.forEach(combination => { + combination.component = component.name + combination.lazy = component.lazy || parent?.lazy + combination.parent = parent + if (combination.state.indexOf('hover') >= 0) { + combination.lazy = true + } + + combinations.push(combination) + + innerComponents.forEach(innerComponent => { + combinations.push(...processInnerComponent(innerComponent, combination)) + }) + }) + + return combinations + } + + const t0 = performance.now() + const combinations = processInnerComponent(components.Root) + const t1 = performance.now() + console.debug('Tree traveral took ' + (t1 - t0) + ' ms') + + const result = combinations.map((combination) => { + if (combination.lazy) { + return async () => processCombination(combination) + } else { + return processCombination(combination) + } + }).filter(x => x) + const t2 = performance.now() + console.debug('Eager processing took ' + (t2 - t1) + ' ms') + + return { + lazy: result.filter(x => typeof x === 'function'), + eager: result.filter(x => typeof x !== 'function'), + staticVars + } +} diff --git a/test/unit/specs/services/theme_data/theme_data3.spec.js b/test/unit/specs/services/theme_data/theme_data3.spec.js @@ -0,0 +1,144 @@ +// import { topoSort } from 'src/services/theme_data/theme_data.service.js' +import { + getAllPossibleCombinations +} from 'src/services/theme_data/iss_utils.js' +import { + init +} from 'src/services/theme_data/theme_data_3.service.js' +import { + basePaletteKeys +} from 'src/services/theme_data/theme2_to_theme3.js' + +describe('Theme Data 3', () => { + describe('getAllPossibleCombinations', () => { + it('test simple 3 values case', () => { + const out = getAllPossibleCombinations([1, 2, 3]).map(x => x.sort((a, b) => a - b)) + expect(out).to.eql([ + [1], [2], [3], + [1, 2], [1, 3], [2, 3], + [1, 2, 3] + ]) + }) + + it('test simple 4 values case', () => { + const out = getAllPossibleCombinations([1, 2, 3, 4]).map(x => x.sort((a, b) => a - b)) + expect(out).to.eql([ + [1], [2], [3], [4], + [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4], + [1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4], + [1, 2, 3, 4] + ]) + }) + + it('test massive 5 values case, using strings', () => { + const out = getAllPossibleCombinations(['a', 'b', 'c', 'd', 'e']).map(x => x.sort((a, b) => a - b)) + expect(out).to.eql([ + // 1 + ['a'], ['b'], ['c'], ['d'], ['e'], + // 2 + ['a', 'b'], ['a', 'c'], ['a', 'd'], ['a', 'e'], + ['b', 'c'], ['b', 'd'], ['b', 'e'], + ['c', 'd'], ['c', 'e'], + ['d', 'e'], + // 3 + ['a', 'b', 'c'], ['a', 'b', 'd'], ['a', 'b', 'e'], + ['a', 'c', 'd'], ['a', 'c', 'e'], + ['a', 'd', 'e'], + + ['b', 'c', 'd'], ['b', 'c', 'e'], + ['b', 'd', 'e'], + + ['c', 'd', 'e'], + // 4 + ['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'e'], + ['a', 'b', 'd', 'e'], + + ['a', 'c', 'd', 'e'], + + ['b', 'c', 'd', 'e'], + // 5 + ['a', 'b', 'c', 'd', 'e'] + ]) + }) + }) + + describe('init', function () { + this.timeout(5000) + + it('Test initialization without anything', () => { + const out = init([], '#DEADAF') + + expect(out).to.have.property('eager') + expect(out).to.have.property('lazy') + expect(out).to.have.property('staticVars') + + expect(out.lazy).to.be.an('array') + expect(out.lazy).to.have.lengthOf.above(1) + expect(out.eager).to.be.an('array') + expect(out.eager).to.have.lengthOf.above(1) + expect(out.staticVars).to.be.an('object') + + // check backwards compat/generic stuff + basePaletteKeys.forEach(key => { + expect(out.staticVars).to.have.property(key) + }) + }) + + it('Test initialization with a basic palette', () => { + const out = init([{ + component: 'Root', + directives: { + '--bg': 'color | #008080', + '--fg': 'color | #00C0A0' + } + }], '#DEADAF') + + expect(out.staticVars).to.have.property('bg').equal('#008080') + expect(out.staticVars).to.have.property('fg').equal('#00C0A0') + + const panelRule = out.eager.filter(x => { + if (x.component !== 'Panel') return false + return true + })[0] + + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked', { r: 0, g: 128, b: 128 }) + }) + + it('Test initialization with opacity', () => { + const out = init([{ + component: 'Root', + directives: { + '--bg': 'color | #008080' + } + }, { + component: 'Panel', + directives: { + opacity: 0.5 + } + }], '#DEADAF') + + expect(out.staticVars).to.have.property('bg').equal('#008080') + + const panelRule = out.eager.filter(x => { + if (x.component !== 'Panel') return false + return true + })[0] + + expect(panelRule).to.have.nested.deep.property('dynamicVars.background', { r: 0, g: 128, b: 128, a: 0.5 }) + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked') + // Somewhat incorrect since we don't do gamma correction + // real expectancy should be this: + /* + + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.r').that.is.closeTo(147.0, 0.01) + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.g').that.is.closeTo(143.2, 0.01) + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.b').that.is.closeTo(144.0, 0.01) + + */ + + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.r').that.is.closeTo(88.8, 0.01) + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.g').that.is.closeTo(133.2, 0.01) + expect(panelRule).to.have.nested.deep.property('dynamicVars.stacked.b').that.is.closeTo(134, 0.01) + }) + }) +}) diff --git a/tools/check-changelog b/tools/check-changelog @@ -6,7 +6,7 @@ git remote add upstream https://git.pleroma.social/pleroma/pleroma-fe.git git fetch upstream ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}:refs/remotes/upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME git diff --raw --no-renames upstream/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME HEAD -- changelog.d | \ - grep ' A\t' | grep '\.\(skip\|add\|remove\|fix\|security\)$' + grep ' A\t' | grep '\.\(skip\|add\|remove\|change\|fix\|security\)$' ret=$? if [ $ret -eq 0 ]; then