logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: d7f744d2819343eced508a589d6fad3842686a3e
parent 23edfe7b918844f8cc2ff5b9b245952092ee1cbf
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date:   Tue, 26 Mar 2024 17:13:41 +0000

Merge branch 'themes3' into 'develop'

Themes 3.0 / Pleroma ISS (Interface Style Sheets) [Side A]

See merge request pleroma/pleroma-fe!1892

Diffstat:

Achangelog.d/themes3.change1+
Msrc/App.scss415++++++++++++++++++++++++++++++++++++++-----------------------------------------
Dsrc/_variables.scss36------------------------------------
Msrc/boot/after_store.js17+++++++----------
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.scss4----
Msrc/components/conversation/conversation.vue78++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
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.vue122+++++++++++++++----------------------------------------------------------------
Msrc/components/post_status_form/post_status_form.vue67++++++++++++++++---------------------------------------------------
Msrc/components/quick_filter_settings/quick_filter_settings.vue26+++++++++++++-------------
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.scss25+++++++++++++++++++++----
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/size_setting.vue2+-
Msrc/components/settings_modal/helpers/string_setting.vue2+-
Msrc/components/settings_modal/settings_modal.scss2--
Msrc/components/settings_modal/settings_modal.vue8++++----
Msrc/components/settings_modal/settings_modal_admin_content.scss7++-----
Msrc/components/settings_modal/settings_modal_user_content.scss7++-----
Msrc/components/settings_modal/tabs/filtering_tab.vue2+-
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.vue8++------
Msrc/components/side_drawer/side_drawer.vue96+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Asrc/components/status/post.style.js33+++++++++++++++++++++++++++++++++
Msrc/components/status/status.scss56++++++++++++++++----------------------------------------
Msrc/components/status/status.vue4++--
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.scss29++++++++++++++---------------
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.scss67++++++++++++++-----------------------------------------------------
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.vue112++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/components/user_reporting_modal/user_reporting_modal.vue10+++-------
Msrc/hocs/with_load_more/with_load_more.scss5+----
Msrc/panel.scss111++++++++++++++++++++++++++++---------------------------------------------------
Msrc/services/color_convert/color_convert.js2+-
Msrc/services/style_setter/style_setter.js436++++++++++++++-----------------------------------------------------------------
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.js534+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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+-
189 files changed, 4628 insertions(+), 2266 deletions(-)

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 @@ -16,16 +17,15 @@ } 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 +42,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 +79,7 @@ body { &::-webkit-scrollbar-button { --___bgPadding: 2px; - color: var(--btnText); + color: var(--text); background-repeat: no-repeat, no-repeat; &:horizontal { @@ -69,15 +87,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 +105,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 +122,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 +145,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 +197,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 +205,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 +366,110 @@ 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); + 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); + + --__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); padding: 0; line-height: unset; cursor: pointer; @@ -473,28 +477,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 +501,9 @@ 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); + box-shadow: var(--shadow); + font-family: var(--font); font-size: 1em; margin: 0; box-sizing: border-box; @@ -528,7 +517,6 @@ textarea, &[disabled="disabled"], &.disabled { cursor: not-allowed; - opacity: 0.5; } &[type="range"] { @@ -543,9 +531,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 +547,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 +567,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 +587,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 +606,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 +634,7 @@ option { li { border: 1px solid var(--border); - border-radius: var(--inputRadius); + border-radius: var(--roundness); padding: 0.5em; margin: 0.25em; } @@ -714,74 +695,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 +767,10 @@ option { &.iconLetter { font-size: 1.1em; } + + &.svg-inline--fa { + vertical-align: -0.15em; + } } .fa-old-padding { @@ -816,6 +785,11 @@ option { opacity: 0.25; } +.timeago { + --link: var(--text); + --linkFaint: var(--textFaint); +} + .login-hint { text-align: center; @@ -914,3 +888,8 @@ option { padding: 0; position: absolute; } + +*::selection { + color: var(--selectionText); + background-color: var(--selectionBackground); +} 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 }) => { 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; @@ -12,8 +10,6 @@ input { background: none; - color: $fallback--lightText; - color: var(--inputText, $fallback--lightText); border: none; padding: 0; margin: 0; diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue @@ -28,7 +28,10 @@ class="rightside-button" /> </div> - <div class="conversation-body panel-body"> + <div + class="conversation-body" + :class="{ 'panel-body': isExpanded }" + > <div v-if="isTreeView" class="thread-body" @@ -78,7 +81,7 @@ :replies="getReplies(status.id)" :in-profile="inProfile" :profile-user-id="profileUserId" - class="conversation-status status-fadein panel-body" + class="conversation-status status-fadein" :simple-tree="treeViewIsSimple" :toggle-thread-display="toggleThreadDisplay" @@ -183,7 +186,7 @@ :replies="getReplies(status.id)" :in-profile="inProfile" :profile-user-id="profileUserId" - class="conversation-status status-fadein panel-body" + class="conversation-status status-fadein" :toggle-thread-display="toggleThreadDisplay" :thread-display-status="threadDisplayStatus" @@ -203,6 +206,7 @@ </div> <div v-else + class="Conversation -hidden" :style="hiddenStyle" /> </template> @@ -210,14 +214,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 +234,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 +262,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 +280,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,42 +51,31 @@ 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; } .dropdown-menu { display: block; - padding: 0.5rem 0; + padding: 0; font-size: 1em; text-align: left; list-style: none; @@ -100,34 +87,16 @@ 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 +107,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 +135,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.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,43 @@ /> </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="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.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,12 @@ vertical-align: middle; object-fit: contain; } + + .greentext { + color: var(--funtextGreentext); + } + + .cyantext { + color: var(--funtextCyantext); + } } 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/size_setting.vue b/src/components/settings_modal/helpers/size_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/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/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/settings_modal_admin_content.scss b/src/components/settings_modal/settings_modal_admin_content.scss @@ -1,10 +1,8 @@ -@import "src/variables"; - .settings_tab-switcher { height: 100%; .setting-item { - border-bottom: 2px solid var(--fg, $fallback--fg); + border-bottom: 2px solid var(--border); margin: 1em 1em 1.4em; padding-bottom: 1.4em; @@ -45,8 +43,7 @@ .unavailable, .unavailable svg { - color: var(--cRed, $fallback--cRed); - color: $fallback--cRed; + color: var(--cRed); } } } diff --git a/src/components/settings_modal/settings_modal_user_content.scss b/src/components/settings_modal/settings_modal_user_content.scss @@ -1,10 +1,8 @@ -@import "src/variables"; - .settings_tab-switcher { height: 100%; .setting-item { - border-bottom: 2px solid var(--fg, $fallback--fg); + border-bottom: 2px solid var(--border); margin: 1em 1em 1.4em; padding-bottom: 1.4em; @@ -45,8 +43,7 @@ .unavailable, .unavailable svg { - color: var(--cRed, $fallback--cRed); - color: $fallback--cRed; + color: var(--cRed); } } } diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue @@ -67,7 +67,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 @@ -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.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 @@ -180,7 +180,7 @@ <span class="heading-right"> <router-link - class="timeago faint-link" + class="timeago faint" :to="{ name: 'conversation', params: { id: status.id } }" > <Timeago @@ -450,7 +450,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,14 @@ } .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); position: relative; white-space: nowrap; padding: 6px 1em; @@ -188,8 +189,6 @@ &.active { background: transparent; z-index: 5; - color: $fallback--text; - color: var(--tabActiveText, $fallback--text); } img { @@ -231,7 +230,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); } } @@ -334,8 +297,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" @@ -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/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/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -1,24 +1,87 @@ -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 applyTheme = async (input) => { + 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 + // 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()) const body = document.body + + const styleSheet = new CSSStyleSheet() + document.adoptedStyleSheets = [styleSheet] body.classList.add('hidden') - const styleEl = document.createElement('style') - head.appendChild(styleEl) - const styleSheet = styleEl.sheet + 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('') + styleSheet.insertRule(newRule, 'index-max') + } else { + styleSheet.insertRule(rule, 'index-max') + } + }) - 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') + + // 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[1], + '}' + ].join('') + styleSheet.insertRule(newRule, 'index-max') + } else { + styleSheet.insertRule(rule, 'index-max') + } + }) + // const t1 = performance.now() + // console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms') + // t0 = t1 + counter += 1 + if (counter < chunks.length) { + setTimeout(processChunk, 0) + } + }) + } + setTimeout(processChunk, 0) + + return Promise.resolve() } const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) => @@ -51,308 +114,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 +143,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 +169,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', + + 'badgeNotification', + 'badgeNotificationText', + + 'badgeNeutral', + 'badgeNeutralText', + + '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,534 @@ +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 { + 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