Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
56 KB
Referenced Files
None
Subscribers
None
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

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)

Event Timeline