Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85826377
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
56 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss
index 3128381f5f..c6680b6b06 100644
--- a/src/components/color_input/color_input.scss
+++ b/src/components/color_input/color_input.scss
@@ -1,105 +1,115 @@
.color-input {
display: inline-flex;
+ flex-wrap: wrap;
+ max-width: 10em;
+ grid-template-columns: 1fr auto;
+ grid-template-rows: auto auto;
+ grid-template-areas:
+ "label checkbox"
+ "input input";
.label {
flex: 1 1 auto;
+ grid-area: label;
}
.opt {
+ grid-area: checkbox;
margin-right: 0.5em;
}
&-field.input {
- display: inline-flex;
- flex: 0 0 0;
- max-width: 9em;
+ flex: 1 1 10em;
+ max-width: 10em;
+ grid-area: input;
+ display: flex;
align-items: stretch;
input {
color: var(--text);
background: none;
border: none;
padding: 0;
margin: 0;
&.textColor {
flex: 1 0 3em;
min-width: 3em;
padding: 0;
}
}
.nativeColor {
cursor: pointer;
flex: 0 0 auto;
padding: 0;
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.2em 0.5em;
min-width: 2em;
align-self: stretch;
min-height: 1.1em;
border-radius: var(--roundness);
}
.invalidIndicator {
background: transparent;
box-sizing: border-box;
border: 2px solid var(--cRed);
}
.transparentIndicator {
// forgot to install counter-strike source, ooops
background-color: #f0f;
position: relative;
&::before,
&::after {
display: block;
content: "";
background-color: #000;
position: absolute;
height: 50%;
width: 50%;
}
&::after {
top: 0;
left: 0;
border-top-left-radius: var(--roundness);
}
&::before {
bottom: 0;
right: 0;
border-bottom-right-radius: var(--roundness);
}
}
&.disabled,
&:disabled {
.nativeColor input,
.computedIndicator,
.validIndicator,
.invalidIndicator,
.transparentIndicator {
/* stylelint-disable-next-line declaration-no-important */
opacity: 0.25 !important;
}
}
}
}
diff --git a/src/components/palette_editor/palette_editor.vue b/src/components/palette_editor/palette_editor.vue
index 252928b9e2..3040696d0f 100644
--- a/src/components/palette_editor/palette_editor.vue
+++ b/src/components/palette_editor/palette_editor.vue
@@ -1,195 +1,222 @@
<template>
<div
class="PaletteEditor"
- :class="{ '-compact': compact, '-apply': apply }"
+ :class="{ '-compact': compact, '-apply': apply, '-mobile': mobile }"
>
- <ColorInput
- v-for="key in paletteKeys"
- :key="key"
- :name="key"
- :model-value="props.modelValue[key]"
- :fallback="fallback(key)"
- :label="$t('settings.style.themes3.palette.' + key)"
- @update:model-value="value => updatePalette(key, value)"
- />
- <button
- class="btn button-default palette-import-button"
- @click="importPalette"
- >
- <FAIcon icon="file-import" />
- {{ $t('settings.style.themes3.palette.import') }}
- </button>
- <button
- class="btn button-default palette-export-button"
- @click="exportPalette"
- >
- <FAIcon icon="file-export" />
- {{ $t('settings.style.themes3.palette.export') }}
- </button>
- <button
- v-if="apply"
- class="btn button-default palette-apply-button"
- :disabled="disabled"
- :class="{ disabled }"
- @click="applyPalette"
- >
- {{ $t('settings.style.themes3.palette.apply') }}
- </button>
+ <div class="palette">
+ <ColorInput
+ v-for="key in paletteKeys"
+ :key="key"
+ :name="key"
+ :model-value="props.modelValue[key]"
+ :fallback="fallback(key)"
+ :label="$t('settings.style.themes3.palette.' + key)"
+ @update:model-value="value => updatePalette(key, value)"
+ />
+ </div>
+ <div class="buttons">
+ <button
+ class="btn button-default palette-import-button"
+ @click="importPalette"
+ >
+ <FAIcon icon="file-import" />
+ {{ $t('settings.style.themes3.palette.import') }}
+ </button>
+ <button
+ class="btn button-default palette-export-button"
+ @click="exportPalette"
+ >
+ <FAIcon icon="file-export" />
+ {{ $t('settings.style.themes3.palette.export') }}
+ </button>
+ </div>
+ <div class="buttons">
+ <button
+ v-if="apply"
+ class="btn button-default palette-apply-button"
+ :disabled="disabled"
+ :class="{ disabled }"
+ @click="applyPalette"
+ >
+ {{ $t('settings.style.themes3.palette.apply') }}
+ </button>
+ </div>
</div>
</template>
<script setup>
+import { computed } from 'vue'
+
import ColorInput from 'src/components/color_input/color_input.vue'
import {
newImporter,
newExporter
} from 'src/services/export_import/export_import.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faFileImport,
faFileExport
} from '@fortawesome/free-solid-svg-icons'
+import { useInterfaceStore } from 'src/stores/interface'
+
library.add(
faFileImport,
faFileExport
)
const paletteKeys = [
'bg',
'fg',
'text',
'link',
'accent',
'cRed',
'cBlue',
'cGreen',
'cOrange',
'wallpaper'
]
const props = defineProps(['modelValue', 'compact', 'apply', 'disabled'])
const emit = defineEmits(['update:modelValue', 'applyPalette'])
const getExportedObject = () => paletteKeys.reduce((acc, key) => {
const value = props.modelValue[key]
if (value == null) {
return acc
} else {
return { ...acc, [key]: props.modelValue[key] }
}
}, {})
const paletteExporter = newExporter({
filename: 'pleroma_palette',
extension: 'json',
getExportedObject
})
const paletteImporter = newImporter({
accept: '.json',
onImport (parsed) {
emit('update:modelValue', parsed)
}
})
const exportPalette = () => {
paletteExporter.exportData()
}
const importPalette = () => {
paletteImporter.importData()
}
const applyPalette = () => {
emit('applyPalette', getExportedObject())
}
+const mobile = computed(() => {
+ return useInterfaceStore().layoutType === 'mobile'
+})
+
const fallback = (key) => {
if (key === 'accent') {
return props.modelValue.link
}
if (key === 'link') {
return props.modelValue.accent
}
if (key.startsWith('extra')) {
return '#FF00FF'
}
if (key.startsWith('wallpaper')) {
return '#008080'
}
}
const updatePalette = (paletteKey, value) => {
emit('update:modelValue', {
...props.modelValue,
[paletteKey]: value
})
}
</script>
<style lang="scss">
.PaletteEditor {
- display: grid;
justify-content: space-around;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(5, 1fr) auto;
grid-gap: 0.5em;
align-items: baseline;
+ .buttons {
+ margin-top: 0.5em;
+ display: grid;
+ gap: 0.5em
+ }
+
+ .palette {
+ display: grid;
+ grid-template-rows: 1fr;
+ grid-auto-flow: row;
+ grid-auto-rows: auto;
+ grid-template-columns: repeat(auto-fill, 10em);
+ grid-gap: 0.5em;
+ margin-bottom: 0.5em;
+ }
+
.palette-import-button {
grid-column: 1 / span 2;
}
.palette-export-button {
grid-column: 3 / span 2;
}
.palette-apply-button {
grid-column: 1 / span 2;
}
.color-input.style-control {
margin: 0;
}
&.-compact {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(5, 1fr) auto;
.palette-import-button {
grid-column: 1;
}
.palette-export-button {
grid-column: 2;
}
&.-apply {
grid-template-rows: repeat(5, 1fr) auto auto;
.palette-apply-button {
grid-column: 1 / span 2;
}
}
+ }
- .-mobile & {
- grid-template-columns: 1fr;
- grid-template-rows: repeat(10, 1fr) auto;
-
- .palette-import-button {
- grid-column: 1;
+ &.-mobile {
+ &.-apply {
+ .palette-apply-button {
+ grid-column: 1 / span 2;
}
+ }
- .palette-export-button {
- grid-column: 1;
- }
+ .color-input {
+ display: grid;
+ gap: 0.5em;
- &.-apply {
- .palette-apply-button {
- grid-column: 1;
- }
+ label {
+ flex: 1;
}
}
}
}
</style>
diff --git a/src/components/settings_modal/helpers/vertical_tab_switcher.jsx b/src/components/settings_modal/helpers/vertical_tab_switcher.jsx
index ce5e3fa201..ea685576ff 100644
--- a/src/components/settings_modal/helpers/vertical_tab_switcher.jsx
+++ b/src/components/settings_modal/helpers/vertical_tab_switcher.jsx
@@ -1,212 +1,212 @@
// eslint-disable-next-line no-unused
import { h, Fragment } from 'vue'
import { mapState } from 'pinia'
import { throttle } from 'lodash'
import { mapState as mapPiniaState } from 'pinia'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
import './vertical_tab_switcher.scss'
import { useInterfaceStore } from 'src/stores/interface'
const findFirstUsable = (slots) => slots.findIndex(_ => _.props)
export default {
name: 'VerticalTabSwitcher',
props: {
renderOnlyFocused: {
required: false,
type: Boolean,
default: false
},
onSwitch: {
required: false,
type: Function,
default: undefined
},
activeTab: {
required: false,
type: String,
default: undefined
},
bodyScrollLock: {
required: false,
type: Boolean,
default: false
},
parentCollapsed: {
required: false,
type: Boolean,
default: null
}
},
data () {
return {
active: findFirstUsable(this.slots()),
resizeHandler: null,
navSide: 'tabs'
}
},
computed: {
activeIndex () {
// In case of controlled component
if (this.activeTab) {
return this.slots().findIndex(slot => slot && slot.props && this.activeTab === slot.props.key)
} else {
return this.active
}
},
isActive () {
return tabName => {
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
},
...mapPiniaState(useInterfaceStore, {
mobileLayout: store => store.layoutType === 'mobile'
}),
},
beforeUpdate () {
const currentSlot = this.slots()[this.active]
if (!currentSlot.props) {
this.active = findFirstUsable(this.slots())
}
},
methods: {
clickTab (index) {
return (e) => {
e.preventDefault()
this.setTab(index)
}
},
setTab (index) {
if (typeof this.onSwitch === 'function') {
this.onSwitch.call(null, this.slots()[index].key)
}
this.active = index
this.changeNavSide('content')
},
changeNavSide (side) {
if (this.navSide !== side) {
this.navSide = side
}
},
// DO NOT put it to computed, it doesn't work (caching?)
slots () {
if (this.$slots.default()[0].type === Fragment) {
return this.$slots.default()[0].children
}
return this.$slots.default()
}
},
render () {
const tabs = this.slots()
.map((slot, index) => {
const props = slot.props
if (!props) return
const classesTab = ['vertical-tab', 'menu-item']
if (this.activeIndex === index && useInterfaceStore().layoutType !== 'mobile') {
classesTab.push('-active')
}
return (
<button
disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
title={props.label}
>
{!props.icon ? '' : (<FAIcon class="tab-icon" size="1x" fixed-width icon={props.icon}/>)}
<span class="text">
{props.label}
</span>
</button>
)
})
const contents = this.slots().map((slot, index) => {
const props = slot.props
if (!props) return
const active = this.activeIndex === index
- const wrapperClasses = ['tab-content-wrapper', active ? '-active' : '-hidden' ]
let delayRender = slot.props['delay-render']
if (delayRender && active) {
slot.props['delay-render'] = false
delayRender = false
}
const renderSlot = (!delayRender && (!this.renderOnlyFocused || active))
? slot
: ''
const headerClasses = ['tab-content-label']
const header = (
<h2 class={headerClasses}>
<button
type="button"
onClick={() => this.changeNavSide('tabs')}
class="button-unstyled"
>
<FAIcon
size="lg"
class="back-button-icon"
icon="chevron-left"
/>
</button>
{props.label}
</h2>
)
+ const wrapperClasses = ['tab-content-wrapper', active ? '-active' : '-hidden' ]
const contentClasses = ['tab-content']
if (props['full-width']) {
contentClasses.push('-full-width')
}
if (props['full-height']) {
contentClasses.push('-full-height')
}
return (
<div class={wrapperClasses} >
<div class="tab-mobile-header">
{header}
</div>
<div class="tab-slot-wrapper">
<div class={contentClasses} >
{renderSlot}
</div>
</div>
</div>
)
})
const rootClasses = ['vertical-tab-switcher']
if (useInterfaceStore().layoutType === 'mobile') {
rootClasses.push('-mobile')
}
if (this.navSide === 'tabs') {
rootClasses.push('-nav-tabs')
} else {
rootClasses.push('-nav-contents')
}
return (
<div ref="root" class={ rootClasses.join(' ') }>
<div
class="tabs"
role="tablist"
ref="nav"
>
{tabs}
</div>
<div
role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock}
ref="contents"
>
{contents}
</div>
</div>
)
}
}
diff --git a/src/components/settings_modal/tabs/appearance_tab.scss b/src/components/settings_modal/tabs/appearance_tab.scss
index 58ec1e8aa6..e589c596f8 100644
--- a/src/components/settings_modal/tabs/appearance_tab.scss
+++ b/src/components/settings_modal/tabs/appearance_tab.scss
@@ -1,174 +1,172 @@
.appearance-tab {
h3 {
border: none
}
.palette,
.theme-notice {
padding: 0.5em;
- margin: 1em;
}
.theme-name {
font-weight: 900;
padding-bottom: 0.5em;
}
-
input[type="file"] {
padding: 0.25em;
height: auto;
}
.banner-background-preview {
max-width: 100%;
width: 300px;
position: relative;
img {
width: 100%;
}
}
.reset-button {
position: absolute;
top: 0.2em;
right: 0.2em;
border-radius: var(--roundness);
background-color: rgb(0 0 0 / 60%);
opacity: 0.7;
width: 1.5em;
height: 1.5em;
text-align: center;
line-height: 1.5em;
font-size: 1.5em;
cursor: pointer;
&:hover {
opacity: 1;
}
svg {
color: white;
}
}
.palettes-container {
height: 15em;
overflow: hidden auto;
scrollbar-gutter: stable;
border-radius: var(--roundness);
border: 1px solid var(--border);
- margin: -0.5em;
+ margin-bottom: 0.5em;
margin-top: 0;
}
.palettes {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 0.5em;
padding: 0.5em;
width: 100%;
h5 {
grid-column: 1 / span 2;
margin-bottom: 0;
}
}
.palette-entry {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
justify-content: space-between;
padding: 0 0.5em;
height: max-content;
.palette-label {
height: auto;
label {
text-align: center;
}
}
.palette-square {
flex: 0 0 auto;
display: inline-block;
min-width: 1em;
min-height: 1em;
}
}
.column-settings {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
}
.column-settings .size-label {
display: block;
margin-bottom: 0.5em;
margin-top: 0.5em;
}
.modal-view.-mobile & {
.palettes {
grid-template-columns: 1fr;
}
.palette-entry {
grid-column: 1;
justify-content: center;
}
.palette-label {
line-height: 1.5em;
margin-top: 0.5em;
}
}
.palette-preview {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-template-rows: 1em 1em;
margin: 0.5em 0;
}
.theme-list {
list-style: none;
display: flex;
flex-wrap: wrap;
margin: -0.5em 0;
height: 25em;
overflow: hidden auto;
scrollbar-gutter: stable;
border-radius: var(--roundness);
border: 1px solid var(--border);
padding: 0;
margin-bottom: 1em;
.theme-preview {
font-size: 1rem; // fix for firefox
width: 14rem;
display: flex;
flex-direction: column;
align-items: center;
margin: 0.5em;
&.placeholder {
opacity: 0.2;
}
.theme-preview-container {
pointer-events: none;
zoom: 0.5;
border: none;
border-radius: var(--roundness);
text-align: left;
}
}
}
}
diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue
index 868fb8bfac..158599a565 100644
--- a/src/components/settings_modal/tabs/appearance_tab.vue
+++ b/src/components/settings_modal/tabs/appearance_tab.vue
@@ -1,234 +1,232 @@
<template>
<div
class="appearance-tab"
:label="$t('settings.interface')"
icon="table-columns"
>
<div
class="setting-item"
:label="$t('settings.theme')"
icon="paintbrush"
>
<h3>{{ $t('settings.style.style_section') }}</h3>
<ul
ref="themeList"
class="theme-list"
>
<button
class="button-default theme-preview"
data-theme-key="stock"
:class="{ toggled: isStyleActive('stock'), disabled: switchInProgress }"
:disabled="switchInProgress"
@click="resetTheming"
>
<preview id="theme-preview-stock" />
<span class="theme-name">
{{ $t('settings.style.stock_theme_used') }}
<span class="alert neutral version">v3</span>
</span>
</button>
<button
v-if="isCustomThemeUsed"
disabled
class="button-default theme-preview toggled"
>
<preview />
<span class="theme-name">
{{ $t('settings.style.custom_theme_used') }}
<span class="alert neutral version">v2</span>
</span>
</button>
<button
v-if="isCustomStyleUsed"
disabled
class="button-default theme-preview toggled"
>
<preview />
<span class="theme-name">
{{ $t('settings.style.custom_style_used') }}
<span class="alert neutral version">v3</span>
</span>
</button>
<button
v-for="style in availableStyles"
:key="style.key"
:data-theme-key="style.key"
class="button-default theme-preview"
:class="{ toggled: isThemeActive(style.key), disabled: switchInProgress }"
:disabled="switchInProgress"
@click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)"
>
<preview :id="'theme-preview-' + style.key" />
<span class="theme-name">
{{ style.name }}
<span class="alert neutral version">{{ style.version }}</span>
</span>
</button>
</ul>
<div class="import-file-container">
<button
class="btn button-default"
:class="{ disabled: switchInProgress }"
:disabled="switchInProgress"
@click="importFile"
>
<FAIcon icon="folder-open" />
{{ $t('settings.style.themes3.editor.load_style') }}
</button>
- </div>
- <div class="setting-item">
<h4>{{ $t('settings.style.themes3.palette.label') }}</h4>
<div
v-if="customThemeVersion === 'v3'"
class="palettes-container"
>
<h5 v-if="stylePalettes?.length > 0">
{{ $t('settings.style.themes3.palette.style') }}
</h5>
<div class="palettes">
<button
v-for="p in stylePalettes || []"
:key="p.name"
class="btn button-default palette-entry"
:class="{ toggled: isPaletteActive(p.key), disabled: switchInProgress }"
:disabled="switchInProgress"
@click="() => setPalette(p.key, p)"
>
<div class="palette-label">
<label>
{{ p.name ?? $t('settings.style.themes3.palette.user') }}
</label>
</div>
<div class="palette-preview">
<span
v-for="c in palettesKeys"
:key="c"
class="palette-square"
:style="{ backgroundColor: p[c], border: '1px solid ' + (p[c] ?? 'var(--text)') }"
/>
</div>
</button>
<h5>{{ $t('settings.style.themes3.palette.bundled') }}</h5>
<button
v-for="p in bundledPalettes"
:key="p.name"
class="btn button-default palette-entry"
:class="{ toggled: isPaletteActive(p.key), disabled: switchInProgress }"
:disabled="switchInProgress"
@click="() => setPalette(p.key, p)"
>
<div class="palette-label">
<label>
{{ p.name }}
</label>
</div>
<div class="palette-preview">
<span
v-for="c in palettesKeys"
:key="c"
class="palette-square"
:style="{ backgroundColor: p[c], border: '1px solid ' + (p[c] ?? 'var(--text)') }"
/>
</div>
</button>
</div>
</div>
<div>
<template v-if="customThemeVersion === 'v3'">
<h5 v-if="expertLevel > 0">
{{ $t('settings.style.themes3.palette.user') }}
</h5>
<PaletteEditor
v-if="expertLevel > 0"
v-model="userPalette"
class="userPalette"
:compact="true"
:apply="true"
:disabled="switchInProgress"
@apply-palette="data => setPaletteCustom(data)"
/>
</template>
<template v-else-if="customThemeVersion === 'v2'">
<div class="alert neutral theme-notice unsupported-theme-v2">
{{ $t('settings.style.themes3.palette.v2_unsupported') }}
</div>
</template>
</div>
</div>
<h3>{{ $t('settings.background') }}</h3>
<div class="banner-background-preview">
<img :src="user.background_image">
<button
v-if="!isDefaultBackground"
class="button-unstyled reset-button"
:title="$t('settings.reset_profile_background')"
@click="resetBackground"
>
<FAIcon
icon="times"
type="button"
/>
</button>
</div>
<p>{{ $t('settings.set_new_background') }}</p>
<img
v-if="backgroundPreview"
class="banner-background-preview"
:src="backgroundPreview"
>
<div>
<input
type="file"
class="input"
@change="uploadFile('background', $event)"
>
</div>
<FAIcon
v-if="backgroundUploading"
class="uploading"
spin
icon="circle-notch"
/>
<button
v-else-if="backgroundPreview"
class="btn button-default"
@click="submitBackground(background)"
>
{{ $t('settings.save') }}
</button>
<h3>{{ $t('settings.visual_tweaks') }}</h3>
<div class="alert neutral theme-notice">
{{ $t("settings.style.visual_tweaks_section_note") }}
</div>
<ul class="setting-list">
<li>
<ChoiceSetting
id="forcedRoundness"
path="forcedRoundness"
:options="forcedRoundnessOptions"
>
{{ $t('settings.style.themes3.hacks.force_interface_roundness') }}
</ChoiceSetting>
</li>
<li>
<ChoiceSetting
id="underlayOverride"
path="theme3hacks.underlay"
:options="underlayOverrideModes"
>
{{ $t('settings.style.themes3.hacks.underlay_overrides') }}
</ChoiceSetting>
</li>
<li v-if="instanceWallpaperUsed">
<BooleanSetting path="hideInstanceWallpaper">
{{ $t('settings.hide_wallpaper') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>
</template>
<script src="./appearance_tab.js"></script>
<style lang="scss" src="./appearance_tab.scss"></style>
diff --git a/src/components/settings_modal/tabs/layout_tab.js b/src/components/settings_modal/tabs/layout_tab.js
index 355933a657..48b6eb9715 100644
--- a/src/components/settings_modal/tabs/layout_tab.js
+++ b/src/components/settings_modal/tabs/layout_tab.js
@@ -1,49 +1,50 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
const GeneralTab = {
props: {
parentCollapsed: {
required: true,
type: Boolean
}
},
data () {
return {
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.third_column_mode_${mode}`)
}))
}
},
components: {
BooleanSetting,
ChoiceSetting,
UnitSetting,
ProfileSettingIndicator
},
computed: {
postFormats () {
return this.$store.state.instance.postFormats || []
},
+ instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
columns () {
const mode = this.$store.getters.mergedConfig.thirdColumnMode
const notif = mode === 'none' ? [] : ['notifs']
if (this.$store.getters.mergedConfig.sidebarRight || mode === 'postform') {
return [...notif, 'content', 'sidebar']
} else {
return ['sidebar', 'content', ...notif]
}
},
...SharedComputedObject(),
}
}
export default GeneralTab
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.scss b/src/components/settings_modal/tabs/style_tab/style_tab.scss
index 545f6b117a..4f02a68c0f 100644
--- a/src/components/settings_modal/tabs/style_tab/style_tab.scss
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.scss
@@ -1,266 +1,289 @@
.StyleTab {
min-width: var(--themeEditorMinWidth, fit-content);
.style-control {
- display: flex;
- flex-wrap: wrap;
align-items: baseline;
margin-bottom: 0.5em;
.label {
margin-right: 0.5em;
flex: 1 1 0;
line-height: 2;
min-height: 2em;
}
&.suboption {
margin-left: 1em;
}
- .color-input {
- flex: 0 0 0;
- }
-
input,
select {
min-width: 3em;
margin: 0;
flex: 0;
&[type="number"] {
min-width: 9em;
&.-small {
min-width: 5em;
}
}
&[type="range"] {
flex: 1;
min-width: 9em;
align-self: center;
margin: 0 0.25em;
}
&[type="checkbox"] + i {
height: 1.1em;
align-self: center;
}
}
}
+ .setting-item.heading {
+ padding: 0;
+ }
+
.meta-preview {
display: grid;
grid-template:
- "meta meta preview preview"
- "meta meta preview preview"
- "meta meta preview preview"
- "meta meta preview preview";
- grid-gap: 0.5em;
- grid-template-columns: min-content min-content 6fr max-content;
+ "meta preview";
+ grid-gap: 1em;
+ grid-template-columns: min-content 6fr;
+
+ .theme-preview-container {
+ margin: 0;
+ }
ul.setting-list {
padding: 0;
margin: 0;
- display: grid;
- grid-template-rows: subgrid;
grid-area: meta;
> li {
margin: 0;
}
.meta-field {
margin: 0;
.setting-label {
display: inline-block;
margin-bottom: 0.5em;
}
}
}
#edited-style-preview {
grid-area: preview;
}
}
.setting-item {
padding-bottom: 0;
.btn {
padding: 0 0.5em;
}
&:not(:first-child) {
margin-top: 0.5em;
}
&:not(:last-child) {
margin-bottom: 0.5em;
}
}
.list-editor {
display: grid;
grid-template-areas:
"label editor"
"selector editor"
"movement editor";
grid-template-columns: 10em 1fr;
grid-template-rows: auto 1fr auto;
grid-gap: 0.5em;
.list-edit-area {
grid-area: editor;
+ align-items: baseline;
}
.list-select {
grid-area: selector;
margin: 0;
&-label {
font-weight: bold;
grid-area: label;
margin: 0;
align-self: baseline;
}
&-movement {
grid-area: movement;
margin: 0;
}
}
}
- .palette-editor {
- width: min-content;
-
- .list-edit-area {
- display: grid;
- align-self: baseline;
- grid-template-rows: subgrid;
- grid-template-columns: 1fr;
- }
-
- .palette-editor-single {
- grid-row: 2 / span 2;
- }
- }
-
.variables-editor {
.variable-selector {
display: grid;
grid-template-columns: auto 1fr auto 10em;
grid-template-rows: subgrid;
align-items: baseline;
grid-gap: 0 0.5em;
}
.list-edit-area {
display: grid;
grid-template-rows: subgrid;
}
.shadow-control {
grid-row: 2 / span 2;
}
}
.component-editor {
display: grid;
grid-template-columns: 6fr 3fr 4fr;
grid-template-rows: auto auto 1fr;
grid-gap: 0.5em;
grid-template-areas:
"component component variant"
"state state state"
"preview settings settings";
.component-selector {
grid-area: component;
align-self: center;
}
.component-selector,
.state-selector,
.variant-selector {
display: grid;
grid-template-columns: 1fr minmax(10em, 1fr);
grid-template-rows: auto;
grid-auto-flow: column;
grid-gap: 0.5em;
align-items: baseline;
> label:not(.Select) {
font-weight: bold;
justify-self: right;
}
}
.state-selector {
grid-area: state;
grid-template-columns: minmax(min-content, 7em) 1fr;
}
.variant-selector {
grid-area: variant;
}
.state-selector-list {
display: grid;
list-style: none;
grid-auto-flow: dense;
grid-template-columns: repeat(5, minmax(min-content, 1fr));
grid-auto-rows: 1fr;
grid-gap: 0.5em;
padding: 0;
margin: 0;
}
.preview-container {
--border: none;
--shadow: none;
--roundness: none;
grid-area: preview;
}
.component-settings {
grid-area: settings;
}
.editor-tab {
display: grid;
grid-template-columns: 1fr 2em;
grid-column-gap: 0.5em;
align-items: center;
grid-auto-rows: min-content;
grid-auto-flow: dense;
border-left: 1px solid var(--border);
border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border);
padding: 0.5em;
}
.shadow-tab {
grid-template-columns: 1fr;
justify-items: center;
}
}
+
+ .-mobile & {
+ .meta-preview {
+ grid-template:
+ "meta"
+ "preview"
+ }
+
+ .list-editor {
+ display: grid;
+ grid-template-areas:
+ "label"
+ "selector"
+ "movement"
+ "editor"
+ "editor"
+ "editor"
+ "editor";
+ grid-template-columns: 1fr;
+ grid-template-rows: auto 1fr auto;
+ }
+
+ .component-editor {
+ grid-template-columns: 1fr;
+ grid-template-rows: auto;
+ grid-template-areas:
+ "component"
+ "variant"
+ "state"
+ "preview"
+ "settings";
+ }
+
+ .variable-selector {
+ grid-template-columns: 1fr;
+ grid-template-rows: auto;
+ grid-auto-flow: row;
+ grid-gap: 0.5em;
+ }
+ }
}
.extra-content {
.style-actions-container {
width: 100%;
display: flex;
justify-content: end;
.style-actions {
display: grid;
grid-template-columns: repeat(4, minmax(7em, 1fr));
grid-gap: 0.25em;
}
}
}
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.vue b/src/components/settings_modal/tabs/style_tab/style_tab.vue
index a386822ee9..967d6983a2 100644
--- a/src/components/settings_modal/tabs/style_tab/style_tab.vue
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.vue
@@ -1,388 +1,390 @@
<script src="./style_tab.js">
</script>
<template>
<div class="StyleTab">
<div class="setting-item heading">
- <h2> {{ $t('settings.style.themes3.editor.title') }} </h2>
<div class="meta-preview">
<Preview id="edited-style-preview" />
<teleport
v-if="isActive"
to="#unscrolled-content"
>
<div class="style-actions-container">
<div class="style-actions">
<button
class="btn button-default button-new"
@click="clearStyle"
>
<FAIcon icon="arrows-rotate" />
{{ $t('settings.style.themes3.editor.reset_style') }}
</button>
<button
class="btn button-default button-load"
@click="importStyle"
>
<FAIcon icon="folder-open" />
{{ $t('settings.style.themes3.editor.load_style') }}
</button>
<button
class="btn button-default button-save"
@click="exportStyle"
>
<FAIcon icon="floppy-disk" />
{{ $t('settings.style.themes3.editor.save_style') }}
</button>
<button
class="btn button-default button-apply"
@click="applyStyle"
>
<FAIcon icon="check" />
{{ $t('settings.style.themes3.editor.apply_preview') }}
</button>
</div>
</div>
</teleport>
<ul class="setting-list style-metadata">
<li>
<StringSetting
v-model="name"
class="meta-field"
>
{{ $t('settings.style.themes3.editor.style_name') }}
</StringSetting>
</li>
<li>
<StringSetting
v-model="author"
class="meta-field"
>
{{ $t('settings.style.themes3.editor.style_author') }}
</StringSetting>
</li>
<li>
<StringSetting
v-model="license"
class="meta-field"
>
{{ $t('settings.style.themes3.editor.style_license') }}
</StringSetting>
</li>
<li>
<StringSetting
v-model="website"
class="meta-field"
>
{{ $t('settings.style.themes3.editor.style_website') }}
</StringSetting>
</li>
</ul>
</div>
</div>
<tab-switcher>
<div
key="component"
class="setting-item component-editor"
:label="$t('settings.style.themes3.editor.component_tab')"
+ :full-width="true"
>
<div class="component-selector">
<label for="component-selector">
{{ $t('settings.style.themes3.editor.component_selector') }}
{{ ' ' }}
</label>
<Select
id="component-selector"
v-model="selectedComponentKey"
>
<option
v-for="key in componentKeys"
:key="'component-' + key"
:value="key"
>
{{ componentsMap.get(key).name }}
</option>
</Select>
</div>
<div
v-if="selectedComponentVariants.length > 1"
class="variant-selector"
>
<label for="variant-selector">
{{ $t('settings.style.themes3.editor.variant_selector') }}
</label>
<Select
v-model="selectedVariant"
>
<option
v-for="variant in selectedComponentVariants"
:key="'component-variant-' + variant"
:value="variant"
>
{{ variant }}
</option>
</Select>
</div>
<div
v-if="selectedComponentStates.length > 0"
class="state-selector"
>
<label>
{{ $t('settings.style.themes3.editor.states_selector') }}
</label>
<ul
class="state-selector-list"
>
<li
v-for="state in selectedComponentStates"
:key="'component-state-' + state"
>
<Checkbox
:value="selectedState.has(state)"
@update:model-value="(v) => updateSelectedStates(state, v)"
>
{{ state }}
</Checkbox>
</li>
</ul>
</div>
<div class="preview-container">
<ComponentPreview
class="component-preview"
:show-text="componentHas('Text')"
:shadow-control="isShadowTabOpen"
:preview-class="previewClass"
:preview-style="editorHintStyle"
:preview-css="previewCss"
:disabled="!editedSubShadow && typeof editedShadow !== 'string'"
:shadow="editedSubShadow"
:no-color-control="true"
@update:shadow="({ axis, value }) => updateSubShadow(axis, value)"
/>
</div>
<tab-switcher
ref="tabSwitcher"
class="component-settings"
:on-switch="onTabSwitch"
>
<div
key="main"
class="editor-tab"
:label="$t('settings.style.themes3.editor.main_tab')"
>
<ColorInput
v-model="editedBackgroundColor"
name="component-background-color"
:fallback="computeColor(editedBackgroundColor) ?? previewColors.background"
:disabled="!isBackgroundColorPresent"
:label="$t('settings.style.themes3.editor.background')"
:hide-optional-checkbox="true"
/>
<Tooltip :text="$t('settings.style.themes3.editor.include_in_rule')">
<Checkbox v-model="isBackgroundColorPresent" />
</Tooltip>
<ColorInput
v-if="componentHas('Text')"
v-model="editedTextColor"
name="component-text-color"
:fallback="computeColor(editedTextColor) ?? previewColors.text"
:label="$t('settings.style.themes3.editor.text_color')"
:disabled="!isTextColorPresent"
:hide-optional-checkbox="true"
/>
<Tooltip
v-if="componentHas('Text')"
:text="$t('settings.style.themes3.editor.include_in_rule')"
>
<Checkbox v-model="isTextColorPresent" />
</Tooltip>
<div
v-if="componentHas('Text')"
class="style-control suboption"
>
<label
for="textAuto"
class="label"
:class="{ faint: !isTextAutoPresent }"
>
{{ $t('settings.style.themes3.editor.text_auto.label') }}
</label>
<Select
id="textAuto"
v-model="editedTextAuto"
:disabled="!isTextAutoPresent"
>
<option value="no-preserve">
{{ $t('settings.style.themes3.editor.text_auto.no-preserve') }}
</option>
<option value="no-auto">
{{ $t('settings.style.themes3.editor.text_auto.no-auto') }}
</option>
<option value="preserve">
{{ $t('settings.style.themes3.editor.text_auto.preserve') }}
</option>
</Select>
</div>
<Tooltip
v-if="componentHas('Text')"
:text="$t('settings.style.themes3.editor.include_in_rule')"
>
<Checkbox v-model="isTextAutoPresent" />
</Tooltip>
<div
v-if="componentHas('Text')"
class="style-control suboption"
>
<label class="label">
{{ $t('settings.style.themes3.editor.contrast') }}
</label>
<ContrastRatio
:show-ratio="true"
:contrast="contrast"
/>
</div>
<div v-if="componentHas('Text')" />
<ColorInput
v-if="componentHas('Link')"
v-model="editedLinkColor"
name="component-link-color"
:fallback="computeColor(editedLinkColor) ?? previewColors.link"
:label="$t('settings.style.themes3.editor.link_color')"
:disabled="!isLinkColorPresent"
:hide-optional-checkbox="true"
/>
<Tooltip
v-if="componentHas('Link')"
:text="$t('settings.style.themes3.editor.include_in_rule')"
>
<Checkbox v-model="isLinkColorPresent" />
</Tooltip>
<ColorInput
v-if="componentHas('Icon')"
v-model="editedIconColor"
name="component-icon-color"
:fallback="computeColor(editedIconColor) ?? previewColors.icon"
:label="$t('settings.style.themes3.editor.icon_color')"
:disabled="!isIconColorPresent"
:hide-optional-checkbox="true"
/>
<Tooltip
v-if="componentHas('Icon')"
:text="$t('settings.style.themes3.editor.include_in_rule')"
>
<Checkbox v-model="isIconColorPresent" />
</Tooltip>
<ColorInput
v-if="componentHas('Border')"
v-model="editedBorderColor"
name="component-border-color"
:fallback="computeColor(editedBorderColor) ?? previewColors.border"
:label="$t('settings.style.themes3.editor.border_color')"
:disabled="!isBorderColorPresent"
:hide-optional-checkbox="true"
/>
<Tooltip
v-if="componentHas('Border')"
:text="$t('settings.style.themes3.editor.include_in_rule')"
>
<Checkbox v-model="isBorderColorPresent" />
</Tooltip>
<OpacityInput
v-model="editedOpacity"
name="component-opacity"
:disabled="!isOpacityPresent"
:label="$t('settings.style.themes3.editor.opacity')"
/>
<Tooltip :text="$t('settings.style.themes3.editor.include_in_rule')">
<Checkbox v-model="isOpacityPresent" />
</Tooltip>
<RoundnessInput
v-model="editedRoundness"
name="component-roundness"
:disabled="!isRoundnessPresent"
:label="$t('settings.style.themes3.editor.roundness')"
/>
<Tooltip :text="$t('settings.style.themes3.editor.include_in_rule')">
<Checkbox v-model="isRoundnessPresent" />
</Tooltip>
</div>
<div
key="shadow"
class="editor-tab shadow-tab"
:label="$t('settings.style.themes3.editor.shadows_tab')"
>
<Checkbox
v-model="isShadowPresent"
class="style-control"
>
{{ $t('settings.style.themes3.editor.include_in_rule') }}
</checkbox>
<ShadowControl
v-model="editedShadow"
:disabled="!isShadowPresent"
:no-preview="true"
:compact="true"
:static-vars="staticVars"
@sub-shadow-selected="onSubShadow"
/>
</div>
</tab-switcher>
</div>
<div
key="palette"
:label="$t('settings.style.themes3.editor.palette_tab')"
class="setting-item list-editor palette-editor"
+ :full-width="true"
>
<label
class="list-select-label"
for="palette-selector"
>
{{ $t('settings.style.themes3.palette.label') }}
{{ ' ' }}
</label>
<Select
id="palette-selector"
v-model="selectedPaletteId"
class="list-select"
size="4"
>
<option
v-for="(p, index) in palettes"
:key="p.name"
:value="index"
>
{{ p.name }}
</option>
</Select>
<SelectMotion
class="list-select-movement"
:model-value="palettes"
:selected-id="selectedPaletteId"
:get-add-value="getNewPalette"
@update:model-value="onPalettesUpdate"
@update:selected-id="e => selectedPaletteId = e"
/>
<div class="list-edit-area">
<StringSetting
v-model="selectedPalette.name"
class="palette-name-input"
>
{{ $t('settings.style.themes3.palette.name_label') }}
</StringSetting>
<PaletteEditor
v-model="selectedPalette"
class="palette-editor-single"
/>
</div>
</div>
<VirtualDirectivesTab
key="variables"
:label="$t('settings.style.themes3.editor.variables_tab')"
:model-value="virtualDirectives"
@update:model-value="updateVirtualDirectives"
+ :full-width="true"
/>
</tab-switcher>
</div>
</template>
<style src="./style_tab.scss" lang="scss"></style>
diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx
index d46e14add0..af30084596 100644
--- a/src/components/tab_switcher/tab_switcher.jsx
+++ b/src/components/tab_switcher/tab_switcher.jsx
@@ -1,183 +1,186 @@
// eslint-disable-next-line no-unused
import { h, Fragment } from 'vue'
import { mapState } from 'pinia'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
import './tab_switcher.scss'
import { useInterfaceStore } from 'src/stores/interface'
const findFirstUsable = (slots) => slots.findIndex(_ => _.props)
export default {
name: 'TabSwitcher',
props: {
renderOnlyFocused: {
required: false,
type: Boolean,
default: false
},
onSwitch: {
required: false,
type: Function,
default: undefined
},
activeTab: {
required: false,
type: String,
default: undefined
},
scrollableTabs: {
required: false,
type: Boolean,
default: false
},
bodyScrollLock: {
required: false,
type: Boolean,
default: false
}
},
data () {
return {
active: findFirstUsable(this.slots())
}
},
computed: {
activeIndex () {
// In case of controlled component
if (this.activeTab) {
return this.slots().findIndex(slot => slot && slot.props && this.activeTab === slot.props.key)
} else {
return this.active
}
},
isActive () {
return tabName => {
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
}
},
beforeUpdate () {
const currentSlot = this.slots()[this.active]
if (!currentSlot.props) {
this.active = findFirstUsable(this.slots())
}
},
methods: {
clickTab (index) {
return (e) => {
e.preventDefault()
this.setTab(index)
}
},
// DO NOT put it to computed, it doesn't work (caching?)
slots () {
if (this.$slots.default()[0].type === Fragment) {
return this.$slots.default()[0].children
}
return this.$slots.default()
},
setTab (index) {
if (typeof this.onSwitch === 'function') {
this.onSwitch.call(null, this.slots()[index].key)
}
this.active = index
if (this.scrollableTabs) {
this.$refs.contents.scrollTop = 0
}
}
},
render () {
const tabs = this.slots()
.map((slot, index) => {
const props = slot.props
if (!props) return
const classesTab = ['tab']
const classesWrapper = ['tab-wrapper']
if (this.activeIndex === index) {
classesTab.push('active')
classesWrapper.push('active')
}
if (props.image) {
return (
<div class={classesWrapper.join(' ')}>
<button
disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
>
<img src={props.image} title={props['image-tooltip']}/>
{props.label ? '' : props.label}
</button>
</div>
)
}
return (
<div class={classesWrapper.join(' ')}>
<button
disabled={props.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
>
{!props.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={props.icon}/>)}
<span class="text">
{props.label}
</span>
</button>
</div>
)
})
const contents = this.slots().map((slot, index) => {
const props = slot.props
if (!props) return
const active = this.activeIndex === index
const classes = [ active ? 'active' : 'hidden' ]
- if (props.fullHeight) {
- classes.push('full-height')
+ if (props.fullHeight || props['full-height']) {
+ classes.push('-full-height')
+ }
+ if (props.fullWidth || props['full-width']) {
+ classes.push('-full-width')
}
let delayRender = slot.props['delay-render']
if (delayRender && active) {
slot.props['delay-render'] = false
delayRender = false
}
const renderSlot = (!delayRender && (!this.renderOnlyFocused || active))
? slot
: ''
return (
<div class={classes}>
{renderSlot}
</div>
)
})
return (
<div
class="tab-switcher top-tabs"
ref="root"
>
<div
class="tabs"
role="tablist"
ref="nav"
>
{tabs}
</div>
<div
role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock}
ref="content"
>
{contents}
</div>
</div>
)
}
}
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index 27214e87e2..f788ea30ef 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -1,152 +1,161 @@
/* stylelint-disable no-descending-specificity */
.tab-switcher {
display: flex;
.tab-icon {
margin: 0.2em auto;
display: block;
}
&.top-tabs {
flex-direction: column;
> .tabs {
width: 100%;
overflow: auto hidden;
padding-top: 5px;
flex-direction: row;
flex: 0 0 auto;
&::after,
&::before {
content: "";
flex: 1 1 auto;
border-bottom: 1px solid;
border-bottom-color: var(--border);
}
.tab-wrapper {
height: 2em;
&:not(.active)::after {
left: 0;
right: 0;
bottom: 0;
border-bottom: 1px solid;
border-bottom-color: var(--border);
}
}
.tab {
width: 100%;
min-width: 1px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
padding-bottom: 99px;
margin-bottom: calc(6px - 99px);
}
}
.contents.scrollable-tabs {
flex-basis: 0;
}
}
.contents {
flex: 1 0 auto;
min-height: 0;
.hidden {
display: none;
}
- .full-height:not(.hidden) {
+ .-full-height:not(.hidden) {
height: 100%;
display: flex;
flex-direction: column;
> *:not(.mobile-label) {
flex: 1;
}
}
+ .-full-width:not(.hidden) {
+ display: flex;
+ flex-direction: column;
+
+ > *:not(.mobile-label) {
+ width: auto;
+ }
+ }
+
&.scrollable-tabs {
overflow-y: auto;
}
}
.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;
&:not(.active) {
z-index: 4;
margin-top: 0.25em;
&:hover {
z-index: 6;
}
}
&.active {
background: transparent;
z-index: 5;
}
img {
max-height: 1.9em;
vertical-align: top;
margin-top: -0.3em;
}
}
.tabs {
display: flex;
position: relative;
box-sizing: border-box;
&::after,
&::before {
display: block;
flex: 1 1 auto;
}
}
.tab-wrapper {
position: relative;
display: flex;
flex: 0 0 auto;
&:not(.active) {
&::after {
content: "";
position: absolute;
z-index: 7;
}
}
}
.mobile-label {
padding-left: 0.3em;
padding-bottom: 0.25em;
margin-top: 0.5em;
margin-left: 0.2em;
margin-bottom: 0.25em;
border-bottom: 1px solid var(--border);
@media all and (width >= 800px) {
display: none;
}
}
}
/* stylelint-enable no-descending-specificity */
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 30, 6:56 AM (23 h, 33 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1799727
Default Alt Text
(56 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline