diff options
Diffstat (limited to 'src')
216 files changed, 7047 insertions, 2346 deletions
diff --git a/src/App.scss b/src/App.scss index ef68ac50..6e0aabca 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,9 +1,10 @@ // stylelint-disable rscss/class-format /* stylelint-disable no-descending-specificity */ -@import "./variables"; @import "./panel"; :root { + --font-size: 14px; + --status-margin: 0.75em; --navbar-height: 3.5rem; --post-line-height: 1.4; // Z-Index stuff @@ -13,19 +14,21 @@ --ZI_navbar_popovers: 7500; --ZI_navbar: 7000; --ZI_popovers: 6000; + + // Fallback for when stuff is loading + --background: var(--bg); } html { - font-size: 14px; + font-size: var(--font-size); // overflow-x: clip causes my browser's tab to crash with SIGILL lul } body { font-family: sans-serif; - font-family: var(--interfaceFont, sans-serif); + font-family: var(--font); margin: 0; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overscroll-behavior-y: none; @@ -42,17 +45,35 @@ body { // have a cursor/pointer to operate them @media (any-pointer: fine) { * { - scrollbar-color: var(--btn) transparent; + scrollbar-color: var(--fg) transparent; &::-webkit-scrollbar { background: transparent; } + &::-webkit-scrollbar-corner { + background: transparent; + } + + &::-webkit-resizer { + /* stylelint-disable-next-line declaration-no-important */ + background-color: transparent !important; + background-image: + linear-gradient( + 135deg, + transparent calc(50% - 1px), + var(--textFaint) 50%, + transparent calc(50% + 1px), + transparent calc(75% - 1px), + var(--textFaint) 75%, + transparent calc(75% + 1px), + ); + } + &::-webkit-scrollbar-button, &::-webkit-scrollbar-thumb { - background-color: var(--btn); - box-shadow: var(--buttonShadow); - border-radius: var(--btnRadius); + box-shadow: var(--shadow); + border-radius: var(--roundness); } // horizontal/vertical/increment/decrement are webkit-specific stuff @@ -61,7 +82,7 @@ body { &::-webkit-scrollbar-button { --___bgPadding: 2px; - color: var(--btnText); + color: var(--text); background-repeat: no-repeat, no-repeat; &:horizontal { @@ -69,15 +90,15 @@ body { &:increment { background-image: - linear-gradient(45deg, var(--btnText) 50%, transparent 51%), - linear-gradient(-45deg, transparent 50%, var(--btnText) 51%); + linear-gradient(45deg, var(--text) 50%, transparent 51%), + linear-gradient(-45deg, transparent 50%, var(--text) 51%); background-position: top var(--___bgPadding) left 50%, right 50% bottom var(--___bgPadding); } &:decrement { background-image: - linear-gradient(45deg, transparent 50%, var(--btnText) 51%), - linear-gradient(-45deg, var(--btnText) 50%, transparent 51%); + linear-gradient(45deg, transparent 50%, var(--text) calc(50% + 1px)), + linear-gradient(-45deg, var(--text) 50%, transparent 51%); background-position: bottom var(--___bgPadding) right 50%, left 50% top var(--___bgPadding); } } @@ -87,15 +108,15 @@ body { &:increment { background-image: - linear-gradient(-45deg, transparent 50%, var(--btnText) 51%), - linear-gradient(45deg, transparent 50%, var(--btnText) 51%); + linear-gradient(-45deg, transparent 50%, var(--text) 51%), + linear-gradient(45deg, transparent 50%, var(--text) 51%); background-position: right var(--___bgPadding) top 50%, left var(--___bgPadding) top 50%; } &:decrement { background-image: - linear-gradient(-45deg, var(--btnText) 50%, transparent 51%), - linear-gradient(45deg, var(--btnText) 50%, transparent 51%); + linear-gradient(-45deg, var(--text) 50%, transparent 51%), + linear-gradient(45deg, var(--text) 50%, transparent 51%); background-position: left var(--___bgPadding) top 50%, right var(--___bgPadding) top 50%; } } @@ -104,15 +125,14 @@ body { } // Body should have background to scrollbar otherwise it will use white (body color?) html { - scrollbar-color: var(--selectedMenu) var(--wallpaper); + scrollbar-color: var(--fg) var(--wallpaper); background: var(--wallpaper); } } a { text-decoration: none; - color: $fallback--link; - color: var(--link, $fallback--link); + color: var(--link); } h4 { @@ -128,27 +148,12 @@ h4 { i[class*="icon-"], .svg-inline--fa, .iconLetter { - color: $fallback--icon; - color: var(--icon, $fallback--icon); -} - -.button-unstyled:hover, -a:hover { - > i[class*="icon-"], - > .svg-inline--fa, - > .iconLetter { - color: var(--text); - } + color: var(--icon); } nav { z-index: var(--ZI_navbar); - background-color: $fallback--fg; - background-color: var(--topBar, $fallback--fg); - color: $fallback--faint; - color: var(--faint, $fallback--faint); - box-shadow: 0 0 4px rgb(0 0 0 / 60%); - box-shadow: var(--topBarShadow); + box-shadow: var(--shadow); box-sizing: border-box; height: var(--navbar-height); position: fixed; @@ -195,8 +200,7 @@ nav { grid-column: 1 / span 3; grid-row: 1 / 1; pointer-events: none; - background-color: rgb(0 0 0 / 15%); - background-color: var(--underlay, rgb(0 0 0 / 15%)); + background-color: var(--underlay); z-index: -1000; } @@ -204,7 +208,6 @@ nav { --miniColumn: 25rem; --maxiColumn: 45rem; --columnGap: 1em; - --status-margin: 0.75em; --effectiveSidebarColumnWidth: minmax(var(--miniColumn), var(--sidebarColumnWidth, var(--miniColumn))); --effectiveNotifsColumnWidth: minmax(var(--miniColumn), var(--notifsColumnWidth, var(--miniColumn))); --effectiveContentColumnWidth: minmax(var(--miniColumn), var(--contentColumnWidth, var(--maxiColumn))); @@ -366,106 +369,113 @@ nav { .button-default { user-select: none; - color: $fallback--text; - color: var(--btnText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); + color: var(--text); border: none; - border-radius: $fallback--btnRadius; - border-radius: var(--btnRadius, $fallback--btnRadius); + border-radius: var(--roundness); cursor: pointer; - box-shadow: $fallback--buttonShadow; - box-shadow: var(--buttonShadow); + background-color: var(--background); + box-shadow: var(--shadow); font-size: 1em; font-family: sans-serif; - font-family: var(--interfaceFont, sans-serif); + font-family: var(--font); - &.-sublime { - background: transparent; + &::-moz-focus-inner { + border: none; } - i[class*="icon-"], - .svg-inline--fa { - color: $fallback--text; - color: var(--btnText, $fallback--text); + &:disabled { + cursor: not-allowed; } +} - &::-moz-focus-inner { - border: none; +.menu-item, +.list-item { + display: block; + box-sizing: border-box; + border: none; + outline: none; + text-align: initial; + font-size: inherit; + font-family: inherit; + font-weight: 400; + cursor: pointer; + color: inherit; + clear: both; + position: relative; + white-space: nowrap; + border-color: var(--border); + border-style: solid; + border-width: 0; + border-top-width: 1px; + width: 100%; + line-height: var(--__line-height); + padding: var(--__vertical-gap) var(--__horizontal-gap); + background: transparent; + + --__line-height: 1.5em; + --__horizontal-gap: 0.75em; + --__vertical-gap: 0.5em; + + &.-non-interactive { + cursor: auto; } + &.-active, &:hover { - box-shadow: 0 0 4px rgb(255 255 255 / 30%); - box-shadow: var(--buttonHoverShadow); - } - - &:active { - box-shadow: - 0 0 4px 0 rgb(255 255 255 / 30%), - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset; - box-shadow: var(--buttonPressedShadow); - color: $fallback--text; - color: var(--btnPressedText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnPressed, $fallback--fg); - - svg, - i { - color: $fallback--text; - color: var(--btnPressedText, $fallback--text); - } + border-top-width: 1px; + border-bottom-width: 1px; } - &:disabled { - cursor: not-allowed; - color: $fallback--text; - color: var(--btnDisabledText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnDisabled, $fallback--fg); - - svg, - i { - color: $fallback--text; - color: var(--btnDisabledText, $fallback--text); - } + &.-active + &, + &:hover + & { + border-top-width: 0; } - &.toggled { - color: $fallback--text; - color: var(--btnToggledText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--btnToggled, $fallback--fg); - box-shadow: - 0 0 4px 0 rgb(255 255 255 / 30%), - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset; - box-shadow: var(--buttonPressedShadow); - - svg, - i { - color: $fallback--text; - color: var(--btnToggledText, $fallback--text); - } + &:hover + .menu-item-collapsible:not(.-expanded) + &, + &.-active + .menu-item-collapsible:not(.-expanded) + & { + border-top-width: 0; + } + + &[aria-expanded="true"] { + border-bottom-width: 1px; + } + + a, + button:not(.button-default) { + text-align: initial; + padding: 0; + background: none; + border: none; + outline: none; + display: inline; + font-size: 100%; + font-family: inherit; + line-height: unset; + color: var(--text); } - &.danger { - // TODO: add better color variable - color: $fallback--text; - color: var(--alertErrorPanelText, $fallback--text); - background-color: $fallback--alertError; - background-color: var(--alertError, $fallback--alertError); + &:first-child { + border-top-right-radius: var(--roundness); + border-top-left-radius: var(--roundness); + border-top-width: 0; + } + + &:last-child { + border-bottom-right-radius: var(--roundness); + border-bottom-left-radius: var(--roundness); + border-bottom-width: 0; } } .button-unstyled { - background: none; border: none; outline: none; display: inline; text-align: initial; font-size: 100%; font-family: inherit; + box-shadow: var(--shadow); + background-color: transparent; padding: 0; line-height: unset; cursor: pointer; @@ -473,28 +483,23 @@ nav { color: inherit; &.-link { - color: $fallback--link; - color: var(--link, $fallback--link); - } - - &.-fullwidth { - width: 100%; - } - - &.-hover-highlight { - &:hover svg { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } + /* stylelint-disable-next-line declaration-no-important */ + color: var(--link) !important; } } input, -textarea, +textarea { + border: none; + display: inline-block; + outline: none; +} + .input { &.unstyled { border-radius: 0; - background: none; + /* stylelint-disable-next-line declaration-no-important */ + background: none !important; box-shadow: none; height: unset; } @@ -502,19 +507,11 @@ textarea, --_padding: 0.5em; border: none; - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); - box-shadow: - 0 1px 0 0 rgb(0 0 0 / 20%) inset, - 0 -1px 0 0 rgb(255 255 255 / 20%) inset, - 0 0 2px 0 rgb(0 0 0 / 100%) inset; - box-shadow: var(--inputShadow); - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); - color: $fallback--lightText; - color: var(--inputText, $fallback--lightText); - font-family: sans-serif; - font-family: var(--inputFont, sans-serif); + border-radius: var(--roundness); + background-color: var(--background); + color: var(--text); + box-shadow: var(--shadow); + font-family: var(--font); font-size: 1em; margin: 0; box-sizing: border-box; @@ -528,7 +525,6 @@ textarea, &[disabled="disabled"], &.disabled { cursor: not-allowed; - opacity: 0.5; } &[type="range"] { @@ -543,9 +539,9 @@ textarea, display: none; &:checked + label::before { - box-shadow: 0 0 2px black inset, 0 0 0 4px $fallback--fg inset; - box-shadow: var(--inputShadow), 0 0 0 4px var(--fg, $fallback--fg) inset; - background-color: var(--accent, $fallback--link); + box-shadow: var(--shadow); + background-color: var(--background); + color: var(--text); } &:disabled { @@ -559,16 +555,14 @@ textarea, + label::before { flex-shrink: 0; display: inline-block; - content: ""; + content: "•"; transition: box-shadow 200ms; width: 1.1em; height: 1.1em; border-radius: 100%; // Radio buttons should always be circle - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); + background-color: var(--background); + box-shadow: var(--shadow); margin-right: 0.5em; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); vertical-align: top; text-align: center; line-height: 1.1; @@ -581,8 +575,9 @@ textarea, &[type="checkbox"] { &:checked + label::before { - color: $fallback--text; - color: var(--inputText, $fallback--text); + color: var(--text); + background-color: var(--background); + box-shadow: var(--shadow); } &:disabled { @@ -600,13 +595,9 @@ textarea, transition: color 200ms; width: 1.1em; height: 1.1em; - border-radius: $fallback--checkboxRadius; - border-radius: var(--checkboxRadius, $fallback--checkboxRadius); - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); + border-radius: var(--roundness); + box-shadow: var(--shadow); margin-right: 0.5em; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); vertical-align: top; text-align: center; line-height: 1.1; @@ -623,16 +614,14 @@ textarea, } // Textareas should have stock line-height + vertical padding instead of huge line-height -textarea { +textarea.input { padding: var(--_padding); line-height: var(--post-line-height); } option { - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); + color: var(--text); + background-color: var(--background); } .hide-number-spinner { @@ -653,7 +642,7 @@ option { li { border: 1px solid var(--border); - border-radius: var(--inputRadius); + border-radius: var(--roundness); padding: 0.5em; margin: 0.25em; } @@ -714,74 +703,58 @@ option { overflow: hidden; text-overflow: ellipsis; - &.badge-notification { - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); - color: white; - color: var(--badgeNotificationText, white); - } -} - -.alert { - margin: 0 0.35em; - padding: 0 0.25em; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - - &.error { - background-color: $fallback--alertError; - background-color: var(--alertError, $fallback--alertError); - color: $fallback--text; - color: var(--alertErrorText, $fallback--text); - - .panel-heading & { - color: $fallback--text; - color: var(--alertErrorPanelText, $fallback--text); - } + &.-dot, + &.-counter { + margin: 0; + position: absolute; } - &.warning { - background-color: $fallback--alertWarning; - background-color: var(--alertWarning, $fallback--alertWarning); - color: $fallback--text; - color: var(--alertWarningText, $fallback--text); - - .panel-heading & { - color: $fallback--text; - color: var(--alertWarningPanelText, $fallback--text); - } + &.-dot { + min-height: 8px; + max-height: 8px; + min-width: 8px; + max-width: 8px; + padding: 0; + line-height: 0; + font-size: 0; + left: calc(50% - 4px); + top: calc(50% - 4px); + margin-left: 6px; + margin-top: -6px; } - &.success { - background-color: var(--alertSuccess, $fallback--alertWarning); - color: var(--alertSuccessText, $fallback--text); - - .panel-heading & { - color: var(--alertSuccessPanelText, $fallback--text); - } + &.-counter { + border-radius: var(--roundness); + font-size: 0.75em; + line-height: 1; + text-align: right; + padding: 0.2em; + min-width: 0; + left: calc(50% - 0.5em); + top: calc(50% - 0.4em); + margin-left: 0.7em; + margin-top: -1em; } } -.faint { - color: $fallback--faint; - color: var(--faint, $fallback--faint); +.alert { + margin: 0 0.35em; + padding: 0 0.25em; + border-radius: var(--roundness); + border: 1px solid var(--border); } -.faint-link { - color: $fallback--faint; - color: var(--faint, $fallback--faint); +.faint { + --text: var(--textFaint); + --link: var(--linkFaint); - &:hover { - text-decoration: underline; - } + color: var(--text); } .visibility-notice { padding: 0.5em; - border: 1px solid $fallback--faint; - border: 1px solid var(--faint, $fallback--faint); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border: 1px solid var(--textFaint); + border-radius: var(--roundness); } .notice-dismissible { @@ -802,6 +775,10 @@ option { &.iconLetter { font-size: 1.1em; } + + &.svg-inline--fa { + vertical-align: -0.15em; + } } .fa-old-padding { @@ -816,6 +793,11 @@ option { opacity: 0.25; } +.timeago { + --link: var(--text); + --linkFaint: var(--textFaint); +} + .login-hint { text-align: center; @@ -914,3 +896,8 @@ option { padding: 0; position: absolute; } + +*::selection { + color: var(--selectionText); + background-color: var(--selectionBackground); +} diff --git a/src/App.vue b/src/App.vue index fe214ce7..9d7ad912 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,6 @@ <template> <div + v-show="$store.state.interface.themeApplied" id="app-loaded" :style="bgStyle" > diff --git a/src/_variables.scss b/src/_variables.scss deleted file mode 100644 index 751fc9a4..00000000 --- a/src/_variables.scss +++ /dev/null @@ -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 index 84fea954..bcab7a66 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -14,7 +14,7 @@ import { windowWidth, windowHeight } from '../services/window_utils/window_utils import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' -import { applyTheme, applyConfig } from '../services/style_setter/style_setter.js' +import { applyTheme, applyConfig, tryLoadCache } from '../services/style_setter/style_setter.js' import FaviconService from '../services/favicon_service/favicon_service.js' import { initServiceWorker, updateFocus } from '../services/sw/sw.js' @@ -261,6 +261,7 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') }) + store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') }) const uploadLimits = metadata.uploadLimits store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) @@ -327,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 }) => { @@ -355,20 +353,25 @@ const afterStoreSetup = async ({ store, i18n }) => { await setConfig({ store }) - const { customTheme, customThemeSource } = store.state.config + const { customTheme, customThemeSource, forceThemeRecompilation } = store.state.config const { theme } = store.state.instance const customThemePresent = customThemeSource || customTheme - if (customThemePresent) { - if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) { - applyTheme(customThemeSource) + if (!forceThemeRecompilation && tryLoadCache()) { + store.commit('setThemeApplied') + } else { + if (customThemePresent) { + if (customThemeSource && customThemeSource.themeEngineVersion === CURRENT_VERSION) { + applyTheme(customThemeSource) + } else { + applyTheme(customTheme) + } + store.commit('setThemeApplied') + } else if (theme) { + // do nothing, it will load asynchronously } else { - applyTheme(customTheme) + console.error('Failed to load any theme!') } - } else if (theme) { - // do nothing, it will load asynchronously - } else { - console.error('Failed to load any theme!') } applyConfig(store.state.config) diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index ce19291a..9a7c6824 100644 --- 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 new file mode 100644 index 00000000..19bd4bbb --- /dev/null +++ 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 index a1c5791e..92880831 100644 --- 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 index 0f29f9f7..c0a3c30a 100644 --- 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 index 78d3ecee..25f830ff 100644 --- 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 index 681bab29..13afbe64 100644 --- 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 new file mode 100644 index 00000000..5fb4701c --- /dev/null +++ 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 index 79f62806..9abc2627 100644 --- 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 index 7b7102fd..04f41801 100644 --- 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 index 2d00cb7b..9252ba32 100644 --- 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 new file mode 100644 index 00000000..0697cac6 --- /dev/null +++ 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 index 705e20f5..9e2b0295 100644 --- 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 new file mode 100644 index 00000000..a87ee9c8 --- /dev/null +++ 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 new file mode 100644 index 00000000..6fec67a0 --- /dev/null +++ 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 new file mode 100644 index 00000000..65b5c57b --- /dev/null +++ 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 index 43e7a5e4..8af710ae 100644 --- 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 new file mode 100644 index 00000000..9ae2b7d7 --- /dev/null +++ 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 index b1e5468c..6efe576b 100644 --- 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 index 27a475ed..b33ea67e 100644 --- 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 index 3a84672b..9711b41d 100644 --- 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 index 69ad609b..0f9d5c5f 100644 --- 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 index fd5b7aa4..f7254ea3 100644 --- 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 new file mode 100644 index 00000000..9b57ad37 --- /dev/null +++ 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 index 381574c3..166889d7 100644 --- 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 index b145ecf9..b84e0beb 100644 --- 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 index 52306c1d..673ba56b 100644 --- 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 index 93db4fa7..68ee7a5a 100644 --- 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 index b8b77e7c..6261bf3a 100644 --- 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 index ca46199a..b0fc879f 100644 --- a/src/components/color_input/color_input.scss +++ b/src/components/color_input/color_input.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .color-input { display: inline-flex; @@ -11,9 +9,8 @@ padding: 0.2em 8px; input { + color: var(--text); background: none; - color: $fallback--lightText; - color: var(--inputText, $fallback--lightText); border: none; padding: 0; margin: 0; @@ -23,21 +20,38 @@ min-width: 3em; padding: 0; } + } + + .nativeColor { + cursor: pointer; + flex: 0 0 auto; - &.nativeColor { - flex: 0 0 2em; - min-width: 2em; - align-self: stretch; - min-height: 100%; + input { + appearance: none; + max-width: 0; + min-width: 0; + max-height: 0; + /* stylelint-disable-next-line declaration-no-important */ + opacity: 0 !important; } } .computedIndicator, + .validIndicator, + .invalidIndicator, .transparentIndicator { flex: 0 0 2em; + margin: 0 0.5em; min-width: 2em; align-self: stretch; - min-height: 100%; + min-height: 1.5em; + border-radius: var(--roundness); + } + + .invalidIndicator { + background: transparent; + box-sizing: border-box; + border: 2px solid var(--cRed); } .transparentIndicator { @@ -58,11 +72,13 @@ &::after { top: 0; left: 0; + border-top-left-radius: var(--roundness); } &::before { bottom: 0; right: 0; + border-bottom-right-radius: var(--roundness); } } } diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue index dfc084f9..66ee9d53 100644 --- a/src/components/color_input/color_input.vue +++ b/src/components/color_input/color_input.vue @@ -25,30 +25,51 @@ :disabled="!present || disabled" @input="$emit('update:modelValue', $event.target.value)" > - <input + <div v-if="validColor" - :id="name" - class="nativeColor unstyled" - type="color" - :value="modelValue || fallback" - :disabled="!present || disabled" - @input="$emit('update:modelValue', $event.target.value)" - > + class="validIndicator" + :style="{backgroundColor: modelValue || fallback}" + /> <div - v-if="transparentColor" + v-else-if="transparentColor" class="transparentIndicator" /> <div - v-if="computedColor" + v-else-if="computedColor" class="computedIndicator" :style="{backgroundColor: fallback}" /> + <div + v-else + class="invalidIndicator" + /> + <label class="nativeColor"> + <FAIcon icon="eye-dropper" /> + <input + :id="name" + class="unstyled" + type="color" + :value="modelValue || fallback" + :disabled="!present || disabled" + @input="$emit('update:modelValue', $event.target.value)" + > + </label> </div> </div> </template> <script> import Checkbox from '../checkbox/checkbox.vue' import { hex2rgb } from '../../services/color_convert/color_convert.js' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEyeDropper +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEyeDropper +) + export default { components: { Checkbox @@ -108,12 +129,3 @@ export default { } </script> <style lang="scss" src="./color_input.scss"></style> - -<style lang="scss"> -.color-control { - input.text-input { - max-width: 7em; - flex: 1; - } -} -</style> diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 85e6d8ad..a94d2130 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -56,7 +56,8 @@ const conversation = { expanded: false, threadDisplayStatusObject: {}, // id => 'showing' | 'hidden' statusContentPropertiesObject: {}, - inlineDivePosition: null + inlineDivePosition: null, + loadStatusError: null } }, props: [ @@ -392,11 +393,15 @@ const conversation = { this.setHighlight(this.originalStatusId) }) } else { + this.loadStatusError = null this.$store.state.api.backendInteractor.fetchStatus({ id: this.statusId }) .then((status) => { this.$store.dispatch('addNewStatuses', { statuses: [status] }) this.fetchConversation() }) + .catch((error) => { + this.loadStatusError = error + }) } }, getReplies (id) { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 7577129e..526de5c3 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -28,7 +28,27 @@ class="rightside-button" /> </div> - <div class="conversation-body panel-body"> + <div + v-if="isPage && !status" + class="conversation-body" + :class="{ 'panel-body': isExpanded }" + > + <p v-if="!loadStatusError"> + <FAIcon + spin + icon="circle-notch" + /> + {{ $t('status.loading') }} + </p> + <p v-else> + {{ $t('status.load_error', { error: loadStatusError }) }} + </p> + </div> + <div + v-else + class="conversation-body" + :class="{ 'panel-body': isExpanded }" + > <div v-if="isTreeView" class="thread-body" @@ -203,6 +223,7 @@ </div> <div v-else + class="Conversation -hidden" :style="hiddenStyle" /> </template> @@ -210,14 +231,17 @@ <script src="./conversation.js"></script> <style lang="scss"> -@import "../../variables"; - .Conversation { z-index: 1; + &.-hidden { + background: var(--__panel-background); + backdrop-filter: var(--__panel-backdrop-filter); + } + .conversation-dive-to-top-level-box { - padding: var(--status-margin, $status-margin); - border-bottom: 1px solid var(--border, $fallback--border); + padding: var(--status-margin); + border-bottom: 1px solid var(--border); border-radius: 0; /* Make the button stretch along the whole row */ @@ -227,20 +251,22 @@ } .thread-ancestors { - margin-left: var(--status-margin, $status-margin); - border-left: 2px solid var(--border, $fallback--border); + margin-left: var(--status-margin); + border-left: 2px solid var(--border); } - .thread-ancestor.-faded .StatusContent { - --link: var(--faintLink); - --text: var(--faint); - - color: var(--text); + .thread-ancestor.-faded .RichContent { + /* stylelint-disable declaration-no-important */ + --text: var(--textFaint) !important; + --link: var(--linkFaint) !important; + --funtextGreentext: var(--funtextGreentextFaint) !important; + --funtextCyantext: var(--funtextCyantextFaint) !important; + /* stylelint-enable declaration-no-important */ } .thread-ancestor-dive-box { - padding-left: var(--status-margin, $status-margin); - border-bottom: 1px solid var(--border, $fallback--border); + padding-left: var(--status-margin); + border-bottom: 1px solid var(--border); border-radius: 0; /* Make the button stretch along the whole row */ @@ -253,16 +279,17 @@ } .thread-ancestor-dive-box-inner { - padding: var(--status-margin, $status-margin); + padding: var(--status-margin); } .conversation-status { - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); border-radius: 0; } .thread-ancestor-has-other-replies .conversation-status, - &:last-child .conversation-status, + &:last-child:not(.-expanded) .conversation-status, + &.-expanded .conversation-status:last-child, .thread-ancestor:last-child .conversation-status, .thread-ancestor:last-child .thread-ancestor-dive-box, &.-expanded .thread-tree .conversation-status { @@ -270,20 +297,36 @@ } .thread-ancestors + .thread-tree > .conversation-status { - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); } /* expanded conversation in timeline */ &.status-fadein.-expanded .thread-body { - border-left: 4px solid $fallback--cRed; - border-left-color: var(--cRed, $fallback--cRed); - border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; - border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); - border-bottom: 1px solid var(--border, $fallback--border); + border-left: 4px solid var(--cRed); + border-radius: var(--roundness); + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom: 1px solid var(--border); } &.-expanded.status-fadein { - margin: calc(var(--status-margin, $status-margin) / 2); + --___margin: calc(var(--status-margin) / 2); + + background: var(--background); + margin: var(--___margin); + + &::before { + z-index: -1; + content: ""; + display: block; + position: absolute; + top: calc(var(--___margin) * -1); + bottom: calc(var(--___margin) * -1); + left: calc(var(--___margin) * -1); + right: calc(var(--___margin) * -1); + background: var(--background); + backdrop-filter: var(--__panel-backdrop-filter); + } } } </style> diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss index c7e02936..61d2541c 100644 --- 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 index 341cf105..3bceff22 100644 --- 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 index 7f9ecc99..9bd5c8f4 100644 --- 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.js b/src/components/emoji_picker/emoji_picker.js index 30c01aa5..eb665c40 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -114,6 +114,7 @@ const EmojiPicker = { groupsScrolledClass: 'scrolled-top', keepOpen: false, customEmojiTimeout: null, + hideCustomEmojiInPicker: false, // Lazy-load only after the first time `showing` becomes true. contentLoaded: false, groupRefs: {}, @@ -286,7 +287,7 @@ const EmojiPicker = { return 0 }, allCustomGroups () { - if (this.hideCustomEmoji) { + if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) { return {} } const emojis = this.$store.getters.groupedCustomEmojis diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss index 5bcff33b..5602a16b 100644 --- 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; @@ -39,11 +28,16 @@ $emoji-picker-emoji-size: 32px; } .keep-open, - .too-many-emoji { + .too-many-emoji, + .hide-custom-emoji { padding: 7px; line-height: normal; } + .hide-custom-emoji { + padding-top: 0; + } + .too-many-emoji { display: flex; flex-direction: column; @@ -76,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; } @@ -104,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 index 0788f34c..7c36deaa 100644 --- 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" > @@ -142,6 +142,17 @@ {{ $t('emoji.keep_open') }} </Checkbox> </div> + <div + v-if="!hideCustomEmoji" + class="hide-custom-emoji" + > + <Checkbox + v-model="hideCustomEmojiInPicker" + @change="onShowing" + > + {{ $t('emoji.hide_custom_emoji') }} + </Checkbox> + </div> </div> <div v-if="showingStickers" diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue index c11b338e..ad4a3c0b 100644 --- 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 index b7d3b1d3..7b38d974 100644 --- 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 index 6e0456a5..600c99bb 100644 --- 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 index 8c883c13..2e0dd047 100644 --- 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 index 9f58d314..c9fc730b 100644 --- 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 index e2ba74d1..d2d1b388 100644 --- 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 new file mode 100644 index 00000000..2d3ac154 --- /dev/null +++ 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 index 96b554e3..40048285 100644 --- 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 index 9e9ec7aa..140d0d54 100644 --- 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 new file mode 100644 index 00000000..6cb9e4e3 --- /dev/null +++ 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 index 8c48a387..8647ed4d 100644 --- 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 index 2a63b31a..12779b45 100644 --- 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 new file mode 100644 index 00000000..139a0034 --- /dev/null +++ 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 index 364791a1..f7d5ef7e 100644 --- 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 index 09f341ac..8eb5a501 100644 --- 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 new file mode 100644 index 00000000..d13cef33 --- /dev/null +++ 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 index a3562c5d..c885bacd 100644 --- 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 new file mode 100644 index 00000000..e82a0a83 --- /dev/null +++ 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 index 925da3a5..a5dc6371 100644 --- 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 index eec0f978..3c2066f6 100644 --- 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 index 6ca107e6..0f6ec125 100644 --- 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 index 829e88ad..adc2dd5b 100644 --- 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 index 2ea5567a..047e3483 100644 --- 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 index 69e9fed1..42f57294 100644 --- 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 index 869a3257..5db837a2 100644 --- 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 index 64c19bf1..daab64a3 100644 --- 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 new file mode 100644 index 00000000..51388155 --- /dev/null +++ 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 index 5988fa51..27184fb4 100644 --- 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 index 709eb9b8..3ec617e9 100644 --- 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 new file mode 100644 index 00000000..398bc186 --- /dev/null +++ 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 index f20a509d..6e134ef2 100644 --- 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 index ef0f51fe..ba77cb2d 100644 --- 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 new file mode 100644 index 00000000..c401a0cd --- /dev/null +++ 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 index b708cdc8..df3bb655 100644 --- 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("admin")" > {{ $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("moderator")" > {{ $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 index 97af4787..6dc86738 100644 --- 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 index 1a826cc4..bf608936 100644 --- 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 index 411ca536..4ea54ee3 100644 --- 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 index 4fbb4f95..36eb1ebe 100644 --- 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 index 654aca3c..2dbced09 100644 --- 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 new file mode 100644 index 00000000..0d36760a --- /dev/null +++ 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 index a8eceab0..632ae6e9 100644 --- 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 index 1315b51a..cc506e78 100644 --- 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 index 5749e430..cfc1f3d6 100644 --- 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 index a0025182..87e7b68d 100644 --- 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 index 15d08e04..a45bdd92 100644 --- 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 new file mode 100644 index 00000000..ad16c18f --- /dev/null +++ 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 new file mode 100644 index 00000000..32464bc5 --- /dev/null +++ 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 index 17458e49..7a832122 100644 --- 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 index d6e10250..037b859e 100644 --- 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.js b/src/components/poll/poll.js index f6001f56..9ce0e29e 100644 --- a/src/components/poll/poll.js +++ b/src/components/poll/poll.js @@ -38,7 +38,7 @@ export default { return (this.poll && this.poll.options) || [] }, expiresAt () { - return (this.poll && this.poll.expires_at) || 0 + return (this.poll && this.poll.expires_at) || null }, expired () { return (this.poll && this.poll.expired) || false diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index b3a74c49..580e5377 100644 --- 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 @@ -75,13 +78,16 @@ </button> <div class="total"> <template v-if="typeof poll.voters_count === 'number'"> - {{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }} · + {{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }} </template> <template v-else> - {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }} · + {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }} </template> + <span v-if="expiresAt !== null"> + · + </span> </div> - <span> + <span v-if="expiresAt !== null"> <i18n-t scope="global" :keypath="expired ? 'polls.expired' : 'polls.expires_in'" @@ -100,8 +106,6 @@ <script src="./poll.js"></script> <style lang="scss"> -@import "../../variables"; - .poll { .votes { display: flex; @@ -111,6 +115,10 @@ .poll-option { margin: 0.75em 0.5em; + + .input { + line-height: inherit; + } } .option-result { @@ -118,8 +126,7 @@ display: flex; flex-direction: row; position: relative; - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); + color: var(--textLight); } .option-result-label { @@ -138,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 index 09d411ca..4eb9d594 100644 --- 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 new file mode 100644 index 00000000..247a266a --- /dev/null +++ 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 new file mode 100644 index 00000000..0197271b --- /dev/null +++ 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 index 1a4bffd9..0c5b372e 100644 --- a/src/components/popover/popover.vue +++ b/src/components/popover/popover.vue @@ -42,8 +42,6 @@ <script src="./popover.js" /> <style lang="scss"> -@import "../../variables"; - .popover-trigger-button { display: inline-block; } @@ -53,81 +51,54 @@ position: fixed; min-width: 0; max-width: calc(100vw - 20px); - box-shadow: 2px 2px 3px rgb(0 0 0 / 50%); - box-shadow: var(--popupShadow); + box-shadow: var(--shadow); } .popover-default { &::after { content: ""; position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - z-index: 3; - box-shadow: 1px 1px 4px rgb(0 0 0 / 60%); - box-shadow: var(--panelShadow); + top: -1px; + bottom: -1px; + left: -1px; + right: -1px; + z-index: -1px; + box-shadow: var(--shadow); pointer-events: none; } - border-radius: $fallback--btnRadius; - border-radius: var(--btnRadius, $fallback--btnRadius); - background-color: $fallback--bg; - background-color: var(--popover, $fallback--bg); - color: $fallback--text; - color: var(--popoverText, $fallback--text); - - --faint: var(--popoverFaintText, $fallback--faint); - --faintLink: var(--popoverFaintLink, $fallback--faint); - --lightText: var(--popoverLightText, $fallback--lightText); - --postLink: var(--popoverPostLink, $fallback--link); - --postFaintLink: var(--popoverPostFaintLink, $fallback--link); - --icon: var(--popoverIcon, $fallback--icon); + border-radius: var(--roundness); + border-color: var(--border); + border-style: solid; + border-width: 1px; + background-color: var(--background); } .dropdown-menu { display: block; - padding: 0.5rem 0; + padding: 0; font-size: 1em; text-align: left; list-style: none; max-width: 100vw; z-index: var(--ZI_popover_override, var(--ZI_popovers)); white-space: nowrap; + background-color: var(--background); .dropdown-divider { height: 0; margin: 0.5rem 0; overflow: hidden; - border-top: 1px solid $fallback--border; - border-top: 1px solid var(--border, $fallback--border); + border-top: 1px solid var(--border); } .dropdown-item { - line-height: 21px; - overflow: hidden; - display: block; - padding: 0.5em 0.75em; - clear: both; - font-weight: 400; - text-align: inherit; - white-space: nowrap; border: none; - border-radius: 0; - background-color: transparent; - box-shadow: none; - width: 100%; - height: 100%; - box-sizing: border-box; - - --btnText: var(--popoverText, $fallback--text); &-icon { svg { - width: 22px; - margin-right: 0.75rem; - color: var(--menuPopoverIcon, $fallback--icon); + width: var(--__line-height); + margin-right: var(--__horizontal-gap); } } @@ -138,40 +109,18 @@ } } - &:active, - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - box-shadow: none; - - --btnText: var(--selectedMenuPopoverText, $fallback--link); - --faint: var(--selectedMenuPopoverFaintText, $fallback--faint); - --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); - --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - - svg { - color: var(--selectedMenuPopoverIcon, $fallback--icon); - - --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - } - } - .menu-checkbox { display: inline-block; vertical-align: middle; - min-width: 22px; - max-width: 22px; - min-height: 22px; - max-height: 22px; - line-height: 22px; + min-width: calc(var(--__line-height) + 1px); + max-width: calc(var(--__line-height) + 1px); + min-height: calc(var(--__line-height) + 1px); + max-height: calc(var(--__line-height) + 1px); + line-height: var(--__line-height); text-align: center; border-radius: 0; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); - box-shadow: 0 0 2px black inset; - box-shadow: var(--inputShadow); - margin-right: 0.75em; + box-shadow: var(--shadow); + margin-right: var(--__horizontal-gap); &.menu-checkbox-checked::after { font-size: 1.25em; @@ -188,30 +137,5 @@ } } } - - .button-default.dropdown-item { - &, - i[class*="icon-"] { - color: $fallback--text; - color: var(--btnText, $fallback--text); - } - - &:active { - background-color: $fallback--lightBg; - background-color: var(--selectedMenuPopover, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuPopoverText, $fallback--link); - } - - &:disabled { - color: $fallback--text; - color: var(--btnDisabledText, $fallback--text); - } - - &.toggled { - color: $fallback--text; - color: var(--btnToggledText, $fallback--text); - } - } } </style> diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 9b108a5a..cf411ff1 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -161,7 +161,7 @@ v-model="newStatus.spoilerText" enable-emoji-picker :suggest="emojiSuggestor" - class="form-control" + class="input form-control" > <template #default="inputProps"> <input @@ -171,7 +171,7 @@ :disabled="posting && !optimisticPosting" v-bind="propsToNative(inputProps)" size="1" - class="form-post-subject" + class="input form-post-subject" > </template> </EmojiInput> @@ -180,7 +180,7 @@ v-model="newStatus.status" :suggest="emojiUserSuggestor" :placement="emojiPickerPlacement" - class="form-control main-input" + class="input form-control main-input" enable-emoji-picker hide-emoji-button :newline-on-ctrl-enter="submitOnEnter" @@ -198,7 +198,7 @@ rows="1" cols="1" :disabled="posting && !optimisticPosting" - class="form-post-body" + class="input form-post-body" :class="{ 'scrollable-form': !!maxHeight }" v-bind="propsToNative(inputProps)" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" @@ -237,7 +237,7 @@ <Select id="post-content-type" v-model="newStatus.contentType" - class="form-control" + class="input form-control" :attrs="{ 'aria-label': $t('post_status.content_type_selection') }" > <option @@ -375,8 +375,6 @@ <script src="./post_status_form.js"></script> <style lang="scss"> -@import "../../variables"; - .post-status-form { position: relative; @@ -437,15 +435,12 @@ .preview-error { font-style: italic; - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } .preview-status { - border: 1px solid $fallback--border; - border: 1px solid var(--border, $fallback--border); - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border: 1px solid var(--border); + border-radius: var(--roundness); padding: 0.5em; margin: 0; } @@ -456,8 +451,7 @@ .text-format { .only-format { - color: $fallback--faint; - color: var(--faint, $fallback--faint); + color: var(--textFaint); } } @@ -503,31 +497,6 @@ padding: 0 0.1em; display: flex; align-items: center; - - &.selected, - &:hover { - // needs to be specific to override icon default color - svg, - i, - label { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - } - } - - &.disabled { - svg, - i { - cursor: not-allowed; - color: $fallback--icon; - color: var(--btnDisabledText, $fallback--icon); - - &:hover { - color: $fallback--icon; - color: var(--btnDisabledText, $fallback--icon); - } - } - } } .error { @@ -580,7 +549,7 @@ line-height: 1.85; } - .form-post-body { + .input.form-post-body { // TODO: make a resizable textarea component? box-sizing: content-box; // needed for easier computation of dynamic size overflow: hidden; @@ -591,6 +560,7 @@ height: calc(var(--post-line-height) * 1em); min-height: calc(var(--post-line-height) * 1em); resize: none; + background: transparent; &.scrollable-form { overflow-y: auto; @@ -609,8 +579,7 @@ margin: 0 0.5em; &.error { - color: $fallback--cRed; - color: var(--cRed, $fallback--cRed); + color: var(--cRed); } } @@ -633,14 +602,10 @@ align-items: center; justify-content: center; opacity: 0.6; - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - border: 2px dashed $fallback--text; - border: 2px dashed var(--text, $fallback--text); + color: var(--text); + background-color: var(--bg); + border-radius: var(--roundness); + border: 2px dashed var(--text); } } </style> diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js index e67e3a4b..0d667df5 100644 --- a/src/components/quick_filter_settings/quick_filter_settings.js +++ b/src/components/quick_filter_settings/quick_filter_settings.js @@ -63,6 +63,13 @@ const QuickFilterSettings = { const value = !this.muteBotStatuses this.$store.dispatch('setOption', { name: 'muteBotStatuses', value }) } + }, + muteSensitiveStatuses: { + get () { return this.mergedConfig.muteSensitiveStatuses }, + set () { + const value = !this.muteSensitiveStatuses + this.$store.dispatch('setOption', { name: 'muteSensitiveStatuses', value }) + } } } } diff --git a/src/components/quick_filter_settings/quick_filter_settings.vue b/src/components/quick_filter_settings/quick_filter_settings.vue index b81215a1..adb96916 100644 --- a/src/components/quick_filter_settings/quick_filter_settings.vue +++ b/src/components/quick_filter_settings/quick_filter_settings.vue @@ -16,39 +16,39 @@ > <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilityAll" role="menuitemradio" @click="replyVisibilityAll = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilityAll }" :aria-hidden="true" />{{ $t('settings.reply_visibility_all') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilityFollowing" role="menuitemradio" @click="replyVisibilityFollowing = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilityFollowing }" :aria-hidden="true" />{{ $t('settings.reply_visibility_following_short') }} </button> <button v-if="!conversation" - class="button-default dropdown-item" + class="menu-item dropdown-item" :aria-checked="replyVisibilitySelf" role="menuitemradio" @click="replyVisibilitySelf = true" > <span - class="menu-checkbox -radio" + class="input menu-checkbox -radio" :class="{ 'menu-checkbox-checked': replyVisibilitySelf }" :aria-hidden="true" />{{ $t('settings.reply_visibility_self_short') }} @@ -60,43 +60,55 @@ /> </div> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="muteBotStatuses" @click="muteBotStatuses = !muteBotStatuses" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': muteBotStatuses }" :aria-hidden="true" />{{ $t('settings.mute_bot_posts') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" + role="menuitemcheckbox" + :aria-checked="muteSensitiveStatuses" + @click="muteSensitiveStatuses = !muteSensitiveStatuses" + > + <span + class="input menu-checkbox" + :class="{ 'menu-checkbox-checked': muteSensitiveStatuses }" + :aria-hidden="true" + />{{ $t('settings.mute_sensitive_posts') }} + </button> + <button + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="hideMedia" @click="hideMedia = !hideMedia" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hideMedia }" :aria-hidden="true" />{{ $t('settings.hide_media_previews') }} </button> <button - class="button-default dropdown-item" + class="menu-item dropdown-item" role="menuitemcheckbox" :aria-checked="hideMutedPosts" @click="hideMutedPosts = !hideMutedPosts" > <span - class="menu-checkbox" + class="input menu-checkbox" :class="{ 'menu-checkbox-checked': hideMutedPosts }" :aria-hidden="true" />{{ $t('settings.hide_all_muted_posts') }} </button> <button - class="button-default dropdown-item dropdown-item-icon" + class="menu-item dropdown-item dropdown-item-icon" role="menuitem" @click="openTab('filtering')" > diff --git a/src/components/quick_view_settings/quick_view_settings.js b/src/components/quick_view_settings/quick_view_settings.js index 67aa6713..e35fd579 100644 --- a/src/components/quick_view_settings/quick_view_settings.js +++ b/src/components/quick_view_settings/quick_view_settings.js @@ -61,6 +61,13 @@ const QuickViewSettings = { const value = !this.muteBotStatuses this.$store.dispatch('setOption', { name: 'muteBotStatuses', value }) } + }, + muteSensitiveStatuses: { + get () { return this.mergedConfig.muteSensitiveStatuses }, + set () { + const value = !this.muteSensitiveStatuses + this.$store.dispatch('setOption', { name: 'muteSensitiveStatuses', value }) + } } } } diff --git a/src/components/quick_view_settings/quick_view_settings.vue b/src/components/quick_view_settings/quick_view_settings.vue index 9f5cdabc..e93dd8e0 100644 --- 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 index 1e720105..2f8645c0 100644 --- 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 index 1b0674e6..e648e7e3 100644 --- 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 index 5c913f94..a2008d87 100644 --- 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 index 60a40a08..87c06e39 100644 --- 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 index 9762400b..4249b850 100644 --- 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 index 1f19cc25..32aaaffd 100644 --- 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 index e1b6b153..adda9a42 100644 --- 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 index ff14a58a..99d7daca 100644 --- 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 index e5d353ac..118b6acf 100644 --- a/src/components/rich_content/rich_content.scss +++ b/src/components/rich_content/rich_content.scss @@ -1,10 +1,19 @@ -@import "../../variables"; - .RichContent { + font-family: var(--font); + + &.-faint { + /* stylelint-disable declaration-no-important */ + --text: var(--textFaint) !important; + --link: var(--linkFaint) !important; + --funtextGreentext: var(--funtextGreentextFaint) !important; + --funtextCyantext: var(--funtextCyantextFaint) !important; + /* stylelint-enable declaration-no-important */ + } + blockquote { margin: 0.2em 0 0.2em 0.2em; font-style: italic; - border-left: 0.2em solid var(--faint, $fallback--faint); + border-left: 0.2em solid var(--textFaint); padding-left: 1em; } @@ -17,7 +26,7 @@ kbd, var, pre { - font-family: var(--postCodeFont, monospace); + font-family: var(--monoFont); } p { @@ -65,4 +74,17 @@ vertical-align: middle; object-fit: contain; } + + .greentext { + color: var(--funtextGreentext); + } + + .cyantext { + color: var(--funtextCyantext); + } +} + +a .RichContent { + /* stylelint-disable-next-line declaration-no-important */ + color: var(--link) !important; } diff --git a/src/components/rich_content/rich_content.style.js b/src/components/rich_content/rich_content.style.js new file mode 100644 index 00000000..c8314000 --- /dev/null +++ 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 new file mode 100644 index 00000000..3e840a52 --- /dev/null +++ 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 index 74bf7284..52cda368 100644 --- 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 index d6e7265b..b90ae020 100644 --- 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 new file mode 100644 index 00000000..94e6135d --- /dev/null +++ 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 new file mode 100644 index 00000000..da942ab2 --- /dev/null +++ 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 index 19b9c577..bd384ad3 100644 --- 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 index 9da2b272..d06b4e77 100644 --- 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 index 1797afc8..32832126 100644 --- 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 index 14910fc5..3d3a5ff0 100644 --- 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.js b/src/components/settings_modal/admin_tabs/emoji_tab.js new file mode 100644 index 00000000..58e1468f --- /dev/null +++ b/src/components/settings_modal/admin_tabs/emoji_tab.js @@ -0,0 +1,257 @@ +import { clone, assign } from 'lodash' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' +import StringSetting from '../helpers/string_setting.vue' +import Checkbox from 'components/checkbox/checkbox.vue' +import StillImage from 'components/still-image/still-image.vue' +import Select from 'components/select/select.vue' +import Popover from 'components/popover/popover.vue' +import ConfirmModal from 'components/confirm_modal/confirm_modal.vue' +import ModifiedIndicator from '../helpers/modified_indicator.vue' +import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue' + +const EmojiTab = { + components: { + TabSwitcher, + StringSetting, + Checkbox, + StillImage, + Select, + Popover, + ConfirmModal, + ModifiedIndicator, + EmojiEditingPopover + }, + + data () { + return { + knownLocalPacks: { }, + knownRemotePacks: { }, + editedMetadata: { }, + packName: '', + newPackName: '', + deleteModalVisible: false, + remotePackInstance: '', + remotePackDownloadAs: '' + } + }, + + provide () { + return { emojiAddr: this.emojiAddr } + }, + + computed: { + pack () { + return this.packName !== '' ? this.knownPacks[this.packName] : undefined + }, + packMeta () { + if (this.editedMetadata[this.packName] === undefined) { + this.editedMetadata[this.packName] = clone(this.pack.pack) + } + + return this.editedMetadata[this.packName] + }, + knownPacks () { + // Copy the object itself but not the children, so they are still passed by reference and modified + const result = clone(this.knownLocalPacks) + for (const instName in this.knownRemotePacks) { + for (const instPack in this.knownRemotePacks[instName]) { + result[`${instPack}@${instName}`] = this.knownRemotePacks[instName][instPack] + } + } + + return result + }, + downloadWillReplaceLocal () { + return (this.remotePackDownloadAs.trim() === '' && this.pack.remote && this.pack.remote.baseName in this.knownLocalPacks) || + (this.remotePackDownloadAs in this.knownLocalPacks) + } + }, + + methods: { + reloadEmoji () { + this.$store.state.api.backendInteractor.reloadEmoji() + }, + importFromFS () { + this.$store.state.api.backendInteractor.importEmojiFromFS() + }, + emojiAddr (name) { + if (this.pack.remote !== undefined) { + // Remote pack + return `${this.pack.remote.instance}/emoji/${encodeURIComponent(this.pack.remote.baseName)}/${name}` + } else { + return `${this.$store.state.instance.server}/emoji/${encodeURIComponent(this.packName)}/${name}` + } + }, + + createEmojiPack () { + this.$store.state.api.backendInteractor.createEmojiPack( + { name: this.newPackName } + ).then(resp => resp.json()).then(resp => { + if (resp === 'ok') { + return this.refreshPackList() + } else { + this.displayError(resp.error) + return Promise.reject(resp) + } + }).then(done => { + this.$refs.createPackPopover.hidePopover() + + this.packName = this.newPackName + this.newPackName = '' + }) + }, + deleteEmojiPack () { + this.$store.state.api.backendInteractor.deleteEmojiPack( + { name: this.packName } + ).then(resp => resp.json()).then(resp => { + if (resp === 'ok') { + return this.refreshPackList() + } else { + this.displayError(resp.error) + return Promise.reject(resp) + } + }).then(done => { + delete this.editedMetadata[this.packName] + + this.deleteModalVisible = false + this.packName = '' + }) + }, + + metaEdited (prop) { + if (!this.pack) return + + const def = this.pack.pack[prop] || '' + const edited = this.packMeta[prop] || '' + return edited !== def + }, + savePackMetadata () { + this.$store.state.api.backendInteractor.saveEmojiPackMetadata({ name: this.packName, newData: this.packMeta }).then( + resp => resp.json() + ).then(resp => { + if (resp.error !== undefined) { + this.displayError(resp.error) + return + } + + // Update actual pack data + this.pack.pack = resp + // Delete edited pack data, should auto-update itself + delete this.editedMetadata[this.packName] + }) + }, + + updatePackFiles (newFiles) { + this.pack.files = newFiles + this.sortPackFiles(this.packName) + }, + + loadPacksPaginated (listFunction) { + const pageSize = 25 + const allPacks = {} + + return listFunction({ instance: this.remotePackInstance, page: 1, pageSize: 0 }) + .then(data => data.json()) + .then(data => { + if (data.error !== undefined) { return Promise.reject(data.error) } + + let resultingPromise = Promise.resolve({}) + for (let i = 0; i < Math.ceil(data.count / pageSize); i++) { + resultingPromise = resultingPromise.then(() => listFunction({ instance: this.remotePackInstance, page: i, pageSize }) + ).then(data => data.json()).then(pageData => { + if (pageData.error !== undefined) { return Promise.reject(pageData.error) } + + assign(allPacks, pageData.packs) + }) + } + + return resultingPromise + }) + .then(finished => allPacks) + .catch(data => { + this.displayError(data) + }) + }, + + refreshPackList () { + this.loadPacksPaginated(this.$store.state.api.backendInteractor.listEmojiPacks) + .then(allPacks => { + this.knownLocalPacks = allPacks + for (const name of Object.keys(this.knownLocalPacks)) { + this.sortPackFiles(name) + } + }) + }, + listRemotePacks () { + this.loadPacksPaginated(this.$store.state.api.backendInteractor.listRemoteEmojiPacks) + .then(allPacks => { + let inst = this.remotePackInstance + if (!inst.startsWith('http')) { inst = 'https://' + inst } + const instUrl = new URL(inst) + inst = instUrl.host + + for (const packName in allPacks) { + allPacks[packName].remote = { + baseName: packName, + instance: instUrl.origin + } + } + + this.knownRemotePacks[inst] = allPacks + for (const pack in this.knownRemotePacks[inst]) { + this.sortPackFiles(`${pack}@${inst}`) + } + + this.$refs.remotePackPopover.hidePopover() + }) + .catch(data => { + this.displayError(data) + }) + }, + downloadRemotePack () { + if (this.remotePackDownloadAs.trim() === '') { + this.remotePackDownloadAs = this.pack.remote.baseName + } + + this.$store.state.api.backendInteractor.downloadRemoteEmojiPack({ + instance: this.pack.remote.instance, packName: this.pack.remote.baseName, as: this.remotePackDownloadAs + }) + .then(data => data.json()) + .then(resp => { + if (resp === 'ok') { + this.$refs.dlPackPopover.hidePopover() + + return this.refreshPackList() + } else { + this.displayError(resp.error) + return Promise.reject(resp) + } + }).then(done => { + this.packName = this.remotePackDownloadAs + this.remotePackDownloadAs = '' + }) + }, + displayError (msg) { + this.$store.dispatch('pushGlobalNotice', { + messageKey: 'admin_dash.emoji.error', + messageArgs: [msg], + level: 'error' + }) + }, + sortPackFiles (nameOfPack) { + // Sort by key + const sorted = Object.keys(this.knownPacks[nameOfPack].files).sort().reduce((acc, key) => { + if (key.length === 0) return acc + acc[key] = this.knownPacks[nameOfPack].files[key] + return acc + }, {}) + this.knownPacks[nameOfPack].files = sorted + } + }, + + mounted () { + this.refreshPackList() + } +} + +export default EmojiTab diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.scss b/src/components/settings_modal/admin_tabs/emoji_tab.scss new file mode 100644 index 00000000..3e77e019 --- /dev/null +++ b/src/components/settings_modal/admin_tabs/emoji_tab.scss @@ -0,0 +1,59 @@ +.emoji-tab { + .btn-group .btn:not(:first-child) { + margin-left: 0.5em; + } + + .pack-info-wrapper { + margin-top: 1em; + } + + .emoji-info-input { + width: 100%; + } + + .emoji-data-input { + width: 40%; + margin-left: 0.5em; + margin-right: 0.5em; + } + + .emoji { + width: 32px; + height: 32px; + } + + .emoji-unsaved { + box-shadow: 0 3px 5px var(--cBlue); + } + + .emoji-list { + display: flex; + flex-wrap: wrap; + gap: 1em 1em; + } +} + +.emoji-tab-popover-button:not(:first-child) { + margin-left: 0.5em; +} + +.emoji-tab-popover-input { + margin-bottom: 0.5em; + + label { + display: block; + margin-bottom: 0.5em; + } + + input { + width: 20em; + } + + .emoji-tab-popover-file { + padding-top: 3px; + } + + .warning { + 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 new file mode 100644 index 00000000..5742d2ce --- /dev/null +++ b/src/components/settings_modal/admin_tabs/emoji_tab.vue @@ -0,0 +1,358 @@ +<template> + <div + class="emoji-tab" + :label="$t('admin_dash.tabs.emoji')" + > + <div class="setting-item"> + <h2>{{ $t('admin_dash.tabs.emoji') }}</h2> + + <ul class="setting-list"> + <h3>{{ $t('admin_dash.emoji.global_actions') }}</h3> + + <li class="btn-group setting-item"> + <button + class="button button-default btn" + type="button" + @click="reloadEmoji" + > + {{ $t('admin_dash.emoji.reload') }} + </button> + <button + class="button button-default btn" + type="button" + @click="importFromFS" + > + {{ $t('admin_dash.emoji.importFS') }} + </button> + </li> + + <li class="btn-group setting-item"> + <button + class="button button-default btn" + type="button" + @click="$refs.remotePackPopover.showPopover" + > + {{ $t('admin_dash.emoji.remote_packs') }} + + <Popover + ref="remotePackPopover" + popover-class="emoji-tab-edit-popover popover-default" + trigger="click" + placement="bottom" + bound-to-selector=".emoji-tab" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + > + <template #content> + <div class="emoji-tab-popover-input"> + <h3>{{ $t('admin_dash.emoji.remote_pack_instance') }}</h3> + <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" + > + {{ $t('admin_dash.emoji.do_list') }} + </button> + </div> + </template> + </Popover> + </button> + </li> + + <h3>{{ $t('admin_dash.emoji.emoji_packs') }}</h3> + + <li> + <h4>{{ $t('admin_dash.emoji.edit_pack') }}</h4> + + <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> + + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="$refs.createPackPopover.showPopover" + > + {{ $t('admin_dash.emoji.create_pack') }} + </button> + <Popover + ref="createPackPopover" + popover-class="emoji-tab-edit-popover popover-default" + trigger="click" + placement="bottom" + bound-to-selector=".emoji-tab" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + > + <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')" + class="input" + > + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="createEmojiPack" + > + {{ $t('admin_dash.emoji.create') }} + </button> + </div> + </template> + </Popover> + </li> + </ul> + + <div v-if="pack"> + <div class="pack-info-wrapper"> + <ul class="setting-list"> + <li> + <label> + {{ $t('admin_dash.emoji.description') }} + <ModifiedIndicator + :changed="metaEdited('description')" + message-key="admin_dash.emoji.metadata_changed" + /> + + <textarea + v-model="packMeta.description" + :disabled="pack.remote !== undefined" + 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" + /> + + <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" + /> + + <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 + v-model="packMeta['fallback-src-sha256']" + :disabled="true" + class="emoji-info-input input" + > + </label> + </li> + <li> + <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" + /> + </li> + <li class="btn-group"> + <button + v-if="pack.remote === undefined" + class="button button-default btn" + type="button" + @click="savePackMetadata" + > + {{ $t('admin_dash.emoji.save_meta') }} + </button> + <button + v-if="pack.remote === undefined" + class="button button-default btn" + type="button" + @click="savePackMetadata" + > + {{ $t('admin_dash.emoji.revert_meta') }} + </button> + + <button + v-if="pack.remote === undefined" + class="button button-default btn" + type="button" + @click="deleteModalVisible = true" + > + {{ $t('admin_dash.emoji.delete_pack') }} + + <ConfirmModal + v-if="deleteModalVisible" + :title="$t('admin_dash.emoji.delete_title')" + :cancel-text="$t('status.delete_confirm_cancel_button')" + :confirm-text="$t('status.delete_confirm_accept_button')" + @cancelled="deleteModalVisible = false" + @accepted="deleteEmojiPack" + > + {{ $t('admin_dash.emoji.delete_confirm', [packName]) }} + </ConfirmModal> + </button> + + <button + v-if="pack.remote !== undefined" + class="button button-default btn" + type="button" + @click="$refs.dlPackPopover.showPopover" + > + {{ $t('admin_dash.emoji.download_pack') }} + + <Popover + ref="dlPackPopover" + trigger="click" + placement="bottom" + bound-to-selector=".emoji-tab" + popover-class="emoji-tab-edit-popover popover-default" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + > + <template #content> + <h3>{{ $t('admin_dash.emoji.downloading_pack', [packName]) }}</h3> + <div> + <div> + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.download_as_name') }} + <input + v-model="remotePackDownloadAs" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.download_as_name_full')" + > + </label> + + <div + v-if="downloadWillReplaceLocal" + class="warning" + > + <em>{{ $t('admin_dash.emoji.replace_warning') }}</em> + </div> + </div> + + <button + class="button button-default btn" + type="button" + @click="downloadRemotePack" + > + {{ $t('admin_dash.emoji.download') }} + </button> + </div> + </div> + </template> + </Popover> + </button> + </li> + </ul> + </div> + + <ul class="setting-list"> + <h4> + {{ $t('admin_dash.emoji.files') }} + + <ModifiedIndicator + v-if="pack" + :changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)" + message-key="admin_dash.emoji.emoji_changed" + /> + </h4> + + <div + v-if="pack" + class="emoji-list" + > + <EmojiEditingPopover + v-if="pack.remote === undefined" + placement="bottom" + new-upload + :title="$t('admin_dash.emoji.adding_new')" + :pack-name="packName" + @updatePackFiles="updatePackFiles" + @displayError="displayError" + > + <template #trigger> + <FAIcon + icon="plus" + size="2x" + :title="$t('admin_dash.emoji.add_file')" + /> + </template> + </EmojiEditingPopover> + + <EmojiEditingPopover + 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" + :pack-name="packName" + @updatePackFiles="updatePackFiles" + @displayError="displayError" + > + <template #trigger> + <StillImage + class="emoji" + :src="emojiAddr(file)" + :title="`:${shortcode}:`" + :alt="`:${shortcode}:`" + /> + </template> + </EmojiEditingPopover> + </div> + </ul> + </div> + </div> + </div> +</template> + +<script src="./emoji_tab.js"></script> + +<style lang="scss" src="./emoji_tab.scss"></style> diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js index 8163af59..f57310ee 100644 --- a/src/components/settings_modal/admin_tabs/frontends_tab.js +++ b/src/components/settings_modal/admin_tabs/frontends_tab.js @@ -55,9 +55,13 @@ const FrontendsTab = { return fe.refs.includes(frontend.ref) }, getSuggestedRef (frontend) { - const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary'] - if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) { - return defaultFe.ref + if (this.adminDraft) { + const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary'] + if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) { + return defaultFe.ref + } else { + return frontend.refs[0] + } } else { return frontend.refs[0] } diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue index dd4c9790..8fb3d399 100644 --- 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"> + <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,8 +26,18 @@ </ul> </li> </ul> + <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 @@ -33,9 +46,9 @@ > <strong>{{ frontend.name }}</strong> {{ ' ' }} - <span v-if="adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name"> + <span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name"> <i18n-t - v-if="adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]" + v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]" keypath="admin_dash.frontend.is_default" /> <i18n-t @@ -43,7 +56,7 @@ keypath="admin_dash.frontend.is_default_custom" > <template #version> - <code>{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code> + <code>{{ adminDraft && adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code> </template> </i18n-t> </span> @@ -103,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" > @@ -134,7 +147,7 @@ class="button button-default btn" type="button" :disabled=" - adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name && + !adminDraft || adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0] " @click="setDefault(frontend)" @@ -160,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 index a0e3351e..32e8df25 100644 --- 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 index b50231f2..96c80ab1 100644 --- 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 new file mode 100644 index 00000000..f0465dd5 --- /dev/null +++ b/src/components/settings_modal/helpers/emoji_editing_popover.vue @@ -0,0 +1,227 @@ +<template> + <Popover + ref="emojiPopover" + trigger="click" + :placement="placement" + bound-to-selector=".emoji-list" + popover-class="emoji-tab-edit-popover popover-default" + :bound-to="{ x: 'container' }" + :offset="{ y: 5 }" + :disabled="disabled" + :class="{'emoji-unsaved': isEdited}" + > + <template #trigger> + <slot name="trigger" /> + </template> + <template #content> + <h3> + {{ title }} + </h3> + + <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 input" + @change="uploadFile = $event.target.files" + > + </div> + <div> + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.shortcode') }} + <input + v-model="editedShortcode" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.new_shortcode')" + > + </label> + </div> + + <div class="emoji-tab-popover-input"> + <label> + {{ $t('admin_dash.emoji.filename') }} + + <input + v-model="editedFile" + class="emoji-data-input input" + :placeholder="$t('admin_dash.emoji.new_filename')" + > + </label> + </div> + + <button + class="button button-default btn" + type="button" + :disabled="newUpload ? uploadFile.length == 0 : !isEdited" + @click="newUpload ? uploadEmoji() : saveEditedEmoji()" + > + {{ $t('admin_dash.emoji.save') }} + </button> + + <template v-if="!newUpload"> + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="deleteModalVisible = true" + > + {{ $t('admin_dash.emoji.delete') }} + </button> + <button + class="button button-default btn emoji-tab-popover-button" + type="button" + @click="revertEmoji" + > + {{ $t('admin_dash.emoji.revert') }} + </button> + <ConfirmModal + v-if="deleteModalVisible" + :title="$t('admin_dash.emoji.delete_title')" + :cancel-text="$t('status.delete_confirm_cancel_button')" + :confirm-text="$t('status.delete_confirm_accept_button')" + @cancelled="deleteModalVisible = false" + @accepted="deleteEmoji" + > + {{ $t('admin_dash.emoji.delete_confirm', [shortcode]) }} + </ConfirmModal> + </template> + </div> + </template> + </Popover> +</template> + +<script> +import Popover from 'components/popover/popover.vue' +import ConfirmModal from 'components/confirm_modal/confirm_modal.vue' +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: [], + editedShortcode: this.shortcode, + editedFile: this.file, + deleteModalVisible: false + } + }, + computed: { + emojiPreview () { + if (this.newUpload && this.uploadFile.length > 0) { + return URL.createObjectURL(this.uploadFile[0]) + } else if (!this.newUpload) { + return this.emojiAddr(this.file) + } + + return null + }, + isEdited () { + return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file) + } + }, + methods: { + saveEditedEmoji () { + if (!this.isEdited) return + + this.$store.state.api.backendInteractor.updateEmojiFile( + { packName: this.packName, shortcode: this.shortcode, newShortcode: this.editedShortcode, newFilename: this.editedFile, force: false } + ).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return Promise.reject(resp.error) + } + + return resp.json() + }).then(resp => this.$emit('updatePackFiles', resp)) + }, + uploadEmoji () { + this.$store.state.api.backendInteractor.addNewEmojiFile({ + packName: this.packName, + file: this.uploadFile[0], + shortcode: this.editedShortcode, + filename: this.editedFile + }).then(resp => resp.json()).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return + } + + this.$emit('updatePackFiles', resp) + this.$refs.emojiPopover.hidePopover() + + this.editedFile = '' + this.editedShortcode = '' + this.uploadFile = [] + }) + }, + revertEmoji () { + this.editedFile = this.file + this.editedShortcode = this.shortcode + }, + deleteEmoji () { + this.deleteModalVisible = false + + this.$store.state.api.backendInteractor.deleteEmojiFile( + { packName: this.packName, shortcode: this.shortcode } + ).then(resp => resp.json()).then(resp => { + if (resp.error !== undefined) { + this.$emit('displayError', resp.error) + return + } + + this.$emit('updatePackFiles', resp) + }) + } + } +} +</script> + +<style lang="scss"> + .emoji-tab-edit-popover { + padding-left: 0.5em; + padding-right: 0.5em; + padding-bottom: 0.5em; + + .emoji { + width: 32px; + height: 32px; + } + } +</style> diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue index 45db3fc2..a747cebd 100644 --- a/src/components/settings_modal/helpers/modified_indicator.vue +++ b/src/components/settings_modal/helpers/modified_indicator.vue @@ -15,7 +15,7 @@ </template> <template #content> <div class="modified-tooltip"> - {{ $t('settings.setting_changed') }} + {{ $t(messageKey) }} </div> </template> </Popover> @@ -33,7 +33,13 @@ library.add( export default { components: { Popover }, - props: ['changed'] + props: { + changed: Boolean, + messageKey: { + type: String, + default: 'settings.setting_changed' + } + } } </script> diff --git a/src/components/settings_modal/helpers/number_setting.vue b/src/components/settings_modal/helpers/number_setting.vue index 93f11331..23c1a5dd 100644 --- 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/setting.js b/src/components/settings_modal/helpers/setting.js index b3add346..abf9cfdf 100644 --- a/src/components/settings_modal/helpers/setting.js +++ b/src/components/settings_modal/helpers/setting.js @@ -195,7 +195,8 @@ export default { } }, canHardReset () { - return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> ')) + return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths && + this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> ')) }, matchesExpertLevel () { return (this.expert || 0) <= this.$store.state.config.expertLevel > 0 diff --git a/src/components/settings_modal/helpers/size_setting.vue b/src/components/settings_modal/helpers/size_setting.vue index 6c3fbaeb..0a2206b2 100644 --- 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 index 0cfa61ce..7b30d1b9 100644 --- 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 index 6bc9459b..d01553db 100644 --- 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 index 4e7fd931..50859c94 100644 --- 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.js b/src/components/settings_modal/settings_modal_admin_content.js index f94721ec..ce835bf2 100644 --- a/src/components/settings_modal/settings_modal_admin_content.js +++ b/src/components/settings_modal/settings_modal_admin_content.js @@ -3,6 +3,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import InstanceTab from './admin_tabs/instance_tab.vue' import LimitsTab from './admin_tabs/limits_tab.vue' import FrontendsTab from './admin_tabs/frontends_tab.vue' +import EmojiTab from './admin_tabs/emoji_tab.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -33,7 +34,8 @@ const SettingsModalAdminContent = { InstanceTab, LimitsTab, - FrontendsTab + FrontendsTab, + EmojiTab }, computed: { user () { diff --git a/src/components/settings_modal/settings_modal_admin_content.scss b/src/components/settings_modal/settings_modal_admin_content.scss index c984d703..bd619d7f 100644 --- 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_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue index a7a2ac9a..65e23b7e 100644 --- a/src/components/settings_modal/settings_modal_admin_content.vue +++ b/src/components/settings_modal/settings_modal_admin_content.vue @@ -60,6 +60,14 @@ > <FrontendsTab /> </div> + + <div + :label="$t('admin_dash.tabs.emoji')" + icon="face-smile-beam" + data-tab-name="emoji" + > + <EmojiTab /> + </div> </tab-switcher> </template> diff --git a/src/components/settings_modal/settings_modal_user_content.scss b/src/components/settings_modal/settings_modal_user_content.scss index c984d703..bd619d7f 100644 --- 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 index 89fdef1a..1f2c0363 100644 --- a/src/components/settings_modal/tabs/filtering_tab.vue +++ b/src/components/settings_modal/tabs/filtering_tab.vue @@ -45,13 +45,36 @@ </BooleanSetting> </li> <li> + <BooleanSetting path="muteSensitiveStatuses"> + {{ $t('settings.mute_sensitive_posts') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="hideMutedFederationRestrictions"> + {{ $t('settings.hide_muted_federation_restrictions') }} + </BooleanSetting> + <ul + class="setting-list suboptions" + :class="[{disabled: !streaming}]" + > + <li + v-for="item in muteFederationRestrictionsLevels" + :key="'mute_' + item + '_federation_restriction'" + > + <BooleanSetting :path="'muteFederationRestrictions.' + item"> + {{ $t('settings.mute_' + item + '_federation_restriction') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> <BooleanSetting path="hidePostStats"> {{ $t('settings.hide_post_stats') }} </BooleanSetting> </li> <li> <BooleanSetting path="hideBotIndication"> - {{ $t('settings.hide_bot_indication') }} + {{ $t('settings.hide_actor_type_indication') }} </BooleanSetting> </li> <ChoiceSetting @@ -67,7 +90,7 @@ <textarea id="muteWords" v-model="muteWordsString" - class="resize-height" + class="input resize-height" /> <div>{{ $t('settings.filtering_explanation') }}</div> </li> diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index f56fa8e0..424ee7e4 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -201,6 +201,14 @@ <h2>{{ $t('settings.post_look_feel') }}</h2> <ul class="setting-list"> <li> + <BooleanSetting + path="forceThemeRecompilation" + :expert="1" + > + {{ $t('settings.force_theme_recompilation_debug') }} + </BooleanSetting> + </li> + <li> <ChoiceSetting id="conversationDisplay" path="conversationDisplay" diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue index 9ace4c36..ca2cf8ad 100644 --- 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.js b/src/components/settings_modal/tabs/profile_tab.js index eeacad48..dee17450 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -9,6 +9,7 @@ import suggestor from 'src/components/emoji_input/suggestor.js' import Autosuggest from 'src/components/autosuggest/autosuggest.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' +import Select from 'src/components/select/select.vue' import BooleanSetting from '../helpers/boolean_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import localeService from 'src/services/locale/locale.service.js' @@ -39,6 +40,7 @@ const ProfileTab = { showRole: this.$store.state.users.currentUser.show_role, role: this.$store.state.users.currentUser.role, bot: this.$store.state.users.currentUser.bot, + actorType: this.$store.state.users.currentUser.actor_type, pickAvatarBtnVisible: true, bannerUploading: false, backgroundUploading: false, @@ -57,7 +59,8 @@ const ProfileTab = { ProgressButton, Checkbox, BooleanSetting, - InterfaceLanguageSwitcher + InterfaceLanguageSwitcher, + Select }, computed: { user () { @@ -116,6 +119,12 @@ const ProfileTab = { bannerImgSrc () { const src = this.$store.state.users.currentUser.cover_photo return (!src) ? this.defaultBanner : src + }, + groupActorAvailable () { + return this.$store.state.instance.groupActorAvailable + }, + availableActorTypes () { + return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service'] } }, methods: { @@ -127,7 +136,7 @@ const ProfileTab = { /* eslint-disable camelcase */ display_name: this.newName, fields_attributes: this.newFields.filter(el => el != null), - bot: this.bot, + actor_type: this.actorType, show_role: this.showRole, birthday: this.newBirthday || '', show_birthday: this.showBirthday diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss index ee253ffe..7eda943b 100644 --- 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 index 1cc850cb..034034a1 100644 --- 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> @@ -109,10 +111,24 @@ </button> </div> <p> - <Checkbox v-model="bot"> - {{ $t('settings.bot') }} - </Checkbox> + <label> + {{ $t('settings.actor_type') }} + <Select v-model="actorType"> + <option + v-for="option in availableActorTypes" + :key="option" + :value="option" + > + {{ $t('settings.actor_type_' + option) }} + </option> + </Select> + </label> </p> + <div v-if="groupActorAvailable"> + <small> + {{ $t('settings.actor_type_description') }} + </small> + </div> <p> <interface-language-switcher :prompt-text="$t('settings.email_language')" @@ -191,6 +207,7 @@ <div> <input type="file" + class="input" @change="uploadFile('banner', $event)" > </div> @@ -233,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 index ee5b6b13..9421b16e 100644 --- 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 index 923161b2..32a8a759 100644 --- 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 index 8e767bd0..99b66818 100644 --- 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 index d36d478f..74103f6f 100644 --- 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 index d755279a..1837620f 100644 --- 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 index 58f8d44a..11c90b03 100644 --- 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 index 9935c2e7..5e633120 100644 --- 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 index a1d1012b..f8e12dbf 100644 --- 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 index 1f3c26aa..c3b956cd 100644 --- 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 index a7013469..9113211e 100644 --- a/src/components/shout_panel/shout_panel.vue +++ b/src/components/shout_panel/shout_panel.vue @@ -5,7 +5,7 @@ > <div class="panel panel-default"> <div - class="panel-heading timeline-heading" + class="panel-heading" :class="{ 'shout-heading': floating }" @click.stop.prevent="togglePanel" > @@ -18,7 +18,7 @@ /> </div> </div> - <div class="shout-window"> + <div class="panel-body shout-window"> <div v-for="message in messages" :key="message.id" @@ -41,10 +41,10 @@ </div> </div> </div> - <div class="shout-input"> + <div class="panel-body shout-input"> <textarea v-model="currentMessage" - class="shout-input-textarea" + class="shout-input-textarea input" rows="1" @keyup.enter="submit(currentMessage)" /> @@ -75,8 +75,6 @@ <script src="./shout_panel.js"></script> <style lang="scss"> -@import "../../variables"; - .floating-shout { position: fixed; bottom: 0.5em; @@ -97,8 +95,7 @@ cursor: pointer; .icon { - color: $fallback--text; - color: var(--panelText, $fallback--text); + color: var(--text); margin-right: 0.5em; } @@ -128,8 +125,7 @@ img { height: 24px; width: 24px; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); margin-right: 0.5em; margin-top: 0.25em; } diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 09588767..608b5608 100644 --- 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 new file mode 100644 index 00000000..8dce527e --- /dev/null +++ b/src/components/status/post.style.js @@ -0,0 +1,33 @@ +export default { + name: 'Post', + selector: '.Status', + states: { + selected: '.-focused' + }, + validInnerComponents: [ + 'Text', + 'Link', + 'Icon', + 'Border', + 'Button', + 'ButtonUnstyled', + 'RichContent', + 'Input', + 'Avatar', + 'Attachment', + 'PollGraph' + ], + defaultRules: [ + { + directives: { + background: '--bg' + } + }, + { + state: ['selected'], + directives: { + background: '--inheritedBackground, 10' + } + } + ] +} diff --git a/src/components/status/status.js b/src/components/status/status.js index 9e6c9528..85a41a9a 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -232,17 +232,14 @@ const Status = { muteWordHits () { return muteWordHits(this.status, this.muteWords) }, - rtBotStatus () { - return this.statusoid.user.bot - }, botStatus () { - return this.status.user.bot + return this.status.user.actor_type === 'Service' }, - botIndicator () { - return this.botStatus && !this.hideBotIndication + showActorTypeIndicator () { + return !this.hideBotIndication }, - rtBotIndicator () { - return this.rtBotStatus && !this.hideBotIndication + sensitiveStatus () { + return this.status.nsfw }, mentionsLine () { if (!this.headTailLinks) return [] @@ -271,7 +268,9 @@ const Status = { // Wordfiltered this.muteWordHits.length > 0 || // bot status - (this.muteBotStatuses && this.botStatus && !this.compact) + (this.muteBotStatuses && this.botStatus && !this.compact) || + // sensitive status + (this.muteSensitiveStatuses && this.sensitiveStatus && !this.compact) return !this.unmuted && !this.shouldNotMute && reasonsToMute }, userIsMuted () { @@ -380,6 +379,9 @@ const Status = { muteBotStatuses () { return this.mergedConfig.muteBotStatuses }, + muteSensitiveStatuses () { + return this.mergedConfig.muteSensitiveStatuses + }, hideBotIndication () { return this.mergedConfig.hideBotIndication }, diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 758a030b..63809ff2 100644 --- 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; @@ -426,8 +402,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 index 54fcc60f..61a58cda 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -31,6 +31,12 @@ /> </small> <small + v-if="muteSensitiveStatuses && status.nsfw" + class="mute-thread" + > + {{ $t('status.sensitive_muted') }} + </small> + <small v-if="showReasonMutedThread" class="mute-thread" > @@ -79,7 +85,7 @@ <UserAvatar v-if="retweet" class="left-side repeater-avatar" - :bot="rtBotIndicator" + :show-actor-type-indicator="showActorTypeIndicator" :better-shadow="betterShadow" :user="statusoid.user" /> @@ -133,7 +139,7 @@ > <UserAvatar class="post-avatar" - :bot="botIndicator" + :show-actor-type-indicator="showActorTypeIndicator" :compact="compact" :better-shadow="betterShadow" :user="status.user" @@ -180,7 +186,7 @@ <span class="heading-right"> <router-link - class="timeago faint-link" + class="timeago faint" :to="{ name: 'conversation', params: { id: status.id } }" > <Timeago @@ -450,7 +456,7 @@ > <button v-if="showOtherRepliesAsButton && replies.length > 1" - class="button-unstyled -link faint" + class="button-unstyled -link" :title="$tc('status.ancestor_follow', replies.length - 1, { numReplies: replies.length - 1 })" @click.prevent="dive" > @@ -572,7 +578,7 @@ <UserAvatar class="post-avatar" :compact="compact" - :bot="botIndicator" + :show-actor-type-indicator="showActorTypeIndicator" /> </div> <div class="right-side"> diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss index 8fd60afc..0a467b4f 100644 --- 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 index fb356360..f21bcc80 100644 --- 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 index 311ca099..839f82e9 100644 --- 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 index 904853c0..6678132e 100644 --- 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 index fc46fbe6..57633827 100644 --- 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 new file mode 100644 index 00000000..eac8aaeb --- /dev/null +++ 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 index b444da43..027a380a 100644 --- 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 index 705925c8..4e242b91 100644 --- a/src/components/tab_switcher/tab_switcher.scss +++ b/src/components/tab_switcher/tab_switcher.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - /* stylelint-disable no-descending-specificity */ .tab-switcher { display: flex; @@ -25,8 +23,7 @@ content: ""; flex: 1 1 auto; border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + border-bottom-color: var(--border); } .tab-wrapper { @@ -37,8 +34,7 @@ right: 0; bottom: 0; border-bottom: 1px solid; - border-bottom-color: $fallback--border; - border-bottom-color: var(--border, $fallback--border); + border-bottom-color: var(--border); } } @@ -80,8 +76,7 @@ flex-basis: 0.5em; content: ""; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &::after { @@ -106,16 +101,14 @@ right: 0; bottom: 0; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &::before { flex: 0 0 6px; content: ""; border-right: 1px solid; - border-right-color: $fallback--border; - border-right-color: var(--border, $fallback--border); + border-right-color: var(--border); } &:last-child .tab { @@ -173,6 +166,15 @@ } .tab { + user-select: none; + color: var(--text); + border: none; + cursor: pointer; + box-shadow: var(--shadow); + font-size: 1em; + font-family: var(--font); + border-radius: var(--roundness); + background-color: var(--background); position: relative; white-space: nowrap; padding: 6px 1em; @@ -188,8 +190,6 @@ &.active { background: transparent; z-index: 5; - color: $fallback--text; - color: var(--tabActiveText, $fallback--text); } img { @@ -231,7 +231,7 @@ margin-top: 0.5em; margin-left: 0.2em; margin-bottom: 0.25em; - border-bottom: 1px solid var(--border, $fallback--border); + border-bottom: 1px solid var(--border); @media all and (min-width: 800px) { display: none; diff --git a/src/components/text.style.js b/src/components/text.style.js new file mode 100644 index 00000000..a254ceb4 --- /dev/null +++ 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 index 04727278..971b7001 100644 --- 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 index 6cb9d1f1..59170f49 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -78,13 +78,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 index 4371947d..0fc0d979 100644 --- 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 index 2279f21a..862a1972 100644 --- 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 index 5f1da1f7..d755b9dd 100644 --- 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 new file mode 100644 index 00000000..46b3fb56 --- /dev/null +++ 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 new file mode 100644 index 00000000..3e0e1bf1 --- /dev/null +++ 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 index 4337acc4..fb2edaa4 100644 --- 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 new file mode 100644 index 00000000..812d45a4 --- /dev/null +++ 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.js b/src/components/user_avatar/user_avatar.js index 33d9a258..ffd81f87 100644 --- a/src/components/user_avatar/user_avatar.js +++ b/src/components/user_avatar/user_avatar.js @@ -3,11 +3,13 @@ import StillImage from '../still-image/still-image.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { - faRobot + faRobot, + faPeopleGroup } from '@fortawesome/free-solid-svg-icons' library.add( - faRobot + faRobot, + faPeopleGroup ) const UserAvatar = { @@ -15,7 +17,7 @@ const UserAvatar = { 'user', 'betterShadow', 'compact', - 'bot' + 'showActorTypeIndicator' ], data () { return { diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue index 91c17611..83608c50 100644 --- a/src/components/user_avatar/user_avatar.vue +++ b/src/components/user_avatar/user_avatar.vue @@ -18,21 +18,24 @@ :class="{ '-compact': compact }" /> <FAIcon - v-if="bot" + v-if="showActorTypeIndicator && user?.actor_type === 'Service'" icon="robot" - class="bot-indicator" + class="actor-type-indicator" + /> + <FAIcon + v-if="showActorTypeIndicator && user?.actor_type === 'Group'" + icon="people-group" + class="actor-type-indicator" /> </span> </template> <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; @@ -43,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); @@ -64,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); } } @@ -79,7 +78,7 @@ height: 100%; } - .bot-indicator { + .actor-type-indicator { position: absolute; bottom: 0; right: 0; @@ -87,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 index 4ab93a8a..70a76d54 100644 --- a/src/components/user_card/user_card.scss +++ b/src/components/user_card/user_card.scss @@ -1,5 +1,3 @@ -@import "../../variables"; - .user-card { position: relative; z-index: 1; @@ -21,14 +19,6 @@ position: relative; } - .panel-body { - word-wrap: break-word; - border-bottom-right-radius: inherit; - border-bottom-left-radius: inherit; - // create new stacking context - position: relative; - } - .background-image { position: absolute; top: 0; @@ -62,11 +52,6 @@ padding: 1em; margin: 0; - a { - color: $fallback--link; - color: var(--postLink, $fallback--link); - } - img { object-fit: contain; vertical-align: middle; @@ -76,53 +61,37 @@ } &.-rounded-t { - border-top-left-radius: $fallback--panelRadius; - border-top-left-radius: var(--panelRadius, $fallback--panelRadius); - border-top-right-radius: $fallback--panelRadius; - border-top-right-radius: var(--panelRadius, $fallback--panelRadius); + border-top-left-radius: var(--roundness); + border-top-right-radius: var(--roundness); - --__roundnessTop: var(--panelRadius); + --__roundnessTop: var(--roundness); --__roundnessBottom: 0; } &.-rounded { - border-radius: $fallback--panelRadius; - border-radius: var(--panelRadius, $fallback--panelRadius); + border-radius: var(--roundness); - --__roundnessTop: var(--panelRadius); - --__roundnessBottom: var(--panelRadius); + --__roundnessTop: var(--roundness); + --__roundnessBottom: var(--roundness); } &.-popover { - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + border-radius: var(--roundness); - --__roundnessTop: var(--tooltipRadius); - --__roundnessBottom: var(--tooltipRadius); + --__roundnessTop: var(--roundness); + --__roundnessBottom: var(--roundness); } &.-bordered { border-width: 1px; border-style: solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + border-color: var(--border); } } .user-info { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); padding: 0 26px; - a { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - - &:hover { - color: var(--icon); - } - } - .container { min-width: 0; padding: 16px 0 6px; @@ -164,8 +133,7 @@ display: flex; justify-content: center; align-items: center; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + border-radius: var(--roundness); opacity: 0; transition: opacity 0.2s ease; @@ -188,8 +156,7 @@ padding: 0.5em 0; &:not(:hover) .icon { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); + color: var(--lightText); } } @@ -203,6 +170,7 @@ } .user-screen-name { + color: var(--text); min-width: 1px; flex: 0 1 auto; text-overflow: ellipsis; @@ -214,16 +182,11 @@ flex: 0 0 auto; margin-left: 1em; font-size: 0.7em; - color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--text); } .user-role { flex: none; - color: $fallback--text; - color: var(--alertNeutralText, $fallback--text); - background-color: $fallback--fg; - background-color: var(--alertNeutral, $fallback--fg); } } @@ -241,6 +204,11 @@ --emoji-size: 1.7em; + .RichContent { + /* stylelint-disable-next-line declaration-no-important */ + --link: var(--text) !important; + } + .top-line, .bottom-line { display: flex; @@ -334,8 +302,6 @@ padding: 0.5em 1.5em 0; text-align: center; justify-content: space-between; - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); flex-wrap: wrap; } diff --git a/src/components/user_card/user_card.style.js b/src/components/user_card/user_card.style.js new file mode 100644 index 00000000..34eaa176 --- /dev/null +++ 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 index 2de14063..70c4f67d 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -113,22 +113,28 @@ <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.bot" - class="alert user-role" + v-if="user.actor_type === 'Service'" + class="alert neutral user-role" > {{ $t('user_card.bot') }} </span> + <span + v-if="user.actor_type === 'Group'" + class="alert user-role" + > + {{ $t('user_card.group') }} + </span> </template> <span v-if="user.locked"> <FAIcon @@ -160,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" > {{ ' ' }} @@ -276,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 index 06947ab7..39ee2451 100644 --- 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 index 8307cc8a..cd134453 100644 --- 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 index 4e05951f..e175ced0 100644 --- 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 index 95ec97af..9323b65e 100644 --- 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 index 3b2bbc45..413b254c 100644 --- 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 index d0618dbb..1ec12d0c 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -4,52 +4,54 @@ v-if="user" class="user-profile panel panel-default" > - <UserCard - :user-id="userId" - :switcher="true" - :selected="timeline.viewing" - avatar-action="zoom" - rounded="top" - :has-note-editor="true" - /> - <span - v-if="!!user.birthday" - class="user-birthday" - > - <FAIcon - class="fa-old-padding" - icon="birthday-cake" + <div class="panel-body"> + <UserCard + :user-id="userId" + :switcher="true" + :selected="timeline.viewing" + avatar-action="zoom" + rounded="top" + :has-note-editor="true" /> - {{ $t('user_card.birthday', { birthday: formattedBirthday }) }} - </span> - <div - v-if="user.fields_html && user.fields_html.length > 0" - class="user-profile-fields" - > - <dl - v-for="(field, index) in user.fields_html" - :key="index" - class="user-profile-field" + <span + v-if="!!user.birthday" + class="user-birthday" > - <dt - :title="user.fields_text[index].name" - class="user-profile-field-name" - > - <RichContent - :html="field.name" - :emoji="user.emoji" - /> - </dt> - <dd - :title="user.fields_text[index].value" - class="user-profile-field-value" + <FAIcon + class="fa-old-padding" + icon="birthday-cake" + /> + {{ $t('user_card.birthday', { birthday: formattedBirthday }) }} + </span> + <div + v-if="user.fields_html && user.fields_html.length > 0" + class="user-profile-fields" + > + <dl + v-for="(field, index) in user.fields_html" + :key="index" + class="user-profile-field" > - <RichContent - :html="field.value" - :emoji="user.emoji" - /> - </dd> - </dl> + <dt + :title="user.fields_text[index].name" + class="user-profile-field-name" + > + <RichContent + :html="field.name" + :emoji="user.emoji" + /> + </dt> + <dd + :title="user.fields_text[index].value" + class="user-profile-field-value" + > + <RichContent + :html="field.value" + :emoji="user.emoji" + /> + </dd> + </dl> + </div> </div> <tab-switcher :active-tab="tab" @@ -72,10 +74,14 @@ <div v-if="followsTabVisible" key="followees" + class="panel-body" :label="$t('user_card.followees')" :disabled="!user.friends_count" > - <FriendList :user-id="userId"> + <FriendList + :user-id="userId" + :non-interactive="true" + > <template #item="{item}"> <FollowCard :user="item" /> </template> @@ -84,10 +90,14 @@ <div v-if="followersTabVisible" key="followers" + class="panel-body" :label="$t('user_card.followers')" :disabled="!user.followers_count" > - <FollowerList :user-id="userId"> + <FollowerList + :user-id="userId" + :non-interactive="true" + > <template #item="{item}"> <FollowCard :user="item" @@ -117,7 +127,7 @@ :title="$t('user_card.favorites')" timeline-name="favorites" :timeline="favorites" - :user-id="userId" + :user-id="isUs ? undefined : userId" :in-profile="true" :footer-slipgate="footerRef" /> @@ -136,7 +146,7 @@ {{ $t('settings.profile_tab') }} </div> </div> - <div class="panel-body"> + <div> <span v-if="error">{{ error }}</span> <FAIcon v-else @@ -151,8 +161,6 @@ <script src="./user_profile.js"></script> <style lang="scss"> -@import "../../variables"; - .user-profile { flex: 2; flex-basis: 500px; @@ -182,9 +190,8 @@ .user-profile-field { display: flex; margin: 0.25em; - border: 1px solid var(--border, $fallback--border); - border-radius: $fallback--inputRadius; - border-radius: var(--inputRadius, $fallback--inputRadius); + border: 1px solid var(--border); + border-radius: var(--roundness); .user-profile-field-name { flex: 0 1 30%; @@ -192,7 +199,7 @@ text-align: right; color: var(--lightText); min-width: 120px; - border-right: 1px solid var(--border, $fallback--border); + border-right: 1px solid var(--border); } .user-profile-field-value { @@ -229,4 +236,5 @@ padding: 7em; } } + </style> diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue index 092c514e..2bda509a 100644 --- 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/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue index 23eb4c55..df763143 100644 --- a/src/components/video_attachment/video_attachment.vue +++ b/src/components/video_attachment/video_attachment.vue @@ -2,7 +2,7 @@ <video class="video" preload="metadata" - :src="attachment.url + '#t=0.5'" + :src="attachment.url + '#t=0.00000000000001'" :loop="loopVideo" :controls="controls" :alt="attachment.description" diff --git a/src/hocs/with_load_more/with_load_more.scss b/src/hocs/with_load_more/with_load_more.scss index bad852f4..524e4012 100644 --- a/src/hocs/with_load_more/with_load_more.scss +++ b/src/hocs/with_load_more/with_load_more.scss @@ -1,12 +1,9 @@ -@import "../../variables"; - .with-load-more { &-footer { padding: 10px; text-align: center; border-top: 1px solid; - border-top-color: $fallback--border; - border-top-color: var(--border, $fallback--border); + border-top-color: var(--border); .error { font-size: 1rem; diff --git a/src/i18n/cs.json b/src/i18n/cs.json index ca87214e..c3ba7ca4 100644 --- a/src/i18n/cs.json +++ b/src/i18n/cs.json @@ -9,7 +9,10 @@ "scope_options": "Možnosti rozsahů", "text_limit": "Textový limit", "title": "Vlastnosti", - "who_to_follow": "Koho sledovat" + "who_to_follow": "Koho sledovat", + "shout": "Shoutbox", + "pleroma_chat_messages": "Pleroma Chat", + "upload_limit": "Limit pro velikost souborů" }, "finder": { "error_fetching_user": "Chyba při načítání uživatele", @@ -20,12 +23,47 @@ "submit": "Odeslat", "more": "Více", "generic_error": "Vyskytla se chyba", - "optional": "volitelné" + "optional": "volitelné", + "retry": "Zkuste to znovu", + "show_more": "Zobrazit více", + "show_less": "Zobrazit méně", + "never_show_again": "Znovu již nezobrazovat", + "dismiss": "Zahodit", + "cancel": "Zrušit", + "disable": "Vypnout", + "enable": "Zapnout", + "close": "Zavřít", + "peek": "Nahlédnout", + "generic_error_message": "Došlo k chybě: {0}", + "error_retry": "Zkuste to prosím znovu", + "confirm": "Potvrdit", + "verify": "Ověřit", + "scope_in_timeline": { + "public": "Veřejné", + "direct": "Přímá", + "unlisted": "Neuvedené", + "private": "Pouze pro sledující" + }, + "scroll_to_top": "Přejít na začátek", + "role": { + "admin": "Správce", + "moderator": "Moderátor" + }, + "pin": "Připnout položku", + "flash_content": "Klikněte pro zobrazení Flash obsahu pomocí Ruffle (Experimentální, nemusí fungovat).", + "flash_security": "Flash obsah může být nebezpečný, protože se jedná o libovolný spustitelný kód.", + "flash_fail": "Nepodařilo se načíst Flash obsah. Podrobnosti naleznete v konzoli.", + "undo": "Vrátit zpět", + "yes": "Ano", + "no": "Ne", + "unpin": "Odepnout položku", + "loading": "Načítání…" }, "image_cropper": { "crop_picture": "Oříznout obrázek", "save": "Uložit", - "cancel": "Zrušit" + "cancel": "Zrušit", + "save_without_cropping": "Uložit bez ořezávání" }, "login": { "login": "Přihlásit", @@ -35,17 +73,31 @@ "placeholder": "např. lain", "register": "Registrovat", "username": "Uživatelské jméno", - "hint": "Chcete-li se přidat do diskuze, přihlaste se" + "hint": "Chcete-li se přidat do diskuze, přihlaste se", + "logout_confirm": "Opravdu se chcete odhlásit?", + "logout_confirm_accept_button": "Odhlásit se", + "logout_confirm_cancel_button": "Neodhlašovat", + "logout_confirm_title": "Potvrzení odhlášení", + "authentication_code": "Ověřovací kód", + "enter_recovery_code": "Zadejte záložní kód", + "enter_two_factor_code": "Zadejte dvoufaktorový ověřovací kód", + "recovery_code": "Záložní kód", + "heading": { + "totp": "Dvoufaktorové ověřování", + "recovery": "Dvoufaktorové obnovení" + } }, "media_modal": { "previous": "Předchozí", - "next": "Další" + "next": "Další", + "counter": "{current} / {total}", + "hide": "Zavřít prohlížeč médií" }, "nav": { "about": "O instanci", "back": "Zpět", "chat": "Místní chat", - "friend_requests": "Požadavky o sledování", + "friend_requests": "Požadavky na sledování", "mentions": "Zmínky", "dms": "Přímé zprávy", "public_tl": "Veřejná časová osa", @@ -53,7 +105,24 @@ "twkn": "Celá známá síť", "user_search": "Hledání uživatelů", "who_to_follow": "Koho sledovat", - "preferences": "Předvolby" + "preferences": "Předvolby", + "home_timeline": "Domovská časová osa", + "timelines": "Časové osy", + "search_close": "Zavřít vyhledávací panel", + "chats": "Chaty", + "lists": "Seznamy", + "edit_nav_mobile": "Upravit navigační panel", + "mobile_sidebar": "Přepnout mobilní postranní panel", + "announcements": "Oznámení", + "mobile_notifications_close": "Uzavřít oznámení", + "mobile_notifications": "Otevřít oznámení (máte nějaké nepřečtené)", + "administration": "Správa", + "bookmarks": "Záložky", + "search": "Hledat", + "edit_pinned": "Upravit připnuté položky", + "edit_finish": "Dokončit úpravu", + "mobile_notifications_mark_as_seen": "Označit vše jako přečtené", + "interactions": "Interakce" }, "notifications": { "broken_favorite": "Neznámý příspěvek, hledám jej…", @@ -61,9 +130,21 @@ "followed_you": "vás nyní sleduje", "load_older": "Načíst starší oznámení", "notifications": "Oznámení", - "read": "Číst!", + "read": "Přečíst!", "repeated_you": "zopakoval/a váš příspěvek", - "no_more_notifications": "Žádná další oznámení" + "no_more_notifications": "Žádná další oznámení", + "error": "Nastala chyba při načítání oznámení: {0}", + "unread_announcements": "{num} nepřečtené oznámení | {num} nepřečtených oznámení", + "unread_chats": "{num} nepřečtených zpráv | {num} nepřečtených zpráv", + "unread_follow_requests": "{num} nový požadavek o sledování | {num} nových požadavků o sledování", + "configuration_tip": "Může upravit co zde zobrazovat v {theSettings}. {dismiss}", + "follow_request": "vás chce sledovat", + "migrated_to": "migroval na", + "poll_ended": "anketa skončila", + "reacted_with": "reagoval/a s {0}", + "submitted_report": "Odeslal/a stížnost", + "configuration_tip_settings": "nastavení", + "configuration_tip_dismiss": "Již nezobrazovat" }, "post_status": { "new_status": "Napsat nový příspěvek", @@ -85,7 +166,27 @@ "private": "Pouze pro sledující - Poslat pouze sledujícím", "public": "Veřejný - Poslat na veřejné časové osy", "unlisted": "Neuvedený - Neposlat na veřejné časové osy" - } + }, + "edit_unsupported_warning": "Pleroma nepodporuje upravování zmínek a anket.", + "edit_status": "Upravit příspěvek", + "media_description": "Popis médií", + "reply_option": "Odpovědět na tento příspěvek", + "content_type_selection": "Formát příspěvku", + "post": "Odeslat", + "empty_status_error": "Nemůžete odeslat prázdný příspěvek bez žádných souborů", + "preview_empty": "Prázdné", + "media_description_error": "Selhání při aktualizaci médií, zkuste to znovu", + "scope_notice": { + "public": "Tento příspěvek bude viditelný pro všechny", + "private": "Tento příspěvek bude viditelný pouze pro vaše sledující", + "unlisted": "Tento příspěvek nebude viditelný ve Veřejné časové ose a časové ose Celá známá síť" + }, + "scope_notice_dismiss": "Zavřít tuto zprávu", + "quote_option": "Citovat tento příspěvek", + "direct_warning_to_all": "Tento příspěvek budou vidět pouze zmínění uživatelé.", + "direct_warning_to_first_only": "Tento příspěvek bude viditelný pouze pro zmíněné uživatele na začátku příspěvku.", + "edit_remote_warning": "Jiné vzdálené instance nemusí podporovat úpravy a nemusí přijmout nejnovější verzi vašeho příspěvku.", + "preview": "Náhled" }, "registration": { "bio": "O vás", @@ -105,8 +206,18 @@ "email_required": "nemůže být prázdný", "password_required": "nemůže být prázdné", "password_confirmation_required": "nemůže být prázdné", - "password_confirmation_match": "musí být stejné jako heslo" - } + "password_confirmation_match": "musí být stejné jako heslo", + "birthday_min_age": "musí být před nebo v {date}", + "birthday_required": "nemůže být ponecháno prázdné" + }, + "birthday_optional": "Datum narození (volitelné):", + "register": "Registrovat", + "reason": "Důvod pro registraci", + "reason_placeholder": "Tato instance schvaluje registrace manuálně,\nZdůvodněte administraci důvod registrace.", + "birthday": "Datum narození:", + "email_language": "V jakém jazyce chcete přijímat emaily z tohoto serveru?", + "email_optional": "Email (volitelný)", + "bio_optional": "Bio (volitelné)" }, "settings": { "app_name": "Název aplikace", @@ -136,7 +247,7 @@ "default_vis": "Výchozí rozsah viditelnosti", "delete_account": "Smazat účet", "delete_account_description": "Trvale smaže váš účet a všechny vaše příspěvky.", - "delete_account_error": "Při mazání vašeho účtu nastala chyba. Pokud tato chyba bude trvat, kontaktujte prosím admministrátora vaší instance.", + "delete_account_error": "Při mazání vašeho účtu nastala chyba. Pokud tato chyba přetrvává, kontaktujte prosím administrátora vaší instance.", "delete_account_instructions": "Pro potvrzení smazání účtu napište své heslo do pole níže.", "avatar_size_instruction": "Doporučená minimální velikost pro avatarové obrázky je 150x150 pixelů.", "export_theme": "Uložit přednastavení", @@ -152,7 +263,7 @@ "general": "Obecné", "hide_attachments_in_convo": "Skrývat přílohy v konverzacích", "hide_attachments_in_tl": "Skrývat přílohy v časové ose", - "max_thumbnails": "Maximální počet miniatur na příspěvek", + "max_thumbnails": "Maximální počet miniatur na příspěvek (prázdné = žádný limit)", "hide_isp": "Skrýt panel specifický pro instanci", "preload_images": "Přednačítat obrázky", "use_one_click_nsfw": "Otevírat citlivé přílohy pouze jedním kliknutím", @@ -181,7 +292,7 @@ "new_password": "Nové heslo", "notification_visibility": "Typy oznámení k zobrazení", "notification_visibility_follows": "Sledující", - "notification_visibility_likes": "Oblíbení", + "notification_visibility_likes": "Oblíbené", "notification_visibility_mentions": "Zmínky", "notification_visibility_repeats": "Zopakování", "no_rich_text_description": "Odstranit ze všech příspěvků formátování textu", @@ -237,18 +348,34 @@ "true": "ano" }, "notifications": "Oznámení", - "enable_web_push_notifications": "Povolit webová push oznámení", + "enable_web_push_notifications": "Povolit web push oznámení", "style": { "switcher": { "keep_color": "Ponechat barvy", "keep_shadows": "Ponechat stíny", "keep_opacity": "Ponechat průhlednost", "keep_roundness": "Ponechat kulatost", - "keep_fonts": "Keep fonts", + "keep_fonts": "Ponechat písma", "save_load_hint": "Možnosti „Ponechat“ dočasně ponechávají aktuálně nastavené možností při volení či nahrávání motivů, také tyto možnosti ukládají při exportování motivu. Pokud není žádné pole zaškrtnuto, uloží export motivu všechno.", "reset": "Resetovat", "clear_all": "Vymazat vše", - "clear_opacity": "Vymazat průhlednost" + "clear_opacity": "Vymazat průhlednost", + "keep_as_is": "Ponechat jak je", + "use_snapshot": "Stará verze", + "help": { + "migration_napshot_gone": "Z nějakého důvodu chyběl snímek, některé věci můžou vypadat jinak, než si pamatujete.", + "fe_upgraded": "Motiv engine PleromaFE byl aktualizován po aktualizaci verze.", + "future_version_imported": "Soubor, který jste importoval/a byl vytvořen pro novější verzi FE.", + "older_version_imported": "Soubor, který jste importoval/a byl vytvořen pro starší verzi FE.", + "v2_imported": "Soubor, který jste importoval/a byl vytvořen pro starší verzi FE. Snažíme se zachovat maximální kompatibilitu, ale může dojít k nekonzistenci.", + "snapshot_present": "Snímek motivu byl načten, takže všechny hodnoty byly přepsány. Místo toho můžete načíst skutečná data motivu.", + "upgraded_from_v2": "PleromaFE bylo aktualizováno, motiv může vypadat trochu jinak, než si pamatujete.", + "snapshot_missing": "V souboru nebyl žádný snímek motivu, takže může vypadat jinak, než bylo původně zamýšleno.", + "fe_downgraded": "Verze PleromaFE byla vrácena zpět.", + "migration_snapshot_ok": "Pro jistotu byl načten snímek motivu. Můžete zkusit načíst data motivu." + }, + "load_theme": "Načíst motiv", + "use_source": "Nová verze" }, "common": { "color": "Barva", @@ -283,7 +410,27 @@ "borders": "Okraje", "buttons": "Tlačítka", "inputs": "Vstupní pole", - "faint_text": "Vybledlý text" + "faint_text": "Vybledlý text", + "popover": "Popisy, menu, popovery", + "underlay": "Podklad", + "pressed": "Zmáčknuté", + "selectedPost": "Vybraný příspěvek", + "selectedMenu": "Vybraná položka menu", + "alert_warning": "Varování", + "alert_neutral": "Neutrální", + "toggled": "Přepnuto", + "disabled": "Vypnuto", + "tabs": "Karty", + "chat": { + "incoming": "Příchozí", + "border": "Okraj", + "outgoing": "Odchozí" + }, + "post": "Příspěvky/Bio uživatelů", + "wallpaper": "Tapeta", + "poll": "Graf ankety", + "icons": "Ikony", + "highlight": "Zvýrazněné prvky" }, "radii": { "_tab_label": "Kulatost" @@ -346,7 +493,252 @@ "checkbox": "Pročetl/a jsem podmínky používání", "link": "hezký malý odkaz" } - } + }, + "added_alias": "Přezdívka přidána.", + "emoji_reactions_scale": "Měřítko zvětšení reakcí", + "file_export_import": { + "backup_restore": "Záloha nastavení", + "errors": { + "file_too_new": "Nekompatibilní hlavní verze: {fileMajor}, tato verze PleromaFE (verze {feMajor}) je příliš stará", + "invalid_file": "Vybraný soubor není podporovaná záloha Pleroma nastavení. Žádné změny nebyli provedeny.", + "file_too_old": "Nekompatibilní verze: {fileMajor}, verze souboru je příliš stará a nepodporovaná (min. verze {feMajor})", + "file_slightly_new": "Menší verze je rozdílná, některé nastavení se nemusí načíst" + }, + "backup_settings": "Zálohovat nastavení do souboru", + "backup_settings_theme": "Zálohovat nastavení a motiv do souboru", + "restore_settings": "Obnovit nastavení ze souboru" + }, + "backup_failed": "Záloha selhala.", + "tree_fade_ancestors": "Zobrazit autory aktuálního příspěvku ve slabém textu", + "mention_link_display_full_for_remote": "jako celá jména pouze pro vzdálené uživatele (např. {'@'}foo{'@'}example.org)", + "autocomplete_select_first": "Automaticky vybrat prvního kandidáta, když výsledky automatického doplnění jsou dostupné", + "import_blocks_from_a_csv_file": "Importovat blokace z csv souboru", + "backup_running": "Tato záloha právě probíhá, zpracován záznam {number}. |Tato záloha právě probíhá, zpracováno {number} záznamů.", + "changed_email": "Email byl úspěšně změněn!", + "chatMessageRadius": "Zpráva chatu", + "confirm_dialogs_delete": "mazání příspěvku", + "disable_sticky_headers": "Nezanechávat záhlaví sloupců na horní část obrazovky", + "third_column_mode_postform": "Editor hlavního příspěvku a navigaci", + "columns": "Sloupce", + "sensitive_by_default": "Označit příspěvky jako citlivé ve výchozím stavu", + "domain_mutes": "Domény", + "fallback_language": "Záložní jazyk {index}:", + "primary_language": "Hlavní jazyk:", + "security": "Zabezpečení", + "enter_current_password_to_confirm": "Zadejte vaše současné heslo pro potvrzení vaší identity", + "post_look_feel": "Vzhled příspěvků", + "mention_links": "Odkazy zmínek", + "mfa": { + "confirm_and_enable": "Potvrdit a zapnout OTP", + "title": "Dvoufázová autentizace", + "scan": { + "title": "Skenovat", + "desc": "Pomocí vaší 2FA aplikace oskenujte QR kód, nebo zadejte klíč:", + "secret_code": "Klíč" + }, + "otp": "OTP", + "generate_new_recovery_codes": "Vygenerovat nové záložní kódy", + "setup_otp": "Nastavit OTP", + "wait_pre_setup_otp": "přednastavení OTP", + "waiting_a_recovery_codes": "Přijímám záložní kódy…", + "recovery_codes_warning": "Zapište nebo uložte si záložní kódy jelikož je znovu již neuvidíte. Pokud ztratíte přístup k vaší 2FA aplikace a záložním kódům nebudete mít možnost se přihlásit k vašemu účtu.", + "recovery_codes": "Záložní kódy.", + "warning_of_generate_new_codes": "Když vygenerujete nové záložní kódy, tak staré přestanou fungovat.", + "authentication_methods": "Autentizační metody", + "verify": { + "desc": "Pro zapnutí dvoufázové autentizace zadejte kód z vaší 2FA aplikace:" + } + }, + "remove_backup": "Odstranit", + "email_language": "Jazyk pro přijímání emailů ze serveru", + "block_export": "Export blokací", + "block_import": "Import blokací", + "block_import_error": "Chyba při importování blokací", + "mute_export": "Exportovat ztlumení", + "mute_export_button": "Exportovat vaše ztlumení jako csv soubor", + "wordfilter": "Filtr slov", + "user_profiles": "Profily uživatelů", + "use_at_icon": "Zobrazovat {'@'} jako ikonu namísto textu", + "notification_visibility_moves": "Migrace uživatelů", + "hide_followers_count_description": "Nezobrazovat počet sledujících uživatelů", + "reply_visibility_self_short": "Zobrazit odpovědi pouze sobě", + "third_column_mode_notifications": "Sloupec oznámení", + "useStreamingApi": "Přijímat příspěvky a oznámení v reálném čase", + "use_websockets": "Používat websockety (Aktualizace v reálném čase)", + "user_mutes": "Uživatelé", + "mention_link_display": "Zobrazit odkazy na zmínky", + "add_language": "Přidat záložní jazyk", + "account_backup": "Zálohování účtu", + "account_alias": "Přezdívky účtu", + "setting_server_side": "Toto nastavení je vázané na váš profil a ovlivňuje všechny vaše sezení a klienty", + "block_export_button": "Exportovat vaše blokace jako csv soubor", + "blocks_imported": "Blokace importovány! Jejich zpracování může chvíli trvat.", + "mute_import": "Importovat ztlumení", + "mute_import_error": "Chyba při importování ztlumení", + "mutes_imported": "Ztlumení importovány! Jejich zpracování může chvíli trvat.", + "account_backup_table_head": "Záloha", + "download_backup": "Stáhnout", + "import_mutes_from_a_csv_file": "Importovat ztlumení z csv souboru", + "account_backup_description": "Toto umožňuje stáhnout archiv vašeho účtu a vašich příspěvků, ale nemůžou být zpětně importovány do Pleroma účtu.", + "backup_not_ready": "Tato záloha není zatím připravená.", + "list_backups_error": "Chyba při získávání seznamu záloh: {error}", + "add_backup": "Vytvořit novou zálohu", + "bot": "Toto je účet robota", + "change_email": "Změnit email", + "change_email_error": "Nastala chyba při změně vašeho emailu.", + "confirm_dialogs": "Požádat o potvrzení při", + "confirm_dialogs_mute": "ztlumení uživatele", + "confirm_dialogs_logout": "odhlašování", + "confirm_dialogs_approve_follow": "schvalování sledujícího", + "confirm_dialogs_deny_follow": "odmítání sledujícího", + "confirm_dialogs_remove_follower": "odstraňování sledujícího", + "mutes_and_blocks": "Ztlumení a Blokace", + "account_alias_table_head": "Přezdívka", + "move_account": "Přesunout účet", + "birthday": { + "show_birthday": "Zobrazit moje datum narození", + "label": "Datum narození" + }, + "account_privacy": "Soukromí", + "notification_visibility_in_column": "Zobrazit ve sloupci/zásuvce oznámení", + "notification_visibility_reports": "Nahlášení", + "notification_visibility_emoji_reactions": "Reakce", + "notification_visibility_polls": "Konce anket, ve kterých jste hlasovali", + "notification_extra_tip": "Zobrazení tipu přizpůsobení pro další oznámení", + "notification_visibility_native_notifications": "Zobrazit nativní oznámení", + "notification_visibility_follow_requests": "Požadavky na sledování", + "mute_bot_posts": "Skrýt příspěvky od robotů", + "hide_bot_indication": "Skrýt indikaci účtů robotů v příspěvcích", + "auto_update": "Zobrazovat nové příspěvky automaticky", + "url": "URL", + "preview": "Náhled", + "profile_fields": { + "label": "Metadata profilu", + "name": "Štítek", + "value": "Obsah", + "add_field": "Přidat pole" + }, + "hide_favorites_description": "Nezobrazovat seznam oblíbených příspěvků (uživatelé stále budou notifikování)", + "right_sidebar": "Prohodit pořadí sloupců", + "hide_scrobbles": "Skrýt scrobbles", + "hide_shoutbox": "Skrýt shoutbox instance", + "new_email": "Nový email", + "notification_show_extra": "Zobrazit další oznámení ve sloupci oznámení", + "reply_visibility_following_short": "Zobrazit odpovědi mým sledujícím", + "search_user_to_block": "Hledat koho chcete zablokovat", + "search_user_to_mute": "Hledat koho chcete ztlumit", + "reset_avatar_confirm": "Opravdu chcete resetovat avatar?", + "tree_advanced": "Umožnit více flexibilní navigaci ve stromovém zobrazení", + "conversation_display_linear_quick": "Lineární zobrazení", + "max_depth_in_thread": "Maximální počet zobrazených úrovní ve vlákně ve výchozím stavu", + "add_backup_error": "Chyba při přidávání nové zálohy: {error}", + "added_backup": "Přidána nová záloha.", + "word_filter_and_more": "Filtr slov a další...", + "posts": "Příspěvky", + "reset_banner_confirm": "Opravdu chcete resetovat banner?", + "reset_background_confirm": "Opravdu chcete resetovat pozadí?", + "reset_avatar": "Resetovat avatar", + "reset_profile_background": "Resetovat pozadí profilu", + "reset_profile_banner": "Resetovat banner profilu", + "type_domains_to_mute": "Hledat domény ke ztlumení", + "virtual_scrolling": "Optimalizovat vykreslování časové osy", + "remove_language": "Odstranit", + "expert_mode": "Zobrazit pokročilé nastavení", + "save": "Uložit změny", + "setting_changed": "Nastavení je rozdílné od výchozího", + "lists_navigation": "Zobrazovat seznamy v navigaci", + "allow_following_move": "Povolit automatické sledování pokud se sledovaný účet přesune", + "confirm_dialogs_repeat": "opakování příspěvku", + "confirm_dialogs_unfollow": "zrušení sledování uživatele", + "confirm_dialogs_block": "blokování uživatele", + "list_aliases_error": "Chyba při zjišťování přezdívek: {error}", + "remove_alias": "Odstranit tuto přezdívku", + "new_alias_target": "Přidat novou přezdívku (např. {example})", + "add_alias_error": "Chyba při přidávání přezdívky: {error}", + "hide_list_aliases_error_action": "Zavřít", + "move_account_notes": "Pokud chcete přesunut účet jinam, musíte jít na cílový účet a přidat přezdívku ukazující na tento účet.", + "move_account_target": "Cílový účet (např. {example})", + "moved_account": "Účet přesunut.", + "move_account_error": "Chyba při přesouvání účtu: {error}", + "discoverable": "Umožnit objevení tohoto účtu ve výsledcích vyhledávání a v jiných službách", + "pad_emoji": "Přidat mezeru okolo emoji při přidávání emoji z výběru", + "emoji_reactions_on_timeline": "Zobrazit emoji reakce u příspěvků", + "hide_media_previews": "Schovat náhledy médií", + "hide_muted_posts": "Skrýt příspěvky od ztlumených uživatelů", + "hide_all_muted_posts": "Skrýt ztlumené uživatele", + "navbar_column_stretch": "Protáhnout navbar na šířku sloupců", + "always_show_post_button": "Vždy zobrazovat plovoucí tlačítko pro nový příspěvek", + "hide_wallpaper": "Skrýt pozadí instance", + "hide_wordfiltered_statuses": "Skrýt slovně filtrované příspěvky", + "hide_muted_threads": "Skrýt ztlumené vlákna", + "notification_extra_chats": "Zobrazit nepřečtené chaty", + "notification_extra_announcements": "Zobrazit nepřečtené oznámení", + "notification_extra_follow_requests": "Zobrazit nové požadavky na sledování", + "hide_follows_count_description": "Nezobrazovat počet sledovaných uživatelů", + "autohide_floating_post_button": "Automaticky skrýt tlačítko nového příspěvku (mobilní zařízení)", + "minimal_scopes_mode": "Minimalizovat možnosti rozsahu příspěvků", + "conversation_display": "Styl zobrazení konverzací", + "conversation_display_tree": "Stromové zobrazení", + "conversation_display_tree_quick": "Stromový styl", + "show_scrollbars": "Zobrazit posuvníky bočních sloupců", + "third_column_mode": "Pokud je volné místo, zobrazit třetí sloupec obsahující", + "third_column_mode_none": "Nikdy nezobrazovat třetí sloupec", + "column_sizes": "Velikost sloupců", + "column_sizes_sidebar": "Postranní panel", + "column_sizes_content": "Obsah", + "column_sizes_notifs": "Oznámení", + "conversation_display_linear": "Lineární styl", + "conversation_other_replies_button": "Zobrazit tlačítko ostatních odpovědí", + "conversation_other_replies_button_below": "Pod příspěvky", + "conversation_other_replies_button_inside": "Uvnitř příspěvků", + "mention_link_display_short": "vždy jako zkrácená jména (např. {'@'}foo)", + "mention_link_display_full": "vždy jako celá jména (např. {'@'}foo{'@'}example.org)", + "enable_web_push_always_show_tip": "Některé prohlížeče (Chromium, Chrome) vyžadují aby push zprávy vždy vytvořili oznámení, jinak obecné oznámení 'Tento web byl aktualizován na pozadí' je zobrazeno, povolte abyste zabránili tomuto oznámení, protože Chrome nejspíš skrývá push oznámení, pokud je panel zobrazen. Může mít za následek duplicitní oznámení v ostatních prohlížečích.", + "actor_type": "Tento účet je:", + "actor_type_description": "Když svůj účet označíte jako skupinu, bude automaticky opakovat všechny příspěvky, které ho zmíní.", + "actor_type_Person": "normální uživatel", + "actor_type_Service": "bot", + "actor_type_Group": "skupina", + "hide_actor_type_indication": "Skrýt označení typu účtu (bot, skupina atd.) v příspěvcích", + "mention_link_show_avatar": "Zobrazit avatar uživatele vedle odkazu", + "mention_link_show_avatar_quick": "Zobrazit avatar uživatele vedle zmínky", + "mention_link_fade_domain": "Zeslabit doménu (např {'@'}example.org v {'@'}foo{'@'}example.org)", + "fun": "Zábava", + "notification_mutes": "Pokud nechcete dostávat oznámení od specifický uživatelů, použijte funkci ztlumení.", + "more_settings": "Víc nastavení", + "user_popover_avatar_action_zoom": "Zvětšit avatar", + "user_popover_avatar_action_close": "Zavřít popover", + "notification_setting_annoyance": "Nepříjemnost", + "user_popover_avatar_action_open": "Otevřit profil", + "notification_setting_drawer_marks_as_seen": "Zavření zásuvky (na mobilu) označí všechny oznámení jako přečtené", + "notification_setting_ignore_inactionable_seen": "Ignorovat stav přečtení pro oznámení bez akce (oblíbené, opakování atd.)", + "notification_setting_unseen_at_top": "Zobrazovat nepřečtené oznámení nad ostatními", + "enable_web_push_always_show": "Vždy zobrazovat web push oznámení", + "notification_setting_privacy": "Soukromí", + "notification_setting_block_from_strangers": "Blokovat oznámení od uživatelů které nesledujete", + "notification_setting_hide_notification_contents": "Schovávat odesílatele a obsah push oznámení", + "notification_blocks": "Blokování uživatele zastaví všechny notifikace a také je odhlásí.", + "mention_link_use_tooltip": "Zobrazit kartu uživatele při kliknutí na zmínku", + "user_popover_avatar_overlay": "Zobrazit popover uživatele přes jeho avatar", + "greentext": "Vtipné šipky", + "mention_link_bolden_you": "Zvýraznit vaši zmínku", + "user_popover_avatar_action": "Popover akce při kliknutí na avatar", + "show_yous": "Zobrazit (Vy)", + "notification_setting_filters": "Filtry", + "notification_setting_ignore_inactionable_seen_tip": "Toto ve skutečnosti neoznačí tyto oznámení jako přečtené a stále o nich budete dostávat oznámení na počítači, pokud si tak vyberete", + "notification_setting_filters_chrome_push": "V některých prohlížečích (Chrome) nemusí být možné kompletně vyfiltrovat oznámení, pokud přijdou jako push oznámení", + "commit_value": "Uložit", + "reset_value": "Resetovat", + "reset_value_tooltip": "Resetovat koncept", + "hard_reset_value": "Tvrdý reset", + "version": { + "title": "Verze", + "backend_version": "Backend verze", + "frontend_version": "Frontend verze" + }, + "commit_value_tooltip": "Hodnota není uložena, stiskněte toto tlačítko pro potvrzení změn", + "hard_reset_value_tooltip": "Odstranit nastavení z úložiště a vynutit výchozí hodnotu", + "accent": "Akcentní barva" }, "time": { "day": "{0} day", @@ -357,8 +749,8 @@ "hours": "{0} hours", "hour_short": "{0}h", "hours_short": "{0}h", - "in_future": "in {0}", - "in_past": "{0} ago", + "in_future": "za {0}", + "in_past": "před {0}", "minute": "{0} minute", "minutes": "{0} minutes", "minute_short": "{0}min", @@ -367,8 +759,8 @@ "months": "{0} měs", "month_short": "{0} měs", "months_short": "{0} měs", - "now": "teď", - "now_short": "teď", + "now": "právě teď", + "now_short": "nyní", "second": "{0} second", "seconds": "{0} seconds", "second_short": "{0}s", @@ -380,7 +772,23 @@ "year": "{0} r", "years": "{0} l", "year_short": "{0}r", - "years_short": "{0}l" + "years_short": "{0}l", + "unit": { + "seconds_short": "{0}s", + "days": "{0} den | {0} dnů", + "days_short": "{0}d", + "hours": "{0} hodina | {0} hodin", + "hours_short": "{0}h", + "minutes": "{0} minuta | {0} minut", + "months": "{0} měsíc | {0} měsíců", + "months_short": "{0}mo", + "minutes_short": "{0}min", + "seconds": "{0} sekunda | {0} sekund", + "weeks": "{0} týden | {0} týdnů", + "weeks_short": "{0}w", + "years": "{0} rok | {0} roky", + "years_short": "{0}y" + } }, "timeline": { "collapse": "Zabalit", @@ -392,11 +800,60 @@ "show_new": "Zobrazit nové", "up_to_date": "Aktuální", "no_more_statuses": "Žádné další příspěvky", - "no_statuses": "Žádné příspěvky" + "no_statuses": "Žádné příspěvky", + "socket_reconnected": "Navázáno spojení v reálném čase", + "error": "Chyba při načítání časové osy: {0}", + "reload": "Načíst znovu", + "socket_broke": "Spojení v reálném čase ztraceno: CloseEvent code {0}" }, "status": { "reply_to": "Odpověď uživateli", - "replies_list": "Odpovědi:" + "replies_list": "Odpovědi:", + "many_attachments": "Příspěvek má {number} příloh(u)", + "collapse_attachments": "Sbalit přílohy", + "unpin": "Odepnout z profilu", + "thread_muted": "Vlákno ztlumeno", + "show_attachment_description": "Popis náhledu (otevřete přílohu pro celý popis)", + "move_down": "Posunout přílohu doprava", + "thread_show": "Zobrazit toto vlákno", + "pin": "Připnout na profil", + "mute_conversation": "Ztlumit konverzaci", + "thread_hide": "Skrýt toto vlákno", + "show_full_subject": "Zobrazit celý předmět", + "edited_at": "(naposledy upraveno {time})", + "repeat_confirm_accept_button": "Zopakovat", + "repeat_confirm_title": "Potvrzení zopakování", + "delete_error": "Chyba při mazání příspěvku: {0}", + "delete_confirm": "Opravdu chcete smazat tento příspěvek?", + "delete_confirm_title": "Potvrzení smazání", + "delete_confirm_accept_button": "Smazat", + "delete_confirm_cancel_button": "Ponechat", + "you": "(Vy)", + "hide_attachment": "Skrýt přílohu", + "remove_attachment": "Odstranit přílohu", + "attachment_stop_flash": "Zastavit Flash player", + "nsfw": "NSFW", + "repeat_confirm_cancel_button": "Neopakovat", + "favorites": "Oblíbené", + "repeats": "Opakovaní", + "repeat_confirm": "Opravdu chcete zopakovat tento příspěvek?", + "delete": "Smazat příspěvek", + "copy_link": "Kopírovat odkaz k příspěvku", + "external_source": "Externí zdroj", + "edit": "Upravit příspěvek", + "bookmark": "Přidat do záložek", + "unbookmark": "Odebrat ze záložek", + "mentions": "Zmínky", + "hide_full_subject": "Skrýt celý předmět", + "show_content": "Zobrazit obsah", + "hide_content": "Skrýt obsah", + "unmute_conversation": "Zrušit ztlumení konverzace", + "status_unavailable": "Příspěvek je nedostupný", + "status_deleted": "Tento příspěvek byl smazán", + "expand": "Rozbalit", + "show_all_attachments": "Zobrazit všechny přílohy", + "move_up": "Posunout přílohu doleva", + "open_gallery": "Otevřít galerii" }, "user_card": { "approve": "Schválit", @@ -406,7 +863,7 @@ "favorites": "Oblíbené", "follow": "Sledovat", "follow_sent": "Požadavek odeslán!", - "follow_progress": "Odeslílám požadavek…", + "follow_progress": "Odesílám požadavek…", "follow_unfollow": "Přestat sledovat", "followees": "Sledovaní", "followers": "Sledující", @@ -455,5 +912,270 @@ "GiB": "GiB", "TiB": "TiB" } + }, + "about": { + "mrf": { + "federation": "Federace", + "keyword": { + "ftl_removal": "Odstranění z časové osy \"Celá známá síť\"", + "reject": "Odmítnout", + "replace": "Nahradit", + "is_replaced_by": "→", + "keyword_policies": "Zásady klíčových slov" + }, + "mrf_policies": "Povolené MRF zásady", + "simple": { + "instance": "Instance", + "reason": "Důvod", + "not_applicable": "N/A", + "accept": "Přijmout", + "accept_desc": "Tato instance přijímá zprávy pouze z následujících instancí:", + "reject": "Odmítnout", + "quarantine": "Karanténa", + "quarantine_desc": "Tato instance bude posílat pouze veřejné zprávy na tyto instance:", + "media_removal": "Odstranění médií", + "media_nsfw_desc": "Tato instance vynucuje média nastavené jako citlivé v příspěvcích z následujících instancí:", + "simple_policies": "Zásady specifické pro danou instanci", + "ftl_removal": "Odstranění z časové osy \"Celá známá síť\"", + "media_nsfw": "Vynutit média jako citlivé", + "reject_desc": "Tato instance nebude přijímat zprávy z následujících instancí:", + "media_removal_desc": "Tato instance odstraňuje média v příspěvcích z následujících instancí:", + "ftl_removal_desc": "Tato instance odstraňuje tyto instance z časové osy \"Celá známá síť\":" + }, + "mrf_policies_desc": "Zásady MRF mění chování federace této instance. Následující MRF zásady jsou povoleny:" + }, + "staff": "Personál" + }, + "exporter": { + "processing": "Zpracovávám, zanedlouho budete vyzváni ke stažení vašeho souboru", + "export": "Exportovat" + }, + "remote_user_resolver": { + "searching_for": "Hledám", + "error": "Nenalezeno." + }, + "polls": { + "multiple_choices": "Výběr více možností", + "expiry": "Doba ankety", + "add_poll": "Přidat anketu", + "add_option": "Přidat možnost", + "single_choice": "Výběr jediné možnosti", + "option": "Možnost", + "votes": "hlasy", + "people_voted_count": "{count} hlasoval/a | {count} voličů", + "votes_count": "{count} hlasovat | {count} hlasů", + "vote": "Hlasovat", + "type": "Typ ankety", + "expires_in": "Anketa končí za {0}", + "expired": "Anketa skončila před {0}", + "not_enough_options": "Příliš málo jedinečných možností v anketě" + }, + "interactions": { + "follows": "Nových sledujících", + "moves": "Uživatel migroval", + "load_older": "Načíst starší interakce", + "emoji_reactions": "Emoji reakce", + "reports": "Stížnosti", + "favs_repeats": "Opakované a oblíbené" + }, + "emoji": { + "unicode_groups": { + "animals-and-nature": "Zvířata a příroda", + "flags": "Vlajky", + "activities": "Aktivity", + "people-and-body": "Lidé a těla", + "food-and-drink": "Jídlo a pití", + "objects": "Objekty", + "smileys-and-emotion": "Smajlíky a emoce", + "symbols": "Symboly", + "travel-and-places": "Cestování a místa" + }, + "unicode": "Unicode emoji", + "load_all": "Načítání všech {emojiAmount} emoji", + "stickers": "Nálepky", + "emoji": "Emoji", + "keep_open": "Ponechat okno výběru otevřené", + "search_emoji": "Hledat emoji", + "add_emoji": "Vložit emoji", + "custom": "Vlastní emoji", + "load_all_hint": "Načteno prvních {saneAmount} emoji, načítání všech emoji může způsobit problémy s výkonem.", + "unpacked": "Rozbalené emoji", + "regional_indicator": "Regionální indikátor {letter}", + "hide_custom_emoji": "Skrýt vlastní emoji" + }, + "importer": { + "submit": "Odeslat", + "success": "Úspěšně importováno.", + "error": "Nastala chyba při importování ze souboru." + }, + "report": { + "reporter": "Nahlašující:", + "reported_user": "Nahlášený uživatel:", + "reported_statuses": "Nahlášené příspěvky:", + "notes": "Poznámky:", + "state": "Stav:", + "state_open": "Otevřeno", + "state_closed": "Uzavřeno", + "state_resolved": "Vyřešeno" + }, + "announcements": { + "mark_as_read_action": "Označit jako přečtené", + "page_header": "Oznámení", + "title": "Oznámení", + "post_form_header": "Vyvěsit oznámení", + "post_placeholder": "Zde napište obsah vašeho oznámení…", + "post_action": "Odeslat", + "post_error": "Chyba: {error}", + "close_error": "Zavřít", + "delete_action": "Smazat", + "start_time_prompt": "Čas začátku: ", + "end_time_prompt": "Čas ukončení: ", + "all_day_prompt": "Toto je celodenní akce", + "published_time_display": "Zveřejněno v {time}", + "start_time_display": "Začíná v {time}", + "end_time_display": "Končí v {time}", + "edit_action": "Upravit", + "submit_edit_action": "Odeslat", + "cancel_edit_action": "Zrušit", + "inactive_message": "Toto oznámení není aktivní" + }, + "shoutbox": { + "title": "Shoutbox" + }, + "domain_mute_card": { + "mute": "Ztlumit", + "mute_progress": "Ztlumuji…", + "unmute": "Zrušit ztlumení", + "unmute_progress": "Ruším ztlumení…" + }, + "errors": { + "storage_unavailable": "Pleroma nemohla získat přístup k úložišti prohlížeče. Vaše přihlášení nebo lokální nastavení se neuloží a můžete narazit na neočekávané problémy. Zkuste povolit soubory cookies." + }, + "selectable_list": { + "select_all": "Vybrat vše" + }, + "admin_dash": { + "window_title": "Administrace", + "commit_all": "Uložit vše", + "tabs": { + "nodb": "Žádné nastavení v databázi", + "frontends": "Frontendy", + "instance": "Instance", + "limits": "Limity", + "emoji": "Emoji" + }, + "nodb": { + "heading": "Nastavení v databázi je vypnuto", + "documentation": "dokumentace", + "text2": "Většina konfiguračních možností nebude dostupná." + }, + "wip_notice": "Tento administrační panel je experimentální a v aktivní vývoji, {adminFeLink}.", + "old_ui_link": "staré administrační rozhraní je dostupné zde", + "reset_all": "Resetovat vše", + "frontend": { + "failure_installing_frontend": "Nepodařilo se nainstalovat frontend {version}: {reason}", + "reinstall": "Přeinstalovat", + "available_frontends": "Dostupné k instalaci", + "is_default": "(Výchozí)", + "versions": "Dostupné verze", + "build_url": "URL sestavení", + "install": "Instalovat", + "install_version": "Instalovat verzi {version}", + "more_install_options": "Více instalačních možností", + "more_default_options": "Více výchozích nastavení pro možnosti", + "set_default": "Nastavit výchozí", + "default_frontend": "Výchozí frontend", + "set_default_version": "Nastavit verzi {version} jako výchozí", + "repository": "Odkaz k repozitáři", + "is_default_custom": "(Výchozí, verze: {version})", + "success_installing_frontend": "Frontend {version} byl úspěšně nainstalován" + }, + "captcha": { + "native": "Nativní", + "kocaptcha": "KoCaptcha" + }, + "instance": { + "instance": "Informace o instanci", + "captcha_header": "CAPTCHA", + "restrict": { + "activities": "Přístup k příspěvkům a aktivitám", + "timelines": "Přístup k časovým osám", + "profiles": "Přístup k uživatelským profilům", + "header": "Omezit přístup pro anonymní návštěvníky" + }, + "registrations": "Registrace uživatelů", + "kocaptcha": "KoCaptcha nastavení" + }, + "limits": { + "posts": "Limity příspěvků", + "uploads": "Limity příloh", + "users": "Limity uživatelských profilů", + "arbitrary_limits": "Libovolné limity", + "profile_fields": "Limity profilových polí", + "user_uploads": "Limity médií profilů" + }, + "emoji": { + "global_actions": "Globální akce", + "reload": "Znovu načíst emoji", + "importFS": "Importovat emoji ze souborového systému", + "error": "Chyba: {0}", + "create_pack": "Vytvořit balíček", + "delete_pack": "Smazat balíček", + "new_pack_name": "Nový název balíčku", + "create": "Vytvořit", + "emoji_packs": "Emoji balíčky", + "remote_packs": "Vzdálené balíčky", + "do_list": "List", + "emoji_pack": "Emoji balíček", + "edit_pack": "Upravit balíček", + "description": "Popis", + "homepage": "Domovská stránka", + "fallback_src": "Záložní zdroj", + "fallback_sha256": "Záložní SHA256", + "share": "Sdílet", + "save": "Uložit", + "save_meta": "Uložit metadata", + "revert_meta": "Vrátit zpět metadata", + "delete": "Smazat", + "add_file": "Přidat soubor", + "adding_new": "Přidávání nových emoji", + "shortcode": "Zkratka", + "filename": "Jméno souboru", + "new_shortcode": "Zkrat, ponechte prázdné pro odvození", + "delete_confirm": "Opravdu chcete smazat {0}?", + "download_pack": "Stáhnout balíček", + "downloading_pack": "Stahování {0}", + "download": "Stáhnout", + "download_as_name": "Nové jméno", + "download_as_name_full": "Nové jméno, pro opakované použití ponechte prázdné", + "files": "Soubory", + "editing": "Upravování {0}", + "delete_title": "Smazat?", + "emoji_changed": "Neuložené změny emoji souborů, zkontrolujte zvýrazněné emoji", + "replace_warning": "Tímto se NAHRADÍ místní balíček se stejným jménem", + "metadata_changed": "Metadata jsou rozdílné od uložených", + "revert": "Vrátit zpět", + "new_filename": "Jméno souboru, ponechte prázdné pro odvození" + }, + "temp_overrides": { + ":pleroma": { + ":instance": { + ":background_image": { + "label": "Obrázek na pozadí", + "description": "Obrázek na pozadí (především používáno PleromaFE)" + }, + ":description_limit": { + "label": "Limit", + "description": "Limit počtu znaků pro popisy příloh" + }, + ":public": { + "label": "Instance je veřejná" + }, + ":limit_to_local_content": { + "label": "Limitovat vyhledávání pouze na místní obsah" + } + } + } + } } } diff --git a/src/i18n/en.json b/src/i18n/en.json index 96b84c90..270e14bd 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -186,7 +186,6 @@ "edit_pinned": "Edit pinned items", "edit_finish": "Done editing", "mobile_sidebar": "Toggle mobile sidebar", - "mobile_notifications": "Open notifications", "mobile_notifications": "Open notifications (there are unread ones)", "mobile_notifications_close": "Close notifications", "mobile_notifications_mark_as_seen": "Mark all as seen", @@ -238,6 +237,7 @@ "search_emoji": "Search for an emoji", "add_emoji": "Insert emoji", "custom": "Custom emoji", + "hide_custom_emoji": "Hide custom emojis", "unpacked": "Unpacked emoji", "unicode": "Unicode emoji", "unicode_groups": { @@ -360,6 +360,11 @@ "remove_language": "Remove", "primary_language": "Primary language:", "fallback_language": "Fallback language {index}:", + "actor_type": "This account is:", + "actor_type_description": "Marking your account as a group will make it automatically repeat statuses that mention it.", + "actor_type_Person": "a normal user", + "actor_type_Service": "a bot", + "actor_type_Group": "a group", "app_name": "App name", "expert_mode": "Show advanced", "save": "Save changes", @@ -424,7 +429,6 @@ "added_backup": "Added a new backup.", "add_backup_error": "Error adding a new backup: {error}", "blocks_tab": "Blocks", - "bot": "This is a bot account", "btnRadius": "Buttons", "cBlue": "Blue (Reply, follow)", "cGreen": "Green (Retweet)", @@ -496,8 +500,9 @@ "hide_media_previews": "Hide media previews", "hide_muted_posts": "Hide posts of muted users", "mute_bot_posts": "Mute bot posts", - "hide_bot_indication": "Hide bot indication in posts", + "hide_actor_type_indication": "Hide actor type (bots, groups, etc.) indication in posts", "hide_scrobbles": "Hide scrobbles", + "mute_sensitive_posts": "Mute sensitive posts", "hide_all_muted_posts": "Hide muted posts", "max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)", "hide_isp": "Hide instance-specific panel", @@ -631,6 +636,7 @@ "subject_line_email": "Like email: \"re: subject\"", "subject_line_mastodon": "Like mastodon: copy as is", "subject_line_noop": "Do not copy", + "force_theme_recompilation_debug": "Disable theme cahe, force recompile on each boot (DEBUG)", "conversation_display": "Conversation display style", "conversation_display_tree": "Tree-style", "conversation_display_tree_quick": "Tree view", @@ -878,7 +884,8 @@ "nodb": "No DB Config", "instance": "Instance", "limits": "Limits", - "frontends": "Front-ends" + "frontends": "Front-ends", + "emoji": "Emoji" }, "nodb": { "heading": "Database config is disabled", @@ -901,7 +908,7 @@ "description": "Detailed setting for allowing/disallowing access to certain aspects of API. By default (indeterminate state) it will disallow if instance is not public, ticked checkbox means disallow access even if instance is public, unticked means allow access even if instance is private. Please note that unexpected behavior might happen if some settings are set, i.e. if profile access is disabled posts will show without profile information.", "timelines": "Timelines access", "profiles": "User profiles access", - "activities": "Statues/activities access" + "activities": "Statuses/activities access" } }, "limits": { @@ -926,12 +933,57 @@ "set_default": "Set default", "set_default_version": "Set version {version} as default", "wip_notice": "Please note that this section is a WIP and lacks certain features as backend implementation of front-end management is incomplete.", - "default_frontend": "Default front-end", - "default_frontend_tip": "Default front-end will be shown to all users. Currently there's no way to for a user to select personal front-end. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.", + "default_frontend": "Default frontend", + "default_frontend_tip": "Default frontend will be shown to all users. Currently there's no way to for a user to select personal frontend. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.", + "default_frontend_unavail": "Default frontend settings are not available, as this requires configuration in the database", "available_frontends": "Available for install", "failure_installing_frontend": "Failed to install frontend {version}: {reason}", "success_installing_frontend": "Frontend {version} successfully installed" }, + "emoji": { + "global_actions": "Global actions", + "reload": "Reload emoji", + "importFS": "Import emoji from filesystem", + "error": "Error: {0}", + "create_pack": "Create pack", + "delete_pack": "Delete pack", + "new_pack_name": "New pack name", + "create": "Create", + "emoji_packs": "Emoji packs", + "remote_packs": "Remote packs", + "do_list": "List", + "remote_pack_instance": "Remote pack instance", + "emoji_pack": "Emoji pack", + "edit_pack": "Edit pack", + "description": "Description", + "homepage": "Homepage", + "fallback_src": "Fallback source", + "fallback_sha256": "Fallback SHA256", + "share": "Share", + "save": "Save", + "save_meta": "Save metadata", + "revert_meta": "Revert metadata", + "delete": "Delete", + "revert": "Revert", + "add_file": "Add file", + "adding_new": "Adding new emoji", + "shortcode": "Shortcode", + "filename": "Filename", + "new_shortcode": "Shortcode, leave blank to infer", + "new_filename": "Filename, leave blank to infer", + "delete_confirm": "Are you sure you want to delete {0}?", + "download_pack": "Download pack", + "downloading_pack": "Downloading {0}", + "download": "Download", + "download_as_name": "New name", + "download_as_name_full": "New name, leave blank to reuse", + "files": "Files", + "editing": "Editing {0}", + "delete_title": "Delete?", + "metadata_changed": "Metadata different from saved", + "emoji_changed": "Unsaved emoji file changes, check highlighted emoji", + "replace_warning": "This will REPLACE the local pack of the same name" + }, "temp_overrides": { ":pleroma": { ":instance": { @@ -1026,6 +1078,7 @@ "external_source": "External source", "thread_muted": "Thread muted", "thread_muted_and_words": ", has words:", + "sensitive_muted": "Muting sensitive content", "show_full_subject": "Show full subject", "hide_full_subject": "Hide full subject", "show_content": "Show content", @@ -1062,7 +1115,9 @@ "hide_quote": "Hide the quoted status", "display_quote": "Display the quoted status", "invisible_quote": "Quoted status unavailable: {link}", - "more_actions": "More actions on this status" + "more_actions": "More actions on this status", + "loading": "Loading...", + "load_error": "Unable to load status: {error}" }, "user_card": { "approve": "Approve", @@ -1129,6 +1184,7 @@ "hide_repeats": "Hide repeats", "show_repeats": "Show repeats", "bot": "Bot", + "group": "Group", "birthday": "Born {birthday}", "admin_menu": { "moderation": "Moderation", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index f86d1821..e2396937 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -90,7 +90,11 @@ "heading": { "totp": "Authentification à double-facteur", "recovery": "Récupération de l'authentification à double-facteur" - } + }, + "logout_confirm_title": "Confirmation de déconnexion", + "logout_confirm": "Souhaitez-vous vous déconnecter ?", + "logout_confirm_accept_button": "Déconnexion", + "logout_confirm_cancel_button": "Ne pas se déconnecter" }, "media_modal": { "previous": "Précédent", @@ -110,7 +114,7 @@ "timeline": "Flux personnel", "twkn": "Réseau connu", "user_search": "Recherche de comptes", - "who_to_follow": "Suggestion de suivit", + "who_to_follow": "Suggestion de suivi", "preferences": "Préférences", "search": "Recherche", "administration": "Administration", @@ -124,7 +128,10 @@ "edit_pinned": "Éditer les éléments agrafés", "edit_finish": "Édition terminée", "mobile_sidebar": "(Dés)activer le panneau latéral", - "mobile_notifications_close": "Fermer les notifications" + "mobile_notifications_close": "Fermer les notifications", + "search_close": "Fermer la barre de recherche", + "announcements": "Annonces", + "mobile_notifications_mark_as_seen": "Marquer tout comme vu" }, "notifications": { "broken_favorite": "Message inconnu, recherche en cours…", @@ -140,7 +147,13 @@ "follow_request": "veut vous suivre", "error": "Erreur de chargement des notifications : {0}", "poll_ended": "Sondage terminé", - "submitted_report": "Rapport envoyé" + "submitted_report": "Rapport envoyé", + "unread_announcements": "{num} annonce non lue | {num} annonces non lues", + "unread_chats": "{num} message non lu | {num} messages non lus", + "configuration_tip_settings": "les préférences", + "unread_follow_requests": "{num} nouvelle demande de suivi | {num} nouvelles demandes de suivi", + "configuration_tip": "Vous pouvez personnaliser ce qui est affiché ici dans {theSettings}. {dismiss}", + "configuration_tip_dismiss": "Ne plus montrer" }, "interactions": { "favs_repeats": "Partages et favoris", @@ -154,7 +167,7 @@ "new_status": "Poster un nouveau statut", "account_not_locked_warning": "Votre compte n'est pas {0}. N'importe qui peut vous suivre pour voir vos billets en Abonné·e·s uniquement.", "account_not_locked_warning_link": "verrouillé", - "attachments_sensitive": "Marquer les pièce-jointes comme sensible", + "attachments_sensitive": "Marquer les pièces jointes comme sensible", "content_type": { "text/plain": "Texte brut", "text/html": "HTML", @@ -183,9 +196,13 @@ "preview": "Prévisualisation", "media_description": "Description de la pièce-jointe", "post": "Post", - "edit_status": "Éditer le status", + "edit_status": "Éditer le statut", "edit_remote_warning": "Des instances distantes pourraient ne pas supporter l'édition et seront incapables de recevoir la nouvelle version de votre post.", - "edit_unsupported_warning": "Pleroma ne supporte pas l'édition de mentions ni de sondages." + "edit_unsupported_warning": "Pleroma ne supporte pas l'édition de mentions ni de sondages.", + "reply_option": "Répondre à ce statut", + "quote_option": "Citer ce statut", + "scope_notice_dismiss": "Fermer ce message", + "content_type_selection": "Format du statut" }, "registration": { "bio": "Biographie", @@ -205,14 +222,18 @@ "email_required": "ne peut pas être laissé vide", "password_required": "ne peut pas être laissé vide", "password_confirmation_required": "ne peut pas être laissé vide", - "password_confirmation_match": "doit être identique au mot de passe" + "password_confirmation_match": "doit être identique au mot de passe", + "birthday_min_age": "doit être le ou avant le {date}", + "birthday_required": "ne peut pas être vide" }, "reason_placeholder": "Cette instance modère les inscriptions manuellement.\nExpliquer ce qui motive votre inscription à l'administration.", "reason": "Motivation d'inscription", "register": "Enregistrer", "email_language": "Dans quelle langue voulez-vous recevoir les emails du server ?", "bio_optional": "Biographie (optionnelle)", - "email_optional": "Courriel (optionnel)" + "email_optional": "Courriel (optionnel)", + "birthday": "Anniversaire :", + "birthday_optional": "Anniversaire (optionnel) :" }, "selectable_list": { "select_all": "Tout selectionner" @@ -684,7 +705,64 @@ "use_websockets": "Utiliser les websockets (mises à jour en temps réel)", "user_popover_avatar_action_zoom": "Zoomer sur l'avatar", "user_popover_avatar_action_open": "Ouvrir le profil", - "conversation_display_tree_quick": "Vue arborescente" + "conversation_display_tree_quick": "Vue arborescente", + "emoji_reactions_scale": "Taille des réactions", + "backup_running": "Cette sauvegarde est en cours, {number} enregistrement effectué. | Cette sauvegarde est en cours, {number} enregistrements effectués.", + "backup_failed": "Cette sauvegarde a échoué.", + "autocomplete_select_first": "Sélectionner automatiquement la première occurrence lorsque les résultats de l'autocomplétion sont disponibles", + "confirm_dialogs_unfollow": "arrête de suivre un utilisateur", + "confirm_dialogs_repeat": "reposte un statut", + "actor_type": "Ce compte est :", + "actor_type_Person": "un utilisateur normal", + "actor_type_Service": "un robot", + "actor_type_Group": "un groupe", + "confirm_dialogs_logout": "à la déconnexion", + "confirm_dialogs_approve_follow": "accepte un nouvel abonné", + "confirm_dialogs_deny_follow": "refuse un nouvel abonné", + "confirm_dialogs_remove_follower": "supprime un abonné", + "actor_type_description": "En marquant votre compte comme un groupe, vous répétez automatiquement les statuts qui le mentionnent.", + "add_language": "Ajouter une langue de remplacement", + "remove_language": "Supprimer", + "primary_language": "Langue principale :", + "fallback_language": "Langue de remplacement {index} :", + "confirm_dialogs": "Demande de confirmation quand", + "confirm_dialogs_block": "bloque un utilisateur", + "confirm_dialogs_mute": "mute un utilisateur", + "confirm_dialogs_delete": "supprime un statut", + "url": "URL", + "preview": "Aperçu", + "reset_value": "Réinitialiser", + "hard_reset_value_tooltip": "Supprime le réglage du stockage, force l'utilisation de la valeur par défaut", + "reset_value_tooltip": "Réinitialiser le brouillon", + "hard_reset_value": "Remise à zéro", + "hide_actor_type_indication": "Cacher le type (robots, groupes, etc.) dans les status", + "notification_extra_follow_requests": "Afficher les nouvelles demandes de suivi", + "user_popover_avatar_action": "Action du clic sur l'avatar", + "user_popover_avatar_action_close": "Fermer la fenêtre contextuelle", + "notification_setting_ignore_inactionable_seen": "Ignorer les status de lecture des notifications non actionnables (favoris, répétitions, etc)", + "notification_setting_ignore_inactionable_seen_tip": "Ceci ne marquera pas ces notifications comme lues, et vous recevrez encore les notifications de bureau si vous le décidez", + "notification_setting_unseen_at_top": "Afficher les notifications non lues au-dessus des autres", + "notification_setting_filters_chrome_push": "Sur certains navigateurs (chrome), il peut être impossible de filtrer complètement les notifications par type lorsqu'elles arrivent", + "enable_web_push_always_show": "Toujours afficher les notifications web", + "commit_value": "Sauvegarder", + "hide_scrobbles": "Masquer les scrobbles", + "notification_setting_annoyance": "Agacement", + "notification_setting_drawer_marks_as_seen": "Fermer le tiroir marque toutes les notifications comme lues (mobile)", + "commit_value_tooltip": "Les valeurs ne sont pas sauvegardées, appuyez sur ce bouton pour soumettre vos changements", + "birthday": { + "show_birthday": "Afficher mon anniversaire", + "label": "Anniversaire" + }, + "notification_visibility_native_notifications": "Afficher une notification native", + "notification_visibility_follow_requests": "Demandes de suivi", + "notification_visibility_reports": "Rapports", + "notification_extra_chats": "Afficher les discussions non lues", + "notification_extra_announcements": "Afficher les annonces non lues", + "notification_extra_tip": "Afficher les astuces de personnalisation pour les notifications extras", + "enable_web_push_always_show_tip": "Certains navigateurs (Chromium, Chrome) exigent que les messages push donnent toujours lieu à une notification, sinon le message générique \"Le site web a été mis à jour en arrière-plan\" s'affiche ; activez cette option pour empêcher l'affichage de cette notification, car Chrome semble masquer les notifications push si l'onglet est au centre de l'attention. Cela peut entraîner l'affichage de notifications en double sur d'autres navigateurs.", + "user_popover_avatar_overlay": "Afficher la fenêtre contextuelle sur l'avatar de l'utilisateur", + "notification_visibility_in_column": "Afficher la colonne / le tiroir de notifications", + "notification_show_extra": "Afficher les extras dans la colonne de notifications" }, "timeline": { "collapse": "Fermer", @@ -758,7 +836,20 @@ "show_all_conversation": "Montrer tout le fil ({numStatus} autre message) | Montrer tout le fil ({numStatus} autre messages)", "edit": "Éditer le status", "edited_at": "(dernière édition {time})", - "status_history": "Historique du status" + "status_history": "Historique du status", + "delete_error": "Erreur de suppression du statut : {0}", + "repeat_confirm": "Voulez-vous réellement reposter ce statut ?", + "reaction_count_label": "{num} personne a réagi | {num} personnes ont réagi", + "repeat_confirm_cancel_button": "Ne pas reposter", + "hide_quote": "Masquer les status cités", + "display_quote": "Afficher les status cités", + "invisible_quote": "Citation de statut non disponible : {link}", + "delete_confirm_title": "Confirmer la suppression", + "more_actions": "Plus d'action sur ce statut", + "delete_confirm_cancel_button": "Conserver", + "repeat_confirm_title": "Confirmer reposte", + "repeat_confirm_accept_button": "Reposter", + "delete_confirm_accept_button": "Supprimer" }, "user_card": { "approve": "Accepter", @@ -828,7 +919,39 @@ "edit_profile": "Éditer le profil", "deactivated": "Désactivé", "follow_cancel": "Annuler la requête", - "remove_follower": "Retirer l'abonné·e" + "remove_follower": "Retirer l'abonné·e", + "remove_follower_confirm_accept_button": "Supprimer", + "approve_confirm_cancel_button": "Ne pas approuver", + "block_confirm_accept_button": "Bloquer", + "mute_confirm_title": "Confirmation de mise en sourdine", + "block_confirm_cancel_button": "Ne pas bloquer", + "unfollow_confirm": "Voulez-vous vraiment arrêter de suivre {user} ?", + "unfollow_confirm_accept_button": "Ne plus suivre", + "birthday": "Né(e) le {birthday}", + "edit_note": "Éditer note", + "edit_note_apply": "Appliquer", + "edit_note_cancel": "Abandonner", + "note": "Note", + "group": "Groupe", + "unfollow_confirm_title": "Confirmer l'arrêt de suivi", + "block_confirm_title": "Confirmer le blocage", + "deny_confirm_accept_button": "Refuser", + "deny_confirm_cancel_button": "Ne pas refuser", + "deny_confirm": "Voulez-vous refuser la demande de suivi de {user} ?", + "deny_confirm_title": "Refuser la confirmation", + "remove_follower_confirm_cancel_button": "Conserver", + "mute_duration_prompt": "Mettre cet utilisateur en sourdine pour (0 pour une durée indéterminée) :", + "remove_follower_confirm_title": "Confirmation de suppression d'utilisateur", + "note_blank": "(Aucun)", + "mute_confirm": "Voulez-vous vraiment mettre {user} en sourdine ?", + "mute_confirm_accept_button": "Mettre en sourdine", + "mute_confirm_cancel_button": "Ne pas mettre en sourdine", + "remove_follower_confirm": "Voulez-vous vraiment supprimer {user} de vos abonnés ?", + "approve_confirm_accept_button": "Approuver", + "approve_confirm": "Voulez-vous approuver la demande de suivi de {user} ?", + "block_confirm": "Voulez-vous vraiment bloquer {user} ?", + "approve_confirm_title": "Approuver confirmation", + "unfollow_confirm_cancel_button": "Ne pas arrêter le suivi" }, "user_profile": { "timeline_title": "Flux du compte", @@ -857,7 +980,10 @@ "add_reaction": "Ajouter une réaction", "accept_follow_request": "Accepter la demande de suivit", "reject_follow_request": "Rejeter la demande de suivit", - "bookmark": "Favori" + "bookmark": "Favori", + "autocomplete_available": "{number} résultat est disponible. Utilisez les touches haut et bas pour naviguer à l'intérieur. | {number} résultats sont disponibles. Utilisez les touches haut et bas pour naviguer à l'intérieur.", + "toggle_expand": "Développer ou réduire la notification pour afficher le message dans son intégralité", + "toggle_mute": "Développer ou réduire la notification pour révéler le contenu en sourdine" }, "upload": { "error": { @@ -950,7 +1076,9 @@ "symbols": "Symboles", "travel-and-places": "Voyages & lieux" }, - "regional_indicator": "Indicateur régional {letter}" + "regional_indicator": "Indicateur régional {letter}", + "unpacked": "Émojis non catégorisés", + "hide_custom_emoji": "Masquer les émojis personnalisés" }, "remote_user_resolver": { "error": "Non trouvé.", @@ -1012,7 +1140,7 @@ "person_talking": "{count} personnes discutant", "hashtags": "Mot-dièses", "people_talking": "{count} personnes discutant", - "no_results": "Aucun résultats", + "no_results": "Aucun résultat", "no_more_results": "Pas de résultats supplémentaires", "load_more": "Charger plus de résultats" }, @@ -1083,7 +1211,8 @@ "update_changelog_here": "Liste compète des changements", "art_by": "Œuvre par {linkToArtist}", "big_update_content": "Nous n'avons pas fait de nouvelle version depuis un moment, les choses peuvent vous paraitre différentes de vos habitudes.", - "update_bugs": "Veuillez rapporter les problèmes sur {pleromaGitlab}, comme beaucoup de changements on été fait, même si nous testons entièrement et utilisons la version de dévelopement nous-même, nous avons pu en louper. Les retours et suggestions sont bienvenues sur ce que vous avez pu rencontrer, ou sur comment améliorer Pleroma (BE) et Pleroma-FE." + "update_bugs": "Veuillez rapporter les problèmes sur {pleromaGitlab}, comme beaucoup de changements on été fait, même si nous testons entièrement et utilisons la version de dévelopement nous-même, nous avons pu en louper. Les retours et suggestions sont bienvenues sur ce que vous avez pu rencontrer, ou sur comment améliorer Pleroma (BE) et Pleroma-FE.", + "big_update_title": "Soyez indulgent avec nous" }, "unicode_domain_indicator": { "tooltip": "Ce domaine contient des caractères non ascii." @@ -1097,5 +1226,158 @@ "state_open": "Ouvert", "state_closed": "Fermé", "state_resolved": "Résolut" + }, + "announcements": { + "page_header": "Annonces", + "title": "Annonce", + "mark_as_read_action": "Marquer comme lu", + "post_form_header": "Faire une annonce", + "post_placeholder": "Écrivez le contenu de l'annonce ici...", + "post_action": "Envoyer", + "post_error": "Erreur : {error}", + "close_error": "Fermer", + "delete_action": "Supprimer", + "start_time_prompt": "Heure de début : ", + "end_time_prompt": "Heure de fin : ", + "all_day_prompt": "L'événement dure toute la journée", + "inactive_message": "Cette annonce n'est pas active", + "published_time_display": "Publié le {time}", + "start_time_display": "Démarre à {time}", + "end_time_display": "Se termine à {time}", + "edit_action": "Modifier", + "submit_edit_action": "Envoyer", + "cancel_edit_action": "Annuler" + }, + "admin_dash": { + "frontend": { + "success_installing_frontend": "Installation réussie de l'interface {version}", + "failure_installing_frontend": "Échec de l'installation de l'interface {version} : {reason}", + "default_frontend_unavail": "Les paramètres de l'interface ne sont pas disponibles, ils doivent être configurés dans la base de données", + "build_url": "Construction URL", + "reinstall": "Réinstaller", + "repository": "Lien du dépôt", + "versions": "Versions disponibles", + "default_frontend_tip": "L'interface par défaut sera affichée à tous les utilisateurs. Si vous décidez de quitter PleromaFE, vous devrez utiliser l'ancienne AdminFE buguée pour configurer votre instance jusqu'à ce que nous la remplacions.", + "is_default": "(Défaut)", + "is_default_custom": "(Défaut, version : {version})", + "install": "Installation", + "install_version": "Installation de la version {version}", + "more_install_options": "Plus d'options d'installation", + "more_default_options": "Plus d'options de paramétrages par défaut", + "set_default": "Définir la valeur par défaut", + "set_default_version": "Définir la version {version} comme version par défaut", + "wip_notice": "Veuillez noter que cette section est en cours de développement et que certaines fonctionnalités de l'interface ne sont pas implémentées côté serveur.", + "default_frontend": "Interface par défaut", + "available_frontends": "Disponible pour installation" + }, + "temp_overrides": { + ":pleroma": { + ":instance": { + ":public": { + "label": "Cette instance est publique", + "description": "En désactivant cette option, toutes les API ne seront accessibles qu'aux utilisateurs connectés, ce qui rendra les chronologies publiques et fédérées inaccessibles aux visiteurs anonymes." + }, + ":limit_to_local_content": { + "label": "Limitez la recherche au contenu local", + "description": "Désactive la recherche globale sur le réseau pour les utilisateurs non authentifiés (par défaut), tous les utilisateurs ou aucun" + }, + ":description_limit": { + "label": "Limite", + "description": "Limite de nombre de caractères pour la description des fichiers joints" + }, + ":background_image": { + "description": "Image de fond (principalement utilisé par PleromaFE)", + "label": "Image de fond d'écran" + } + } + } + }, + "tabs": { + "emoji": "Émoji", + "limits": "Limites", + "frontends": "Interfaces", + "instance": "Instance", + "nodb": "Pas de configuration de base de données" + }, + "instance": { + "kocaptcha": "Réglages KoCaptcha", + "access": "Accès à l'instance", + "restrict": { + "header": "Restreindre l'accès aux visiteurs anonymes", + "profiles": "Accès aux profils d'utilisateur", + "activities": "Accès aux status/activités", + "description": "Paramètre détaillé permettant d'autoriser/interdire l'accès à certains aspects de l'API. Par défaut (état indéterminé), l'accès est interdit si l'instance n'est pas publique ; si la case est cochée, l'accès est interdit même si l'instance est publique ; si la case n'est pas cochée, l'accès est autorisé même si l'instance est privée. Veuillez noter qu'un comportement inattendu peut se produire si certains paramètres sont définis, par exemple si l'accès au profil est désactivé, les messages s'afficheront sans les informations relatives au profil.", + "timelines": "Accès aux flux" + }, + "registrations": "Inscription des utilisateurs", + "captcha_header": "CAPTCHA", + "instance": "Informations sur l'instance" + }, + "emoji": { + "global_actions": "Actions globales", + "reload": "Recharger les émojis", + "importFS": "Importer les émojis depuis le système de fichiers", + "error": "Erreur : {0}", + "create_pack": "Créer un pack", + "delete_pack": "Supprimer un paquet", + "new_pack_name": "Renommer le pack", + "create": "Créer", + "emoji_packs": "Pack d'émojis", + "remote_packs": "Packs d'autres instances", + "do_list": "Liste", + "remote_pack_instance": "Instance du pack", + "emoji_pack": "Pack d'émoji", + "edit_pack": "Modifier le pack", + "description": "Description", + "homepage": "Page d'accueil", + "fallback_src": "Source de remplacement", + "fallback_sha256": "Remplacement SHA256", + "share": "Partager", + "save": "Sauvegarder", + "save_meta": "Sauvegarder les métadonnées", + "revert_meta": "Annuler métadonnées", + "delete": "Supprimer", + "revert": "Revenir en arrière", + "add_file": "Ajouter un fichier", + "adding_new": "Ajouter un nouvel émoji", + "shortcode": "Shortcode", + "filename": "Nom du fichier", + "new_filename": "Nom de fichier, laisser blanc pour inférer", + "delete_confirm": "Êtes-vous sûr de vouloir supprimer {0} ?", + "download_pack": "Télécharger pack", + "downloading_pack": "Télécharge {0}", + "download": "Téléchargement", + "download_as_name": "Nouveau nom", + "download_as_name_full": "Nouveau nom, laissez blanc pour réutiliser le précédent", + "files": "Fichiers", + "editing": "Édition de {0}", + "delete_title": "Supprimer ?", + "metadata_changed": "Métadonnées différentes de celles sauvegardées", + "emoji_changed": "Modifications du fichier émoji non sauvegardées, vérifier l'émoji surligné", + "replace_warning": "Vous allez REMPLACER le pack local qui porte ce nom" + }, + "window_title": "Administration", + "nodb": { + "heading": "La configuration de base de données est désactivée", + "documentation": "documentation", + "text2": "La majorité des options de configuration ne seront pas disponibles.", + "text": "Vous devez modifier les fichiers de configuration du serveur pour que {property} soit définie à {value}, plus de détails dans la {documentation}." + }, + "limits": { + "arbitrary_limits": "Limites arbitraires", + "posts": "Limites des statuts", + "uploads": "Limites des pièces jointes", + "users": "Limites du profil d'utilisateur", + "profile_fields": "Limites des champs du profile", + "user_uploads": "Limites des médias du profil" + }, + "captcha": { + "native": "Natif", + "kocaptcha": "KoCaptcha" + }, + "wip_notice": "Ce tableau de bord d'administration est expérimental et en cours de développement, {adminFeLink}.", + "old_ui_link": "L'ancien espace d'administration est disponible ici", + "reset_all": "Tout réinitialiser", + "commit_all": "Tout sauvegarder" } } diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json index 0b20ffff..616905fe 100644 --- a/src/i18n/ja_pedantic.json +++ b/src/i18n/ja_pedantic.json @@ -85,11 +85,11 @@ "hint": "会話に加わるには、ログインしてください", "authentication_code": "認証コード", "enter_recovery_code": "リカバリーコードを入力", - "enter_two_factor_code": "2段階認証コードを入力", + "enter_two_factor_code": "二段階認証コードを入力", "recovery_code": "リカバリーコード", "heading": { - "totp": "2段階認証", - "recovery": "2段階認証リカバリー" + "totp": "二段階認証", + "recovery": "二段階認証リカバリー" }, "logout_confirm": "本当にログアウトしますか?", "logout_confirm_accept_button": "ログアウト", @@ -149,7 +149,10 @@ "configuration_tip_dismiss": "二度と表示しない", "unread_announcements": "未読のお知らせが{num}件あります | 未読のお知らせが{num}件あります", "unread_chats": "未読のチャットが{num}件あります | 未読のチャットが{num}件あります", - "unread_follow_requests": "フォローリクエストが{num}件来ています | フォローリクエストが{num}件来ています" + "unread_follow_requests": "フォローリクエストが{num}件来ています | フォローリクエストが{num}件来ています", + "configuration_tip": "ここに表示する通知の種類は{theSettings}にて変更することができます。 {dismiss}", + "submitted_report": "通報が送信されました", + "configuration_tip_settings": "設定" }, "polls": { "add_poll": "投票を追加", @@ -177,7 +180,8 @@ "unicode": "Unicode絵文字", "load_all": "全 {emojiAmount} 絵文字を読み込む", "load_all_hint": "最初の {saneAmount} 件の絵文字を読み込みました。すべて読み込むとパフォーマンスに影響を与える可能性があります。", - "unpacked": "パック外の絵文字" + "unpacked": "パック外の絵文字", + "hide_custom_emoji": "カスタム絵文字を表示しない" }, "stickers": { "add_sticker": "ステッカーを追加" @@ -275,7 +279,7 @@ "setup_otp": "OTPのセットアップ", "wait_pre_setup_otp": "OTPのプリセット", "confirm_and_enable": "OTPの確認と有効化", - "title": "2段階認証", + "title": "二段階認証", "generate_new_recovery_codes": "新しいリカバリーコードを生成", "warning_of_generate_new_codes": "新しいリカバリーコードを生成すると、古いコードは使用できなくなります。", "recovery_codes": "リカバリーコード。", @@ -288,7 +292,7 @@ "secret_code": "キー" }, "verify": { - "desc": "2段階認証を有効にするには、2段階認証アプリに表示されたコードを入力してください:" + "desc": "二段階認証を有効にするには、二段階認証アプリに表示されたコードを入力してください:" } }, "attachmentRadius": "ファイル", @@ -329,7 +333,7 @@ "pad_emoji": "絵文字ピッカーから絵文字を挿入するとき、絵文字の前後に空白を挿入する", "export_theme": "ファイルにテーマを出力", "filtering": "フィルタリング", - "filtering_explanation": "以下の単語を含むステータスはミュートされます。(1行に1単語)", + "filtering_explanation": "これらの単語を含むステータスはミュートされます。(1行に1単語)", "follow_export": "フォローのエクスポート", "follow_export_button": "エクスポート", "follow_export_processing": "お待ちください。まもなくファイルをダウンロードできます。", @@ -728,7 +732,7 @@ "remove_backup": "削除", "add_backup": "新規バックアップを作成", "account_backup_description": "アカウント情報と投稿のアーカイブをダウンロードできます。開発段階の機能であり、現状、ダウンロードしたデータをインポートすることはできません。", - "mute_bot_posts": "bot アカウントの投稿をミュートする", + "mute_bot_posts": "BOTアカウントの投稿をミュートする", "auto_update": "自動でタイムラインを更新する", "enable_web_push_always_show_tip": "この設定は、Chromeなどのブラウザで「このサイトはバックグラウンドで更新されました」という通知が表示されることを防止します。その他のブラウザでこの設定を有効化すると、通知が二重で表示されることがあります。", "backup_failed": "バックアップに失敗しました。", @@ -770,7 +774,24 @@ "add_backup_error": "バックアップの追加に失敗しました: {error}", "user_popover_avatar_action_close": "ユーザーカードを閉じる", "tree_advanced": "高度なナビゲーションボタンを表示する", - "tree_fade_ancestors": "スレッド上で祖先にあたるステータスを薄いテキストで表示する" + "tree_fade_ancestors": "スレッド上で祖先にあたるステータスを薄いテキストで表示する", + "actor_type_description": "グループとして設定されたアカウントは、メンションのついたステータスを自動的にリピートします。", + "actor_type_Person": "通常アカウント", + "actor_type_Service": "BOTアカウント", + "actor_type_Group": "グループアカウント", + "notification_visibility_in_column": "通知カラム(PC)、通知サイドバー(モバイル)に表示する", + "notification_setting_annoyance": "通知のカスタマイズ", + "notification_setting_unseen_at_top": "未読の通知を最上部に表示する", + "enable_web_push_always_show": "プッシュ通知を常に表示する", + "hide_scrobbles": "Scrobbleを表示しない", + "actor_type": "アカウントタイプ:", + "hide_actor_type_indication": "投稿にアカウントタイプ(BOTアカウント、グループアカウントなど)を示すアイコンを表示しない", + "notification_show_extra": "その他の通知を通知カラムに表示する", + "notification_setting_drawer_marks_as_seen": "モバイルUIで、通知サイドバーを閉じた時すべての通知を既読にする", + "notification_setting_ignore_inactionable_seen_tip": "この設定は通知を自動的に既読にするわけではなく、この設定を有効にしてもプッシュ通知などは届きます", + "notification_setting_ignore_inactionable_seen": "お気に入りやリピートの通知など、アクション不可な通知を未読として扱わない", + "notification_extra_tip": "通知カラムをカスマイズするためのヒントを表示する", + "use_at_icon": "メンションリンク内の{'@'}記号を画像にする" }, "time": { "day": "{0}日", @@ -1007,7 +1028,8 @@ "unfollow_confirm_accept_button": "解除する", "remove_follower_confirm_accept_button": "解除する", "mute_confirm_title": "ミュートの確認", - "deactivated": "無効化済み" + "deactivated": "無効化済み", + "group": "グループ" }, "user_profile": { "timeline_title": "ユーザータイムライン", @@ -1036,7 +1058,9 @@ "bookmark": "ブックマーク", "reject_follow_request": "フォローリクエストを拒否", "accept_follow_request": "フォローリクエストを承認", - "add_reaction": "リアクションを追加" + "add_reaction": "リアクションを追加", + "toggle_mute": "ミュートされた通知を開く/閉じる", + "toggle_expand": "この投稿を開く/閉じる" }, "upload": { "error": { @@ -1080,7 +1104,7 @@ "federation": "連合", "simple": { "media_nsfw_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを閲覧注意に設定します:", - "media_nsfw": "メディアを閲覧注意に設定", + "media_nsfw": "強制閲覧注意", "media_removal_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを除去します:", "media_removal": "メディア除去", "ftl_removal": "「すべてのネットワーク」タイムラインから除外", @@ -1090,14 +1114,14 @@ "reject_desc": "このインスタンスでは、以下のインスタンスからのメッセージを受け付けません:", "accept_desc": "このインスタンスでは、以下のインスタンスからのメッセージのみを受け付けます:", "accept": "許可", - "simple_policies": "インスタンス固有のポリシー", + "simple_policies": "インスタンスに対するポリシー", "reject": "拒否", "instance": "インスタンス", "reason": "理由" }, "mrf_policies": "有効なMRFポリシー", "keyword": { - "replace": "置き換え", + "replace": "置換", "ftl_removal": "「すべてのネットワーク」タイムラインから除外", "keyword_policies": "キーワードポリシー", "is_replaced_by": "→", @@ -1219,7 +1243,8 @@ "tabs": { "limits": "制限", "instance": "インスタンス", - "frontends": "フロントエンド" + "frontends": "フロントエンド", + "emoji": "絵文字" }, "limits": { "arbitrary_limits": "変更可能な制限", @@ -1228,6 +1253,27 @@ "profile_fields": "追加情報欄の制限", "user_uploads": "プロフィール画像の制限", "users": "ユーザープロフィールの設定" + }, + "emoji": { + "create_pack": "パックを作成", + "delete_pack": "パックを削除", + "create": "作成", + "emoji_packs": "絵文字パック", + "remote_packs": "リモートのパック", + "emoji_pack": "絵文字パック", + "edit_pack": "パックを編集", + "homepage": "ホームページ", + "save": "保存", + "save_meta": "メタデータを保存", + "shortcode": "ショートコード", + "filename": "ファイル名", + "delete_confirm": "{0}を削除してもよろしいですか?", + "download_pack": "パックをダウンロード", + "downloading_pack": "{0}をダウンロード中", + "download": "ダウンロード", + "editing": "{0}を編集中", + "error": "エラー: {0}", + "delete": "削除" } }, "lists": { diff --git a/src/i18n/nan-TW.json b/src/i18n/nan-TW.json index 5e0df8ec..782a75f5 100644 --- a/src/i18n/nan-TW.json +++ b/src/i18n/nan-TW.json @@ -189,7 +189,8 @@ "mobile_notifications": "拍開通知(有無讀ê)", "mobile_notifications_close": "關掉通知", "announcements": "公告", - "search": "Tshuē" + "search": "Tshuē", + "mobile_notifications_mark_as_seen": "Lóng 標做有讀" }, "notifications": { "broken_favorite": "狀態毋知影,leh tshiau-tshuē……", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index b997701c..76119eda 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -11,7 +11,8 @@ "title": "Características", "who_to_follow": "Quem seguir", "upload_limit": "Limite de carregamento", - "pleroma_chat_messages": "Chat do Pleroma" + "pleroma_chat_messages": "Chat do Pleroma", + "shout": "Shoutbox" }, "finder": { "error_fetching_user": "Erro ao pesquisar utilizador", @@ -36,11 +37,27 @@ "error_retry": "Por favor, tenta novamente", "loading": "A carregar…", "dismiss": "Ignorar", - "role": - { + "role": { "moderator": "Moderador", "admin": "Admin" - } + }, + "undo": "Refazer", + "yes": "Sim", + "no": "Não", + "unpin": "Desafixar o item", + "scroll_to_top": "Rolar para o topo", + "flash_content": "Clique para mostrar conteúdo Flash usando o Ruffle (Experimental, talvez não funcione).", + "flash_security": "Note que isso pode ser potencialmente perigoso dado que o conteúdo Flash ainda é código arbitrário.", + "flash_fail": "Falha ao carregar conteúdo flash, veja o console para detalhes.", + "scope_in_timeline": { + "direct": "Direct", + "private": "Apenas-seguidores", + "public": "Público", + "unlisted": "Não-listado" + }, + "pin": "Fixar o item", + "generic_error_message": "Um erro ocorreu: {0}", + "never_show_again": "Não mostrar mais" }, "image_cropper": { "crop_picture": "Cortar imagem", @@ -64,11 +81,17 @@ "recovery_code": "Código de recuperação", "authentication_code": "Código de autenticação", "enter_two_factor_code": "Introduza o código de dois fatores", - "enter_recovery_code": "Introduza um código de recuperação" + "enter_recovery_code": "Introduza um código de recuperação", + "logout_confirm_title": "Confirmação de logoff", + "logout_confirm": "Você realmente quer sair?", + "logout_confirm_accept_button": "Sair", + "logout_confirm_cancel_button": "Não sair" }, "media_modal": { "previous": "Anterior", - "next": "Próximo" + "next": "Próximo", + "counter": "{current} / {total}", + "hide": "Fechar visualizador de mídia" }, "nav": { "about": "Sobre", @@ -88,7 +111,18 @@ "administration": "Administração", "chats": "Salas de Chat", "timelines": "Cronologias", - "bookmarks": "Itens Guardados" + "bookmarks": "Itens Guardados", + "home_timeline": "Timeline da home", + "lists": "Listas", + "edit_pinned": "Editar itens fixados", + "edit_nav_mobile": "Customizar barra de navegação", + "mobile_notifications_mark_as_seen": "Marcar todas como vistas", + "search_close": "Fechar barra de busca", + "mobile_notifications_close": "Fechar notificações", + "announcements": "Anúncios", + "edit_finish": "Edição finalizada", + "mobile_sidebar": "Alternar barra lateral móvel", + "mobile_notifications": "Abrir notificações (há notificações não lidas)" }, "notifications": { "broken_favorite": "Publicação desconhecida, a procurar…", @@ -102,7 +136,15 @@ "reacted_with": "reagiu com {0}", "migrated_to": "migrou para", "follow_request": "quer seguir-te", - "error": "Erro ao obter notificações: {0}" + "error": "Erro ao obter notificações: {0}", + "unread_announcements": "{num} anúncio não lido | {num} anúncios não lidos", + "unread_chats": "{num} mensagem não lida | {num} mensagens não lidas", + "configuration_tip": "Você pode customizar o que você deseja mostrar aqui em {theSettings}. {dismiss}", + "unread_follow_requests": "{num} novo pedido de seguidor | {num} novos pedidos de seguidores", + "configuration_tip_settings": "as configurações", + "configuration_tip_dismiss": "Não mostrar novamente", + "poll_ended": "enquete finalizada", + "submitted_report": "enviado um relatório" }, "post_status": { "new_status": "Publicar nova publicação", @@ -136,7 +178,14 @@ "media_description": "Descrição da multimédia", "media_description_error": "Falha ao atualizar ficheiro, tente novamente", "direct_warning_to_first_only": "Esta publicação só será visível para os utilizadores mencionados no início da mensagem.", - "direct_warning_to_all": "Esta publicação será visível para todos os utilizadores mencionados." + "direct_warning_to_all": "Esta publicação será visível para todos os utilizadores mencionados.", + "edit_status": "Editar status", + "reply_option": "Responder a esse status", + "quote_option": "Citar esse status", + "edit_remote_warning": "Outras instâncias remotas talvez não suportem edição e sejam incapazes de receber a última versão do seu post.", + "content_type_selection": "Formato do post", + "scope_notice_dismiss": "Fechar essa notificação", + "edit_unsupported_warning": "Pleroma não suporta editar menções ou enquetes." }, "registration": { "bio": "Biografia", @@ -156,8 +205,18 @@ "email_required": "não pode ser deixado em branco", "password_required": "não pode ser deixado em branco", "password_confirmation_required": "não pode ser deixado em branco", - "password_confirmation_match": "deve corresponder à palavra-passe" - } + "password_confirmation_match": "deve corresponder à palavra-passe", + "birthday_required": "não pode ser deixado em branco", + "birthday_min_age": "deve ser em ou antes de {date}" + }, + "birthday": "Data de nascimento:", + "reason": "Razão para registrar", + "register": "Registrar", + "reason_placeholder": "Essa instância aprova os registros manualmente.\nPermita ao administrador saber o porquê do seu registro.", + "birthday_optional": "Data de nascimento (opcional):", + "bio_optional": "Bio (opcional)", + "email_optional": "Email (opcional)", + "email_language": "Em qual linguagem você deseja receber emails do servidor?" }, "settings": { "app_name": "Nome da aplicação", @@ -523,7 +582,56 @@ "autohide_floating_post_button": "Automaticamente ocultar o botão 'Nova Publicação' (telemóvel)", "notification_visibility_moves": "Utilizador Migrado", "accent": "Destaque", - "pad_emoji": "Preencher espaços ao adicionar emojis do seletor" + "pad_emoji": "Preencher espaços ao adicionar emojis do seletor", + "confirm_dialogs_logout": "saindo", + "move_account_error": "Erro ao mover conta: {error}", + "confirm_dialogs_delete": "excluindo um status", + "save": "Salvar mudanças", + "lists_navigation": "Mostrar listas na navegação", + "email_language": "Linguagem para receber emails do servidor", + "account_backup_description": "Isso permite a você baixar um arquivo das informações da sua conta e os seus posts, mas eles ainda não podem ser importados para uma conta do Pleroma.", + "add_backup_error": "Erro ao adicionar um novo backup: {error}", + "confirm_dialogs": "Pedir por confirmação quando", + "confirm_dialogs_repeat": "repetindo um status", + "account_alias": "Apelidos de conta", + "account_alias_table_head": "Apelido", + "list_aliases_error": "Erro ao buscar por apelidos: {error}", + "hide_list_aliases_error_action": "Fechar", + "confirm_dialogs_deny_follow": "negando um seguidor", + "confirm_dialogs_approve_follow": "aprovando um seguidor", + "backup_running": "Esse backup está em andamento, {number} registro processado. | Esse backup está em progresso, {number} registros processados.", + "add_backup": "Criar um novo backup", + "added_backup": "Adicionado um novo backup.", + "backup_failed": "Esse backup falhou.", + "list_backups_error": "Erro ao buscar a lista de backup: {error}", + "move_account_notes": "Se você deseja mover a conta para outro lugar, você deve ir para sua conta de destino e adicionar um apelido apontando para cá.", + "add_alias_error": "Erro ao adicionar apelido: {error}", + "move_account": "Mover conta", + "actor_type": "Essa conta é:", + "actor_type_description": "Marcando a sua conta como um grupo irá fazer com que ela automaticamente repita os status que a mencionam.", + "actor_type_Person": "um usuário normal", + "actor_type_Service": "um bot", + "actor_type_Group": "um grupo", + "account_backup": "Backup da conta", + "confirm_dialogs_unfollow": "deixando de seguir usuário", + "confirm_dialogs_block": "bloqueando um usuário", + "confirm_dialogs_remove_follower": "removendo um seguidor", + "remove_alias": "Remover esse apelido", + "new_alias_target": "Adicionar um novo apelido (e.g. {example})", + "added_alias": "Apelido adicionado.", + "move_account_target": "Conta de destino (e.g. {example})", + "moved_account": "Conta movida.", + "remove_language": "Remover", + "primary_language": "Linguagem primária:", + "fallback_language": "Linguagem de reserva {index}:", + "add_language": "Adicionar linguagem de reserva", + "expert_mode": "Mostrar avançados", + "setting_changed": "As configurações são diferentes do padrão", + "setting_server_side": "Essas configurações estão atreladas ao seu perfil e afetarão todas as sessões e clientes", + "mention_links": "Links de menção", + "confirm_dialogs_mute": "mutando um usuário", + "backup_not_ready": "Esse backup não está pronto ainda.", + "remove_backup": "Remover" }, "timeline": { "collapse": "Esconder", @@ -699,7 +807,20 @@ "load_all": "A carregar todos os {emojiAmount} emojis", "load_all_hint": "Carregado o primeiro emoji {saneAmount}, carregar todos os emojis pode causar problemas de desempenho.", "keep_open": "Manter o seletor aberto", - "stickers": "Autocolantes" + "stickers": "Autocolantes", + "hide_custom_emoji": "Ocultar emojis customizados", + "unicode_groups": { + "symbols": "Símbolos", + "activities": "Atividades", + "animals-and-nature": "Animais & Natureza", + "people-and-body": "Pessoas & Corpo", + "smileys-and-emotion": "Sorriso & Emoção", + "travel-and-places": "Viagem & Lugares", + "food-and-drink": "Comida & Bebidas", + "objects": "Objetos" + }, + "regional_indicator": "Indicador regional {letter}", + "unpacked": "Emoji desempacotado" }, "polls": { "single_choice": "Escolha única", @@ -713,7 +834,9 @@ "expiry": "Tempo para finalizar sondagem", "multiple_choices": "Escolha múltipla", "type": "Tipo de sondagem", - "add_poll": "Adicionar Sondagem" + "add_poll": "Adicionar Sondagem", + "votes_count": "{count} voto | {count} votos", + "people_voted_count": "{count} pessoa votou | {count} pessoas votaram" }, "importer": { "error": "Ocorreu um erro ao importar este ficheiro.", @@ -737,7 +860,9 @@ "load_older": "Carregar interações mais antigas", "follows": "Novos seguidores", "favs_repeats": "Gostos e Partilhas", - "moves": "O utilizador migra" + "moves": "O utilizador migra", + "emoji_reactions": "Reações de Emoji", + "reports": "Relatórios" }, "errors": { "storage_unavailable": "O Pleroma não conseguiu aceder ao armazenamento do navegador. A sua sessão ou definições locais não serão armazenadas e poderá encontrar problemas inesperados. Tente ativar as cookies." @@ -828,5 +953,35 @@ "day_short": "{0}d", "days": "{0} dias", "day": "{0} dia" + }, + "report": { + "state_closed": "Fechar", + "reported_statuses": "Estado das denúncias:", + "reported_user": "Usuário denunciado:", + "state_resolved": "Resolvido", + "state": "Estado:", + "state_open": "Abrir", + "notes": "Notas:" + }, + "announcements": { + "start_time_display": "Inicia às {time}", + "post_form_header": "Enviar anúncio", + "post_placeholder": "Digite o conteúdo do seu anúncio aqui...", + "page_header": "Anúncios", + "title": "Anúncio", + "mark_as_read_action": "Marcar como lido", + "post_action": "Postar", + "post_error": "Erro: {error}", + "close_error": "Fechar", + "delete_action": "Apagar", + "start_time_prompt": "Tempo de início: ", + "end_time_prompt": "Tempo de término: ", + "all_day_prompt": "Esse é um evento para o dia todo", + "published_time_display": "Publicado às {time}", + "end_time_display": "Finaliza às {time}", + "edit_action": "Editar", + "submit_edit_action": "Enviar", + "cancel_edit_action": "Cancelar", + "inactive_message": "Esse anúncio está inativo" } } diff --git a/src/modules/config.js b/src/modules/config.js index abb57272..75b1a0c4 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -28,6 +28,7 @@ export const defaultState = { theme: undefined, customTheme: undefined, customThemeSource: undefined, + forceThemeRecompilation: false, hideISP: false, hideInstanceWallpaper: false, hideShoutbox: false, @@ -36,6 +37,7 @@ export const defaultState = { hideMutedThreads: undefined, // instance default hideWordFilteredPosts: undefined, // instance default muteBotStatuses: undefined, // instance default + muteSensitiveStatuses: undefined, // instance default collapseMessageWithSubject: undefined, // instance default padEmoji: true, hideAttachments: false, diff --git a/src/modules/instance.js b/src/modules/instance.js index 3022a41a..0a5c1ae7 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -71,6 +71,7 @@ const defaultState = { hideSitename: false, hideUserStats: false, muteBotStatuses: false, + muteSensitiveStatuses: false, modalOnRepeat: false, modalOnUnfollow: false, modalOnBlock: true, @@ -137,6 +138,7 @@ const defaultState = { suggestionsEnabled: false, suggestionsWeb: '', quotingAvailable: false, + groupActorAvailable: false, // Html stuff instanceSpecificPanelContent: '', @@ -376,7 +378,8 @@ const instance = { commit('setInstanceOption', { name: 'themeData', value: themeData }) // No need to apply theme if there's user theme already const { customTheme } = rootState.config - if (customTheme) return + const { themeApplied } = rootState.interface + if (customTheme || themeApplied) return // New theme presets don't have 'theme' property, they use 'source' const themeSource = themeData.source @@ -385,6 +388,7 @@ const instance = { } else { applyTheme(themeData.theme) } + commit('setThemeApplied') }) }, fetchEmoji ({ dispatch, state }) { diff --git a/src/modules/interface.js b/src/modules/interface.js index f8d62d87..39242b9d 100644 --- a/src/modules/interface.js +++ b/src/modules/interface.js @@ -1,4 +1,5 @@ const defaultState = { + themeApplied: false, settingsModalState: 'hidden', settingsModalLoadedUser: false, settingsModalLoadedAdmin: false, @@ -35,6 +36,9 @@ const interfaceMod = { state.settings.currentSaveStateNotice = { error: true, errorData: error } } }, + setThemeApplied (state) { + state.themeApplied = true + }, setNotificationPermission (state, permission) { state.notificationPermission = permission }, diff --git a/src/panel.scss b/src/panel.scss index 9471da11..833e4208 100644 --- 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/api/api.service.js b/src/services/api/api.service.js index b99dfa92..fa417193 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -116,6 +116,15 @@ const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions' const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends' const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install' +const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji' +const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import' +const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) => `/api/v1/pleroma/emoji/packs?page=${page}&page_size=${pageSize}` +const PLEROMA_EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}` +const PLEROMA_EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download' +const PLEROMA_EMOJI_PACKS_LS_REMOTE_URL = + (url, page, pageSize) => `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}` +const PLEROMA_EMOJI_UPDATE_FILE_URL = (name) => `/api/v1/pleroma/emoji/packs/files?name=${name}` + const oldfetch = window.fetch const fetch = (url, options) => { @@ -1800,6 +1809,90 @@ const fetchScrobbles = ({ accountId, limit = 1 }) => { }) } +const deleteEmojiPack = ({ name }) => { + return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'DELETE' }) +} + +const reloadEmoji = () => { + return fetch(PLEROMA_EMOJI_RELOAD_URL, { method: 'POST' }) +} + +const importEmojiFromFS = () => { + return fetch(PLEROMA_EMOJI_IMPORT_FS_URL) +} + +const createEmojiPack = ({ name }) => { + return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'POST' }) +} + +const listEmojiPacks = ({ page, pageSize }) => { + return fetch(PLEROMA_EMOJI_PACKS_URL(page, pageSize)) +} + +const listRemoteEmojiPacks = ({ instance, page, pageSize }) => { + if (!instance.startsWith('http')) { + instance = 'https://' + instance + } + + return fetch( + PLEROMA_EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize), + { + headers: { 'Content-Type': 'application/json' } + } + ) +} + +const downloadRemoteEmojiPack = ({ instance, packName, as }) => { + return fetch( + PLEROMA_EMOJI_PACKS_DL_REMOTE_URL, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: instance, name: packName, as + }) + } + ) +} + +const saveEmojiPackMetadata = ({ name, newData }) => { + return fetch( + PLEROMA_EMOJI_PACK_URL(name), + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ metadata: newData }) + } + ) +} + +const addNewEmojiFile = ({ packName, file, shortcode, filename }) => { + const data = new FormData() + if (filename.trim() !== '') { data.set('filename', filename) } + if (shortcode.trim() !== '') { data.set('shortcode', shortcode) } + data.set('file', file) + + return fetch( + PLEROMA_EMOJI_UPDATE_FILE_URL(packName), + { method: 'POST', body: data } + ) +} + +const updateEmojiFile = ({ packName, shortcode, newShortcode, newFilename, force }) => { + return fetch( + PLEROMA_EMOJI_UPDATE_FILE_URL(packName), + { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ shortcode, new_shortcode: newShortcode, new_filename: newFilename, force }) + } + ) +} + +const deleteEmojiFile = ({ packName, shortcode }) => { + return fetch(`${PLEROMA_EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, { method: 'DELETE' }) +} + const apiService = { verifyCredentials, fetchTimeline, @@ -1919,7 +2012,18 @@ const apiService = { fetchInstanceConfigDescriptions, fetchAvailableFrontends, pushInstanceDBConfig, - installFrontend + installFrontend, + importEmojiFromFS, + reloadEmoji, + listEmojiPacks, + createEmojiPack, + deleteEmojiPack, + saveEmojiPackMetadata, + addNewEmojiFile, + updateEmojiFile, + deleteEmojiFile, + listRemoteEmojiPacks, + downloadRemoteEmojiPack } export default apiService diff --git a/src/services/color_convert/color_convert.js b/src/services/color_convert/color_convert.js index 47d6344e..d92bbbe0 100644 --- 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/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 860f20c6..e41e7125 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -166,6 +166,7 @@ export const parseUser = (data) => { output.show_role = data.source.pleroma.show_role output.discoverable = data.source.pleroma.discoverable output.show_birthday = data.pleroma.show_birthday + output.actor_type = data.source.pleroma.actor_type } } diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index c91a86c8..6403693c 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -8,8 +8,11 @@ const mastoApiNotificationTypes = [ 'favourite', 'reblog', 'follow', + 'follow_request', 'move', + 'poll', 'pleroma:emoji_reaction', + 'pleroma:chat_mention', 'pleroma:report' ] diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 43fe3c73..369d2c9f 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -1,24 +1,151 @@ -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, getEngineChecksum } from '../theme_data/theme_data_3.service.js' +import { convertTheme2To3 } from '../theme_data/theme2_to_theme3.js' +import { getCssRules } from '../theme_data/css_utils.js' import { defaultState } from '../../modules/config.js' +import { chunk } from 'lodash' + +export const generateTheme = async (input, callbacks) => { + const { + onNewRule = (rule, isLazy) => {}, + onLazyFinished = () => {}, + onEagerFinished = () => {} + } = callbacks + + let extraRules + if (input.themeFileVersion === 1) { + extraRules = convertTheme2To3(input) + } else { + const { theme } = generatePreset(input) + extraRules = convertTheme2To3(theme) + } -export const applyTheme = (input) => { - const { rules } = generatePreset(input) - const head = document.head - const body = document.body - body.classList.add('hidden') + // Assuming that "worst case scenario background" is panel background since it's the most likely one + const themes3 = init(extraRules, extraRules[0].directives['--bg'].split('|')[1].trim()) + + getCssRules(themes3.eager, themes3.staticVars).forEach(rule => { + // Hacks to support multiple selectors on same component + if (rule.match(/::-webkit-scrollbar-button/)) { + const parts = rule.split(/[{}]/g) + const newRule = [ + parts[0], + ', ', + parts[0].replace(/button/, 'thumb'), + ', ', + parts[0].replace(/scrollbar-button/, 'resizer'), + ' {', + parts[1], + '}' + ].join('') + onNewRule(newRule, false) + } else { + onNewRule(rule, false) + } + }) + onEagerFinished() + + // Optimization - instead of processing all lazy rules in one go, process them in small chunks + // so that UI can do other things and be somewhat responsive while less important rules are being + // processed + let counter = 0 + const chunks = chunk(themes3.lazy, 200) + // let t0 = performance.now() + const processChunk = () => { + const chunk = chunks[counter] + Promise.all(chunk.map(x => x())).then(result => { + getCssRules(result.filter(x => x), themes3.staticVars).forEach(rule => { + if (rule.match(/\.modal-view/)) { + const parts = rule.split(/[{}]/g) + const newRule = [ + parts[0], + ', ', + parts[0].replace(/\.modal-view/, '#modal'), + ', ', + parts[0].replace(/\.modal-view/, '.shout-panel'), + ' {', + parts[1], + '}' + ].join('') + onNewRule(newRule, true) + } else { + onNewRule(rule, true) + } + }) + // const t1 = performance.now() + // console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms') + // t0 = t1 + counter += 1 + if (counter < chunks.length) { + setTimeout(processChunk, 0) + } else { + onLazyFinished() + } + }) + } - const styleEl = document.createElement('style') - head.appendChild(styleEl) - const styleSheet = styleEl.sheet + return { lazyProcessFunc: processChunk } +} - styleSheet.toString() - styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max') - styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max') - body.classList.remove('hidden') +export const tryLoadCache = () => { + const json = localStorage.getItem('pleroma-fe-theme-cache') + if (!json) return null + let cache + try { + cache = JSON.parse(json) + } catch (e) { + console.error('Failed to decode theme cache:', e) + return false + } + if (cache.engineChecksum === getEngineChecksum()) { + const styleSheet = new CSSStyleSheet() + const lazyStyleSheet = new CSSStyleSheet() + + cache.data[0].forEach(rule => styleSheet.insertRule(rule, 'index-max')) + cache.data[1].forEach(rule => lazyStyleSheet.insertRule(rule, 'index-max')) + + document.adoptedStyleSheets = [styleSheet, lazyStyleSheet] + + return true + } else { + console.warn('Engine checksum doesn\'t match, cache not usable, clearing') + localStorage.removeItem('pleroma-fe-theme-cache') + } +} + +export const applyTheme = async (input, onFinish = (data) => {}) => { + const styleSheet = new CSSStyleSheet() + const styleArray = [] + const lazyStyleSheet = new CSSStyleSheet() + const lazyStyleArray = [] + + const { lazyProcessFunc } = await generateTheme( + input, + { + onNewRule (rule, isLazy) { + if (isLazy) { + lazyStyleSheet.insertRule(rule, 'index-max') + lazyStyleArray.push(rule) + } else { + styleSheet.insertRule(rule, 'index-max') + styleArray.push(rule) + } + }, + onEagerFinished () { + document.adoptedStyleSheets = [styleSheet] + }, + onLazyFinished () { + document.adoptedStyleSheets = [styleSheet, lazyStyleSheet] + const cache = { engineChecksum: getEngineChecksum(), data: [styleArray, lazyStyleArray] } + onFinish(cache) + localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache)) + } + } + ) + + setTimeout(lazyProcessFunc, 0) + + return Promise.resolve() } const configColumns = ({ sidebarColumnWidth, contentColumnWidth, notifsColumnWidth, emojiReactionsScale }) => @@ -51,308 +178,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 +207,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 +233,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 new file mode 100644 index 00000000..a89eac3b --- /dev/null +++ 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 new file mode 100644 index 00000000..2d9dd0b5 --- /dev/null +++ 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 new file mode 100644 index 00000000..db612a5b --- /dev/null +++ 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 new file mode 100644 index 00000000..ffc5627c --- /dev/null +++ b/src/services/theme_data/theme2_keys.js @@ -0,0 +1,177 @@ +export default [ + 'bg', + 'wallpaper', + 'fg', + 'text', + 'underlay', + 'link', + 'accent', + 'faint', + 'faintLink', + 'postFaintLink', + + 'cBlue', + 'cRed', + 'cGreen', + 'cOrange', + + 'profileBg', + 'profileTint', + + 'highlight', + 'highlightLightText', + 'highlightPostLink', + 'highlightFaintText', + 'highlightFaintLink', + 'highlightPostFaintLink', + 'highlightText', + 'highlightLink', + 'highlightIcon', + + 'popover', + 'popoverLightText', + 'popoverPostLink', + 'popoverFaintText', + 'popoverFaintLink', + 'popoverPostFaintLink', + 'popoverText', + 'popoverLink', + 'popoverIcon', + + 'selectedPost', + 'selectedPostFaintText', + 'selectedPostLightText', + 'selectedPostPostLink', + 'selectedPostFaintLink', + 'selectedPostText', + 'selectedPostLink', + 'selectedPostIcon', + + 'selectedMenu', + 'selectedMenuLightText', + 'selectedMenuFaintText', + 'selectedMenuFaintLink', + 'selectedMenuText', + 'selectedMenuLink', + 'selectedMenuIcon', + + 'selectedMenuPopover', + 'selectedMenuPopoverLightText', + 'selectedMenuPopoverFaintText', + 'selectedMenuPopoverFaintLink', + 'selectedMenuPopoverText', + 'selectedMenuPopoverLink', + 'selectedMenuPopoverIcon', + + 'lightText', + + 'postLink', + + 'postGreentext', + + 'postCyantext', + + 'border', + + 'poll', + 'pollText', + + 'icon', + + // Foreground, + 'fgText', + 'fgLink', + + // Panel header, + 'panel', + 'panelText', + 'panelFaint', + 'panelLink', + + // Top bar, + 'topBar', + 'topBarText', + 'topBarLink', + + // Tabs, + 'tab', + 'tabText', + 'tabActiveText', + + // Buttons, + 'btn', + 'btnText', + 'btnPanelText', + 'btnTopBarText', + + // Buttons: pressed, + 'btnPressed', + 'btnPressedText', + 'btnPressedPanel', + 'btnPressedPanelText', + 'btnPressedTopBar', + 'btnPressedTopBarText', + + // Buttons: toggled, + 'btnToggled', + 'btnToggledText', + 'btnToggledPanelText', + 'btnToggledTopBarText', + + // Buttons: disabled, + 'btnDisabled', + 'btnDisabledText', + 'btnDisabledPanelText', + 'btnDisabledTopBarText', + + // Input fields, + 'input', + 'inputText', + 'inputPanelText', + 'inputTopbarText', + + 'alertError', + 'alertErrorText', + 'alertErrorPanelText', + + 'alertWarning', + 'alertWarningText', + 'alertWarningPanelText', + + 'alertSuccess', + 'alertSuccessText', + 'alertSuccessPanelText', + + 'alertNeutral', + 'alertNeutralText', + 'alertNeutralPanelText', + + 'alertPopupError', + 'alertPopupErrorText', + + 'alertPopupWarning', + 'alertPopupWarningText', + + 'alertPopupSuccess', + 'alertPopupSuccessText', + + 'alertPopupNeutral', + 'alertPopupNeutralText', + + 'badgeNeutral', + 'badgeNeutralText', + + 'badgeNotification', + 'badgeNotificationText', + + 'chatBg', + + 'chatMessageIncomingBg', + 'chatMessageIncomingText', + 'chatMessageIncomingLink', + 'chatMessageIncomingBorder', + 'chatMessageOutgoingBg', + 'chatMessageOutgoingText', + 'chatMessageOutgoingLink', + 'chatMessageOutgoingBorder' +] diff --git a/src/services/theme_data/theme2_to_theme3.js b/src/services/theme_data/theme2_to_theme3.js new file mode 100644 index 00000000..6ea12836 --- /dev/null +++ b/src/services/theme_data/theme2_to_theme3.js @@ -0,0 +1,536 @@ +import { convert } from 'chromatism' +import allKeys from './theme2_keys' + +// keys that are meant to be used globally, i.e. what's the rest of the theme is based upon. +export const basePaletteKeys = new Set([ + 'bg', + 'fg', + 'text', + 'link', + 'accent', + + 'cBlue', + 'cRed', + 'cGreen', + 'cOrange' +]) + +export const fontsKeys = new Set([ + 'interface', + 'input', + 'post', + 'postCode' +]) + +export const opacityKeys = new Set([ + 'alert', + 'alertPopup', + 'bg', + 'border', + 'btn', + 'faint', + 'input', + 'panel', + 'popover', + 'profileTint', + 'underlay' +]) + +export const shadowsKeys = new Set([ + 'panel', + 'topBar', + 'popup', + 'avatar', + 'avatarStatus', + 'panelHeader', + 'button', + 'buttonHover', + 'buttonPressed', + 'input' +]) + +export const radiiKeys = new Set([ + 'btn', + 'input', + 'checkbox', + 'panel', + 'avatar', + 'avatarAlt', + 'tooltip', + 'attachment', + 'chatMessage' +]) + +// Keys that are not available in editor and never meant to be edited +export const hiddenKeys = new Set([ + 'profileBg', + 'profileTint' +]) + +export const extendedBasePrefixes = [ + 'border', + 'icon', + 'highlight', + 'lightText', + + 'popover', + + 'panel', + 'topBar', + 'tab', + 'btn', + 'input', + 'selectedMenu', + + 'alert', + 'alertPopup', + 'badge', + + 'post', + 'selectedPost', // wrong nomenclature + 'poll', + + 'chatBg', + 'chatMessage' +] +export const nonComponentPrefixes = new Set([ + 'border', + 'icon', + 'highlight', + 'lightText', + 'chatBg' +]) + +export const extendedBaseKeys = Object.fromEntries( + extendedBasePrefixes.map(prefix => [ + prefix, + allKeys.filter(k => { + if (prefix === 'alert') { + return k.startsWith(prefix) && !k.startsWith('alertPopup') + } + return k.startsWith(prefix) + }) + ]) +) + +// Keysets that are only really used intermideately, i.e. to generate other colors +export const temporary = new Set([ + '', + 'highlight' +]) + +export const temporaryColors = {} + +export const convertTheme2To3 = (data) => { + data.colors.accent = data.colors.accent || data.colors.link + data.colors.link = data.colors.link || data.colors.accent + const generateRoot = () => { + const directives = {} + basePaletteKeys.forEach(key => { directives['--' + key] = 'color | ' + convert(data.colors[key]).hex }) + return { + component: 'Root', + directives + } + } + + const convertOpacity = () => { + const newRules = [] + Object.keys(data.opacity || {}).forEach(key => { + if (!opacityKeys.has(key) || data.opacity[key] === undefined) return null + const originalOpacity = data.opacity[key] + const rule = {} + + switch (key) { + case 'alert': + rule.component = 'Alert' + break + case 'alertPopup': + rule.component = 'Alert' + rule.parent = { component: 'Popover' } + break + case 'bg': + rule.component = 'Panel' + break + case 'border': + rule.component = 'Border' + break + case 'btn': + rule.component = 'Button' + break + case 'faint': + rule.component = 'Text' + rule.state = ['faint'] + break + case 'input': + rule.component = 'Input' + break + case 'panel': + rule.component = 'PanelHeader' + break + case 'popover': + rule.component = 'Popover' + break + case 'profileTint': + return null + case 'underlay': + rule.component = 'Underlay' + break + } + + switch (key) { + case 'alert': + case 'alertPopup': + case 'bg': + case 'btn': + case 'input': + case 'panel': + case 'popover': + case 'underlay': + rule.directives = { opacity: originalOpacity } + break + case 'faint': + case 'border': + rule.directives = { textOpacity: originalOpacity } + break + } + + newRules.push(rule) + + if (rule.component === 'Button') { + newRules.push({ ...rule, component: 'ScrollbarElement' }) + newRules.push({ ...rule, component: 'Tab' }) + newRules.push({ ...rule, component: 'Tab', state: ['active'], directives: { opacity: 0 } }) + } + if (rule.component === 'Panel') { + newRules.push({ ...rule, component: 'Post' }) + } + }) + return newRules + } + + const convertRadii = () => { + const newRules = [] + Object.keys(data.radii || {}).forEach(key => { + if (!radiiKeys.has(key) || data.radii[key] === undefined) return null + const originalRadius = data.radii[key] + const rule = {} + + switch (key) { + case 'btn': + rule.component = 'Button' + break + case 'tab': + rule.component = 'Tab' + break + case 'input': + rule.component = 'Input' + break + case 'checkbox': + rule.component = 'Input' + rule.variant = 'checkbox' + break + case 'panel': + rule.component = 'Panel' + break + case 'avatar': + rule.component = 'Avatar' + break + case 'avatarAlt': + rule.component = 'Avatar' + rule.variant = 'compact' + break + case 'tooltip': + rule.component = 'Popover' + break + case 'attachment': + rule.component = 'Attachment' + break + case 'ChatMessage': + rule.component = 'Button' + break + } + rule.directives = { + roundness: originalRadius + } + newRules.push(rule) + if (rule.component === 'Button') { + newRules.push({ ...rule, component: 'ScrollbarElement' }) + newRules.push({ ...rule, component: 'Tab' }) + } + }) + return newRules + } + + const convertFonts = () => { + const newRules = [] + Object.keys(data.fonts || {}).forEach(key => { + if (!fontsKeys.has(key)) return + const originalFont = data.fonts[key].family + const rule = {} + + switch (key) { + case 'interface': + case 'postCode': + rule.component = 'Root' + break + case 'input': + rule.component = 'Input' + break + case 'post': + rule.component = 'RichContent' + break + } + switch (key) { + case 'interface': + case 'input': + case 'post': + rule.directives = { '--font': 'generic | ' + originalFont } + break + case 'postCode': + rule.directives = { '--monoFont': 'generic | ' + originalFont } + newRules.push({ ...rule, component: 'RichContent' }) + break + } + newRules.push(rule) + }) + return newRules + } + const convertShadows = () => { + const newRules = [] + Object.keys(data.shadows || {}).forEach(key => { + if (!shadowsKeys.has(key)) return + const originalShadow = data.shadows[key] + const rule = {} + + switch (key) { + case 'panel': + rule.component = 'Panel' + break + case 'topBar': + rule.component = 'TopBar' + break + case 'popup': + rule.component = 'Popover' + break + case 'avatar': + rule.component = 'Avatar' + break + case 'avatarStatus': + rule.component = 'Avatar' + rule.parent = { component: 'Post' } + break + case 'panelHeader': + rule.component = 'PanelHeader' + break + case 'button': + rule.component = 'Button' + break + case 'buttonHover': + rule.component = 'Button' + rule.state = ['hover'] + break + case 'buttonPressed': + rule.component = 'Button' + rule.state = ['pressed'] + break + case 'input': + rule.component = 'Input' + break + } + rule.directives = { + shadow: originalShadow + } + newRules.push(rule) + if (key === 'topBar') { + newRules.push({ ...rule, component: 'PanelHeader', parent: { component: 'MobileDrawer' } }) + } + if (key === 'avatarStatus') { + newRules.push({ ...rule, parent: { component: 'Notification' } }) + } + if (key === 'buttonPressed') { + newRules.push({ ...rule, state: ['toggled'] }) + newRules.push({ ...rule, state: ['toggled', 'focus'] }) + newRules.push({ ...rule, state: ['pressed', 'focus'] }) + } + if (key === 'buttonHover') { + newRules.push({ ...rule, state: ['toggled', 'hover'] }) + newRules.push({ ...rule, state: ['pressed', 'hover'] }) + newRules.push({ ...rule, state: ['toggled', 'focus', 'hover'] }) + newRules.push({ ...rule, state: ['pressed', 'focus', 'hover'] }) + } + + if (rule.component === 'Button') { + newRules.push({ ...rule, component: 'ScrollbarElement' }) + newRules.push({ ...rule, component: 'Tab' }) + } + }) + return newRules + } + + const extendedRules = Object.entries(extendedBaseKeys).map(([prefix, keys]) => { + if (nonComponentPrefixes.has(prefix)) return null + const rule = {} + if (prefix === 'alertPopup') { + rule.component = 'Alert' + rule.parent = { component: 'Popover' } + } else if (prefix === 'selectedPost') { + rule.component = 'Post' + rule.state = ['selected'] + } else if (prefix === 'selectedMenu') { + rule.component = 'MenuItem' + rule.state = ['hover'] + } else if (prefix === 'chatMessageIncoming') { + rule.component = 'ChatMessage' + } else if (prefix === 'chatMessageOutgoing') { + rule.component = 'ChatMessage' + rule.variant = 'outgoing' + } else if (prefix === 'panel') { + rule.component = 'PanelHeader' + } else if (prefix === 'topBar') { + rule.component = 'TopBar' + } else if (prefix === 'chatMessage') { + rule.component = 'ChatMessage' + } else if (prefix === 'poll') { + rule.component = 'PollGraph' + } else if (prefix === 'btn') { + rule.component = 'Button' + } else { + rule.component = prefix[0].toUpperCase() + prefix.slice(1).toLowerCase() + } + return keys.map((key) => { + if (!data.colors[key]) return null + const leftoverKey = key.replace(prefix, '') + const parts = (leftoverKey || 'Bg').match(/[A-Z][a-z]*/g) + const last = parts.slice(-1)[0] + let newRule = { directives: {} } + let variantArray = [] + + switch (last) { + case 'Text': + case 'Faint': // typo + case 'Link': + case 'Icon': + case 'Greentext': + case 'Cyantext': + case 'Border': + newRule.parent = rule + newRule.directives.textColor = data.colors[key] + newRule.directives.textAuto = 'no-auto' + variantArray = parts.slice(0, -1) + break + default: + newRule = { ...rule, directives: {} } + newRule.directives.background = data.colors[key] + variantArray = parts + break + } + + if (last === 'Text' || last === 'Link') { + const secondLast = parts.slice(-2)[0] + if (secondLast === 'Light') { + return null // unsupported + } else if (secondLast === 'Faint') { + newRule.state = ['faint'] + variantArray = parts.slice(0, -2) + } + } + + switch (last) { + case 'Text': + case 'Link': + case 'Icon': + case 'Border': + newRule.component = last + break + case 'Greentext': + case 'Cyantext': + newRule.component = 'FunText' + newRule.variant = last.toLowerCase() + break + case 'Faint': + newRule.component = 'Text' + newRule.state = ['faint'] + break + } + + variantArray = variantArray.filter(x => x !== 'Bg') + + if (last === 'Link' && prefix === 'selectedPost') { + // selectedPost has typo - duplicate 'Post' + variantArray = variantArray.filter(x => x !== 'Post') + } + + if (prefix === 'popover' && variantArray[0] === 'Post') { + newRule.component = 'Post' + newRule.parent = { component: 'Popover' } + variantArray = variantArray.filter(x => x !== 'Post') + } + + if (prefix === 'selectedMenu' && variantArray[0] === 'Popover') { + newRule.parent = { component: 'Popover' } + variantArray = variantArray.filter(x => x !== 'Popover') + } + + switch (prefix) { + case 'btn': + case 'input': + case 'alert': { + const hasPanel = variantArray.find(x => x === 'Panel') + if (hasPanel) { + newRule.parent = { component: 'PanelHeader' } + variantArray = variantArray.filter(x => x !== 'Panel') + } + const hasTop = variantArray.find(x => x === 'Top') // TopBar + if (hasTop) { + newRule.parent = { component: 'TopBar' } + variantArray = variantArray.filter(x => x !== 'Top' && x !== 'Bar') + } + break + } + } + + if (variantArray.length > 0) { + if (prefix === 'btn') { + newRule.state = variantArray.map(x => x.toLowerCase()) + } else { + newRule.variant = variantArray[0].toLowerCase() + } + } + + if (newRule.component === 'Panel') { + return [newRule, { ...newRule, component: 'MobileDrawer' }] + } else if (newRule.component === 'Button') { + const rules = [ + newRule, + { ...newRule, component: 'Tab' }, + { ...newRule, component: 'ScrollbarElement' } + ] + if (newRule.state?.indexOf('toggled') >= 0) { + rules.push({ ...newRule, state: [...newRule.state, 'focused'] }) + rules.push({ ...newRule, state: [...newRule.state, 'hover'] }) + rules.push({ ...newRule, state: [...newRule.state, 'hover', 'focused'] }) + } + if (newRule.state?.indexOf('hover') >= 0) { + rules.push({ ...newRule, state: [...newRule.state, 'focused'] }) + } + return rules + } else if (newRule.component === 'Badge') { + if (newRule.variant === 'notification') { + return [newRule, { component: 'Root', directives: { '--badgeNotification': 'color | ' + newRule.directives.background } }] + } else if (newRule.variant === 'neutral') { + return [{ ...newRule, variant: 'normal' }] + } else { + return [newRule] + } + } else if (newRule.component === 'TopBar') { + return [newRule, { ...newRule, parent: { component: 'MobileDrawer' }, component: 'PanelHeader' }] + } else { + return [newRule] + } + }) + }) + + const flatExtRules = extendedRules.filter(x => x).reduce((acc, x) => [...acc, ...x], []).filter(x => x).reduce((acc, x) => [...acc, ...x], []) + + return [generateRoot(), ...convertShadows(), ...convertRadii(), ...convertOpacity(), ...convertFonts(), ...flatExtRules] +} diff --git a/src/services/theme_data/theme3_slot_functions.js b/src/services/theme_data/theme3_slot_functions.js new file mode 100644 index 00000000..074a88f0 --- /dev/null +++ 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 index b376ef4d..6e477674 100644 --- 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 new file mode 100644 index 00000000..15b4493e --- /dev/null +++ b/src/services/theme_data/theme_data_3.service.js @@ -0,0 +1,474 @@ +import { convert, brightness } from 'chromatism' +import sum from 'hash-sum' +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 engineChecksum = sum(components) + +const ruleToSelector = genericRuleToSelector(components) + +export const getEngineChecksum = () => engineChecksum + +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, + engineChecksum + } +} |
