Merge branch 'themes3-grand-finale-maybe' into 'develop'
Themes 3 See merge request pleroma/pleroma-fe!1951
This commit is contained in:
commit
cbe9427123
1
changelog.d/custom.add
Normal file
1
changelog.d/custom.add
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Added support for fetching /{resource}.custom.ext to allow adding instance-specific themes without altering sourcetree
|
1
changelog.d/tabs.change
Normal file
1
changelog.d/tabs.change
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Tabs now have indentation for better visibility of which tab is currently active
|
1
changelog.d/themes3.add
Normal file
1
changelog.d/themes3.add
Normal file
|
@ -0,0 +1 @@
|
||||||
|
UI for making v3 themes and palettes, support for bundling v3 themes
|
|
@ -35,6 +35,7 @@
|
||||||
"hash-sum": "^2.0.0",
|
"hash-sum": "^2.0.0",
|
||||||
"js-cookie": "3.0.5",
|
"js-cookie": "3.0.5",
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
|
"pako": "^2.1.0",
|
||||||
"parse-link-header": "2.0.0",
|
"parse-link-header": "2.0.0",
|
||||||
"phoenix": "1.7.7",
|
"phoenix": "1.7.7",
|
||||||
"punycode.js": "2.3.0",
|
"punycode.js": "2.3.0",
|
||||||
|
|
|
@ -67,6 +67,9 @@ export default {
|
||||||
themeApplied () {
|
themeApplied () {
|
||||||
return this.$store.state.interface.themeApplied
|
return this.$store.state.interface.themeApplied
|
||||||
},
|
},
|
||||||
|
layoutModalClass () {
|
||||||
|
return '-' + this.layoutType
|
||||||
|
},
|
||||||
classes () {
|
classes () {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
<PostStatusModal />
|
<PostStatusModal />
|
||||||
<EditStatusModal v-if="editingAvailable" />
|
<EditStatusModal v-if="editingAvailable" />
|
||||||
<StatusHistoryModal v-if="editingAvailable" />
|
<StatusHistoryModal v-if="editingAvailable" />
|
||||||
<SettingsModal />
|
<SettingsModal :class="layoutModalClass"/>
|
||||||
<UpdateNotification />
|
<UpdateNotification />
|
||||||
<GlobalNoticeList />
|
<GlobalNoticeList />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -123,6 +123,8 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
copyInstanceOption('theme')
|
copyInstanceOption('theme')
|
||||||
|
copyInstanceOption('style')
|
||||||
|
copyInstanceOption('palette')
|
||||||
copyInstanceOption('nsfwCensorImage')
|
copyInstanceOption('nsfwCensorImage')
|
||||||
copyInstanceOption('background')
|
copyInstanceOption('background')
|
||||||
copyInstanceOption('hidePostStats')
|
copyInstanceOption('hidePostStats')
|
||||||
|
@ -351,7 +353,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
await setConfig({ store })
|
await setConfig({ store })
|
||||||
document.querySelector('#status').textContent = i18n.global.t('splash.theme')
|
document.querySelector('#status').textContent = i18n.global.t('splash.theme')
|
||||||
try {
|
try {
|
||||||
await store.dispatch('setTheme').catch((e) => { console.error('Error setting theme', e) })
|
await store.dispatch('applyTheme').catch((e) => { console.error('Error setting theme', e) })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
}
|
}
|
||||||
|
@ -391,6 +393,13 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
app.use(store)
|
app.use(store)
|
||||||
app.use(i18n)
|
app.use(i18n)
|
||||||
|
|
||||||
|
// Little thing to get out of invalid theme state
|
||||||
|
window.resetThemes = () => {
|
||||||
|
store.dispatch('resetThemeV3')
|
||||||
|
store.dispatch('resetThemeV3Palette')
|
||||||
|
store.dispatch('resetThemeV2')
|
||||||
|
}
|
||||||
|
|
||||||
app.use(vClickOutside)
|
app.use(vClickOutside)
|
||||||
app.use(VBodyScrollLock)
|
app.use(VBodyScrollLock)
|
||||||
app.use(VueVirtualScroller)
|
app.use(VueVirtualScroller)
|
||||||
|
|
|
@ -14,6 +14,10 @@ export default {
|
||||||
warning: '.warning',
|
warning: '.warning',
|
||||||
success: '.success'
|
success: '.success'
|
||||||
},
|
},
|
||||||
|
editor: {
|
||||||
|
border: 1,
|
||||||
|
aspect: '3 / 1'
|
||||||
|
},
|
||||||
defaultRules: [
|
defaultRules: [
|
||||||
{
|
{
|
||||||
directives: {
|
directives: {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'Attachment',
|
name: 'Attachment',
|
||||||
selector: '.Attachment',
|
selector: '.Attachment',
|
||||||
|
notEditable: true,
|
||||||
validInnerComponents: [
|
validInnerComponents: [
|
||||||
'Border',
|
'Border',
|
||||||
'ButtonUnstyled',
|
'ButtonUnstyled',
|
||||||
|
|
|
@ -5,7 +5,7 @@ export default {
|
||||||
defaultRules: [
|
defaultRules: [
|
||||||
{
|
{
|
||||||
directives: {
|
directives: {
|
||||||
textColor: '$mod(--parent, 10)',
|
textColor: '$mod(--parent 10)',
|
||||||
textAuto: 'no-auto'
|
textAuto: 'no-auto'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ export default {
|
||||||
// However, cascading still works, so resulting state will be result of merging of all relevant states/variants
|
// 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
|
// normal: '' // normal state is implicitly added, it is always included
|
||||||
toggled: '.toggled',
|
toggled: '.toggled',
|
||||||
pressed: ':active',
|
focused: ':focus-visible',
|
||||||
|
pressed: ':focus:active',
|
||||||
hover: ':hover:not(:disabled)',
|
hover: ':hover:not(:disabled)',
|
||||||
focused: ':focus-within',
|
|
||||||
disabled: ':disabled'
|
disabled: ':disabled'
|
||||||
},
|
},
|
||||||
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
|
// Variants are mutually exclusive, each component implicitly has "normal" variant, and all other variants inherit from it.
|
||||||
|
@ -22,6 +22,9 @@ export default {
|
||||||
// Overall the compuation difficulty is N*((1/6)M^3+M) where M is number of distinct states and N is number of variants.
|
// 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 (currently) is further multipled by number of places where component can exist.
|
||||||
},
|
},
|
||||||
|
editor: {
|
||||||
|
aspect: '2 / 1'
|
||||||
|
},
|
||||||
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
|
// This lists all other components that can possibly exist within one. Recursion is currently not supported (and probably won't be supported ever).
|
||||||
validInnerComponents: [
|
validInnerComponents: [
|
||||||
'Text',
|
'Text',
|
||||||
|
@ -32,10 +35,11 @@ export default {
|
||||||
{
|
{
|
||||||
component: 'Root',
|
component: 'Root',
|
||||||
directives: {
|
directives: {
|
||||||
'--defaultButtonHoverGlow': 'shadow | 0 0 4 --text',
|
'--buttonDefaultHoverGlow': 'shadow | 0 0 4 --text / 0.5',
|
||||||
'--defaultButtonShadow': 'shadow | 0 0 2 #000000',
|
'--buttonDefaultFocusGlow': 'shadow | 0 0 4 4 --link / 0.5',
|
||||||
'--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF, top, 0.2), $borderSide(#000000, bottom, 0.2)',
|
'--buttonDefaultShadow': 'shadow | 0 0 2 #000000',
|
||||||
'--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)'
|
'--buttonDefaultBevel': 'shadow | $borderSide(#FFFFFF top 0.2 2), $borderSide(#000000 bottom 0.2 2)',
|
||||||
|
'--buttonPressedBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2 2), $borderSide(#000000 top 0.2 2)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -43,47 +47,60 @@ export default {
|
||||||
// like within it
|
// like within it
|
||||||
directives: {
|
directives: {
|
||||||
background: '--fg',
|
background: '--fg',
|
||||||
shadow: ['--defaultButtonShadow', '--defaultButtonBevel'],
|
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
|
||||||
roundness: 3
|
roundness: 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: ['hover'],
|
state: ['hover'],
|
||||||
directives: {
|
directives: {
|
||||||
shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel']
|
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: ['focused'],
|
||||||
|
directives: {
|
||||||
|
shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: ['pressed'],
|
state: ['pressed'],
|
||||||
directives: {
|
directives: {
|
||||||
shadow: ['--defaultButtonShadow', '--pressedButtonBevel']
|
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: ['hover', 'pressed'],
|
state: ['pressed', 'hover'],
|
||||||
directives: {
|
directives: {
|
||||||
shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel']
|
shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: ['toggled'],
|
state: ['toggled'],
|
||||||
directives: {
|
directives: {
|
||||||
background: '--inheritedBackground,-14.2',
|
background: '--inheritedBackground,-14.2',
|
||||||
shadow: ['--defaultButtonShadow', '--pressedButtonBevel']
|
shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: ['toggled', 'hover'],
|
state: ['toggled', 'hover'],
|
||||||
directives: {
|
directives: {
|
||||||
background: '--inheritedBackground,-14.2',
|
background: '--inheritedBackground,-14.2',
|
||||||
shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel']
|
shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: ['toggled', 'disabled'],
|
||||||
|
directives: {
|
||||||
|
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||||
|
shadow: ['--buttonPressedBevel']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: ['disabled'],
|
state: ['disabled'],
|
||||||
directives: {
|
directives: {
|
||||||
background: '$blend(--inheritedBackground, 0.25, --parent)',
|
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||||
shadow: ['--defaultButtonBevel']
|
shadow: ['--buttonDefaultBevel']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'ButtonUnstyled',
|
name: 'ButtonUnstyled',
|
||||||
selector: '.button-unstyled',
|
selector: '.button-unstyled',
|
||||||
|
notEditable: true,
|
||||||
states: {
|
states: {
|
||||||
toggled: '.toggled',
|
toggled: '.toggled',
|
||||||
disabled: ':disabled',
|
disabled: ':disabled',
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.opt {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
&-field.input {
|
&-field.input {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex: 0 0 0;
|
flex: 0 0 0;
|
||||||
|
|
|
@ -11,11 +11,11 @@
|
||||||
{{ label }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="typeof fallback !== 'undefined' && showOptionalTickbox"
|
v-if="typeof fallback !== 'undefined' && showOptionalCheckbox && !hideOptionalCheckbox"
|
||||||
:model-value="present"
|
:model-value="present"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
class="opt"
|
class="opt"
|
||||||
@update:modelValue="update(typeof modelValue === 'undefined' ? fallback : undefined)"
|
@update:modelValue="updateValue(typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="input color-input-field"
|
class="input color-input-field"
|
||||||
|
@ -112,10 +112,16 @@ export default {
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
// Show "optional" tickbox, for when value might become mandatory
|
// Show "optional" tickbox, for when value might become mandatory
|
||||||
showOptionalTickbox: {
|
showOptionalCheckbox: {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
// Force "optional" tickbox to hide
|
||||||
|
hideOptionalCheckbox: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
|
@ -130,7 +136,7 @@ export default {
|
||||||
return this.modelValue === 'transparent'
|
return this.modelValue === 'transparent'
|
||||||
},
|
},
|
||||||
computedColor () {
|
computedColor () {
|
||||||
return this.modelValue && this.modelValue.startsWith('--')
|
return this.modelValue && (this.modelValue.startsWith('--') || this.modelValue.startsWith('$'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -1,88 +1,190 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
|
||||||
class="ComponentPreview"
|
|
||||||
:class="{ '-shadow-controls': shadowControl }"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="header"
|
|
||||||
v-show="shadowControl"
|
|
||||||
:class="{ faint: disabled }"
|
|
||||||
>
|
|
||||||
{{ $t('settings.style.shadows.offset') }}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-show="shadowControl"
|
|
||||||
:value="shadow?.y"
|
|
||||||
:disabled="disabled"
|
|
||||||
:class="{ disabled }"
|
|
||||||
class="input input-number y-shift-number"
|
|
||||||
type="number"
|
|
||||||
@input="e => updateProperty('y', e.target.value)"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-show="shadowControl"
|
|
||||||
:value="shadow?.y"
|
|
||||||
:disabled="disabled"
|
|
||||||
:class="{ disabled }"
|
|
||||||
class="input input-range y-shift-slider"
|
|
||||||
type="range"
|
|
||||||
max="20"
|
|
||||||
min="-20"
|
|
||||||
@input="e => updateProperty('y', e.target.value)"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="preview-window"
|
class="ComponentPreview"
|
||||||
:class="{ '-light-grid': lightGrid }"
|
:class="{ '-shadow-controls': shadowControl }"
|
||||||
>
|
>
|
||||||
<div
|
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||||
class="preview-block"
|
<component
|
||||||
:style="previewStyle"
|
:is="'style'"
|
||||||
|
v-html="previewCss"
|
||||||
/>
|
/>
|
||||||
|
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||||
|
<label
|
||||||
|
v-show="shadowControl"
|
||||||
|
role="heading"
|
||||||
|
class="header"
|
||||||
|
:class="{ faint: disabled }"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.shadows.offset') }}
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-show="shadowControl && !hideControls"
|
||||||
|
class="x-shift-number"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.shadows.offset-x') }}
|
||||||
|
<input
|
||||||
|
:value="shadow?.x"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="{ disabled }"
|
||||||
|
class="input input-number"
|
||||||
|
type="number"
|
||||||
|
@input="e => updateProperty('x', e.target.value)"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
class="y-shift-number"
|
||||||
|
v-show="shadowControl && !hideControls"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.shadows.offset-y') }}
|
||||||
|
<input
|
||||||
|
:value="shadow?.y"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="{ disabled }"
|
||||||
|
class="input input-number"
|
||||||
|
type="number"
|
||||||
|
@input="e => updateProperty('y', e.target.value)"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-show="shadowControl && !hideControls"
|
||||||
|
:value="shadow?.x"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="{ disabled }"
|
||||||
|
class="input input-range x-shift-slider"
|
||||||
|
type="range"
|
||||||
|
max="20"
|
||||||
|
min="-20"
|
||||||
|
@input="e => updateProperty('x', e.target.value)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-show="shadowControl && !hideControls"
|
||||||
|
:value="shadow?.y"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="{ disabled }"
|
||||||
|
class="input input-range y-shift-slider"
|
||||||
|
type="range"
|
||||||
|
max="20"
|
||||||
|
min="-20"
|
||||||
|
@input="e => updateProperty('y', e.target.value)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="preview-window"
|
||||||
|
:class="{ '-light-grid': lightGrid }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="preview-block"
|
||||||
|
:class="previewClass"
|
||||||
|
:style="style"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.themes3.editor.test_string') }}
|
||||||
|
</div>
|
||||||
|
<div v-if="invalid" class="invalid-container">
|
||||||
|
<div class="alert error invalid-label">
|
||||||
|
{{ $t('settings.style.themes3.editor.invalid') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="assists">
|
||||||
|
<Checkbox
|
||||||
|
v-model="lightGrid"
|
||||||
|
name="lightGrid"
|
||||||
|
class="input-light-grid"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.shadows.light_grid') }}
|
||||||
|
</Checkbox>
|
||||||
|
<div class="style-control">
|
||||||
|
<label class="label">
|
||||||
|
{{ $t('settings.style.shadows.zoom') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="zoom"
|
||||||
|
class="input input-number y-shift-number"
|
||||||
|
type="number"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<ColorInput
|
||||||
|
v-if="!noColorControl"
|
||||||
|
class="input-color-input"
|
||||||
|
v-model="colorOverride"
|
||||||
|
fallback="#606060"
|
||||||
|
:label="$t('settings.style.shadows.color_override')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
|
||||||
v-show="shadowControl"
|
|
||||||
:value="shadow?.x"
|
|
||||||
:disabled="disabled"
|
|
||||||
:class="{ disabled }"
|
|
||||||
class="input input-number x-shift-number"
|
|
||||||
type="number"
|
|
||||||
@input="e => updateProperty('x', e.target.value)"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-show="shadowControl"
|
|
||||||
:value="shadow?.x"
|
|
||||||
:disabled="disabled"
|
|
||||||
:class="{ disabled }"
|
|
||||||
class="input input-range x-shift-slider"
|
|
||||||
type="range"
|
|
||||||
max="20"
|
|
||||||
min="-20"
|
|
||||||
@input="e => updateProperty('x', e.target.value)"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
id="lightGrid"
|
|
||||||
v-model="lightGrid"
|
|
||||||
:disabled="shadow == null"
|
|
||||||
name="lightGrid"
|
|
||||||
class="input-light-grid"
|
|
||||||
>
|
|
||||||
{{ $t('settings.style.shadows.light_grid') }}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
ColorInput
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
'shadow',
|
||||||
|
'shadowControl',
|
||||||
|
'previewClass',
|
||||||
|
'previewStyle',
|
||||||
|
'previewCss',
|
||||||
|
'disabled',
|
||||||
|
'invalid',
|
||||||
|
'noColorControl'
|
||||||
|
],
|
||||||
|
emits: ['update:shadow'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
colorOverride: undefined,
|
||||||
|
lightGrid: false,
|
||||||
|
zoom: 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
style () {
|
||||||
|
const result = [
|
||||||
|
this.previewStyle,
|
||||||
|
`zoom: ${this.zoom / 100}`
|
||||||
|
]
|
||||||
|
if (this.colorOverride) result.push(`--background: ${this.colorOverride}`)
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
hideControls () {
|
||||||
|
return typeof this.shadow === 'string'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateProperty (axis, value) {
|
||||||
|
this.$emit('update:shadow', { axis, value: Number(value) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.ComponentPreview {
|
.ComponentPreview {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 3em 1fr 3em;
|
grid-template-columns: 1em 1fr 1fr 1em;
|
||||||
grid-template-rows: 2em 1fr 2em;
|
grid-template-rows: 2em 1fr 1fr 1fr 1em 2em max-content;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
". header y-num "
|
"header header header header "
|
||||||
". preview y-slide"
|
"preview preview preview y-slide"
|
||||||
"x-num x-slide . "
|
"preview preview preview y-slide"
|
||||||
"options options options";
|
"preview preview preview y-slide"
|
||||||
|
"x-slide x-slide x-slide . "
|
||||||
|
"x-num x-num y-num y-num "
|
||||||
|
"assists assists assists assists";
|
||||||
grid-gap: 0.5em;
|
grid-gap: 0.5em;
|
||||||
|
|
||||||
|
&:not(.-shadow-controls) {
|
||||||
|
grid-template-areas:
|
||||||
|
"header header header header "
|
||||||
|
"preview preview preview y-slide"
|
||||||
|
"preview preview preview y-slide"
|
||||||
|
"preview preview preview y-slide"
|
||||||
|
"assists assists assists assists";
|
||||||
|
grid-template-rows: 2em 1fr 1fr 1fr max-content;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
|
@ -90,8 +192,31 @@
|
||||||
line-height: 2;
|
line-height: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invalid-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
justify-items: center;
|
||||||
|
background-color: rgba(100 0 0 / 50%);
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.assists {
|
||||||
|
grid-area: assists;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: rows;
|
||||||
|
grid-auto-rows: 2em;
|
||||||
|
grid-gap: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.input-light-grid {
|
.input-light-grid {
|
||||||
grid-area: options;
|
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +226,19 @@
|
||||||
|
|
||||||
.x-shift-number {
|
.x-shift-number {
|
||||||
grid-area: x-num;
|
grid-area: x-num;
|
||||||
|
justify-self: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.y-shift-number {
|
||||||
|
grid-area: y-num;
|
||||||
|
justify-self: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-shift-number,
|
||||||
|
.y-shift-number {
|
||||||
|
input {
|
||||||
|
max-width: 4em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.x-shift-slider {
|
.x-shift-slider {
|
||||||
|
@ -110,10 +248,6 @@
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.y-shift-number {
|
|
||||||
grid-area: y-num;
|
|
||||||
}
|
|
||||||
|
|
||||||
.y-shift-slider {
|
.y-shift-slider {
|
||||||
grid-area: y-slide;
|
grid-area: y-slide;
|
||||||
writing-mode: vertical-lr;
|
writing-mode: vertical-lr;
|
||||||
|
@ -139,6 +273,7 @@
|
||||||
--__grid-color2-disabled: rgba(255 255 255 / 20%);
|
--__grid-color2-disabled: rgba(255 255 255 / 20%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
position: relative;
|
||||||
grid-area: preview;
|
grid-area: preview;
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -183,30 +318,3 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: [
|
|
||||||
'shadow',
|
|
||||||
'shadowControl',
|
|
||||||
'previewClass',
|
|
||||||
'previewStyle',
|
|
||||||
'disabled'
|
|
||||||
],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
lightGrid: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['update:shadow'],
|
|
||||||
components: {
|
|
||||||
Checkbox
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateProperty (axis, value) {
|
|
||||||
this.$emit('update:shadow', { axis, value })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -3,39 +3,44 @@
|
||||||
v-if="contrast"
|
v-if="contrast"
|
||||||
class="contrast-ratio"
|
class="contrast-ratio"
|
||||||
>
|
>
|
||||||
<span
|
<span v-if="showRatio">
|
||||||
:title="hint"
|
{{ contrast.text }}
|
||||||
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
:text="hint"
|
||||||
class="rating"
|
class="rating"
|
||||||
>
|
>
|
||||||
<span v-if="contrast.aaa">
|
<span v-if="contrast.aaa">
|
||||||
<FAIcon icon="thumbs-up" />
|
<FAIcon icon="thumbs-up" :size="showRatio ? 'lg' : ''" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!contrast.aaa && contrast.aa">
|
<span v-if="!contrast.aaa && contrast.aa">
|
||||||
<FAIcon icon="adjust" />
|
<FAIcon icon="adjust" :size="showRatio ? 'lg' : ''" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!contrast.aaa && !contrast.aa">
|
<span v-if="!contrast.aaa && !contrast.aa">
|
||||||
<FAIcon icon="exclamation-triangle" />
|
<FAIcon icon="exclamation-triangle" :size="showRatio ? 'lg' : ''" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</Tooltip>
|
||||||
<span
|
<Tooltip
|
||||||
v-if="contrast && large"
|
v-if="contrast && large"
|
||||||
|
:text="hint_18pt"
|
||||||
class="rating"
|
class="rating"
|
||||||
:title="hint_18pt"
|
|
||||||
>
|
>
|
||||||
<span v-if="contrast.laaa">
|
<span v-if="contrast.laaa">
|
||||||
<FAIcon icon="thumbs-up" />
|
<FAIcon icon="thumbs-up" :size="showRatio ? 'large' : ''" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!contrast.laaa && contrast.laa">
|
<span v-if="!contrast.laaa && contrast.laa">
|
||||||
<FAIcon icon="adjust" />
|
<FAIcon icon="adjust" :size="showRatio ? 'lg' : ''" />
|
||||||
</span>
|
</span>
|
||||||
<span v-if="!contrast.laaa && !contrast.laa">
|
<span v-if="!contrast.laaa && !contrast.laa">
|
||||||
<FAIcon icon="exclamation-triangle" />
|
<FAIcon icon="exclamation-triangle" :size="showRatio ? 'lg' : ''" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Tooltip from 'src/components/tooltip/tooltip.vue'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faAdjust,
|
faAdjust,
|
||||||
|
@ -62,8 +67,16 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
showRatio: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
components: {
|
||||||
|
Tooltip
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hint () {
|
hint () {
|
||||||
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
const levelVal = this.contrast.aaa ? 'aaa' : (this.contrast.aa ? 'aa' : 'bad')
|
||||||
|
@ -87,8 +100,7 @@ export default {
|
||||||
.contrast-ratio {
|
.contrast-ratio {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: -4px;
|
align-items: baseline;
|
||||||
margin-bottom: 5px;
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
|
@ -96,7 +108,6 @@ export default {
|
||||||
|
|
||||||
.rating {
|
.rating {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ export default {
|
||||||
{
|
{
|
||||||
component: 'Icon',
|
component: 'Icon',
|
||||||
directives: {
|
directives: {
|
||||||
textColor: '$blend(--stack, 0.5, --parent--text)',
|
textColor: '$blend(--stack 0.5 --parent--text)',
|
||||||
textAuto: 'no-auto'
|
textAuto: 'no-auto'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,3 @@
|
||||||
const hoverGlow = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
blur: 4,
|
|
||||||
spread: 0,
|
|
||||||
color: '--text',
|
|
||||||
alpha: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Input',
|
name: 'Input',
|
||||||
selector: '.input',
|
selector: '.input',
|
||||||
|
@ -27,7 +18,9 @@ export default {
|
||||||
{
|
{
|
||||||
component: 'Root',
|
component: 'Root',
|
||||||
directives: {
|
directives: {
|
||||||
'--defaultInputBevel': 'shadow | $borderSide(#FFFFFF, bottom, 0.2), $borderSide(#000000, top, 0.2)'
|
'--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2)',
|
||||||
|
'--defaultInputHoverGlow': 'shadow | 0 0 4 --text / 0.5',
|
||||||
|
'--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -54,7 +47,19 @@ export default {
|
||||||
{
|
{
|
||||||
state: ['hover'],
|
state: ['hover'],
|
||||||
directives: {
|
directives: {
|
||||||
shadow: [hoverGlow, '--defaultInputBevel']
|
shadow: ['--defaultInputHoverGlow', '--defaultInputBevel']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: ['focused'],
|
||||||
|
directives: {
|
||||||
|
shadow: ['--defaultInputFocusGlow', '--defaultInputBevel']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
state: ['focused', 'hover'],
|
||||||
|
directives: {
|
||||||
|
shadow: ['--defaultInputFocusGlow', '--defaultInputHoverGlow', '--defaultInputBevel']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,21 +24,21 @@ export default {
|
||||||
{
|
{
|
||||||
state: ['hover'],
|
state: ['hover'],
|
||||||
directives: {
|
directives: {
|
||||||
background: '$mod(--bg, 5)',
|
background: '$mod(--bg 5)',
|
||||||
opacity: 1
|
opacity: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: ['active'],
|
state: ['active'],
|
||||||
directives: {
|
directives: {
|
||||||
background: '$mod(--bg, 10)',
|
background: '$mod(--bg 10)',
|
||||||
opacity: 1
|
opacity: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: ['active', 'hover'],
|
state: ['active', 'hover'],
|
||||||
directives: {
|
directives: {
|
||||||
background: '$mod(--bg, 15)',
|
background: '$mod(--bg 15)',
|
||||||
opacity: 1
|
opacity: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'Modals',
|
name: 'Modals',
|
||||||
selector: '.modal-view',
|
selector: ['.modal-view', '#modal', '.shout-panel'],
|
||||||
lazy: true,
|
lazy: true,
|
||||||
|
notEditable: true,
|
||||||
validInnerComponents: [
|
validInnerComponents: [
|
||||||
'Panel'
|
'Panel'
|
||||||
],
|
],
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
class="label"
|
class="label"
|
||||||
:class="{ faint: !present || disabled }"
|
:class="{ faint: !present || disabled }"
|
||||||
>
|
>
|
||||||
{{ $t('settings.style.common.opacity') }}
|
{{ label }}
|
||||||
</label>
|
</label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
v-if="typeof fallback !== 'undefined'"
|
v-if="typeof fallback !== 'undefined'"
|
||||||
|
@ -39,7 +39,7 @@ export default {
|
||||||
Checkbox
|
Checkbox
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'name', 'modelValue', 'fallback', 'disabled'
|
'name', 'label', 'modelValue', 'fallback', 'disabled'
|
||||||
],
|
],
|
||||||
emits: ['update:modelValue'],
|
emits: ['update:modelValue'],
|
||||||
computed: {
|
computed: {
|
||||||
|
|
192
src/components/palette_editor/palette_editor.vue
Normal file
192
src/components/palette_editor/palette_editor.vue
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="PaletteEditor"
|
||||||
|
:class="{ '-compact': compact, '-apply': apply }"
|
||||||
|
>
|
||||||
|
<ColorInput
|
||||||
|
v-for="key in paletteKeys"
|
||||||
|
:key="key"
|
||||||
|
:model-value="props.modelValue[key]"
|
||||||
|
:fallback="fallback(key)"
|
||||||
|
:label="$t('settings.style.themes3.palette.' + key)"
|
||||||
|
@update:modelValue="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"
|
||||||
|
@click="applyPalette"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.themes3.palette.apply') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
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'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faFileImport,
|
||||||
|
faFileExport
|
||||||
|
)
|
||||||
|
|
||||||
|
const paletteKeys = [
|
||||||
|
'bg',
|
||||||
|
'fg',
|
||||||
|
'text',
|
||||||
|
'link',
|
||||||
|
'accent',
|
||||||
|
'cRed',
|
||||||
|
'cBlue',
|
||||||
|
'cGreen',
|
||||||
|
'cOrange',
|
||||||
|
'wallpaper'
|
||||||
|
]
|
||||||
|
|
||||||
|
const props = defineProps(['modelValue', 'compact', 'apply'])
|
||||||
|
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, filename) {
|
||||||
|
emit('update:modelValue', parsed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const exportPalette = () => {
|
||||||
|
paletteExporter.exportData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const importPalette = () => {
|
||||||
|
paletteImporter.importData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyPalette = (data) => {
|
||||||
|
emit('applyPalette', getExportedObject())
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-export-button {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.-apply {
|
||||||
|
.palette-apply-button {
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'RichContent',
|
name: 'RichContent',
|
||||||
selector: '.RichContent',
|
selector: '.RichContent',
|
||||||
|
notEditable: true,
|
||||||
validInnerComponents: [
|
validInnerComponents: [
|
||||||
'Text',
|
'Text',
|
||||||
'FunText',
|
'FunText',
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'Root',
|
name: 'Root',
|
||||||
selector: ':root',
|
selector: ':root',
|
||||||
|
notEditable: true,
|
||||||
validInnerComponents: [
|
validInnerComponents: [
|
||||||
'Underlay',
|
'Underlay',
|
||||||
'Modals',
|
'Modals',
|
||||||
|
@ -42,7 +43,7 @@ export default {
|
||||||
|
|
||||||
// Selection colors
|
// Selection colors
|
||||||
'--selectionBackground': 'color | --accent',
|
'--selectionBackground': 'color | --accent',
|
||||||
'--selectionText': 'color | $textColor(--accent, --text, no-preserve)'
|
'--selectionText': 'color | $textColor(--accent --text no-preserve)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
51
src/components/roundness_input/roundness_input.vue
Normal file
51
src/components/roundness_input/roundness_input.vue
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="roundness-control style-control"
|
||||||
|
:class="{ disabled: !present || disabled }"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
:for="name"
|
||||||
|
class="label"
|
||||||
|
:class="{ faint: !present || disabled }"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</label>
|
||||||
|
<Checkbox
|
||||||
|
v-if="typeof fallback !== 'undefined'"
|
||||||
|
:model-value="present"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="opt"
|
||||||
|
@update:modelValue="$emit('update:modelValue', !present ? fallback : undefined)"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
:id="name"
|
||||||
|
class="input input-number"
|
||||||
|
type="number"
|
||||||
|
:value="modelValue || fallback"
|
||||||
|
:disabled="!present || disabled"
|
||||||
|
:class="{ disabled: !present || disabled }"
|
||||||
|
max="999"
|
||||||
|
min="0"
|
||||||
|
step="1"
|
||||||
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
|
props: [
|
||||||
|
'name', 'label', 'modelValue', 'fallback', 'disabled'
|
||||||
|
],
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
computed: {
|
||||||
|
present () {
|
||||||
|
return typeof this.modelValue !== 'undefined'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,6 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'Scrollbar',
|
name: 'Scrollbar',
|
||||||
selector: '::-webkit-scrollbar',
|
selector: ['::-webkit-scrollbar-button', '::-webkit-scrollbar-thumb', '::-webkit-resizer'],
|
||||||
|
notEditable: true, // for now
|
||||||
defaultRules: [
|
defaultRules: [
|
||||||
{
|
{
|
||||||
directives: {
|
directives: {
|
||||||
|
|
|
@ -31,6 +31,7 @@ const hoverGlow = {
|
||||||
export default {
|
export default {
|
||||||
name: 'ScrollbarElement',
|
name: 'ScrollbarElement',
|
||||||
selector: '::-webkit-scrollbar-button',
|
selector: '::-webkit-scrollbar-button',
|
||||||
|
notEditable: true, // for now
|
||||||
states: {
|
states: {
|
||||||
pressed: ':active',
|
pressed: ':active',
|
||||||
hover: ':hover:not(:disabled)',
|
hover: ':hover:not(:disabled)',
|
||||||
|
@ -82,7 +83,7 @@ export default {
|
||||||
{
|
{
|
||||||
state: ['disabled'],
|
state: ['disabled'],
|
||||||
directives: {
|
directives: {
|
||||||
background: '$blend(--inheritedBackground, 0.25, --parent)',
|
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||||
shadow: [...buttonInsetFakeBorders]
|
shadow: [...buttonInsetFakeBorders]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -49,6 +49,7 @@ label.Select {
|
||||||
option {
|
option {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|
||||||
|
&:checked,
|
||||||
&.-active {
|
&.-active {
|
||||||
color: var(--selectionText);
|
color: var(--selectionText);
|
||||||
background-color: var(--selectionBackground);
|
background-color: var(--selectionBackground);
|
||||||
|
|
136
src/components/select/select_motion.vue
Normal file
136
src/components/select/select_motion.vue
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="SelectMotion btn-group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="add"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="plus"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
:disabled="disabled || !moveUpValid"
|
||||||
|
:class="{ disabled: disabled || !moveUpValid }"
|
||||||
|
@click="moveUp"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="chevron-up"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
:disabled="disabled || !moveDnValid"
|
||||||
|
:class="{ disabled: disabled || !moveDnValid }"
|
||||||
|
@click="moveDn"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="chevron-down"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
:disabled="disabled || !present"
|
||||||
|
:class="{ disabled: disabled || !present }"
|
||||||
|
@click="del"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
icon="times"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, defineEmits, defineProps, nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectedId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
getAddValue: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'update:selectedId'])
|
||||||
|
|
||||||
|
const moveUpValid = computed(() => {
|
||||||
|
return props.selectedId > 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const present = computed(() => props.modelValue[props.selectedId] != null)
|
||||||
|
|
||||||
|
const moveUp = async () => {
|
||||||
|
const newModel = [...props.modelValue]
|
||||||
|
const movable = newModel.splice(props.selectedId, 1)[0]
|
||||||
|
newModel.splice(props.selectedId - 1, 0, movable)
|
||||||
|
|
||||||
|
emit('update:modelValue', newModel)
|
||||||
|
await nextTick()
|
||||||
|
emit('update:selectedId', props.selectedId - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveDnValid = computed(() => {
|
||||||
|
return props.selectedId < props.modelValue.length - 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const moveDn = async () => {
|
||||||
|
const newModel = [...props.modelValue]
|
||||||
|
const movable = newModel.splice(props.selectedId.value, 1)[0]
|
||||||
|
newModel.splice(props.selectedId + 1, 0, movable)
|
||||||
|
|
||||||
|
emit('update:modelValue', newModel)
|
||||||
|
await nextTick()
|
||||||
|
emit('update:selectedId', props.selectedId + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const add = async () => {
|
||||||
|
const newModel = [...props.modelValue, props.getAddValue()]
|
||||||
|
|
||||||
|
emit('update:modelValue', newModel)
|
||||||
|
await nextTick()
|
||||||
|
emit('update:selectedId', Math.max(newModel.length - 1, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
const del = async () => {
|
||||||
|
const newModel = [...props.modelValue]
|
||||||
|
newModel.splice(props.selectedId, 1)
|
||||||
|
|
||||||
|
emit('update:modelValue', newModel)
|
||||||
|
await nextTick()
|
||||||
|
emit('update:selectedId', newModel.length === 0 ? undefined : Math.max(props.selectedId - 1, 0))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.SelectMotion {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-columns: 1fr;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
|
||||||
|
.button-default {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -10,9 +10,13 @@ export default {
|
||||||
ProfileSettingIndicator
|
ProfileSettingIndicator
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
path: {
|
path: {
|
||||||
type: [String, Array],
|
type: [String, Array],
|
||||||
required: true
|
required: false
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -68,7 +72,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
if (this.realDraftMode && this.realSource !== 'admin') {
|
if (this.realDraftMode && (this.realSource !== 'admin' || this.path == null)) {
|
||||||
this.draft = this.state
|
this.draft = this.state
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -76,14 +80,14 @@ export default {
|
||||||
draft: {
|
draft: {
|
||||||
// TODO allow passing shared draft object?
|
// TODO allow passing shared draft object?
|
||||||
get () {
|
get () {
|
||||||
if (this.realSource === 'admin') {
|
if (this.realSource === 'admin' || this.path == null) {
|
||||||
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
||||||
} else {
|
} else {
|
||||||
return this.localDraft
|
return this.localDraft
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
set (value) {
|
set (value) {
|
||||||
if (this.realSource === 'admin') {
|
if (this.realSource === 'admin' || this.path == null) {
|
||||||
this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
|
this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
|
||||||
} else {
|
} else {
|
||||||
this.localDraft = value
|
this.localDraft = value
|
||||||
|
@ -91,6 +95,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
state () {
|
state () {
|
||||||
|
if (this.path == null) {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
const value = get(this.configSource, this.canonPath)
|
const value = get(this.configSource, this.canonPath)
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return this.defaultState
|
return this.defaultState
|
||||||
|
@ -145,6 +152,9 @@ export default {
|
||||||
return this.backendDescription?.suggestions
|
return this.backendDescription?.suggestions
|
||||||
},
|
},
|
||||||
shouldBeDisabled () {
|
shouldBeDisabled () {
|
||||||
|
if (this.path == null) {
|
||||||
|
return this.disabled
|
||||||
|
}
|
||||||
const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
|
const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
|
||||||
return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
|
return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
|
||||||
},
|
},
|
||||||
|
@ -159,6 +169,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
configSink () {
|
configSink () {
|
||||||
|
if (this.path == null) {
|
||||||
|
return (k, v) => this.$emit('update:modelValue', v)
|
||||||
|
}
|
||||||
switch (this.realSource) {
|
switch (this.realSource) {
|
||||||
case 'profile':
|
case 'profile':
|
||||||
return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
|
return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
|
||||||
|
@ -184,6 +197,7 @@ export default {
|
||||||
return this.realSource === 'profile'
|
return this.realSource === 'profile'
|
||||||
},
|
},
|
||||||
isChanged () {
|
isChanged () {
|
||||||
|
if (this.path == null) return false
|
||||||
switch (this.realSource) {
|
switch (this.realSource) {
|
||||||
case 'profile':
|
case 'profile':
|
||||||
case 'admin':
|
case 'admin':
|
||||||
|
@ -193,9 +207,11 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
canonPath () {
|
canonPath () {
|
||||||
|
if (this.path == null) return null
|
||||||
return Array.isArray(this.path) ? this.path : this.path.split('.')
|
return Array.isArray(this.path) ? this.path : this.path.split('.')
|
||||||
},
|
},
|
||||||
isDirty () {
|
isDirty () {
|
||||||
|
if (this.path == null) return false
|
||||||
if (this.realSource === 'admin' && this.canonPath.length > 3) {
|
if (this.realSource === 'admin' && this.canonPath.length > 3) {
|
||||||
return false // should not show draft buttons for "grouped" values
|
return false // should not show draft buttons for "grouped" values
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:for="path"
|
:for="path"
|
||||||
|
class="setting-label"
|
||||||
:class="{ 'faint': shouldBeDisabled }"
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
>
|
>
|
||||||
<template v-if="backendDescriptionLabel">
|
<template v-if="backendDescriptionLabel">
|
||||||
|
@ -15,6 +16,7 @@
|
||||||
</template>
|
</template>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
</label>
|
</label>
|
||||||
|
{{ ' ' }}
|
||||||
<input
|
<input
|
||||||
:id="path"
|
:id="path"
|
||||||
class="input string-input"
|
class="input string-input"
|
||||||
|
|
|
@ -10,31 +10,33 @@
|
||||||
<slot />
|
<slot />
|
||||||
</label>
|
</label>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
<input
|
<span class="no-break">
|
||||||
:id="path"
|
<input
|
||||||
class="input number-input"
|
:id="path"
|
||||||
type="number"
|
class="input number-input"
|
||||||
:step="step"
|
type="number"
|
||||||
:disabled="disabled"
|
:step="step"
|
||||||
:min="min || 0"
|
:disabled="disabled"
|
||||||
:value="stateValue"
|
:min="min || 0"
|
||||||
@change="updateValue"
|
:value="stateValue"
|
||||||
>
|
@change="updateValue"
|
||||||
<Select
|
|
||||||
:id="path"
|
|
||||||
:model-value="stateUnit"
|
|
||||||
:disabled="disabled"
|
|
||||||
class="unit-input unstyled"
|
|
||||||
@change="updateUnit"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="option in units"
|
|
||||||
:key="option"
|
|
||||||
:value="option"
|
|
||||||
>
|
>
|
||||||
{{ getUnitString(option) }}
|
<Select
|
||||||
</option>
|
:id="path"
|
||||||
</Select>
|
:model-value="stateUnit"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="unit-input unstyled"
|
||||||
|
@change="updateUnit"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="option in units"
|
||||||
|
:key="option"
|
||||||
|
:value="option"
|
||||||
|
>
|
||||||
|
{{ getUnitString(option) }}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
</span>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
<ModifiedIndicator
|
<ModifiedIndicator
|
||||||
:changed="isChanged"
|
:changed="isChanged"
|
||||||
|
@ -47,6 +49,10 @@
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.UnitSetting {
|
.UnitSetting {
|
||||||
|
.no-break {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.number-input {
|
.number-input {
|
||||||
max-width: 6.5em;
|
max-width: 6.5em;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
|
|
||||||
|
.btn:not(.dropdown-button) {
|
||||||
|
padding: 0 2em;
|
||||||
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
@ -54,10 +58,6 @@
|
||||||
.btn {
|
.btn {
|
||||||
min-height: 2em;
|
min-height: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:not(.dropdown-button) {
|
|
||||||
padding: 0 2em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,6 +76,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.-mobile {
|
||||||
|
.setting-list,
|
||||||
|
.option-list {
|
||||||
|
padding-left: 0.25em;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
margin: 1em 0;
|
||||||
|
line-height: 1.5em;
|
||||||
|
vertical-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.two-column {
|
||||||
|
column-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.peek {
|
&.peek {
|
||||||
.settings-modal-panel {
|
.settings-modal-panel {
|
||||||
/* Explanation:
|
/* Explanation:
|
||||||
|
|
|
@ -17,10 +17,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-multiple {
|
.select-multiple {
|
||||||
|
margin-top: 0.5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.option-list {
|
.option-list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin-top: 0.5em;
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import GeneralTab from './tabs/general_tab.vue'
|
||||||
import AppearanceTab from './tabs/appearance_tab.vue'
|
import AppearanceTab from './tabs/appearance_tab.vue'
|
||||||
import VersionTab from './tabs/version_tab.vue'
|
import VersionTab from './tabs/version_tab.vue'
|
||||||
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
|
import ThemeTab from './tabs/theme_tab/theme_tab.vue'
|
||||||
|
import StyleTab from './tabs/style_tab/style_tab.vue'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -17,6 +18,7 @@ import {
|
||||||
faUser,
|
faUser,
|
||||||
faFilter,
|
faFilter,
|
||||||
faPaintBrush,
|
faPaintBrush,
|
||||||
|
faPalette,
|
||||||
faBell,
|
faBell,
|
||||||
faDownload,
|
faDownload,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
|
@ -29,6 +31,7 @@ library.add(
|
||||||
faUser,
|
faUser,
|
||||||
faFilter,
|
faFilter,
|
||||||
faPaintBrush,
|
faPaintBrush,
|
||||||
|
faPalette,
|
||||||
faBell,
|
faBell,
|
||||||
faDownload,
|
faDownload,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
|
@ -48,6 +51,7 @@ const SettingsModalContent = {
|
||||||
ProfileTab,
|
ProfileTab,
|
||||||
GeneralTab,
|
GeneralTab,
|
||||||
AppearanceTab,
|
AppearanceTab,
|
||||||
|
StyleTab,
|
||||||
VersionTab,
|
VersionTab,
|
||||||
ThemeTab
|
ThemeTab
|
||||||
},
|
},
|
||||||
|
@ -60,6 +64,12 @@ const SettingsModalContent = {
|
||||||
},
|
},
|
||||||
bodyLock () {
|
bodyLock () {
|
||||||
return this.$store.state.interface.settingsModalState === 'visible'
|
return this.$store.state.interface.settingsModalState === 'visible'
|
||||||
|
},
|
||||||
|
expertLevel () {
|
||||||
|
return this.$store.state.config.expertLevel
|
||||||
|
},
|
||||||
|
isMobileLayout () {
|
||||||
|
return this.$store.state.interface.layoutType === 'mobile'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -1,6 +1,21 @@
|
||||||
.settings_tab-switcher {
|
.settings_tab-switcher {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
.setting-item {
|
.setting-item {
|
||||||
border-bottom: 2px solid var(--border);
|
border-bottom: 2px solid var(--border);
|
||||||
margin: 1em 1em 1.4em;
|
margin: 1em 1em 1.4em;
|
||||||
|
@ -8,7 +23,6 @@
|
||||||
|
|
||||||
> div,
|
> div,
|
||||||
> label {
|
> label {
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
|
@ -17,10 +31,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-multiple {
|
.select-multiple {
|
||||||
|
margin-top: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.option-list {
|
.option-list {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin-top: 0.5em;
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,16 @@
|
||||||
<AppearanceTab />
|
<AppearanceTab />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
:label="$t('settings.theme')"
|
v-if="expertLevel > 0 && !isMobileLayout"
|
||||||
|
:label="$t('settings.style.themes3.editor.title')"
|
||||||
|
icon="palette"
|
||||||
|
data-tab-name="style"
|
||||||
|
>
|
||||||
|
<StyleTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="expertLevel > 0 && !isMobileLayout"
|
||||||
|
:label="$t('settings.theme_old')"
|
||||||
icon="paint-brush"
|
icon="paint-brush"
|
||||||
data-tab-name="theme"
|
data-tab-name="theme"
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,20 +3,20 @@ import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
import FloatSetting from '../helpers/float_setting.vue'
|
import FloatSetting from '../helpers/float_setting.vue'
|
||||||
import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue'
|
import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue'
|
||||||
|
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
|
||||||
|
|
||||||
import FontControl from 'src/components/font_control/font_control.vue'
|
import FontControl from 'src/components/font_control/font_control.vue'
|
||||||
|
|
||||||
import { normalizeThemeData } from 'src/modules/interface'
|
import { normalizeThemeData } from 'src/modules/interface'
|
||||||
|
|
||||||
import {
|
import { newImporter } from 'src/services/export_import/export_import.js'
|
||||||
getThemes
|
|
||||||
} from 'src/services/style_setter/style_setter.js'
|
|
||||||
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
|
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
|
||||||
import { init } from 'src/services/theme_data/theme_data_3.service.js'
|
import { init } from 'src/services/theme_data/theme_data_3.service.js'
|
||||||
import {
|
import {
|
||||||
getCssRules,
|
getCssRules,
|
||||||
getScopedVersion
|
getScopedVersion
|
||||||
} from 'src/services/theme_data/css_utils.js'
|
} from 'src/services/theme_data/css_utils.js'
|
||||||
|
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
||||||
|
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||||
|
@ -27,6 +27,10 @@ import {
|
||||||
|
|
||||||
import Preview from './theme_tab/theme_preview.vue'
|
import Preview from './theme_tab/theme_preview.vue'
|
||||||
|
|
||||||
|
// helper for debugging
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faGlobe
|
faGlobe
|
||||||
)
|
)
|
||||||
|
@ -34,7 +38,28 @@ library.add(
|
||||||
const AppearanceTab = {
|
const AppearanceTab = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
availableStyles: [],
|
availableThemesV3: [],
|
||||||
|
availableThemesV2: [],
|
||||||
|
bundledPalettes: [],
|
||||||
|
compilationCache: {},
|
||||||
|
fileImporter: newImporter({
|
||||||
|
accept: '.json, .piss',
|
||||||
|
validator: this.importValidator,
|
||||||
|
onImport: this.onImport,
|
||||||
|
parser: this.importParser,
|
||||||
|
onImportFailure: this.onImportFailure
|
||||||
|
}),
|
||||||
|
palettesKeys: [
|
||||||
|
'bg',
|
||||||
|
'fg',
|
||||||
|
'link',
|
||||||
|
'text',
|
||||||
|
'cRed',
|
||||||
|
'cGreen',
|
||||||
|
'cBlue',
|
||||||
|
'cOrange'
|
||||||
|
],
|
||||||
|
userPalette: {},
|
||||||
intersectionObserver: null,
|
intersectionObserver: null,
|
||||||
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
|
thirdColumnModeOptions: ['none', 'notifications', 'postform'].map(mode => ({
|
||||||
key: mode,
|
key: mode,
|
||||||
|
@ -61,33 +86,69 @@ const AppearanceTab = {
|
||||||
UnitSetting,
|
UnitSetting,
|
||||||
ProfileSettingIndicator,
|
ProfileSettingIndicator,
|
||||||
FontControl,
|
FontControl,
|
||||||
Preview
|
Preview,
|
||||||
|
PaletteEditor
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
getThemes()
|
this.$store.dispatch('getThemeData')
|
||||||
.then((promises) => {
|
|
||||||
return Promise.all(
|
const updateIndex = (resource) => {
|
||||||
Object.entries(promises)
|
const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
|
||||||
.map(([k, v]) => v.then(res => [k, res]))
|
const currentIndex = this.$store.state.instance[`${resource}sIndex`]
|
||||||
)
|
|
||||||
|
let promise
|
||||||
|
if (currentIndex) {
|
||||||
|
promise = Promise.resolve(currentIndex)
|
||||||
|
} else {
|
||||||
|
promise = this.$store.dispatch(`fetch${capitalizedResource}sIndex`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then(index => {
|
||||||
|
return Object
|
||||||
|
.entries(index)
|
||||||
|
.map(([k, func]) => [k, func()])
|
||||||
})
|
})
|
||||||
.then(themes => themes.reduce((acc, [k, v]) => {
|
}
|
||||||
if (v) {
|
|
||||||
return [
|
updateIndex('style').then(styles => {
|
||||||
...acc,
|
styles.forEach(([key, stylePromise]) => stylePromise.then(data => {
|
||||||
{
|
const meta = data.find(x => x.component === '@meta')
|
||||||
name: v.name || v[0],
|
this.availableThemesV3.push({ key, data, name: meta.directives.name, version: 'v3' })
|
||||||
key: k,
|
}))
|
||||||
data: v
|
})
|
||||||
}
|
|
||||||
]
|
updateIndex('theme').then(themes => {
|
||||||
|
themes.forEach(([key, themePromise]) => themePromise.then(data => {
|
||||||
|
this.availableThemesV2.push({ key, data, name: data.name, version: 'v2' })
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.userPalette = this.$store.state.interface.paletteDataUsed || {}
|
||||||
|
|
||||||
|
updateIndex('palette').then(bundledPalettes => {
|
||||||
|
bundledPalettes.forEach(([key, palettePromise]) => palettePromise.then(v => {
|
||||||
|
let palette
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
const [
|
||||||
|
name,
|
||||||
|
bg,
|
||||||
|
fg,
|
||||||
|
text,
|
||||||
|
link,
|
||||||
|
cRed = '#FF0000',
|
||||||
|
cGreen = '#00FF00',
|
||||||
|
cBlue = '#0000FF',
|
||||||
|
cOrange = '#E3FF00'
|
||||||
|
] = v
|
||||||
|
palette = { key, name, bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
|
||||||
} else {
|
} else {
|
||||||
return acc
|
palette = { key, ...v }
|
||||||
}
|
}
|
||||||
}, []))
|
if (!palette.key.startsWith('style.')) {
|
||||||
.then((themesComplete) => {
|
this.bundledPalettes.push(palette)
|
||||||
this.availableStyles = themesComplete
|
}
|
||||||
})
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
if (window.IntersectionObserver) {
|
if (window.IntersectionObserver) {
|
||||||
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||||
|
@ -111,7 +172,65 @@ const AppearanceTab = {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
paletteDataUsed () {
|
||||||
|
this.userPalette = this.paletteDataUsed || {}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
paletteDataUsed () {
|
||||||
|
return this.$store.state.interface.paletteDataUsed
|
||||||
|
},
|
||||||
|
availableStyles () {
|
||||||
|
return [
|
||||||
|
...this.availableThemesV3,
|
||||||
|
...this.availableThemesV2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
availablePalettes () {
|
||||||
|
return [
|
||||||
|
...this.bundledPalettes,
|
||||||
|
...this.stylePalettes
|
||||||
|
]
|
||||||
|
},
|
||||||
|
stylePalettes () {
|
||||||
|
const ruleset = this.$store.state.interface.styleDataUsed || []
|
||||||
|
if (!ruleset && ruleset.length === 0) return
|
||||||
|
const meta = ruleset.find(x => x.component === '@meta')
|
||||||
|
const result = ruleset.filter(x => x.component.startsWith('@palette'))
|
||||||
|
.map(x => {
|
||||||
|
const { variant, directives } = x
|
||||||
|
const {
|
||||||
|
bg,
|
||||||
|
fg,
|
||||||
|
text,
|
||||||
|
link,
|
||||||
|
accent,
|
||||||
|
cRed,
|
||||||
|
cBlue,
|
||||||
|
cGreen,
|
||||||
|
cOrange,
|
||||||
|
wallpaper
|
||||||
|
} = directives
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
name: `${meta.directives.name || this.$t('settings.style.themes3.palette.imported')}: ${variant}`,
|
||||||
|
key: `style.${variant.toLowerCase().replace(/ /g, '_')}`,
|
||||||
|
bg,
|
||||||
|
fg,
|
||||||
|
text,
|
||||||
|
link,
|
||||||
|
accent,
|
||||||
|
cRed,
|
||||||
|
cBlue,
|
||||||
|
cGreen,
|
||||||
|
cOrange,
|
||||||
|
wallpaper
|
||||||
|
}
|
||||||
|
return Object.fromEntries(Object.entries(result).filter(([k, v]) => v))
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
},
|
||||||
noIntersectionObserver () {
|
noIntersectionObserver () {
|
||||||
return !window.IntersectionObserver
|
return !window.IntersectionObserver
|
||||||
},
|
},
|
||||||
|
@ -144,15 +263,22 @@ const AppearanceTab = {
|
||||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
customThemeVersion () {
|
||||||
|
const { themeVersion } = this.$store.state.interface
|
||||||
|
return themeVersion
|
||||||
|
},
|
||||||
isCustomThemeUsed () {
|
isCustomThemeUsed () {
|
||||||
const { theme } = this.mergedConfig
|
const { customTheme, customThemeSource } = this.mergedConfig
|
||||||
return theme === 'custom' || theme === null
|
return customTheme != null || customThemeSource != null
|
||||||
|
},
|
||||||
|
isCustomStyleUsed (name) {
|
||||||
|
const { styleCustomData } = this.mergedConfig
|
||||||
|
return styleCustomData != null
|
||||||
},
|
},
|
||||||
...SharedComputedObject()
|
...SharedComputedObject()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateFont (key, value) {
|
updateFont (key, value) {
|
||||||
console.log(key, value)
|
|
||||||
this.$store.dispatch('setOption', {
|
this.$store.dispatch('setOption', {
|
||||||
name: 'theme3hacks',
|
name: 'theme3hacks',
|
||||||
value: {
|
value: {
|
||||||
|
@ -164,25 +290,120 @@ const AppearanceTab = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
importFile () {
|
||||||
|
this.fileImporter.importData()
|
||||||
|
},
|
||||||
|
importParser (file, filename) {
|
||||||
|
if (filename.endsWith('.json')) {
|
||||||
|
return JSON.parse(file)
|
||||||
|
} else if (filename.endsWith('.piss')) {
|
||||||
|
return deserialize(file)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onImport (parsed, filename) {
|
||||||
|
if (filename.endsWith('.json')) {
|
||||||
|
this.$store.dispatch('setThemeCustom', parsed.source || parsed.theme)
|
||||||
|
} else if (filename.endsWith('.piss')) {
|
||||||
|
this.$store.dispatch('setStyleCustom', parsed)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onImportFailure (result) {
|
||||||
|
console.error('Failure importing theme:', result)
|
||||||
|
this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||||
|
},
|
||||||
|
importValidator (parsed, filename) {
|
||||||
|
if (filename.endsWith('.json')) {
|
||||||
|
const version = parsed._pleroma_theme_version
|
||||||
|
return version >= 1 || version <= 2
|
||||||
|
} else if (filename.endsWith('.piss')) {
|
||||||
|
if (!Array.isArray(parsed)) return false
|
||||||
|
if (parsed.length < 1) return false
|
||||||
|
if (parsed.find(x => x.component === '@meta') == null) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
isThemeActive (key) {
|
isThemeActive (key) {
|
||||||
const { theme } = this.mergedConfig
|
return key === (this.mergedConfig.theme || this.$store.state.instance.theme)
|
||||||
return key === theme
|
},
|
||||||
|
isStyleActive (key) {
|
||||||
|
return key === (this.mergedConfig.style || this.$store.state.instance.style)
|
||||||
|
},
|
||||||
|
isPaletteActive (key) {
|
||||||
|
return key === (this.mergedConfig.palette || this.$store.state.instance.palette)
|
||||||
|
},
|
||||||
|
setStyle (name) {
|
||||||
|
this.$store.dispatch('setStyle', name)
|
||||||
},
|
},
|
||||||
setTheme (name) {
|
setTheme (name) {
|
||||||
this.$store.dispatch('setTheme', { themeName: name, saveData: true, recompile: true })
|
this.$store.dispatch('setTheme', name)
|
||||||
},
|
},
|
||||||
previewTheme (key, input) {
|
setPalette (name, data) {
|
||||||
const style = normalizeThemeData(input)
|
this.$store.dispatch('setPalette', name)
|
||||||
const x = 2
|
this.userPalette = data
|
||||||
if (x === 1) return
|
},
|
||||||
const theme2 = convertTheme2To3(style)
|
setPaletteCustom (data) {
|
||||||
const theme3 = init({
|
this.$store.dispatch('setPaletteCustom', data)
|
||||||
inputRuleset: theme2,
|
this.userPalette = data
|
||||||
ultimateBackgroundColor: '#000000',
|
},
|
||||||
liteMode: true,
|
resetTheming (name) {
|
||||||
debug: true,
|
this.$store.dispatch('setStyle', 'stock')
|
||||||
onlyNormalState: true
|
},
|
||||||
})
|
previewTheme (key, version, input) {
|
||||||
|
let theme3
|
||||||
|
if (this.compilationCache[key]) {
|
||||||
|
theme3 = this.compilationCache[key]
|
||||||
|
} else if (input) {
|
||||||
|
if (version === 'v2') {
|
||||||
|
const style = normalizeThemeData(input)
|
||||||
|
const theme2 = convertTheme2To3(style)
|
||||||
|
theme3 = init({
|
||||||
|
inputRuleset: theme2,
|
||||||
|
ultimateBackgroundColor: '#000000',
|
||||||
|
liteMode: true,
|
||||||
|
debug: true,
|
||||||
|
onlyNormalState: true
|
||||||
|
})
|
||||||
|
} else if (version === 'v3') {
|
||||||
|
const palette = input.find(x => x.component === '@palette')
|
||||||
|
let paletteRule
|
||||||
|
if (palette) {
|
||||||
|
const { directives } = palette
|
||||||
|
directives.link = directives.link || directives.accent
|
||||||
|
directives.accent = directives.accent || directives.link
|
||||||
|
paletteRule = {
|
||||||
|
component: 'Root',
|
||||||
|
directives: Object.fromEntries(
|
||||||
|
Object
|
||||||
|
.entries(directives)
|
||||||
|
.filter(([k, v]) => k && k !== 'name')
|
||||||
|
.map(([k, v]) => ['--' + k, 'color | ' + v])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paletteRule = null
|
||||||
|
}
|
||||||
|
|
||||||
|
theme3 = init({
|
||||||
|
inputRuleset: [...input, paletteRule].filter(x => x),
|
||||||
|
ultimateBackgroundColor: '#000000',
|
||||||
|
liteMode: true,
|
||||||
|
debug: true,
|
||||||
|
onlyNormalState: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
theme3 = init({
|
||||||
|
inputRuleset: [],
|
||||||
|
ultimateBackgroundColor: '#000000',
|
||||||
|
liteMode: true,
|
||||||
|
debug: true,
|
||||||
|
onlyNormalState: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.compilationCache[key]) {
|
||||||
|
this.compilationCache[key] = theme3
|
||||||
|
}
|
||||||
|
|
||||||
return getScopedVersion(
|
return getScopedVersion(
|
||||||
getCssRules(theme3.eager),
|
getCssRules(theme3.eager),
|
||||||
|
|
120
src/components/settings_modal/tabs/appearance_tab.scss
Normal file
120
src/components/settings_modal/tabs/appearance_tab.scss
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
.appearance-tab {
|
||||||
|
.palette,
|
||||||
|
.theme-notice {
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
padding-bottom: 0;
|
||||||
|
|
||||||
|
&.heading {
|
||||||
|
display: grid;
|
||||||
|
align-items: baseline;
|
||||||
|
grid-template-columns: 1fr auto auto auto;
|
||||||
|
grid-gap: 0.5em;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.palettes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-gap: 0.5em;
|
||||||
|
|
||||||
|
h4,
|
||||||
|
.unsupported-theme-v2,
|
||||||
|
.userPalette {
|
||||||
|
grid-column: 1 / span 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-entry {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 0.5em;
|
||||||
|
|
||||||
|
.palette-label 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 & {
|
||||||
|
.palette-entry {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-label {
|
||||||
|
line-height: 1.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-preview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
grid-template-rows: 1em 1em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-list {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: -0.5em 0;
|
||||||
|
height: 25em;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: 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: 19rem;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,44 +1,161 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="appearance-tab" :label="$t('settings.general')">
|
<div
|
||||||
|
class="appearance-tab"
|
||||||
|
:label="$t('settings.general')"
|
||||||
|
>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{ $t('settings.theme') }}</h2>
|
<h2>{{ $t('settings.theme') }}</h2>
|
||||||
<ul
|
<ul
|
||||||
class="theme-list"
|
|
||||||
ref="themeList"
|
ref="themeList"
|
||||||
|
class="theme-list"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="isCustomThemeUsed"
|
|
||||||
disabled
|
|
||||||
class="button-default theme-preview"
|
class="button-default theme-preview"
|
||||||
>
|
data-theme-key="stock"
|
||||||
<preview />
|
:class="{ toggled: isStyleActive('stock') }"
|
||||||
<h4 class="theme-name">{{ $t('settings.style.custom_theme_used') }}</h4>
|
@click="resetTheming"
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-for="style in availableStyles"
|
|
||||||
:data-theme-key="style.key"
|
|
||||||
:key="style.key"
|
|
||||||
class="button-default theme-preview"
|
|
||||||
:class="{ toggled: isThemeActive(style.key) }"
|
|
||||||
@click="setTheme(style.key)"
|
|
||||||
>
|
>
|
||||||
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||||
<component
|
<component
|
||||||
:is="'style'"
|
:is="'style'"
|
||||||
v-if="style.ready || noIntersectionObserver"
|
v-html="previewTheme('stock', 'v3')"
|
||||||
v-html="previewTheme(style.key, style.data)"
|
|
||||||
/>
|
/>
|
||||||
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||||
<preview :class="{ placeholder: ready }" :id="'theme-preview-' + style.key"/>
|
<preview id="theme-preview-stock" />
|
||||||
<h4 class="theme-name">{{ style.name }}</h4>
|
<h4 class="theme-name">
|
||||||
|
{{ $t('settings.style.stock_theme_used') }}
|
||||||
|
<span class="alert neutral version">v3</span>
|
||||||
|
</h4>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isCustomThemeUsed"
|
||||||
|
disabled
|
||||||
|
class="button-default theme-preview toggled"
|
||||||
|
>
|
||||||
|
<preview />
|
||||||
|
<h4 class="theme-name">
|
||||||
|
{{ $t('settings.style.custom_theme_used') }}
|
||||||
|
<span class="alert neutral version">v2</span>
|
||||||
|
</h4>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isCustomStyleUsed"
|
||||||
|
disabled
|
||||||
|
class="button-default theme-preview toggled"
|
||||||
|
>
|
||||||
|
<preview />
|
||||||
|
<h4 class="theme-name">
|
||||||
|
{{ $t('settings.style.custom_style_used') }}
|
||||||
|
<span class="alert neutral version">v3</span>
|
||||||
|
</h4>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="style in availableStyles"
|
||||||
|
:key="style.key"
|
||||||
|
:data-theme-key="style.key"
|
||||||
|
class="button-default theme-preview"
|
||||||
|
:class="{ toggled: isStyleActive(style.key) }"
|
||||||
|
@click="style.version === 'v2' ? setTheme(style.key) : setStyle(style.key)"
|
||||||
|
>
|
||||||
|
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||||
|
<div v-if="style.ready || noIntersectionObserver">
|
||||||
|
<component
|
||||||
|
:is="'style'"
|
||||||
|
v-html="previewTheme(style.key, style.version, style.data)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||||
|
<preview :id="'theme-preview-' + style.key" />
|
||||||
|
<h4 class="theme-name">
|
||||||
|
{{ style.name }}
|
||||||
|
<span class="alert neutral version">{{ style.version }}</span>
|
||||||
|
</h4>
|
||||||
</button>
|
</button>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
<div class="import-file-container">
|
||||||
<div class="alert neutral theme-notice">
|
<button
|
||||||
{{ $t("settings.style.appearance_tab_note") }}
|
class="btn button-default"
|
||||||
|
@click="importFile"
|
||||||
|
>
|
||||||
|
<FAIcon icon="folder-open" />
|
||||||
|
{{ $t('settings.style.themes3.editor.load_style') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.style.themes3.palette.label') }}</h2>
|
||||||
|
<div class="palettes">
|
||||||
|
<template v-if="customThemeVersion === 'v3'">
|
||||||
|
<h4>{{ $t('settings.style.themes3.palette.bundled') }}</h4>
|
||||||
|
<button
|
||||||
|
v-for="p in bundledPalettes"
|
||||||
|
:key="p.name"
|
||||||
|
class="btn button-default palette-entry"
|
||||||
|
:class="{ toggled: isPaletteActive(p.key) }"
|
||||||
|
@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>
|
||||||
|
<h4 v-if="stylePalettes?.length > 0">
|
||||||
|
{{ $t('settings.style.themes3.palette.style') }}
|
||||||
|
</h4>
|
||||||
|
<button
|
||||||
|
v-for="p in stylePalettes || []"
|
||||||
|
:key="p.name"
|
||||||
|
class="btn button-default palette-entry"
|
||||||
|
:class="{ toggled: isPaletteActive(p.key) }"
|
||||||
|
@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>
|
||||||
|
<h4 v-if="expertLevel > 0">
|
||||||
|
{{ $t('settings.style.themes3.palette.user') }}
|
||||||
|
</h4>
|
||||||
|
<PaletteEditor
|
||||||
|
v-if="expertLevel > 0"
|
||||||
|
class="userPalette"
|
||||||
|
v-model="userPalette"
|
||||||
|
:compact="true"
|
||||||
|
:apply="true"
|
||||||
|
@applyPalette="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>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{ $t('settings.scale_and_layout') }}</h2>
|
<h2>{{ $t('settings.scale_and_layout') }}</h2>
|
||||||
|
<div class="alert neutral theme-notice">
|
||||||
|
{{ $t("settings.style.appearance_tab_note") }}
|
||||||
|
</div>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li>
|
<li>
|
||||||
<UnitSetting
|
<UnitSetting
|
||||||
|
@ -60,7 +177,7 @@
|
||||||
<code>px</code>
|
<code>px</code>
|
||||||
<code>rem</code>
|
<code>rem</code>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<br/>
|
<br>
|
||||||
<i18n-t
|
<i18n-t
|
||||||
scope="global"
|
scope="global"
|
||||||
keypath="settings.text_size_tip2"
|
keypath="settings.text_size_tip2"
|
||||||
|
@ -256,58 +373,4 @@
|
||||||
|
|
||||||
<script src="./appearance_tab.js"></script>
|
<script src="./appearance_tab.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" src="./appearance_tab.scss"></style>
|
||||||
.appearance-tab {
|
|
||||||
.theme-notice {
|
|
||||||
padding: 0.5em;
|
|
||||||
margin: 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-list {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin: -0.5em 0;
|
|
||||||
height: 25em;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-gutter: stable;
|
|
||||||
border-radius: var(--roundness);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
.theme-preview {
|
|
||||||
font-size: 1rem; // fix for firefox
|
|
||||||
width: 19rem;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
835
src/components/settings_modal/tabs/style_tab/style_tab.js
Normal file
835
src/components/settings_modal/tabs/style_tab/style_tab.js
Normal file
|
@ -0,0 +1,835 @@
|
||||||
|
import { ref, reactive, computed, watch, watchEffect, provide, getCurrentInstance } from 'vue'
|
||||||
|
import { useStore } from 'vuex'
|
||||||
|
import { get, set, unset, throttle } from 'lodash'
|
||||||
|
|
||||||
|
import Select from 'src/components/select/select.vue'
|
||||||
|
import SelectMotion from 'src/components/select/select_motion.vue'
|
||||||
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
import ComponentPreview from 'src/components/component_preview/component_preview.vue'
|
||||||
|
import StringSetting from '../../helpers/string_setting.vue'
|
||||||
|
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
|
||||||
|
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||||
|
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
|
||||||
|
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
|
||||||
|
import RoundnessInput from 'src/components/roundness_input/roundness_input.vue'
|
||||||
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||||
|
import Tooltip from 'src/components/tooltip/tooltip.vue'
|
||||||
|
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
|
||||||
|
import Preview from '../theme_tab/theme_preview.vue'
|
||||||
|
|
||||||
|
import VirtualDirectivesTab from './virtual_directives_tab.vue'
|
||||||
|
|
||||||
|
import { init, findColor } from 'src/services/theme_data/theme_data_3.service.js'
|
||||||
|
import {
|
||||||
|
getCssRules,
|
||||||
|
getScopedVersion
|
||||||
|
} from 'src/services/theme_data/css_utils.js'
|
||||||
|
import { serialize } from 'src/services/theme_data/iss_serializer.js'
|
||||||
|
import { deserializeShadow, deserialize } from 'src/services/theme_data/iss_deserializer.js'
|
||||||
|
import {
|
||||||
|
rgb2hex,
|
||||||
|
hex2rgb,
|
||||||
|
getContrastRatio
|
||||||
|
} from 'src/services/color_convert/color_convert.js'
|
||||||
|
import {
|
||||||
|
newImporter,
|
||||||
|
newExporter
|
||||||
|
} from 'src/services/export_import/export_import.js'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faFloppyDisk,
|
||||||
|
faFolderOpen,
|
||||||
|
faFile,
|
||||||
|
faArrowsRotate,
|
||||||
|
faCheck
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
// helper for debugging
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
|
||||||
|
|
||||||
|
// helper to make states comparable
|
||||||
|
const normalizeStates = (states) => ['normal', ...(states?.filter(x => x !== 'normal') || [])].join(':')
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faFile,
|
||||||
|
faFloppyDisk,
|
||||||
|
faFolderOpen,
|
||||||
|
faArrowsRotate,
|
||||||
|
faCheck
|
||||||
|
)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Select,
|
||||||
|
SelectMotion,
|
||||||
|
Checkbox,
|
||||||
|
Tooltip,
|
||||||
|
StringSetting,
|
||||||
|
ComponentPreview,
|
||||||
|
TabSwitcher,
|
||||||
|
ShadowControl,
|
||||||
|
ColorInput,
|
||||||
|
PaletteEditor,
|
||||||
|
OpacityInput,
|
||||||
|
RoundnessInput,
|
||||||
|
ContrastRatio,
|
||||||
|
Preview,
|
||||||
|
VirtualDirectivesTab
|
||||||
|
},
|
||||||
|
setup (props, context) {
|
||||||
|
const exports = {}
|
||||||
|
const store = useStore()
|
||||||
|
// All rules that are made by editor
|
||||||
|
const allEditedRules = ref(store.state.interface.styleDataUsed || {})
|
||||||
|
const styleDataUsed = computed(() => store.state.interface.styleDataUsed)
|
||||||
|
|
||||||
|
watch([styleDataUsed], (value) => {
|
||||||
|
onImport(store.state.interface.styleDataUsed)
|
||||||
|
}, { once: true })
|
||||||
|
|
||||||
|
exports.isActive = computed(() => {
|
||||||
|
const tabSwitcher = getCurrentInstance().parent.ctx
|
||||||
|
return tabSwitcher ? tabSwitcher.isActive('style') : false
|
||||||
|
})
|
||||||
|
|
||||||
|
// ## Meta stuff
|
||||||
|
exports.name = ref('')
|
||||||
|
exports.author = ref('')
|
||||||
|
exports.license = ref('')
|
||||||
|
exports.website = ref('')
|
||||||
|
|
||||||
|
const metaOut = computed(() => {
|
||||||
|
return [
|
||||||
|
'@meta {',
|
||||||
|
` name: ${exports.name.value};`,
|
||||||
|
` author: ${exports.author.value};`,
|
||||||
|
` license: ${exports.license.value};`,
|
||||||
|
` website: ${exports.website.value};`,
|
||||||
|
'}'
|
||||||
|
].join('\n')
|
||||||
|
})
|
||||||
|
|
||||||
|
const metaRule = computed(() => ({
|
||||||
|
component: '@meta',
|
||||||
|
directives: {
|
||||||
|
name: exports.name.value,
|
||||||
|
author: exports.author.value,
|
||||||
|
license: exports.license.value,
|
||||||
|
website: exports.website.value
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
// ## Palette stuff
|
||||||
|
const palettes = reactive([
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
bg: '#121a24',
|
||||||
|
fg: '#182230',
|
||||||
|
text: '#b9b9ba',
|
||||||
|
link: '#d8a070',
|
||||||
|
accent: '#d8a070',
|
||||||
|
cRed: '#FF0000',
|
||||||
|
cBlue: '#0095ff',
|
||||||
|
cGreen: '#0fa00f',
|
||||||
|
cOrange: '#ffa500'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'light',
|
||||||
|
bg: '#f2f6f9',
|
||||||
|
fg: '#d6dfed',
|
||||||
|
text: '#304055',
|
||||||
|
underlay: '#5d6086',
|
||||||
|
accent: '#f55b1b',
|
||||||
|
cBlue: '#0095ff',
|
||||||
|
cRed: '#d31014',
|
||||||
|
cGreen: '#0fa00f',
|
||||||
|
cOrange: '#ffa500',
|
||||||
|
border: '#d8e6f9'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
exports.palettes = palettes
|
||||||
|
|
||||||
|
// This is kinda dumb but you cannot "replace" reactive() object
|
||||||
|
// and so v-model simply fails when you try to chage (increase only?)
|
||||||
|
// length of the array. Since linter complains about mutating modelValue
|
||||||
|
// inside SelectMotion, the next best thing is to just wipe existing array
|
||||||
|
// and replace it with new one.
|
||||||
|
|
||||||
|
const onPalettesUpdate = (e) => {
|
||||||
|
palettes.splice(0, palettes.length)
|
||||||
|
palettes.push(...e)
|
||||||
|
}
|
||||||
|
exports.onPalettesUpdate = onPalettesUpdate
|
||||||
|
|
||||||
|
const selectedPaletteId = ref(0)
|
||||||
|
const selectedPalette = computed({
|
||||||
|
get () {
|
||||||
|
return palettes[selectedPaletteId.value]
|
||||||
|
},
|
||||||
|
set (newPalette) {
|
||||||
|
palettes[selectedPaletteId.value] = newPalette
|
||||||
|
}
|
||||||
|
})
|
||||||
|
exports.selectedPaletteId = selectedPaletteId
|
||||||
|
exports.selectedPalette = selectedPalette
|
||||||
|
provide('selectedPalette', selectedPalette)
|
||||||
|
|
||||||
|
watch([selectedPalette], () => updateOverallPreview())
|
||||||
|
|
||||||
|
exports.getNewPalette = () => ({
|
||||||
|
name: 'new palette',
|
||||||
|
bg: '#121a24',
|
||||||
|
fg: '#182230',
|
||||||
|
text: '#b9b9ba',
|
||||||
|
link: '#d8a070',
|
||||||
|
accent: '#d8a070',
|
||||||
|
cRed: '#FF0000',
|
||||||
|
cBlue: '#0095ff',
|
||||||
|
cGreen: '#0fa00f',
|
||||||
|
cOrange: '#ffa500'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Raw format
|
||||||
|
const palettesRule = computed(() => {
|
||||||
|
return palettes.map(palette => {
|
||||||
|
const { name, ...rest } = palette
|
||||||
|
return {
|
||||||
|
component: '@palette',
|
||||||
|
variant: name,
|
||||||
|
directives: Object
|
||||||
|
.entries(rest)
|
||||||
|
.filter(([k, v]) => v && k)
|
||||||
|
.reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Text format
|
||||||
|
const palettesOut = computed(() => {
|
||||||
|
return palettes.map(({ name, ...palette }) => {
|
||||||
|
const entries = Object
|
||||||
|
.entries(palette)
|
||||||
|
.filter(([k, v]) => v && k)
|
||||||
|
.map(([slot, data]) => ` ${slot}: ${data};`)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
return `@palette.${name} {\n${entries}\n}`
|
||||||
|
}).join('\n\n')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ## Components stuff
|
||||||
|
// Getting existing components
|
||||||
|
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
|
||||||
|
const componentKeysAll = componentsContext.keys()
|
||||||
|
const componentsMap = new Map(
|
||||||
|
componentKeysAll
|
||||||
|
.map(
|
||||||
|
key => [key, componentsContext(key).default]
|
||||||
|
).filter(([key, component]) => !component.virtual && !component.notEditable)
|
||||||
|
)
|
||||||
|
exports.componentsMap = componentsMap
|
||||||
|
const componentKeys = [...componentsMap.keys()]
|
||||||
|
exports.componentKeys = componentKeys
|
||||||
|
|
||||||
|
// Component list and selection
|
||||||
|
const selectedComponentKey = ref(componentsMap.keys().next().value)
|
||||||
|
exports.selectedComponentKey = selectedComponentKey
|
||||||
|
|
||||||
|
const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value))
|
||||||
|
const selectedComponentName = computed(() => selectedComponent.value.name)
|
||||||
|
|
||||||
|
// Selection basis
|
||||||
|
exports.selectedComponentVariants = computed(() => {
|
||||||
|
return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) })
|
||||||
|
})
|
||||||
|
exports.selectedComponentStates = computed(() => {
|
||||||
|
const all = Object.keys({ normal: null, ...(selectedComponent.value.states || {}) })
|
||||||
|
return all.filter(x => x !== 'normal')
|
||||||
|
})
|
||||||
|
|
||||||
|
// selection
|
||||||
|
const selectedVariant = ref('normal')
|
||||||
|
exports.selectedVariant = selectedVariant
|
||||||
|
const selectedState = reactive(new Set())
|
||||||
|
exports.selectedState = selectedState
|
||||||
|
exports.updateSelectedStates = (state, v) => {
|
||||||
|
if (v) {
|
||||||
|
selectedState.add(state)
|
||||||
|
} else {
|
||||||
|
selectedState.delete(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset variant and state on component change
|
||||||
|
const updateSelectedComponent = () => {
|
||||||
|
selectedVariant.value = 'normal'
|
||||||
|
selectedState.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
selectedComponentName,
|
||||||
|
updateSelectedComponent
|
||||||
|
)
|
||||||
|
|
||||||
|
// ### Rules stuff aka meat and potatoes
|
||||||
|
// The native structure of separate rules and the child -> parent
|
||||||
|
// relation isn't very convenient for editor, we replace the array
|
||||||
|
// and child -> parent structure with map and parent -> child structure
|
||||||
|
const rulesToEditorFriendly = (rules, root = {}) => rules.reduce((acc, rule) => {
|
||||||
|
const { parent: rParent, component: rComponent } = rule
|
||||||
|
const parent = rParent ?? rule
|
||||||
|
const hasChildren = !!rParent
|
||||||
|
const child = hasChildren ? rule : null
|
||||||
|
|
||||||
|
const {
|
||||||
|
component: pComponent,
|
||||||
|
variant: pVariant = 'normal',
|
||||||
|
state: pState = [] // no relation to Intel CPUs whatsoever
|
||||||
|
} = parent
|
||||||
|
|
||||||
|
const pPath = `${hasChildren ? pComponent : rComponent}.${pVariant}.${normalizeStates(pState)}`
|
||||||
|
|
||||||
|
let output = get(acc, pPath)
|
||||||
|
if (!output) {
|
||||||
|
set(acc, pPath, {})
|
||||||
|
output = get(acc, pPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
output._children = output._children ?? {}
|
||||||
|
const {
|
||||||
|
component: cComponent,
|
||||||
|
variant: cVariant = 'normal',
|
||||||
|
state: cState = [],
|
||||||
|
directives
|
||||||
|
} = child
|
||||||
|
|
||||||
|
const cPath = `${cComponent}.${cVariant}.${normalizeStates(cState)}`
|
||||||
|
set(output._children, cPath, { directives })
|
||||||
|
} else {
|
||||||
|
output.directives = parent.directives
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, root)
|
||||||
|
|
||||||
|
const editorFriendlyFallbackStructure = computed(() => {
|
||||||
|
const root = {}
|
||||||
|
|
||||||
|
componentKeys.forEach((componentKey) => {
|
||||||
|
const componentValue = componentsMap.get(componentKey)
|
||||||
|
const { defaultRules, name } = componentValue
|
||||||
|
rulesToEditorFriendly(
|
||||||
|
defaultRules.map((rule) => ({ ...rule, component: name })),
|
||||||
|
root
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return root
|
||||||
|
})
|
||||||
|
|
||||||
|
// Checking whether component can support some "directives" which
|
||||||
|
// are actually virtual subcomponents, i.e. Text, Link etc
|
||||||
|
exports.componentHas = (subComponent) => {
|
||||||
|
return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path for lodash's get and set
|
||||||
|
const getPath = (component, directive) => {
|
||||||
|
const pathSuffix = component ? `._children.${component}.normal.normal` : ''
|
||||||
|
const path = `${selectedComponentName.value}.${selectedVariant.value}.${normalizeStates([...selectedState])}${pathSuffix}.directives.${directive}`
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
// Templates for directives
|
||||||
|
const isElementPresent = (component, directive, defaultValue = '') => computed({
|
||||||
|
get () {
|
||||||
|
return get(allEditedRules.value, getPath(component, directive)) != null
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
if (value) {
|
||||||
|
const fallback = get(
|
||||||
|
editorFriendlyFallbackStructure.value,
|
||||||
|
getPath(component, directive)
|
||||||
|
)
|
||||||
|
set(allEditedRules.value, getPath(component, directive), fallback ?? defaultValue)
|
||||||
|
} else {
|
||||||
|
unset(allEditedRules.value, getPath(component, directive))
|
||||||
|
}
|
||||||
|
exports.updateOverallPreview()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const getEditedElement = (component, directive, postProcess = x => x) => computed({
|
||||||
|
get () {
|
||||||
|
let usedRule
|
||||||
|
const fallback = editorFriendlyFallbackStructure.value
|
||||||
|
const real = allEditedRules.value
|
||||||
|
const path = getPath(component, directive)
|
||||||
|
|
||||||
|
usedRule = get(real, path) // get real
|
||||||
|
if (!usedRule) {
|
||||||
|
usedRule = get(fallback, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return postProcess(usedRule)
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
if (value) {
|
||||||
|
set(allEditedRules.value, getPath(component, directive), value)
|
||||||
|
} else {
|
||||||
|
unset(allEditedRules.value, getPath(component, directive))
|
||||||
|
}
|
||||||
|
exports.updateOverallPreview()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// All the editable stuff for the component
|
||||||
|
exports.editedBackgroundColor = getEditedElement(null, 'background')
|
||||||
|
exports.isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
|
||||||
|
exports.editedOpacity = getEditedElement(null, 'opacity')
|
||||||
|
exports.isOpacityPresent = isElementPresent(null, 'opacity', 1)
|
||||||
|
exports.editedRoundness = getEditedElement(null, 'roundness')
|
||||||
|
exports.isRoundnessPresent = isElementPresent(null, 'roundness', '0')
|
||||||
|
exports.editedTextColor = getEditedElement('Text', 'textColor')
|
||||||
|
exports.isTextColorPresent = isElementPresent('Text', 'textColor', '#000000')
|
||||||
|
exports.editedTextAuto = getEditedElement('Text', 'textAuto')
|
||||||
|
exports.isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000')
|
||||||
|
exports.editedLinkColor = getEditedElement('Link', 'textColor')
|
||||||
|
exports.isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080')
|
||||||
|
exports.editedIconColor = getEditedElement('Icon', 'textColor')
|
||||||
|
exports.isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090')
|
||||||
|
exports.editedBorderColor = getEditedElement('Border', 'textColor')
|
||||||
|
exports.isBorderColorPresent = isElementPresent('Border', 'textColor', '#909090')
|
||||||
|
|
||||||
|
const getContrast = (bg, text) => {
|
||||||
|
try {
|
||||||
|
const bgRgb = hex2rgb(bg)
|
||||||
|
const textRgb = hex2rgb(text)
|
||||||
|
|
||||||
|
const ratio = getContrastRatio(bgRgb, textRgb)
|
||||||
|
return {
|
||||||
|
// TODO this ideally should be part of <ContractRatio />
|
||||||
|
ratio,
|
||||||
|
text: ratio.toPrecision(3) + ':1',
|
||||||
|
// AA level, AAA level
|
||||||
|
aa: ratio >= 4.5,
|
||||||
|
aaa: ratio >= 7,
|
||||||
|
// same but for 18pt+ texts
|
||||||
|
laa: ratio >= 3,
|
||||||
|
laaa: ratio >= 4.5
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failure computing contrast', e)
|
||||||
|
return { error: e }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeShadows = (shadows) => {
|
||||||
|
return shadows?.map(shadow => {
|
||||||
|
if (typeof shadow === 'object') {
|
||||||
|
return shadow
|
||||||
|
}
|
||||||
|
if (typeof shadow === 'string') {
|
||||||
|
try {
|
||||||
|
return deserializeShadow(shadow)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
return shadow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
provide('normalizeShadows', normalizeShadows)
|
||||||
|
|
||||||
|
// Shadow is partially edited outside the ShadowControl
|
||||||
|
// for better space utilization
|
||||||
|
const editedShadow = getEditedElement(null, 'shadow', normalizeShadows)
|
||||||
|
exports.editedShadow = editedShadow
|
||||||
|
const editedSubShadowId = ref(null)
|
||||||
|
exports.editedSubShadowId = editedSubShadowId
|
||||||
|
const editedSubShadow = computed(() => {
|
||||||
|
if (editedShadow.value == null || editedSubShadowId.value == null) return null
|
||||||
|
return editedShadow.value[editedSubShadowId.value]
|
||||||
|
})
|
||||||
|
exports.editedSubShadow = editedSubShadow
|
||||||
|
exports.isShadowPresent = isElementPresent(null, 'shadow', [])
|
||||||
|
exports.onSubShadow = (id) => {
|
||||||
|
if (id != null) {
|
||||||
|
editedSubShadowId.value = id
|
||||||
|
} else {
|
||||||
|
editedSubShadow.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.updateSubShadow = (axis, value) => {
|
||||||
|
if (!editedSubShadow.value || editedSubShadowId.value == null) return
|
||||||
|
const newEditedShadow = [...editedShadow.value]
|
||||||
|
|
||||||
|
newEditedShadow[editedSubShadowId.value] = {
|
||||||
|
...newEditedShadow[editedSubShadowId.value],
|
||||||
|
[axis]: value
|
||||||
|
}
|
||||||
|
|
||||||
|
editedShadow.value = newEditedShadow
|
||||||
|
}
|
||||||
|
exports.isShadowTabOpen = ref(false)
|
||||||
|
exports.onTabSwitch = (tab) => {
|
||||||
|
exports.isShadowTabOpen.value = tab === 'shadow'
|
||||||
|
}
|
||||||
|
|
||||||
|
// component preview
|
||||||
|
exports.editorHintStyle = computed(() => {
|
||||||
|
const editorHint = selectedComponent.value.editor
|
||||||
|
const styles = []
|
||||||
|
if (editorHint && Object.keys(editorHint).length > 0) {
|
||||||
|
if (editorHint.aspect != null) {
|
||||||
|
styles.push(`aspect-ratio: ${editorHint.aspect} !important;`)
|
||||||
|
}
|
||||||
|
if (editorHint.border != null) {
|
||||||
|
styles.push(`border-width: ${editorHint.border}px !important;`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return styles.join('; ')
|
||||||
|
})
|
||||||
|
|
||||||
|
const editorFriendlyToOriginal = computed(() => {
|
||||||
|
const resultRules = []
|
||||||
|
|
||||||
|
const convert = (component, data = {}, parent) => {
|
||||||
|
const variants = Object.entries(data || {})
|
||||||
|
|
||||||
|
variants.forEach(([variant, variantData]) => {
|
||||||
|
const states = Object.entries(variantData)
|
||||||
|
|
||||||
|
states.forEach(([jointState, stateData]) => {
|
||||||
|
const state = jointState.split(/:/g)
|
||||||
|
const result = {
|
||||||
|
component,
|
||||||
|
variant,
|
||||||
|
state,
|
||||||
|
directives: stateData.directives || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
result.parent = {
|
||||||
|
component: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultRules.push(result)
|
||||||
|
|
||||||
|
// Currently we only support single depth for simplicity's sake
|
||||||
|
if (!parent) {
|
||||||
|
Object.entries(stateData._children || {}).forEach(([cName, child]) => convert(cName, child, component))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
[...componentsMap.values()].forEach(({ name }) => {
|
||||||
|
convert(name, allEditedRules.value[name])
|
||||||
|
})
|
||||||
|
|
||||||
|
return resultRules
|
||||||
|
})
|
||||||
|
|
||||||
|
const allCustomVirtualDirectives = [...componentsMap.values()]
|
||||||
|
.map(c => {
|
||||||
|
return c
|
||||||
|
.defaultRules
|
||||||
|
.filter(c => c.component === 'Root')
|
||||||
|
.map(x => Object.entries(x.directives))
|
||||||
|
.flat()
|
||||||
|
})
|
||||||
|
.filter(x => x)
|
||||||
|
.flat()
|
||||||
|
.map(([name, value]) => {
|
||||||
|
const [valType, valVal] = value.split('|')
|
||||||
|
return {
|
||||||
|
name: name.substring(2),
|
||||||
|
valType: valType?.trim(),
|
||||||
|
value: valVal?.trim()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const virtualDirectives = ref(allCustomVirtualDirectives)
|
||||||
|
exports.virtualDirectives = virtualDirectives
|
||||||
|
exports.updateVirtualDirectives = (value) => {
|
||||||
|
virtualDirectives.value = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Raw format
|
||||||
|
const virtualDirectivesRule = computed(() => ({
|
||||||
|
component: 'Root',
|
||||||
|
directives: Object.fromEntries(
|
||||||
|
virtualDirectives.value.map(vd => [`--${vd.name}`, `${vd.valType} | ${vd.value}`])
|
||||||
|
)
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Text format
|
||||||
|
const virtualDirectivesOut = computed(() => {
|
||||||
|
return [
|
||||||
|
'Root {',
|
||||||
|
...virtualDirectives.value
|
||||||
|
.filter(vd => vd.name && vd.valType && vd.value)
|
||||||
|
.map(vd => ` --${vd.name}: ${vd.valType} | ${vd.value};`),
|
||||||
|
'}'
|
||||||
|
].join('\n')
|
||||||
|
})
|
||||||
|
|
||||||
|
exports.computeColor = (color) => {
|
||||||
|
let computedColor
|
||||||
|
try {
|
||||||
|
computedColor = findColor(color, { dynamicVars: dynamicVars.value, staticVars: staticVars.value })
|
||||||
|
if (computedColor) {
|
||||||
|
return rgb2hex(computedColor)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
provide('computeColor', exports.computeColor)
|
||||||
|
|
||||||
|
exports.contrast = computed(() => {
|
||||||
|
return getContrast(
|
||||||
|
exports.computeColor(previewColors.value.background),
|
||||||
|
exports.computeColor(previewColors.value.text)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ## Export and Import
|
||||||
|
const styleExporter = newExporter({
|
||||||
|
filename: () => exports.name.value ?? 'pleroma_theme',
|
||||||
|
mime: 'text/plain',
|
||||||
|
extension: 'piss',
|
||||||
|
getExportedObject: () => exportStyleData.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const onImport = parsed => {
|
||||||
|
const editorComponents = parsed.filter(x => x.component.startsWith('@'))
|
||||||
|
const rootComponent = parsed.find(x => x.component === 'Root')
|
||||||
|
const rules = parsed.filter(x => !x.component.startsWith('@') && x.component !== 'Root')
|
||||||
|
const metaIn = editorComponents.find(x => x.component === '@meta').directives
|
||||||
|
const palettesIn = editorComponents.filter(x => x.component === '@palette')
|
||||||
|
|
||||||
|
exports.name.value = metaIn.name
|
||||||
|
exports.license.value = metaIn.license
|
||||||
|
exports.author.value = metaIn.author
|
||||||
|
exports.website.value = metaIn.website
|
||||||
|
|
||||||
|
const newVirtualDirectives = Object
|
||||||
|
.entries(rootComponent.directives)
|
||||||
|
.map(([name, value]) => {
|
||||||
|
const [valType, valVal] = value.split('|').map(x => x.trim())
|
||||||
|
return { name: name.substring(2), valType, value: valVal }
|
||||||
|
})
|
||||||
|
virtualDirectives.value = newVirtualDirectives
|
||||||
|
|
||||||
|
onPalettesUpdate(palettesIn.map(x => ({ name: x.variant, ...x.directives })))
|
||||||
|
|
||||||
|
allEditedRules.value = rulesToEditorFriendly(rules)
|
||||||
|
|
||||||
|
exports.updateOverallPreview()
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleImporter = newImporter({
|
||||||
|
accept: '.piss',
|
||||||
|
parser (string) { return deserialize(string) },
|
||||||
|
onImportFailure (result) {
|
||||||
|
console.error('Failure importing style:', result)
|
||||||
|
this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
|
||||||
|
},
|
||||||
|
onImport
|
||||||
|
})
|
||||||
|
|
||||||
|
// Raw format
|
||||||
|
const exportRules = computed(() => [
|
||||||
|
metaRule.value,
|
||||||
|
...palettesRule.value,
|
||||||
|
virtualDirectivesRule.value,
|
||||||
|
...editorFriendlyToOriginal.value
|
||||||
|
])
|
||||||
|
|
||||||
|
// Text format
|
||||||
|
const exportStyleData = computed(() => {
|
||||||
|
return [
|
||||||
|
metaOut.value,
|
||||||
|
palettesOut.value,
|
||||||
|
virtualDirectivesOut.value,
|
||||||
|
serialize(editorFriendlyToOriginal.value)
|
||||||
|
].join('\n\n')
|
||||||
|
})
|
||||||
|
|
||||||
|
exports.clearStyle = () => {
|
||||||
|
onImport(store.state.interface.styleDataUsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.exportStyle = () => {
|
||||||
|
styleExporter.exportData()
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.importStyle = () => {
|
||||||
|
styleImporter.importData()
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.applyStyle = () => {
|
||||||
|
store.dispatch('setStyleCustom', exportRules.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const overallPreviewRules = ref([])
|
||||||
|
exports.overallPreviewRules = overallPreviewRules
|
||||||
|
|
||||||
|
const overallPreviewCssRules = ref([])
|
||||||
|
watchEffect(throttle(() => {
|
||||||
|
try {
|
||||||
|
overallPreviewCssRules.value = getScopedVersion(
|
||||||
|
getCssRules(overallPreviewRules.value),
|
||||||
|
'#edited-style-preview'
|
||||||
|
).join('\n')
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}, 500))
|
||||||
|
|
||||||
|
exports.overallPreviewCssRules = overallPreviewCssRules
|
||||||
|
|
||||||
|
const updateOverallPreview = throttle(() => {
|
||||||
|
try {
|
||||||
|
overallPreviewRules.value = init({
|
||||||
|
inputRuleset: [
|
||||||
|
...exportRules.value,
|
||||||
|
{
|
||||||
|
component: 'Root',
|
||||||
|
directives: Object.fromEntries(
|
||||||
|
Object
|
||||||
|
.entries(selectedPalette.value)
|
||||||
|
.filter(([k, v]) => k && v && k !== 'name')
|
||||||
|
.map(([k, v]) => [`--${k}`, `color | ${v}`])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
ultimateBackgroundColor: '#000000',
|
||||||
|
debug: true
|
||||||
|
}).eager
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Could not compile preview theme', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
//
|
||||||
|
// Apart from "hover" we can't really show how component looks like in
|
||||||
|
// certain states, so we have to fake them.
|
||||||
|
const simulatePseudoSelectors = (css, prefix) => css
|
||||||
|
.replace(prefix, '.component-preview .preview-block')
|
||||||
|
.replace(':active', '.preview-active')
|
||||||
|
.replace(':hover', '.preview-hover')
|
||||||
|
.replace(':active', '.preview-active')
|
||||||
|
.replace(':focus', '.preview-focus')
|
||||||
|
.replace(':focus-within', '.preview-focus-within')
|
||||||
|
.replace(':disabled', '.preview-disabled')
|
||||||
|
|
||||||
|
const previewRules = computed(() => {
|
||||||
|
const filtered = overallPreviewRules.value.filter(r => {
|
||||||
|
const componentMatch = r.component === selectedComponentName.value
|
||||||
|
const parentComponentMatch = r.parent?.component === selectedComponentName.value
|
||||||
|
if (!componentMatch && !parentComponentMatch) return false
|
||||||
|
const rule = parentComponentMatch ? r.parent : r
|
||||||
|
if (rule.component !== selectedComponentName.value) return false
|
||||||
|
if (rule.variant !== selectedVariant.value) return false
|
||||||
|
const ruleState = new Set(rule.state.filter(x => x !== 'normal'))
|
||||||
|
const differenceA = [...ruleState].filter(x => !selectedState.has(x))
|
||||||
|
const differenceB = [...selectedState].filter(x => !ruleState.has(x))
|
||||||
|
return (differenceA.length + differenceB.length) === 0
|
||||||
|
})
|
||||||
|
const sorted = [...filtered]
|
||||||
|
.filter(x => x.component === selectedComponentName.value)
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aSelectorLength = a.selector.split(/ /g).length
|
||||||
|
const bSelectorLength = b.selector.split(/ /g).length
|
||||||
|
return aSelectorLength - bSelectorLength
|
||||||
|
})
|
||||||
|
|
||||||
|
const prefix = sorted[0].selector
|
||||||
|
|
||||||
|
return filtered.filter(x => x.selector.startsWith(prefix))
|
||||||
|
})
|
||||||
|
|
||||||
|
exports.previewClass = computed(() => {
|
||||||
|
const selectors = []
|
||||||
|
if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') {
|
||||||
|
selectors.push(selectedComponent.value.variants[selectedVariant.value])
|
||||||
|
}
|
||||||
|
if (selectedState.size > 0) {
|
||||||
|
selectedState.forEach(state => {
|
||||||
|
const original = selectedComponent.value.states[state]
|
||||||
|
selectors.push(simulatePseudoSelectors(original))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return selectors.map(x => x.substring(1)).join('')
|
||||||
|
})
|
||||||
|
|
||||||
|
exports.previewCss = computed(() => {
|
||||||
|
try {
|
||||||
|
const prefix = previewRules.value[0].selector
|
||||||
|
const scoped = getCssRules(previewRules.value).map(x => simulatePseudoSelectors(x, prefix))
|
||||||
|
return scoped.join('\n')
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid ruleset', e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dynamicVars = computed(() => {
|
||||||
|
return previewRules.value[0].dynamicVars
|
||||||
|
})
|
||||||
|
|
||||||
|
const staticVars = computed(() => {
|
||||||
|
const rootComponent = overallPreviewRules.value.find(r => {
|
||||||
|
return r.component === 'Root'
|
||||||
|
})
|
||||||
|
const rootDirectivesEntries = Object.entries(rootComponent.directives)
|
||||||
|
const directives = {}
|
||||||
|
rootDirectivesEntries
|
||||||
|
.filter(([k, v]) => k.startsWith('--') && v.startsWith('color | '))
|
||||||
|
.map(([k, v]) => [k.substring(2), v.substring('color | '.length)])
|
||||||
|
.forEach(([k, v]) => {
|
||||||
|
directives[k] = findColor(v, { dynamicVars: {}, staticVars: directives })
|
||||||
|
})
|
||||||
|
return directives
|
||||||
|
})
|
||||||
|
provide('staticVars', staticVars)
|
||||||
|
exports.staticVars = staticVars
|
||||||
|
|
||||||
|
const previewColors = computed(() => {
|
||||||
|
const stacked = dynamicVars.value.stacked
|
||||||
|
const background = typeof stacked === 'string' ? stacked : rgb2hex(stacked)
|
||||||
|
return {
|
||||||
|
text: previewRules.value.find(r => r.component === 'Text')?.virtualDirectives['--text'],
|
||||||
|
link: previewRules.value.find(r => r.component === 'Link')?.virtualDirectives['--link'],
|
||||||
|
border: previewRules.value.find(r => r.component === 'Border')?.virtualDirectives['--border'],
|
||||||
|
icon: previewRules.value.find(r => r.component === 'Icon')?.virtualDirectives['--icon'],
|
||||||
|
background
|
||||||
|
}
|
||||||
|
})
|
||||||
|
exports.previewColors = previewColors
|
||||||
|
exports.updateOverallPreview = updateOverallPreview
|
||||||
|
|
||||||
|
updateOverallPreview()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[
|
||||||
|
allEditedRules.value,
|
||||||
|
palettes,
|
||||||
|
selectedPalette,
|
||||||
|
selectedState,
|
||||||
|
selectedVariant
|
||||||
|
],
|
||||||
|
updateOverallPreview
|
||||||
|
)
|
||||||
|
|
||||||
|
return exports
|
||||||
|
}
|
||||||
|
}
|
264
src/components/settings_modal/tabs/style_tab/style_tab.scss
Normal file
264
src/components/settings_modal/tabs/style_tab/style_tab.scss
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
.StyleTab {
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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(1fr, 10em);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
383
src/components/settings_modal/tabs/style_tab/style_tab.vue
Normal file
383
src/components/settings_modal/tabs/style_tab/style_tab.vue
Normal file
|
@ -0,0 +1,383 @@
|
||||||
|
<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">
|
||||||
|
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
|
||||||
|
<component
|
||||||
|
:is="'style'"
|
||||||
|
v-html="overallPreviewCssRules"
|
||||||
|
/>
|
||||||
|
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
|
||||||
|
<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 class="meta-field" v-model="name">
|
||||||
|
{{ $t('settings.style.themes3.editor.style_name') }}
|
||||||
|
</StringSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<StringSetting class="meta-field" v-model="author">
|
||||||
|
{{ $t('settings.style.themes3.editor.style_author') }}
|
||||||
|
</StringSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<StringSetting class="meta-field" v-model="license">
|
||||||
|
{{ $t('settings.style.themes3.editor.style_license') }}
|
||||||
|
</StringSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<StringSetting class="meta-field" v-model="website">
|
||||||
|
{{ $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')"
|
||||||
|
>
|
||||||
|
<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:modelValue="(v) => updateSelectedStates(state, v)"
|
||||||
|
>
|
||||||
|
{{ state }}
|
||||||
|
</Checkbox>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="preview-container">
|
||||||
|
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||||
|
<component
|
||||||
|
:is="'style'"
|
||||||
|
v-html="previewCss"
|
||||||
|
/>
|
||||||
|
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
|
||||||
|
<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"
|
||||||
|
: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"
|
||||||
|
: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
|
||||||
|
class="style-control suboption"
|
||||||
|
v-if="componentHas('Text')"
|
||||||
|
>
|
||||||
|
<label class="label">
|
||||||
|
{{$t('settings.style.themes3.editor.contrast') }}
|
||||||
|
</label>
|
||||||
|
<ContrastRatio
|
||||||
|
:show-ratio="true"
|
||||||
|
:contrast="contrast"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="componentHas('Text')">
|
||||||
|
</div>
|
||||||
|
<ColorInput
|
||||||
|
v-if="componentHas('Link')"
|
||||||
|
v-model="editedLinkColor"
|
||||||
|
: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"
|
||||||
|
: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"
|
||||||
|
: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"
|
||||||
|
: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"
|
||||||
|
: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"
|
||||||
|
@subShadowSelected="onSubShadow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</tab-switcher>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="palette"
|
||||||
|
:label="$t('settings.style.themes3.editor.palette_tab')"
|
||||||
|
class="setting-item list-editor palette-editor"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
:modelValue="palettes"
|
||||||
|
@update:modelValue="onPalettesUpdate"
|
||||||
|
:selected-id="selectedPaletteId"
|
||||||
|
:get-add-value="getNewPalette"
|
||||||
|
@update:selectedId="e => selectedPaletteId = e"
|
||||||
|
/>
|
||||||
|
<div class="list-edit-area">
|
||||||
|
<StringSetting
|
||||||
|
class="palette-name-input"
|
||||||
|
v-model="selectedPalette.name"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.themes3.palette.name_label') }}
|
||||||
|
</StringSetting>
|
||||||
|
<PaletteEditor
|
||||||
|
class="palette-editor-single"
|
||||||
|
v-model="selectedPalette"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VirtualDirectivesTab
|
||||||
|
key="variables"
|
||||||
|
:label="$t('settings.style.themes3.editor.variables_tab')"
|
||||||
|
:model-value="virtualDirectives"
|
||||||
|
@update:modelValue="updateVirtualDirectives"
|
||||||
|
:normalize-shadows="normalizeShadows"
|
||||||
|
/>
|
||||||
|
</tab-switcher>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style src="./style_tab.scss" lang="scss"></style>
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { ref, computed, watch, inject } from 'vue'
|
||||||
|
|
||||||
|
import Select from 'src/components/select/select.vue'
|
||||||
|
import SelectMotion from 'src/components/select/select_motion.vue'
|
||||||
|
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
|
||||||
|
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||||
|
|
||||||
|
import { serializeShadow } from 'src/services/theme_data/iss_serializer.js'
|
||||||
|
|
||||||
|
// helper for debugging
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Select,
|
||||||
|
SelectMotion,
|
||||||
|
ShadowControl,
|
||||||
|
ColorInput
|
||||||
|
},
|
||||||
|
props: ['modelValue'],
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup (props, context) {
|
||||||
|
const exports = {}
|
||||||
|
const emit = context.emit
|
||||||
|
|
||||||
|
exports.emit = emit
|
||||||
|
exports.computeColor = inject('computeColor')
|
||||||
|
exports.staticVars = inject('staticVars')
|
||||||
|
|
||||||
|
const selectedVirtualDirectiveId = ref(0)
|
||||||
|
exports.selectedVirtualDirectiveId = selectedVirtualDirectiveId
|
||||||
|
|
||||||
|
const selectedVirtualDirective = computed({
|
||||||
|
get () {
|
||||||
|
return props.modelValue[selectedVirtualDirectiveId.value]
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
const newVD = [...props.modelValue]
|
||||||
|
newVD[selectedVirtualDirectiveId.value] = value
|
||||||
|
|
||||||
|
emit('update:modelValue', newVD)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
exports.selectedVirtualDirective = selectedVirtualDirective
|
||||||
|
|
||||||
|
exports.selectedVirtualDirectiveValType = computed({
|
||||||
|
get () {
|
||||||
|
return props.modelValue[selectedVirtualDirectiveId.value].valType
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
const newValType = value
|
||||||
|
let newValue
|
||||||
|
switch (value) {
|
||||||
|
case 'shadow':
|
||||||
|
newValue = '0 0 0 #000000 / 1'
|
||||||
|
break
|
||||||
|
case 'color':
|
||||||
|
newValue = '#000000'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
newValue = 'none'
|
||||||
|
}
|
||||||
|
const newName = props.modelValue[selectedVirtualDirectiveId.value].name
|
||||||
|
props.modelValue[selectedVirtualDirectiveId.value] = {
|
||||||
|
name: newName,
|
||||||
|
value: newValue,
|
||||||
|
valType: newValType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const draftVirtualDirectiveValid = ref(true)
|
||||||
|
const draftVirtualDirective = ref({})
|
||||||
|
exports.draftVirtualDirective = draftVirtualDirective
|
||||||
|
const normalizeShadows = inject('normalizeShadows')
|
||||||
|
|
||||||
|
watch(
|
||||||
|
selectedVirtualDirective,
|
||||||
|
(directive) => {
|
||||||
|
switch (directive.valType) {
|
||||||
|
case 'shadow': {
|
||||||
|
if (Array.isArray(directive.value)) {
|
||||||
|
draftVirtualDirective.value = normalizeShadows(directive.value)
|
||||||
|
} else {
|
||||||
|
const splitShadow = directive.value.split(/,/g).map(x => x.trim())
|
||||||
|
draftVirtualDirective.value = normalizeShadows(splitShadow)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'color':
|
||||||
|
draftVirtualDirective.value = directive.value
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
draftVirtualDirective.value = directive.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
draftVirtualDirective,
|
||||||
|
(directive) => {
|
||||||
|
try {
|
||||||
|
switch (selectedVirtualDirective.value.valType) {
|
||||||
|
case 'shadow': {
|
||||||
|
props.modelValue[selectedVirtualDirectiveId.value].value =
|
||||||
|
directive.map(x => serializeShadow(x)).join(', ')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
props.modelValue[selectedVirtualDirectiveId.value].value = directive
|
||||||
|
}
|
||||||
|
draftVirtualDirectiveValid.value = true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Invalid virtual directive value', e)
|
||||||
|
draftVirtualDirectiveValid.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
exports.getNewVirtualDirective = () => ({
|
||||||
|
name: 'newDirective',
|
||||||
|
valType: 'generic',
|
||||||
|
value: 'foobar'
|
||||||
|
})
|
||||||
|
|
||||||
|
return exports
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
<script src="./virtual_directives_tab.js"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="setting-item list-editor variables-editor">
|
||||||
|
<label
|
||||||
|
class="list-select-label"
|
||||||
|
for="variables-selector"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.themes3.editor.variables.label') }}
|
||||||
|
{{ ' ' }}
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
id="variables-selector"
|
||||||
|
v-model="selectedVirtualDirectiveId"
|
||||||
|
class="list-select"
|
||||||
|
size="20"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="(p, index) in modelValue"
|
||||||
|
:key="p.name"
|
||||||
|
:value="index"
|
||||||
|
>
|
||||||
|
{{ p.name }}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
<SelectMotion
|
||||||
|
class="list-select-movement"
|
||||||
|
:model-value="modelValue"
|
||||||
|
@update:modelValue="e => emit('update:modelValue', e)"
|
||||||
|
:selected-id="selectedVirtualDirectiveId"
|
||||||
|
@update:selectedId="e => selectedVirtualDirectiveId = e"
|
||||||
|
:get-add-value="getNewVirtualDirective"
|
||||||
|
/>
|
||||||
|
<div class="list-edit-area">
|
||||||
|
<div class="variable-selector">
|
||||||
|
<label
|
||||||
|
class="variable-name-label"
|
||||||
|
for="variables-selector"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.themes3.editor.variables.name_label') }}
|
||||||
|
{{ ' ' }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
v-model="selectedVirtualDirective.name"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="variable-type-label"
|
||||||
|
for="variables-selector"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.themes3.editor.variables.type_label') }}
|
||||||
|
{{ ' ' }}
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
v-model="selectedVirtualDirectiveValType"
|
||||||
|
>
|
||||||
|
<option value='shadow'>
|
||||||
|
{{ $t('settings.style.themes3.editor.variables.type_shadow') }}
|
||||||
|
</option>
|
||||||
|
<option value='color'>
|
||||||
|
{{ $t('settings.style.themes3.editor.variables.type_color') }}
|
||||||
|
</option>
|
||||||
|
<option value='generic'>
|
||||||
|
{{ $t('settings.style.themes3.editor.variables.type_generic') }}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<ShadowControl
|
||||||
|
v-if="selectedVirtualDirectiveValType === 'shadow'"
|
||||||
|
v-model="draftVirtualDirective"
|
||||||
|
:static-vars="staticVars"
|
||||||
|
:compact="true"
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
v-if="selectedVirtualDirectiveValType === 'color'"
|
||||||
|
v-model="draftVirtualDirective"
|
||||||
|
:fallback="computeColor(draftVirtualDirective)"
|
||||||
|
:label="$t('settings.style.themes3.editor.variables.virtual_color')"
|
||||||
|
:hide-optional-checkbox="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -4,9 +4,6 @@ import {
|
||||||
getContrastRatioLayers,
|
getContrastRatioLayers,
|
||||||
relativeLuminance
|
relativeLuminance
|
||||||
} from 'src/services/color_convert/color_convert.js'
|
} from 'src/services/color_convert/color_convert.js'
|
||||||
import {
|
|
||||||
getThemes
|
|
||||||
} from 'src/services/style_setter/style_setter.js'
|
|
||||||
import {
|
import {
|
||||||
newImporter,
|
newImporter,
|
||||||
newExporter
|
newExporter
|
||||||
|
@ -123,31 +120,24 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
const self = this
|
const currentIndex = this.$store.state.instance.themesIndex
|
||||||
|
|
||||||
getThemes()
|
let promise
|
||||||
.then((promises) => {
|
if (currentIndex) {
|
||||||
return Promise.all(
|
promise = Promise.resolve(currentIndex)
|
||||||
Object.entries(promises)
|
} else {
|
||||||
.map(([k, v]) => v.then(res => [k, res]))
|
promise = this.$store.dispatch('fetchThemesIndex')
|
||||||
)
|
}
|
||||||
})
|
|
||||||
.then(themes => themes.reduce((acc, [k, v]) => {
|
promise.then(themesIndex => {
|
||||||
if (v) {
|
Object
|
||||||
return {
|
.values(themesIndex)
|
||||||
...acc,
|
.forEach(themeFunc => {
|
||||||
[k]: v
|
themeFunc().then(themeData => this.availableStyles.push(themeData))
|
||||||
}
|
})
|
||||||
} else {
|
})
|
||||||
return acc
|
|
||||||
}
|
|
||||||
}, {}))
|
|
||||||
.then((themesComplete) => {
|
|
||||||
self.availableStyles = themesComplete
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.loadThemeFromLocalStorage()
|
|
||||||
if (typeof this.shadowSelected === 'undefined') {
|
if (typeof this.shadowSelected === 'undefined') {
|
||||||
this.shadowSelected = this.shadowsAvailable[0]
|
this.shadowSelected = this.shadowsAvailable[0]
|
||||||
}
|
}
|
||||||
|
@ -305,6 +295,9 @@ export default {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
themeDataUsed () {
|
||||||
|
return this.$store.state.interface.themeDataUsed
|
||||||
|
},
|
||||||
shadowsAvailable () {
|
shadowsAvailable () {
|
||||||
return Object.keys(DEFAULT_SHADOWS).sort()
|
return Object.keys(DEFAULT_SHADOWS).sort()
|
||||||
},
|
},
|
||||||
|
@ -412,9 +405,6 @@ export default {
|
||||||
forceUseSource = false
|
forceUseSource = false
|
||||||
) {
|
) {
|
||||||
this.dismissWarning()
|
this.dismissWarning()
|
||||||
if (!source && !theme) {
|
|
||||||
throw new Error('Can\'t load theme: empty')
|
|
||||||
}
|
|
||||||
const version = (origin === 'localStorage' && !theme.colors)
|
const version = (origin === 'localStorage' && !theme.colors)
|
||||||
? 'l1'
|
? 'l1'
|
||||||
: fileVersion
|
: fileVersion
|
||||||
|
@ -490,22 +480,11 @@ export default {
|
||||||
this.dismissWarning()
|
this.dismissWarning()
|
||||||
},
|
},
|
||||||
loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
|
loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
|
||||||
const {
|
const theme = this.themeDataUsed?.source
|
||||||
customTheme: theme,
|
if (theme) {
|
||||||
customThemeSource: source
|
|
||||||
} = this.$store.getters.mergedConfig
|
|
||||||
if (!theme && !source) {
|
|
||||||
// Anon user or never touched themes
|
|
||||||
this.loadTheme(
|
|
||||||
this.$store.state.instance.themeData,
|
|
||||||
'defaults',
|
|
||||||
confirmLoadSource
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.loadTheme(
|
this.loadTheme(
|
||||||
{
|
{
|
||||||
theme,
|
theme
|
||||||
source: forceSnapshot ? theme : source
|
|
||||||
},
|
},
|
||||||
'localStorage',
|
'localStorage',
|
||||||
confirmLoadSource
|
confirmLoadSource
|
||||||
|
@ -724,6 +703,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
themeDataUsed () {
|
||||||
|
this.loadThemeFromLocalStorage()
|
||||||
|
},
|
||||||
currentRadii () {
|
currentRadii () {
|
||||||
try {
|
try {
|
||||||
this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii
|
this.previewTheme.radii = generateRadii({ radii: this.currentRadii }).theme.radii
|
||||||
|
|
|
@ -45,12 +45,16 @@
|
||||||
flex: 0;
|
flex: 0;
|
||||||
|
|
||||||
&[type="number"] {
|
&[type="number"] {
|
||||||
min-width: 5em;
|
min-width: 9em;
|
||||||
|
|
||||||
|
&.-small {
|
||||||
|
min-width: 5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[type="range"] {
|
&[type="range"] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 2em;
|
min-width: 9em;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin: 0 0.5em;
|
margin: 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,14 +187,14 @@
|
||||||
name="accentColor"
|
name="accentColor"
|
||||||
:fallback="previewTheme.colors?.link"
|
:fallback="previewTheme.colors?.link"
|
||||||
:label="$t('settings.accent')"
|
:label="$t('settings.accent')"
|
||||||
:show-optional-tickbox="typeof linkColorLocal !== 'undefined'"
|
:show-optional-checkbox="typeof linkColorLocal !== 'undefined'"
|
||||||
/>
|
/>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
v-model="linkColorLocal"
|
v-model="linkColorLocal"
|
||||||
name="linkColor"
|
name="linkColor"
|
||||||
:fallback="previewTheme.colors?.accent"
|
:fallback="previewTheme.colors?.accent"
|
||||||
:label="$t('settings.links')"
|
:label="$t('settings.links')"
|
||||||
:show-optional-tickbox="typeof accentColorLocal !== 'undefined'"
|
:show-optional-checkbox="typeof accentColorLocal !== 'undefined'"
|
||||||
/>
|
/>
|
||||||
<ContrastRatio :contrast="previewContrast.bgLink" />
|
<ContrastRatio :contrast="previewContrast.bgLink" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -957,6 +957,8 @@
|
||||||
v-model="currentShadow"
|
v-model="currentShadow"
|
||||||
:separate-inset="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"
|
:separate-inset="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"
|
||||||
:fallback="currentShadowFallback"
|
:fallback="currentShadowFallback"
|
||||||
|
:static-vars="previewTheme.colors"
|
||||||
|
:compact="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
import ColorInput from 'src/components/color_input/color_input.vue'
|
import ColorInput from 'src/components/color_input/color_input.vue'
|
||||||
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
|
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
|
||||||
import Select from 'src/components/select/select.vue'
|
import Select from 'src/components/select/select.vue'
|
||||||
|
import SelectMotion from 'src/components/select/select_motion.vue'
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import Popover from 'src/components/popover/popover.vue'
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
import ComponentPreview from 'src/components/component_preview/component_preview.vue'
|
import ComponentPreview from 'src/components/component_preview/component_preview.vue'
|
||||||
import { getCssShadow, getCssShadowFilter } from '../../services/theme_data/theme_data.service.js'
|
import { rgb2hex } from 'src/services/color_convert/color_convert.js'
|
||||||
|
import { serializeShadow } from 'src/services/theme_data/iss_serializer.js'
|
||||||
|
import { deserializeShadow } from 'src/services/theme_data/iss_deserializer.js'
|
||||||
|
import { getCssShadow, getCssShadowFilter } from 'src/services/theme_data/css_utils.js'
|
||||||
|
import { findShadow, findColor } from 'src/services/theme_data/theme_data_3.service.js'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { throttle } from 'lodash'
|
import { throttle, flattenDeep } from 'lodash'
|
||||||
import {
|
import {
|
||||||
faTimes,
|
faTimes,
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
|
@ -21,50 +26,83 @@ library.add(
|
||||||
faPlus
|
faPlus
|
||||||
)
|
)
|
||||||
|
|
||||||
const toModel = (object = {}) => ({
|
const toModel = (input) => {
|
||||||
x: 0,
|
if (typeof input === 'object') {
|
||||||
y: 0,
|
return {
|
||||||
blur: 0,
|
x: 0,
|
||||||
spread: 0,
|
y: 0,
|
||||||
inset: false,
|
blur: 0,
|
||||||
color: '#000000',
|
spread: 0,
|
||||||
alpha: 1,
|
inset: false,
|
||||||
...object
|
color: '#000000',
|
||||||
})
|
alpha: 1,
|
||||||
|
...input
|
||||||
|
}
|
||||||
|
} else if (typeof input === 'string') {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: [
|
props: [
|
||||||
'modelValue', 'fallback', 'separateInset', 'noPreview', 'disabled'
|
'modelValue',
|
||||||
|
'fallback',
|
||||||
|
'separateInset',
|
||||||
|
'noPreview',
|
||||||
|
'disabled',
|
||||||
|
'staticVars',
|
||||||
|
'compact'
|
||||||
],
|
],
|
||||||
emits: ['update:modelValue', 'subShadowSelected'],
|
emits: ['update:modelValue', 'subShadowSelected'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
selectedId: 0,
|
selectedId: 0,
|
||||||
// TODO there are some bugs regarding display of array (it's not getting updated when deleting for some reason)
|
invalid: false
|
||||||
cValue: (this.modelValue ?? this.fallback ?? []).map(toModel)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ColorInput,
|
ColorInput,
|
||||||
OpacityInput,
|
OpacityInput,
|
||||||
Select,
|
Select,
|
||||||
|
SelectMotion,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Popover,
|
Popover,
|
||||||
ComponentPreview
|
ComponentPreview
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
|
||||||
this.cValue = (this.modelValue ?? this.fallback ?? []).map(toModel)
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
selected () {
|
cValue: {
|
||||||
const selected = this.cValue[this.selectedId]
|
get () {
|
||||||
if (selected) {
|
return (this.modelValue ?? this.fallback ?? []).map(toModel)
|
||||||
return { ...selected }
|
},
|
||||||
|
set (newVal) {
|
||||||
|
this.$emit('update:modelValue', newVal)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedType: {
|
||||||
|
get () {
|
||||||
|
return typeof this.selected
|
||||||
|
},
|
||||||
|
set (newType) {
|
||||||
|
this.selected = toModel(newType === 'object' ? {} : '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
get () {
|
||||||
|
const selected = this.cValue[this.selectedId]
|
||||||
|
if (selected && typeof selected === 'object') {
|
||||||
|
return { ...selected }
|
||||||
|
} else if (typeof selected === 'string') {
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
this.cValue[this.selectedId] = toModel(value)
|
||||||
|
this.$emit('update:modelValue', this.cValue)
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
},
|
},
|
||||||
present () {
|
present () {
|
||||||
return this.selected != null && !this.usingFallback
|
return this.selected != null && this.modelValue != null
|
||||||
},
|
},
|
||||||
shadowsAreNull () {
|
shadowsAreNull () {
|
||||||
return this.modelValue == null
|
return this.modelValue == null
|
||||||
|
@ -72,24 +110,43 @@ export default {
|
||||||
currentFallback () {
|
currentFallback () {
|
||||||
return this.fallback?.[this.selectedId]
|
return this.fallback?.[this.selectedId]
|
||||||
},
|
},
|
||||||
moveUpValid () {
|
getColorFallback () {
|
||||||
return this.selectedId > 0
|
if (this.staticVars && this.selected?.color) {
|
||||||
},
|
try {
|
||||||
moveDnValid () {
|
const computedColor = findColor(this.selected.color, { dynamicVars: {}, staticVars: this.staticVars }, true)
|
||||||
return this.selectedId < this.cValue.length - 1
|
if (computedColor) return rgb2hex(computedColor)
|
||||||
},
|
return null
|
||||||
usingFallback () {
|
} catch (e) {
|
||||||
return this.modelValue == null
|
console.warn(e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.currentFallback?.color
|
||||||
|
}
|
||||||
},
|
},
|
||||||
style () {
|
style () {
|
||||||
if (this.separateInset) {
|
try {
|
||||||
return {
|
let result
|
||||||
filter: getCssShadowFilter(this.cValue),
|
const serialized = this.cValue.map(x => serializeShadow(x)).join(',')
|
||||||
boxShadow: getCssShadow(this.cValue, true)
|
serialized.split(/,/).map(deserializeShadow) // validate
|
||||||
|
const expandedShadow = flattenDeep(findShadow(this.cValue, { dynamicVars: {}, staticVars: this.staticVars }))
|
||||||
|
const fixedShadows = expandedShadow.map(x => ({ ...x, color: console.log(x) || rgb2hex(x.color) }))
|
||||||
|
|
||||||
|
if (this.separateInset) {
|
||||||
|
result = {
|
||||||
|
filter: getCssShadowFilter(fixedShadows),
|
||||||
|
boxShadow: getCssShadow(fixedShadows, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = {
|
||||||
|
boxShadow: getCssShadow(fixedShadows)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
this.invalid = false
|
||||||
return {
|
return result
|
||||||
boxShadow: getCssShadow(this.cValue)
|
} catch (e) {
|
||||||
|
console.error('Invalid shadow', e)
|
||||||
|
this.invalid = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -99,34 +156,25 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getNewSubshadow () {
|
||||||
|
return toModel(this.selected)
|
||||||
|
},
|
||||||
|
onSelectChange (id) {
|
||||||
|
this.selectedId = id
|
||||||
|
},
|
||||||
|
getSubshadowLabel (shadow, index) {
|
||||||
|
if (typeof shadow === 'object') {
|
||||||
|
return shadow?.name ?? this.$t('settings.style.shadows.shadow_id', { value: index })
|
||||||
|
} else if (typeof shadow === 'string') {
|
||||||
|
return shadow || this.$t('settings.style.shadows.empty_expression')
|
||||||
|
}
|
||||||
|
},
|
||||||
updateProperty: throttle(function (prop, value) {
|
updateProperty: throttle(function (prop, value) {
|
||||||
this.cValue[this.selectedId][prop] = value
|
this.cValue[this.selectedId][prop] = value
|
||||||
if (prop === 'inset' && value === false && this.separateInset) {
|
if (prop === 'inset' && value === false && this.separateInset) {
|
||||||
this.cValue[this.selectedId].spread = 0
|
this.cValue[this.selectedId].spread = 0
|
||||||
}
|
}
|
||||||
this.$emit('update:modelValue', this.cValue)
|
this.$emit('update:modelValue', this.cValue)
|
||||||
}, 100),
|
}, 100)
|
||||||
add () {
|
|
||||||
this.cValue.push(toModel(this.selected))
|
|
||||||
this.selectedId = Math.max(this.cValue.length - 1, 0)
|
|
||||||
this.$emit('update:modelValue', this.cValue)
|
|
||||||
},
|
|
||||||
del () {
|
|
||||||
this.cValue.splice(this.selectedId, 1)
|
|
||||||
this.selectedId = this.cValue.length === 0 ? undefined : Math.max(this.selectedId - 1, 0)
|
|
||||||
this.$emit('update:modelValue', this.cValue)
|
|
||||||
},
|
|
||||||
moveUp () {
|
|
||||||
const movable = this.cValue.splice(this.selectedId, 1)[0]
|
|
||||||
this.cValue.splice(this.selectedId - 1, 0, movable)
|
|
||||||
this.selectedId -= 1
|
|
||||||
this.$emit('update:modelValue', this.cValue)
|
|
||||||
},
|
|
||||||
moveDn () {
|
|
||||||
const movable = this.cValue.splice(this.selectedId, 1)[0]
|
|
||||||
this.cValue.splice(this.selectedId + 1, 0, movable)
|
|
||||||
this.selectedId += 1
|
|
||||||
this.$emit('update:modelValue', this.cValue)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,29 @@
|
||||||
.settings-modal .settings-modal-panel .shadow-control {
|
.ShadowControl {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: 10em 1fr 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-areas: "selector preview tweak";
|
||||||
|
grid-gap: 0.5em;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
grid-gap: 0.25em;
|
|
||||||
margin-bottom: 1em;
|
&.-compact {
|
||||||
|
grid-template-columns: 10em 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"selector preview"
|
||||||
|
"tweak tweak";
|
||||||
|
|
||||||
|
&.-no-preview {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 10em 1fr;
|
||||||
|
grid-template-areas:
|
||||||
|
"selector"
|
||||||
|
"tweak";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.shadow-switcher {
|
.shadow-switcher {
|
||||||
|
grid-area: selector;
|
||||||
order: 1;
|
order: 1;
|
||||||
flex: 1 0 6em;
|
flex: 1 0 6em;
|
||||||
min-width: 6em;
|
min-width: 6em;
|
||||||
|
@ -16,27 +34,18 @@
|
||||||
.shadow-list {
|
.shadow-list {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrange-buttons {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
display: grid;
|
|
||||||
grid-auto-columns: 1fr;
|
|
||||||
grid-auto-flow: column;
|
|
||||||
margin-top: 0.25em;
|
|
||||||
|
|
||||||
.button-default {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-tweak {
|
.shadow-tweak {
|
||||||
|
grid-area: tweak;
|
||||||
order: 3;
|
order: 3;
|
||||||
flex: 2 0 10em;
|
flex: 2 0 10em;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
margin-left: 0.125em;
|
margin-left: 0.125em;
|
||||||
margin-right: 0.125em;
|
margin-right: 0.125em;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
grid-gap: 0.25em;
|
||||||
|
|
||||||
/* hack */
|
/* hack */
|
||||||
.input-boolean {
|
.input-boolean {
|
||||||
|
@ -52,6 +61,11 @@
|
||||||
flex: 1 0 5em;
|
flex: 1 0 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shadow-expression {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.id-control {
|
.id-control {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
|
@ -69,6 +83,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-no-preview {
|
&.-no-preview {
|
||||||
|
grid-template-columns: 10em 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
grid-template-areas: "selector tweak";
|
||||||
|
|
||||||
.shadow-tweak {
|
.shadow-tweak {
|
||||||
order: 0;
|
order: 0;
|
||||||
flex: 2 0 8em;
|
flex: 2 0 8em;
|
||||||
|
@ -91,15 +109,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-preview {
|
.shadow-preview {
|
||||||
order: 2;
|
grid-area: preview;
|
||||||
flex: 3 3 15em;
|
min-width: 25em;
|
||||||
min-width: 10em;
|
|
||||||
margin-left: 0.125em;
|
margin-left: 0.125em;
|
||||||
align-self: start;
|
align-self: start;
|
||||||
|
justify-self: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inset-tooltip {
|
.inset-tooltip {
|
||||||
padding: 0.5em;
|
|
||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="label shadow-control"
|
class="ShadowControl label shadow-control"
|
||||||
:class="{ disabled: disabled || !present, '-no-preview': noPreview }"
|
:class="{ disabled: disabled || !present, '-no-preview': noPreview, '-compact': compact }"
|
||||||
>
|
>
|
||||||
<ComponentPreview
|
<ComponentPreview
|
||||||
v-if="!noPreview"
|
v-if="!noPreview"
|
||||||
|
:invalid="invalid"
|
||||||
class="shadow-preview"
|
class="shadow-preview"
|
||||||
:shadow-control="true"
|
:shadow-control="true"
|
||||||
:shadow="selected"
|
:shadow="selected"
|
||||||
|
@ -17,8 +18,8 @@
|
||||||
id="shadow-list"
|
id="shadow-list"
|
||||||
v-model="selectedId"
|
v-model="selectedId"
|
||||||
class="shadow-list"
|
class="shadow-list"
|
||||||
size="10"
|
size="4"
|
||||||
:disabled="shadowsAreNull"
|
:disabled="disabled || shadowsAreNull"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="(shadow, index) in cValue"
|
v-for="(shadow, index) in cValue"
|
||||||
|
@ -26,227 +27,208 @@
|
||||||
:value="index"
|
:value="index"
|
||||||
:class="{ '-active': index === Number(selectedId) }"
|
:class="{ '-active': index === Number(selectedId) }"
|
||||||
>
|
>
|
||||||
{{ shadow?.name ?? $t('settings.style.shadows.shadow_id', { value: index }) }}
|
{{ getSubshadowLabel(shadow, index) }}
|
||||||
</option>
|
</option>
|
||||||
</Select>
|
</Select>
|
||||||
<div
|
<SelectMotion
|
||||||
class="id-control btn-group arrange-buttons"
|
v-model="cValue"
|
||||||
>
|
:selected-id="selectedId"
|
||||||
<button
|
:get-add-value="getNewSubshadow"
|
||||||
class="btn button-default"
|
:disabled="disabled"
|
||||||
:disabled="disabled || shadowsAreNull"
|
@update:selectedId="onSelectChange"
|
||||||
@click="add"
|
/>
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
icon="plus"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn button-default"
|
|
||||||
:disabled="disabled || !moveUpValid"
|
|
||||||
:class="{ disabled: disabled || !moveUpValid }"
|
|
||||||
@click="moveUp"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
icon="chevron-up"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn button-default"
|
|
||||||
:disabled="disabled || !moveDnValid"
|
|
||||||
:class="{ disabled: disabled || !moveDnValid }"
|
|
||||||
@click="moveDn"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
icon="chevron-down"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn button-default"
|
|
||||||
:disabled="disabled || !present"
|
|
||||||
:class="{ disabled: disabled || !present }"
|
|
||||||
@click="del"
|
|
||||||
>
|
|
||||||
<FAIcon
|
|
||||||
fixed-width
|
|
||||||
icon="times"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="shadow-tweak">
|
<div class="shadow-tweak">
|
||||||
<div
|
<Select
|
||||||
:class="{ disabled: disabled || !present }"
|
v-model="selectedType"
|
||||||
class="name-control style-control"
|
:disabled="disabled || !present"
|
||||||
>
|
>
|
||||||
<label
|
<option value="object">
|
||||||
for="name"
|
{{ $t('settings.style.shadows.raw') }}
|
||||||
class="label"
|
</option>
|
||||||
:class="{ faint: disabled || !present }"
|
<option value="string">
|
||||||
>
|
{{ $t('settings.style.shadows.expression') }}
|
||||||
{{ $t('settings.style.shadows.name') }}
|
</option>
|
||||||
</label>
|
</Select>
|
||||||
<input
|
<template v-if="selectedType === 'string'">
|
||||||
id="name"
|
<textarea
|
||||||
:value="selected?.name"
|
v-model="selected"
|
||||||
:disabled="disabled || !present"
|
class="input shadow-expression"
|
||||||
|
:disabled="disabled || shadowsAreNull"
|
||||||
|
:class="{disabled: disabled || shadowsAreNull}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="selectedType === 'object'">
|
||||||
|
<div
|
||||||
:class="{ disabled: disabled || !present }"
|
:class="{ disabled: disabled || !present }"
|
||||||
name="name"
|
class="name-control style-control"
|
||||||
class="input input-string"
|
|
||||||
@input="e => updateProperty('name', e.target.value)"
|
|
||||||
>
|
>
|
||||||
</div>
|
<label
|
||||||
<div
|
for="name"
|
||||||
:disabled="disabled || !present"
|
class="label"
|
||||||
class="inset-control style-control"
|
:class="{ faint: disabled || !present }"
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
id="inset"
|
|
||||||
:value="selected?.inset"
|
|
||||||
:disabled="disabled || !present"
|
|
||||||
name="inset"
|
|
||||||
class="input-inset input-boolean"
|
|
||||||
@input="e => updateProperty('inset', e.target.checked)"
|
|
||||||
>
|
|
||||||
<template #before>
|
|
||||||
{{ $t('settings.style.shadows.inset') }}
|
|
||||||
</template>
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
:disabled="disabled || !present"
|
|
||||||
:class="{ disabled: disabled || !present }"
|
|
||||||
class="blur-control style-control"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
for="blur"
|
|
||||||
class="label"
|
|
||||||
:class="{ faint: disabled || !present }"
|
|
||||||
>
|
|
||||||
{{ $t('settings.style.shadows.blur') }}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="blur"
|
|
||||||
:value="selected?.blur"
|
|
||||||
:disabled="disabled || !present"
|
|
||||||
:class="{ disabled: disabled || !present }"
|
|
||||||
name="blur"
|
|
||||||
class="input input-range"
|
|
||||||
type="range"
|
|
||||||
max="20"
|
|
||||||
min="0"
|
|
||||||
@input="e => updateProperty('blur', e.target.value)"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:value="selected?.blur"
|
|
||||||
class="input input-number -small"
|
|
||||||
:disabled="disabled || !present"
|
|
||||||
:class="{ disabled: disabled || !present }"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
@input="e => updateProperty('blur', e.target.value)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="spread-control style-control"
|
|
||||||
:class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
for="spread"
|
|
||||||
class="label"
|
|
||||||
:class="{ faint: disabled || !present || (separateInset && !selected?.inset) }"
|
|
||||||
>
|
|
||||||
{{ $t('settings.style.shadows.spread') }}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="spread"
|
|
||||||
:value="selected?.spread"
|
|
||||||
:disabled="disabled || !present || (separateInset && !selected?.inset)"
|
|
||||||
:class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }"
|
|
||||||
name="spread"
|
|
||||||
class="input input-range"
|
|
||||||
type="range"
|
|
||||||
max="20"
|
|
||||||
min="-20"
|
|
||||||
@input="e => updateProperty('spread', e.target.value)"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:value="selected?.spread"
|
|
||||||
class="input input-number -small"
|
|
||||||
:class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }"
|
|
||||||
:disabled="{ disabled: disabled || !present || (separateInset && !selected?.inset) }"
|
|
||||||
type="number"
|
|
||||||
@input="e => updateProperty('spread', e.target.value)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<ColorInput
|
|
||||||
:model-value="selected?.color"
|
|
||||||
:disabled="disabled || !present"
|
|
||||||
:label="$t('settings.style.common.color')"
|
|
||||||
:fallback="currentFallback?.color"
|
|
||||||
:show-optional-tickbox="false"
|
|
||||||
name="shadow"
|
|
||||||
@update:modelValue="e => updateProperty('color', e)"
|
|
||||||
/>
|
|
||||||
<OpacityInput
|
|
||||||
:model-value="selected?.alpha"
|
|
||||||
:disabled="disabled || !present"
|
|
||||||
@update:modelValue="e => updateProperty('alpha', e)"
|
|
||||||
/>
|
|
||||||
<i18n-t
|
|
||||||
scope="global"
|
|
||||||
keypath="settings.style.shadows.hintV3"
|
|
||||||
:class="{ faint: disabled || !present }"
|
|
||||||
tag="p"
|
|
||||||
>
|
|
||||||
<code>--variable,mod</code>
|
|
||||||
</i18n-t>
|
|
||||||
<Popover
|
|
||||||
v-if="separateInset"
|
|
||||||
trigger="hover"
|
|
||||||
>
|
|
||||||
<template #trigger>
|
|
||||||
<div
|
|
||||||
class="inset-alert alert warning"
|
|
||||||
>
|
>
|
||||||
<FAIcon icon="exclamation-triangle" />
|
{{ $t('settings.style.shadows.name') }}
|
||||||
|
</label>
|
||||||
{{ $t('settings.style.shadows.filter_hint.avatar_inset_short') }}
|
<input
|
||||||
</div>
|
id="name"
|
||||||
</template>
|
:value="selected?.name"
|
||||||
<template #content>
|
:disabled="disabled || !present"
|
||||||
<div class="inset-tooltip">
|
:class="{ disabled: disabled || !present }"
|
||||||
<i18n-t
|
name="name"
|
||||||
scope="global"
|
class="input input-string"
|
||||||
keypath="settings.style.shadows.filter_hint.always_drop_shadow"
|
@input="e => updateProperty('name', e.target.value)"
|
||||||
tag="p"
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:disabled="disabled || !present"
|
||||||
|
class="inset-control style-control"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
id="inset"
|
||||||
|
:value="selected?.inset"
|
||||||
|
:disabled="disabled || !present"
|
||||||
|
name="inset"
|
||||||
|
class="input-inset input-boolean"
|
||||||
|
@input="e => updateProperty('inset', e.target.checked)"
|
||||||
|
>
|
||||||
|
<template #before>
|
||||||
|
{{ $t('settings.style.shadows.inset') }}
|
||||||
|
</template>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:disabled="disabled || !present"
|
||||||
|
:class="{ disabled: disabled || !present }"
|
||||||
|
class="blur-control style-control"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
for="blur"
|
||||||
|
class="label"
|
||||||
|
:class="{ faint: disabled || !present }"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.shadows.blur') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="blur"
|
||||||
|
:value="selected?.blur"
|
||||||
|
:disabled="disabled || !present"
|
||||||
|
:class="{ disabled: disabled || !present }"
|
||||||
|
name="blur"
|
||||||
|
class="input input-range"
|
||||||
|
type="range"
|
||||||
|
max="20"
|
||||||
|
min="0"
|
||||||
|
@input="e => updateProperty('blur', e.target.value)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:value="selected?.blur"
|
||||||
|
class="input input-number -small"
|
||||||
|
:disabled="disabled || !present"
|
||||||
|
:class="{ disabled: disabled || !present }"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
@input="e => updateProperty('blur', e.target.value)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="spread-control style-control"
|
||||||
|
:class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
for="spread"
|
||||||
|
class="label"
|
||||||
|
:class="{ faint: disabled || !present || (separateInset && !selected?.inset) }"
|
||||||
|
>
|
||||||
|
{{ $t('settings.style.shadows.spread') }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="spread"
|
||||||
|
:value="selected?.spread"
|
||||||
|
:class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }"
|
||||||
|
:disabled="disabled || !present || (separateInset && !selected?.inset)"
|
||||||
|
name="spread"
|
||||||
|
class="input input-range"
|
||||||
|
type="range"
|
||||||
|
max="20"
|
||||||
|
min="-20"
|
||||||
|
@input="e => updateProperty('spread', e.target.value)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:value="selected?.spread"
|
||||||
|
class="input input-number -small"
|
||||||
|
:class="{ disabled: disabled || !present || (separateInset && !selected?.inset) }"
|
||||||
|
:disabled="disabled || !present || (separateInset && !selected?.inset)"
|
||||||
|
type="number"
|
||||||
|
@input="e => updateProperty('spread', e.target.value)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<ColorInput
|
||||||
|
:model-value="selected?.color"
|
||||||
|
:disabled="disabled || !present"
|
||||||
|
:label="$t('settings.style.common.color')"
|
||||||
|
:fallback="getColorFallback"
|
||||||
|
:show-optional-checkbox="false"
|
||||||
|
name="shadow"
|
||||||
|
@update:modelValue="e => updateProperty('color', e)"
|
||||||
|
/>
|
||||||
|
<OpacityInput
|
||||||
|
:model-value="selected?.alpha"
|
||||||
|
:disabled="disabled || !present"
|
||||||
|
@update:modelValue="e => updateProperty('alpha', e)"
|
||||||
|
/>
|
||||||
|
<i18n-t
|
||||||
|
scope="global"
|
||||||
|
keypath="settings.style.shadows.hintV3"
|
||||||
|
:class="{ faint: disabled || !present }"
|
||||||
|
tag="p"
|
||||||
|
>
|
||||||
|
<code>--variable,mod</code>
|
||||||
|
</i18n-t>
|
||||||
|
<Popover
|
||||||
|
v-if="separateInset"
|
||||||
|
trigger="hover"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<div
|
||||||
|
class="inset-alert alert warning"
|
||||||
>
|
>
|
||||||
<code>filter: drop-shadow()</code>
|
<FAIcon icon="exclamation-triangle" />
|
||||||
</i18n-t>
|
|
||||||
<p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p>
|
{{ $t('settings.style.shadows.filter_hint.avatar_inset_short') }}
|
||||||
<i18n-t
|
</div>
|
||||||
scope="global"
|
</template>
|
||||||
keypath="settings.style.shadows.filter_hint.drop_shadow_syntax"
|
<template #content>
|
||||||
tag="p"
|
<div class="inset-tooltip tooltip">
|
||||||
>
|
<i18n-t
|
||||||
<code>drop-shadow</code>
|
scope="global"
|
||||||
<code>spread-radius</code>
|
keypath="settings.style.shadows.filter_hint.always_drop_shadow"
|
||||||
<code>inset</code>
|
tag="p"
|
||||||
</i18n-t>
|
>
|
||||||
<i18n-t
|
<code>filter: drop-shadow()</code>
|
||||||
scope="global"
|
</i18n-t>
|
||||||
keypath="settings.style.shadows.filter_hint.inset_classic"
|
<p>{{ $t('settings.style.shadows.filter_hint.avatar_inset') }}</p>
|
||||||
tag="p"
|
<i18n-t
|
||||||
>
|
scope="global"
|
||||||
<code>box-shadow</code>
|
keypath="settings.style.shadows.filter_hint.drop_shadow_syntax"
|
||||||
</i18n-t>
|
tag="p"
|
||||||
<p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p>
|
>
|
||||||
</div>
|
<code>drop-shadow</code>
|
||||||
</template>
|
<code>spread-radius</code>
|
||||||
</Popover>
|
<code>inset</code>
|
||||||
|
</i18n-t>
|
||||||
|
<i18n-t
|
||||||
|
scope="global"
|
||||||
|
keypath="settings.style.shadows.filter_hint.inset_classic"
|
||||||
|
tag="p"
|
||||||
|
>
|
||||||
|
<code>box-shadow</code>
|
||||||
|
</i18n-t>
|
||||||
|
<p>{{ $t('settings.style.shadows.filter_hint.spread_zero') }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -14,14 +14,14 @@ export default {
|
||||||
{
|
{
|
||||||
directives: {
|
directives: {
|
||||||
background: '--fg',
|
background: '--fg',
|
||||||
shadow: ['--defaultButtonShadow', '--defaultButtonBevel'],
|
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
|
||||||
roundness: 3
|
roundness: 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: ['hover'],
|
state: ['hover'],
|
||||||
directives: {
|
directives: {
|
||||||
shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel']
|
shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -33,14 +33,14 @@ export default {
|
||||||
{
|
{
|
||||||
state: ['hover', 'active'],
|
state: ['hover', 'active'],
|
||||||
directives: {
|
directives: {
|
||||||
shadow: ['--defaultButtonShadow', '--defaultButtonBevel']
|
shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: ['disabled'],
|
state: ['disabled'],
|
||||||
directives: {
|
directives: {
|
||||||
background: '$blend(--inheritedBackground, 0.25, --parent)',
|
background: '$blend(--inheritedBackground 0.25 --parent)',
|
||||||
shadow: ['--defaultButtonBevel']
|
shadow: ['--buttonDefaultBevel']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -119,7 +119,7 @@
|
||||||
.tab {
|
.tab {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
min-width: 10em;
|
max-width: 9em;
|
||||||
min-width: 1px;
|
min-width: 1px;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
|
@ -128,12 +128,22 @@
|
||||||
margin-right: -200px;
|
margin-right: -200px;
|
||||||
margin-left: 1em;
|
margin-left: 1em;
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
@media all and (max-width: 800px) {
|
@media all and (max-width: 800px) {
|
||||||
padding-left: 0.25em;
|
padding-left: 0.25em;
|
||||||
padding-right: calc(0.25em + 200px);
|
padding-right: calc(0.25em + 200px);
|
||||||
margin-right: calc(0.25em - 200px);
|
margin-right: calc(0.25em - 200px);
|
||||||
margin-left: 0.25em;
|
margin-left: 0.25em;
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -181,6 +191,7 @@
|
||||||
|
|
||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
|
|
24
src/components/tooltip/tooltip.vue
Normal file
24
src/components/tooltip/tooltip.vue
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<Popover trigger="hover">
|
||||||
|
<template #trigger>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="tooltip">
|
||||||
|
{{ props.text }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
|
||||||
|
const props = defineProps(['text'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.tooltip {
|
||||||
|
margin: 0.5em 1em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,7 @@
|
||||||
export default {
|
export default {
|
||||||
name: 'UserCard',
|
name: 'UserCard',
|
||||||
selector: '.user-card',
|
selector: '.user-card',
|
||||||
|
notEditable: true,
|
||||||
validInnerComponents: [
|
validInnerComponents: [
|
||||||
'Text',
|
'Text',
|
||||||
'Link',
|
'Link',
|
||||||
|
@ -25,7 +26,7 @@ export default {
|
||||||
color: '#000000',
|
color: '#000000',
|
||||||
alpha: 0.6
|
alpha: 0.6
|
||||||
}],
|
}],
|
||||||
'--profileTint': 'color | $alpha(--background, 0.5)'
|
'--profileTint': 'color | $alpha(--background 0.5)'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -701,6 +701,7 @@
|
||||||
"use_websockets": "Use websockets (Realtime updates)",
|
"use_websockets": "Use websockets (Realtime updates)",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
|
"theme_old": "Theme editor (old)",
|
||||||
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
|
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
|
||||||
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
||||||
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
||||||
|
@ -750,11 +751,81 @@
|
||||||
"more_settings": "More settings",
|
"more_settings": "More settings",
|
||||||
"style": {
|
"style": {
|
||||||
"custom_theme_used": "(Custom theme)",
|
"custom_theme_used": "(Custom theme)",
|
||||||
|
"custom_style_used": "(Custom style)",
|
||||||
|
"stock_theme_used": "(Stock theme)",
|
||||||
"themes2_outdated": "Editor for Themes V2 is being phased out and will eventually be replaced with a new one that takes advantage of new Themes V3 engine. It should still work but experience might be degraded and inconsistent.",
|
"themes2_outdated": "Editor for Themes V2 is being phased out and will eventually be replaced with a new one that takes advantage of new Themes V3 engine. It should still work but experience might be degraded and inconsistent.",
|
||||||
"appearance_tab_note": "Changes on this tab do not affect the theme used, so exported theme will be different from what seen in the UI",
|
"appearance_tab_note": "Changes on this tab do not affect the theme used, so exported theme will be different from what seen in the UI",
|
||||||
"update_preview": "Update preview",
|
"update_preview": "Update preview",
|
||||||
"themes3": {
|
"themes3": {
|
||||||
"define": "Override",
|
"define": "Override",
|
||||||
|
"palette": {
|
||||||
|
"label": "Color schemes",
|
||||||
|
"name_label": "Color scheme name",
|
||||||
|
"import": "Import palette",
|
||||||
|
"export": "Export palette",
|
||||||
|
"apply": "Apply palette",
|
||||||
|
"bg": "Panel background",
|
||||||
|
"fg": "Buttons etc.",
|
||||||
|
"text": "Text",
|
||||||
|
"link": "Links",
|
||||||
|
"accent": "Accent color",
|
||||||
|
"cRed": "Red color",
|
||||||
|
"cBlue": "Blue color",
|
||||||
|
"cGreen": "Green color",
|
||||||
|
"cOrange": "Orange color",
|
||||||
|
"wallpaper": "Wallpaper",
|
||||||
|
"v2_unsupported": "Older v2 themes don't support palettes. Switch to v3 theme to make use of palettes",
|
||||||
|
"bundled": "Bundled palettes",
|
||||||
|
"style": "Palettes provided by selected style",
|
||||||
|
"user": "Custom palette",
|
||||||
|
"imported": "Imported"
|
||||||
|
},
|
||||||
|
"editor": {
|
||||||
|
"title": "Style editor",
|
||||||
|
"reset_style": "Reset",
|
||||||
|
"load_style": "Open from file",
|
||||||
|
"save_style": "Save",
|
||||||
|
"style_name": "Stylesheet name",
|
||||||
|
"style_author": "Made by",
|
||||||
|
"style_license": "License",
|
||||||
|
"style_website": "Website",
|
||||||
|
"component_selector": "Component",
|
||||||
|
"variant_selector": "Variant",
|
||||||
|
"states_selector": "States",
|
||||||
|
"main_tab": "Main",
|
||||||
|
"shadows_tab": "Shadows",
|
||||||
|
"background": "Background color",
|
||||||
|
"text_color": "Text color",
|
||||||
|
"icon_color": "Icon color",
|
||||||
|
"link_color": "Link color",
|
||||||
|
"contrast": "Text contrast",
|
||||||
|
"roundness": "Roundness",
|
||||||
|
"opacity": "Opacity",
|
||||||
|
"border_color": "Border color",
|
||||||
|
"include_in_rule": "Add to rule",
|
||||||
|
"test_string": "TEST",
|
||||||
|
"invalid": "Invalid",
|
||||||
|
"refresh_preview": "Refresh preview",
|
||||||
|
"apply_preview": "Apply",
|
||||||
|
"text_auto": {
|
||||||
|
"label": "Auto-contrast",
|
||||||
|
"no-preserve": "Black or White",
|
||||||
|
"preserve": "Keep color",
|
||||||
|
"no-auto": "Disabled"
|
||||||
|
},
|
||||||
|
"component_tab": "Components style",
|
||||||
|
"palette_tab": "Color schemes",
|
||||||
|
"variables_tab": "Variables (Advanced)",
|
||||||
|
"variables": {
|
||||||
|
"label": "Variables",
|
||||||
|
"name_label": "Name:",
|
||||||
|
"type_label": "Type:",
|
||||||
|
"type_shadow": "Shadow",
|
||||||
|
"type_color": "Color",
|
||||||
|
"type_generic": "Generic",
|
||||||
|
"virtual_color": "Variable color value"
|
||||||
|
}
|
||||||
|
},
|
||||||
"hacks": {
|
"hacks": {
|
||||||
"underlay_overrides": "Change underlay",
|
"underlay_overrides": "Change underlay",
|
||||||
"underlay_override_mode_none": "Theme default",
|
"underlay_override_mode_none": "Theme default",
|
||||||
|
@ -877,11 +948,18 @@
|
||||||
"override": "Override",
|
"override": "Override",
|
||||||
"shadow_id": "Shadow #{value}",
|
"shadow_id": "Shadow #{value}",
|
||||||
"offset": "Shadow offset",
|
"offset": "Shadow offset",
|
||||||
|
"zoom": "Zoom",
|
||||||
|
"offset-x": "x:",
|
||||||
|
"offset-y": "y:",
|
||||||
"light_grid": "Use light checkerboard",
|
"light_grid": "Use light checkerboard",
|
||||||
|
"color_override": "Use different color",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"blur": "Blur",
|
"blur": "Blur",
|
||||||
"spread": "Spread",
|
"spread": "Spread",
|
||||||
"inset": "Inset",
|
"inset": "Inset",
|
||||||
|
"raw": "Plain shadow",
|
||||||
|
"expression": "Expression (advanced)",
|
||||||
|
"empty_expression": "Empty expression",
|
||||||
"hintV3": "For shadows you can also use the {0} notation to use other color slot.",
|
"hintV3": "For shadows you can also use the {0} notation to use other color slot.",
|
||||||
"filter_hint": {
|
"filter_hint": {
|
||||||
"always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.",
|
"always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.",
|
||||||
|
|
|
@ -47,6 +47,10 @@ export const defaultState = {
|
||||||
customThemeSource: undefined, // "source", stores original theme data
|
customThemeSource: undefined, // "source", stores original theme data
|
||||||
|
|
||||||
// V3
|
// V3
|
||||||
|
style: null,
|
||||||
|
styleCustomData: null,
|
||||||
|
palette: null,
|
||||||
|
paletteCustomData: null,
|
||||||
themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
|
themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions
|
||||||
forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists
|
forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists
|
||||||
theme3hacks: { // Hacks, user overrides that are independent of theme used
|
theme3hacks: { // Hacks, user overrides that are independent of theme used
|
||||||
|
|
|
@ -42,6 +42,9 @@ const defaultState = {
|
||||||
registrationOpen: true,
|
registrationOpen: true,
|
||||||
server: 'http://localhost:4040/',
|
server: 'http://localhost:4040/',
|
||||||
textlimit: 5000,
|
textlimit: 5000,
|
||||||
|
themesIndex: undefined,
|
||||||
|
stylesIndex: undefined,
|
||||||
|
palettesIndex: undefined,
|
||||||
themeData: undefined, // used for theme editor v2
|
themeData: undefined, // used for theme editor v2
|
||||||
vapidPublicKey: undefined,
|
vapidPublicKey: undefined,
|
||||||
|
|
||||||
|
@ -96,6 +99,8 @@ const defaultState = {
|
||||||
sidebarRight: false,
|
sidebarRight: false,
|
||||||
subjectLineBehavior: 'email',
|
subjectLineBehavior: 'email',
|
||||||
theme: 'pleroma-dark',
|
theme: 'pleroma-dark',
|
||||||
|
palette: null,
|
||||||
|
style: null,
|
||||||
emojiReactionsScale: 0.5,
|
emojiReactionsScale: 0.5,
|
||||||
textSize: '14px',
|
textSize: '14px',
|
||||||
emojiSize: '2.2rem',
|
emojiSize: '2.2rem',
|
||||||
|
|
|
@ -1,10 +1,23 @@
|
||||||
import { getPreset, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js'
|
import { getResourcesIndex, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js'
|
||||||
import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js'
|
import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js'
|
||||||
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
|
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
|
||||||
|
import { deserialize } from '../services/theme_data/iss_deserializer.js'
|
||||||
|
|
||||||
|
// helper for debugging
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
|
||||||
|
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
localFonts: null,
|
localFonts: null,
|
||||||
themeApplied: false,
|
themeApplied: false,
|
||||||
|
themeVersion: 'v3',
|
||||||
|
styleNameUsed: null,
|
||||||
|
styleDataUsed: null,
|
||||||
|
useStylePalette: false, // hack for applying styles from appearance tab
|
||||||
|
paletteNameUsed: null,
|
||||||
|
paletteDataUsed: null,
|
||||||
|
themeNameUsed: null,
|
||||||
|
themeDataUsed: null,
|
||||||
temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout
|
temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout
|
||||||
temporaryChangesConfirm: () => {}, // used for applying temporary options
|
temporaryChangesConfirm: () => {}, // used for applying temporary options
|
||||||
temporaryChangesRevert: () => {}, // used for reverting temporary options
|
temporaryChangesRevert: () => {}, // used for reverting temporary options
|
||||||
|
@ -212,142 +225,450 @@ const interfaceMod = {
|
||||||
setLastTimeline ({ commit }, value) {
|
setLastTimeline ({ commit }, value) {
|
||||||
commit('setLastTimeline', value)
|
commit('setLastTimeline', value)
|
||||||
},
|
},
|
||||||
setTheme ({ commit, rootState }, { themeName, themeData, recompile, saveData } = {}) {
|
async fetchPalettesIndex ({ commit, state }) {
|
||||||
|
try {
|
||||||
|
const value = await getResourcesIndex('/static/palettes/index.json')
|
||||||
|
commit('setInstanceOption', { name: 'palettesIndex', value })
|
||||||
|
return value
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Could not fetch palettes index', e)
|
||||||
|
commit('setInstanceOption', { name: 'palettesIndex', value: { _error: e } })
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setPalette ({ dispatch, commit }, value) {
|
||||||
|
dispatch('resetThemeV3Palette')
|
||||||
|
dispatch('resetThemeV2')
|
||||||
|
|
||||||
|
commit('setOption', { name: 'palette', value })
|
||||||
|
|
||||||
|
dispatch('applyTheme', { recompile: true })
|
||||||
|
},
|
||||||
|
setPaletteCustom ({ dispatch, commit }, value) {
|
||||||
|
dispatch('resetThemeV3Palette')
|
||||||
|
dispatch('resetThemeV2')
|
||||||
|
|
||||||
|
commit('setOption', { name: 'paletteCustomData', value })
|
||||||
|
|
||||||
|
dispatch('applyTheme', { recompile: true })
|
||||||
|
},
|
||||||
|
async fetchStylesIndex ({ commit, state }) {
|
||||||
|
try {
|
||||||
|
const value = await getResourcesIndex(
|
||||||
|
'/static/styles/index.json',
|
||||||
|
deserialize
|
||||||
|
)
|
||||||
|
commit('setInstanceOption', { name: 'stylesIndex', value })
|
||||||
|
return value
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Could not fetch styles index', e)
|
||||||
|
commit('setInstanceOption', { name: 'stylesIndex', value: { _error: e } })
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setStyle ({ dispatch, commit, state }, value) {
|
||||||
|
dispatch('resetThemeV3')
|
||||||
|
dispatch('resetThemeV2')
|
||||||
|
dispatch('resetThemeV3Palette')
|
||||||
|
|
||||||
|
commit('setOption', { name: 'style', value })
|
||||||
|
state.useStylePalette = true
|
||||||
|
|
||||||
|
dispatch('applyTheme', { recompile: true }).then(() => {
|
||||||
|
state.useStylePalette = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setStyleCustom ({ dispatch, commit, state }, value) {
|
||||||
|
dispatch('resetThemeV3')
|
||||||
|
dispatch('resetThemeV2')
|
||||||
|
dispatch('resetThemeV3Palette')
|
||||||
|
|
||||||
|
commit('setOption', { name: 'styleCustomData', value })
|
||||||
|
|
||||||
|
state.useStylePalette = true
|
||||||
|
dispatch('applyTheme', { recompile: true }).then(() => {
|
||||||
|
state.useStylePalette = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async fetchThemesIndex ({ commit, state }) {
|
||||||
|
try {
|
||||||
|
const value = await getResourcesIndex('/static/styles.json')
|
||||||
|
commit('setInstanceOption', { name: 'themesIndex', value })
|
||||||
|
return value
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Could not fetch themes index', e)
|
||||||
|
commit('setInstanceOption', { name: 'themesIndex', value: { _error: e } })
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setTheme ({ dispatch, commit }, value) {
|
||||||
|
dispatch('resetThemeV3')
|
||||||
|
dispatch('resetThemeV3Palette')
|
||||||
|
dispatch('resetThemeV2')
|
||||||
|
|
||||||
|
commit('setOption', { name: 'theme', value })
|
||||||
|
|
||||||
|
dispatch('applyTheme', { recompile: true })
|
||||||
|
},
|
||||||
|
setThemeCustom ({ dispatch, commit }, value) {
|
||||||
|
dispatch('resetThemeV3')
|
||||||
|
dispatch('resetThemeV3Palette')
|
||||||
|
dispatch('resetThemeV2')
|
||||||
|
|
||||||
|
commit('setOption', { name: 'customTheme', value })
|
||||||
|
commit('setOption', { name: 'customThemeSource', value })
|
||||||
|
|
||||||
|
dispatch('applyTheme', { recompile: true })
|
||||||
|
},
|
||||||
|
resetThemeV3 ({ dispatch, commit }) {
|
||||||
|
commit('setOption', { name: 'style', value: null })
|
||||||
|
commit('setOption', { name: 'styleCustomData', value: null })
|
||||||
|
},
|
||||||
|
resetThemeV3Palette ({ dispatch, commit }) {
|
||||||
|
commit('setOption', { name: 'palette', value: null })
|
||||||
|
commit('setOption', { name: 'paletteCustomData', value: null })
|
||||||
|
},
|
||||||
|
resetThemeV2 ({ dispatch, commit }) {
|
||||||
|
commit('setOption', { name: 'theme', value: null })
|
||||||
|
commit('setOption', { name: 'customTheme', value: null })
|
||||||
|
commit('setOption', { name: 'customThemeSource', value: null })
|
||||||
|
},
|
||||||
|
async getThemeData ({ dispatch, commit, rootState, state }) {
|
||||||
|
const getData = async (resource, index, customData, name) => {
|
||||||
|
const capitalizedResource = resource[0].toUpperCase() + resource.slice(1)
|
||||||
|
const result = {}
|
||||||
|
|
||||||
|
if (customData) {
|
||||||
|
result.nameUsed = 'custom' // custom data overrides name
|
||||||
|
result.dataUsed = customData
|
||||||
|
} else {
|
||||||
|
result.nameUsed = name
|
||||||
|
|
||||||
|
if (result.nameUsed == null) {
|
||||||
|
result.dataUsed = null
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchFunc = index[result.nameUsed]
|
||||||
|
// Fallbacks
|
||||||
|
if (!fetchFunc) {
|
||||||
|
if (resource === 'style' || resource === 'palette') {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
const newName = Object.keys(index)[0]
|
||||||
|
fetchFunc = index[newName]
|
||||||
|
console.warn(`${capitalizedResource} with id '${state.styleNameUsed}' not found, trying back to '${newName}'`)
|
||||||
|
if (!fetchFunc) {
|
||||||
|
console.warn(`${capitalizedResource} doesn't have a fallback, defaulting to stock.`)
|
||||||
|
fetchFunc = () => Promise.resolve(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.dataUsed = await fetchFunc()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
theme: instanceThemeName
|
style: instanceStyleName,
|
||||||
|
palette: instancePaletteName
|
||||||
|
} = rootState.instance
|
||||||
|
|
||||||
|
let {
|
||||||
|
theme: instanceThemeV2Name,
|
||||||
|
themesIndex,
|
||||||
|
stylesIndex,
|
||||||
|
palettesIndex
|
||||||
} = rootState.instance
|
} = rootState.instance
|
||||||
|
|
||||||
const {
|
const {
|
||||||
theme: userThemeName,
|
style: userStyleName,
|
||||||
customTheme: userThemeSnapshot,
|
styleCustomData: userStyleCustomData,
|
||||||
customThemeSource: userThemeSource,
|
palette: userPaletteName,
|
||||||
|
paletteCustomData: userPaletteCustomData
|
||||||
|
} = rootState.config
|
||||||
|
|
||||||
|
let {
|
||||||
|
theme: userThemeV2Name,
|
||||||
|
customTheme: userThemeV2Snapshot,
|
||||||
|
customThemeSource: userThemeV2Source
|
||||||
|
} = rootState.config
|
||||||
|
|
||||||
|
let majorVersionUsed
|
||||||
|
|
||||||
|
console.debug(
|
||||||
|
`User V3 palette: ${userPaletteName}, style: ${userStyleName} , custom: ${!!userStyleCustomData}`
|
||||||
|
)
|
||||||
|
console.debug(
|
||||||
|
`User V2 name: ${userThemeV2Name}, source: ${!!userThemeV2Source}, snapshot: ${!!userThemeV2Snapshot}`
|
||||||
|
)
|
||||||
|
|
||||||
|
console.debug(`Instance V3 palette: ${instancePaletteName}, style: ${instanceStyleName}`)
|
||||||
|
console.debug('Instance V2 theme: ' + instanceThemeV2Name)
|
||||||
|
|
||||||
|
if (userPaletteName || userPaletteCustomData ||
|
||||||
|
userStyleName || userStyleCustomData ||
|
||||||
|
(
|
||||||
|
// User V2 overrides instance V3
|
||||||
|
(instancePaletteName ||
|
||||||
|
instanceStyleName) &&
|
||||||
|
instanceThemeV2Name == null &&
|
||||||
|
userThemeV2Name == null
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Palette and/or style overrides V2 themes
|
||||||
|
instanceThemeV2Name = null
|
||||||
|
userThemeV2Name = null
|
||||||
|
userThemeV2Source = null
|
||||||
|
userThemeV2Snapshot = null
|
||||||
|
|
||||||
|
majorVersionUsed = 'v3'
|
||||||
|
} else if (
|
||||||
|
(userThemeV2Name ||
|
||||||
|
userThemeV2Snapshot ||
|
||||||
|
userThemeV2Source ||
|
||||||
|
instanceThemeV2Name)
|
||||||
|
) {
|
||||||
|
majorVersionUsed = 'v2'
|
||||||
|
} else {
|
||||||
|
// if all fails fallback to v3
|
||||||
|
majorVersionUsed = 'v3'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (majorVersionUsed === 'v3') {
|
||||||
|
const result = await Promise.all([
|
||||||
|
dispatch('fetchPalettesIndex'),
|
||||||
|
dispatch('fetchStylesIndex')
|
||||||
|
])
|
||||||
|
|
||||||
|
palettesIndex = result[0]
|
||||||
|
stylesIndex = result[1]
|
||||||
|
} else {
|
||||||
|
// Promise.all just to be uniform with v3
|
||||||
|
const result = await Promise.all([
|
||||||
|
dispatch('fetchThemesIndex')
|
||||||
|
])
|
||||||
|
|
||||||
|
themesIndex = result[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
state.themeVersion = majorVersionUsed
|
||||||
|
|
||||||
|
console.debug('Version used', majorVersionUsed)
|
||||||
|
|
||||||
|
if (majorVersionUsed === 'v3') {
|
||||||
|
state.themeDataUsed = null
|
||||||
|
state.themeNameUsed = null
|
||||||
|
|
||||||
|
const style = await getData(
|
||||||
|
'style',
|
||||||
|
stylesIndex,
|
||||||
|
userStyleCustomData,
|
||||||
|
userStyleName || instanceStyleName
|
||||||
|
)
|
||||||
|
state.styleNameUsed = style.nameUsed
|
||||||
|
state.styleDataUsed = style.dataUsed
|
||||||
|
|
||||||
|
let firstStylePaletteName = null
|
||||||
|
style
|
||||||
|
.dataUsed
|
||||||
|
?.filter(x => x.component === '@palette')
|
||||||
|
.map(x => {
|
||||||
|
const cleanDirectives = Object.fromEntries(
|
||||||
|
Object
|
||||||
|
.entries(x.directives)
|
||||||
|
.filter(([k, v]) => k)
|
||||||
|
)
|
||||||
|
|
||||||
|
return { name: x.variant, ...cleanDirectives }
|
||||||
|
})
|
||||||
|
.forEach(palette => {
|
||||||
|
const key = 'style.' + palette.name.toLowerCase().replace(/ /g, '_')
|
||||||
|
if (!firstStylePaletteName) firstStylePaletteName = key
|
||||||
|
palettesIndex[key] = () => Promise.resolve(palette)
|
||||||
|
})
|
||||||
|
|
||||||
|
const palette = await getData(
|
||||||
|
'palette',
|
||||||
|
palettesIndex,
|
||||||
|
userPaletteCustomData,
|
||||||
|
state.useStylePalette ? firstStylePaletteName : (userPaletteName || instancePaletteName)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.useStylePalette) {
|
||||||
|
commit('setOption', { name: 'palette', value: firstStylePaletteName })
|
||||||
|
}
|
||||||
|
|
||||||
|
state.paletteNameUsed = palette.nameUsed
|
||||||
|
state.paletteDataUsed = palette.dataUsed
|
||||||
|
|
||||||
|
if (state.paletteDataUsed) {
|
||||||
|
state.paletteDataUsed.link = state.paletteDataUsed.link || state.paletteDataUsed.accent
|
||||||
|
state.paletteDataUsed.accent = state.paletteDataUsed.accent || state.paletteDataUsed.link
|
||||||
|
}
|
||||||
|
if (Array.isArray(state.paletteDataUsed)) {
|
||||||
|
const [
|
||||||
|
name,
|
||||||
|
bg,
|
||||||
|
fg,
|
||||||
|
text,
|
||||||
|
link,
|
||||||
|
cRed = '#FF0000',
|
||||||
|
cGreen = '#00FF00',
|
||||||
|
cBlue = '#0000FF',
|
||||||
|
cOrange = '#E3FF00'
|
||||||
|
] = palette.dataUsed
|
||||||
|
state.paletteDataUsed = {
|
||||||
|
name,
|
||||||
|
bg,
|
||||||
|
fg,
|
||||||
|
text,
|
||||||
|
link,
|
||||||
|
accent: link,
|
||||||
|
cRed,
|
||||||
|
cBlue,
|
||||||
|
cGreen,
|
||||||
|
cOrange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.debug('Palette data used', palette.dataUsed)
|
||||||
|
} else {
|
||||||
|
state.styleNameUsed = null
|
||||||
|
state.styleDataUsed = null
|
||||||
|
state.paletteNameUsed = null
|
||||||
|
state.paletteDataUsed = null
|
||||||
|
|
||||||
|
const theme = await getData(
|
||||||
|
'theme',
|
||||||
|
themesIndex,
|
||||||
|
userThemeV2Source || userThemeV2Snapshot,
|
||||||
|
userThemeV2Name || instanceThemeV2Name
|
||||||
|
)
|
||||||
|
state.themeNameUsed = theme.nameUsed
|
||||||
|
state.themeDataUsed = theme.dataUsed
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async applyTheme (
|
||||||
|
{ dispatch, commit, rootState, state },
|
||||||
|
{ recompile = false } = {}
|
||||||
|
) {
|
||||||
|
const {
|
||||||
forceThemeRecompilation,
|
forceThemeRecompilation,
|
||||||
themeDebug,
|
themeDebug,
|
||||||
theme3hacks
|
theme3hacks
|
||||||
} = rootState.config
|
} = rootState.config
|
||||||
|
|
||||||
const actualThemeName = userThemeName || instanceThemeName
|
|
||||||
|
|
||||||
const forceRecompile = forceThemeRecompilation || recompile
|
|
||||||
|
|
||||||
let promise = null
|
|
||||||
|
|
||||||
if (themeData) {
|
|
||||||
promise = Promise.resolve(normalizeThemeData(themeData))
|
|
||||||
} else if (themeName) {
|
|
||||||
promise = getPreset(themeName).then(themeData => normalizeThemeData(themeData))
|
|
||||||
} else if (userThemeSource || userThemeSnapshot) {
|
|
||||||
promise = Promise.resolve(normalizeThemeData({
|
|
||||||
_pleroma_theme_version: 2,
|
|
||||||
theme: userThemeSnapshot,
|
|
||||||
source: userThemeSource
|
|
||||||
}))
|
|
||||||
} else if (actualThemeName && actualThemeName !== 'custom') {
|
|
||||||
promise = getPreset(actualThemeName).then(themeData => {
|
|
||||||
const realThemeData = normalizeThemeData(themeData)
|
|
||||||
if (actualThemeName === instanceThemeName) {
|
|
||||||
// This sole line is the reason why this whole block is above the recompilation check
|
|
||||||
commit('setInstanceOption', { name: 'themeData', value: { theme: realThemeData } })
|
|
||||||
}
|
|
||||||
return realThemeData
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw new Error('Cannot load any theme!')
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we're not not forced to recompile try using
|
// If we're not not forced to recompile try using
|
||||||
// cache (tryLoadCache return true if load successful)
|
// cache (tryLoadCache return true if load successful)
|
||||||
if (!forceRecompile && !themeDebug && tryLoadCache()) {
|
|
||||||
commit('setThemeApplied')
|
const forceRecompile = forceThemeRecompilation || recompile
|
||||||
return
|
if (!forceRecompile && !themeDebug && await tryLoadCache()) {
|
||||||
|
return commit('setThemeApplied')
|
||||||
}
|
}
|
||||||
|
await dispatch('getThemeData')
|
||||||
|
|
||||||
promise
|
const paletteIss = (() => {
|
||||||
.then(realThemeData => {
|
if (!state.paletteDataUsed) return null
|
||||||
const theme2ruleset = convertTheme2To3(realThemeData)
|
const result = {
|
||||||
|
component: 'Root',
|
||||||
|
directives: {}
|
||||||
|
}
|
||||||
|
|
||||||
if (saveData) {
|
Object
|
||||||
commit('setOption', { name: 'theme', value: themeName || actualThemeName })
|
.entries(state.paletteDataUsed)
|
||||||
commit('setOption', { name: 'customTheme', value: realThemeData })
|
.filter(([k]) => k !== 'name')
|
||||||
commit('setOption', { name: 'customThemeSource', value: realThemeData })
|
.forEach(([k, v]) => {
|
||||||
}
|
let issRootDirectiveName
|
||||||
const hacks = []
|
switch (k) {
|
||||||
|
case 'background':
|
||||||
Object.entries(theme3hacks).forEach(([key, value]) => {
|
issRootDirectiveName = 'bg'
|
||||||
switch (key) {
|
|
||||||
case 'fonts': {
|
|
||||||
Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => {
|
|
||||||
if (!font?.family) return
|
|
||||||
switch (fontKey) {
|
|
||||||
case 'interface':
|
|
||||||
hacks.push({
|
|
||||||
component: 'Root',
|
|
||||||
directives: {
|
|
||||||
'--font': 'generic | ' + font.family
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'input':
|
|
||||||
hacks.push({
|
|
||||||
component: 'Input',
|
|
||||||
directives: {
|
|
||||||
'--font': 'generic | ' + font.family
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'post':
|
|
||||||
hacks.push({
|
|
||||||
component: 'RichContent',
|
|
||||||
directives: {
|
|
||||||
'--font': 'generic | ' + font.family
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'monospace':
|
|
||||||
hacks.push({
|
|
||||||
component: 'Root',
|
|
||||||
directives: {
|
|
||||||
'--monoFont': 'generic | ' + font.family
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
}
|
case 'foreground':
|
||||||
case 'underlay': {
|
issRootDirectiveName = 'fg'
|
||||||
if (value !== 'none') {
|
|
||||||
const newRule = {
|
|
||||||
component: 'Underlay',
|
|
||||||
directives: {}
|
|
||||||
}
|
|
||||||
if (value === 'opaque') {
|
|
||||||
newRule.directives.opacity = 1
|
|
||||||
newRule.directives.background = '--wallpaper'
|
|
||||||
}
|
|
||||||
if (value === 'transparent') {
|
|
||||||
newRule.directives.opacity = 0
|
|
||||||
}
|
|
||||||
hacks.push(newRule)
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
default:
|
||||||
|
issRootDirectiveName = k
|
||||||
}
|
}
|
||||||
|
result.directives['--' + issRootDirectiveName] = 'color | ' + v
|
||||||
})
|
})
|
||||||
|
return result
|
||||||
|
})()
|
||||||
|
|
||||||
const ruleset = [
|
const theme2ruleset = state.themeDataUsed && convertTheme2To3(normalizeThemeData(state.themeDataUsed))
|
||||||
...theme2ruleset,
|
const hacks = []
|
||||||
...hacks
|
|
||||||
]
|
|
||||||
|
|
||||||
applyTheme(
|
Object.entries(theme3hacks).forEach(([key, value]) => {
|
||||||
ruleset,
|
switch (key) {
|
||||||
() => commit('setThemeApplied'),
|
case 'fonts': {
|
||||||
themeDebug
|
Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => {
|
||||||
)
|
if (!font?.family) return
|
||||||
})
|
switch (fontKey) {
|
||||||
|
case 'interface':
|
||||||
|
hacks.push({
|
||||||
|
component: 'Root',
|
||||||
|
directives: {
|
||||||
|
'--font': 'generic | ' + font.family
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'input':
|
||||||
|
hacks.push({
|
||||||
|
component: 'Input',
|
||||||
|
directives: {
|
||||||
|
'--font': 'generic | ' + font.family
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'post':
|
||||||
|
hacks.push({
|
||||||
|
component: 'RichContent',
|
||||||
|
directives: {
|
||||||
|
'--font': 'generic | ' + font.family
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'monospace':
|
||||||
|
hacks.push({
|
||||||
|
component: 'Root',
|
||||||
|
directives: {
|
||||||
|
'--monoFont': 'generic | ' + font.family
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'underlay': {
|
||||||
|
if (value !== 'none') {
|
||||||
|
const newRule = {
|
||||||
|
component: 'Underlay',
|
||||||
|
directives: {}
|
||||||
|
}
|
||||||
|
if (value === 'opaque') {
|
||||||
|
newRule.directives.opacity = 1
|
||||||
|
newRule.directives.background = '--wallpaper'
|
||||||
|
}
|
||||||
|
if (value === 'transparent') {
|
||||||
|
newRule.directives.opacity = 0
|
||||||
|
}
|
||||||
|
hacks.push(newRule)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return promise
|
const rulesetArray = [
|
||||||
|
theme2ruleset,
|
||||||
|
state.styleDataUsed,
|
||||||
|
paletteIss,
|
||||||
|
hacks
|
||||||
|
].filter(x => x)
|
||||||
|
|
||||||
|
return applyTheme(
|
||||||
|
rulesetArray.flat(),
|
||||||
|
() => commit('setThemeApplied'),
|
||||||
|
themeDebug
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -355,19 +676,6 @@ const interfaceMod = {
|
||||||
export default interfaceMod
|
export default interfaceMod
|
||||||
|
|
||||||
export const normalizeThemeData = (input) => {
|
export const normalizeThemeData = (input) => {
|
||||||
if (Array.isArray(input)) {
|
|
||||||
const themeData = { colors: {} }
|
|
||||||
themeData.colors.bg = input[1]
|
|
||||||
themeData.colors.fg = input[2]
|
|
||||||
themeData.colors.text = input[3]
|
|
||||||
themeData.colors.link = input[4]
|
|
||||||
themeData.colors.cRed = input[5]
|
|
||||||
themeData.colors.cGreen = input[6]
|
|
||||||
themeData.colors.cBlue = input[7]
|
|
||||||
themeData.colors.cOrange = input[8]
|
|
||||||
return generatePreset(themeData).theme
|
|
||||||
}
|
|
||||||
|
|
||||||
let themeData, themeSource
|
let themeData, themeSource
|
||||||
|
|
||||||
if (input.themeFileVerison === 1) {
|
if (input.themeFileVerison === 1) {
|
||||||
|
@ -381,7 +689,10 @@ export const normalizeThemeData = (input) => {
|
||||||
// We got passed a full theme file
|
// We got passed a full theme file
|
||||||
themeData = input.theme
|
themeData = input.theme
|
||||||
themeSource = input.source
|
themeSource = input.source
|
||||||
} else if (Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion')) {
|
} else if (
|
||||||
|
Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion') ||
|
||||||
|
Object.prototype.hasOwnProperty.call(input, 'colors')
|
||||||
|
) {
|
||||||
// We got passed a source/snapshot
|
// We got passed a source/snapshot
|
||||||
themeData = input
|
themeData = input
|
||||||
themeSource = input
|
themeSource = input
|
||||||
|
|
|
@ -2,15 +2,23 @@ import utf8 from 'utf8'
|
||||||
|
|
||||||
export const newExporter = ({
|
export const newExporter = ({
|
||||||
filename = 'data',
|
filename = 'data',
|
||||||
|
mime = 'application/json',
|
||||||
|
extension = '.json',
|
||||||
getExportedObject
|
getExportedObject
|
||||||
}) => ({
|
}) => ({
|
||||||
exportData () {
|
exportData () {
|
||||||
const stringified = utf8.encode(JSON.stringify(getExportedObject(), null, 2)) // Pretty-print and indent with 2 spaces
|
let stringified
|
||||||
|
if (mime === 'application/json') {
|
||||||
|
stringified = utf8.encode(JSON.stringify(getExportedObject(), null, 2)) // Pretty-print and indent with 2 spaces
|
||||||
|
} else {
|
||||||
|
stringified = utf8.encode(getExportedObject()) // Pretty-print and indent with 2 spaces
|
||||||
|
}
|
||||||
|
|
||||||
// Create an invisible link with a data url and simulate a click
|
// Create an invisible link with a data url and simulate a click
|
||||||
const e = document.createElement('a')
|
const e = document.createElement('a')
|
||||||
e.setAttribute('download', `${filename}.json`)
|
const realFilename = typeof filename === 'function' ? filename() : filename
|
||||||
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
|
e.setAttribute('download', `${realFilename}.${extension}`)
|
||||||
|
e.setAttribute('href', `data:${mime};base64, ${window.btoa(stringified)}`)
|
||||||
e.style.display = 'none'
|
e.style.display = 'none'
|
||||||
|
|
||||||
document.body.appendChild(e)
|
document.body.appendChild(e)
|
||||||
|
@ -20,6 +28,8 @@ export const newExporter = ({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const newImporter = ({
|
export const newImporter = ({
|
||||||
|
accept = '.json',
|
||||||
|
parser = (string) => JSON.parse(string),
|
||||||
onImport,
|
onImport,
|
||||||
onImportFailure,
|
onImportFailure,
|
||||||
validator = () => true
|
validator = () => true
|
||||||
|
@ -27,18 +37,19 @@ export const newImporter = ({
|
||||||
importData () {
|
importData () {
|
||||||
const filePicker = document.createElement('input')
|
const filePicker = document.createElement('input')
|
||||||
filePicker.setAttribute('type', 'file')
|
filePicker.setAttribute('type', 'file')
|
||||||
filePicker.setAttribute('accept', '.json')
|
filePicker.setAttribute('accept', accept)
|
||||||
|
|
||||||
filePicker.addEventListener('change', event => {
|
filePicker.addEventListener('change', event => {
|
||||||
if (event.target.files[0]) {
|
if (event.target.files[0]) {
|
||||||
|
const filename = event.target.files[0].name
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = ({ target }) => {
|
reader.onload = ({ target }) => {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(target.result)
|
const parsed = parser(target.result, filename)
|
||||||
const validationResult = validator(parsed)
|
const validationResult = validator(parsed, filename)
|
||||||
if (validationResult === true) {
|
if (validationResult === true) {
|
||||||
onImport(parsed)
|
onImport(parsed, filename)
|
||||||
} else {
|
} else {
|
||||||
onImportFailure({ validationResult })
|
onImportFailure({ validationResult })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { hex2rgb } from '../color_convert/color_convert.js'
|
|
||||||
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
|
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
|
||||||
import { getCssRules } from '../theme_data/css_utils.js'
|
import { getCssRules } from '../theme_data/css_utils.js'
|
||||||
import { defaultState } from '../../modules/config.js'
|
import { defaultState } from '../../modules/config.js'
|
||||||
import { chunk } from 'lodash'
|
import { chunk } from 'lodash'
|
||||||
|
import pako from 'pako'
|
||||||
|
import localforage from 'localforage'
|
||||||
|
|
||||||
// On platforms where this is not supported, it will return undefined
|
// On platforms where this is not supported, it will return undefined
|
||||||
// Otherwise it will return an array
|
// Otherwise it will return an array
|
||||||
|
@ -52,29 +53,12 @@ export const generateTheme = (inputRuleset, callbacks, debug) => {
|
||||||
|
|
||||||
const themes3 = init({
|
const themes3 = init({
|
||||||
inputRuleset,
|
inputRuleset,
|
||||||
// Assuming that "worst case scenario background" is panel background since it's the most likely one
|
|
||||||
ultimateBackgroundColor: inputRuleset[0].directives['--bg'].split('|')[1].trim(),
|
|
||||||
debug
|
debug
|
||||||
})
|
})
|
||||||
|
|
||||||
getCssRules(themes3.eager, debug).forEach(rule => {
|
getCssRules(themes3.eager, debug).forEach(rule => {
|
||||||
// Hacks to support multiple selectors on same component
|
// Hacks to support multiple selectors on same component
|
||||||
if (rule.match(/::-webkit-scrollbar-button/)) {
|
onNewRule(rule, false)
|
||||||
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()
|
onEagerFinished()
|
||||||
|
|
||||||
|
@ -88,22 +72,7 @@ export const generateTheme = (inputRuleset, callbacks, debug) => {
|
||||||
const chunk = chunks[counter]
|
const chunk = chunks[counter]
|
||||||
Promise.all(chunk.map(x => x())).then(result => {
|
Promise.all(chunk.map(x => x())).then(result => {
|
||||||
getCssRules(result.filter(x => x), debug).forEach(rule => {
|
getCssRules(result.filter(x => x), debug).forEach(rule => {
|
||||||
if (rule.match(/\.modal-view/)) {
|
onNewRule(rule, true)
|
||||||
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()
|
// const t1 = performance.now()
|
||||||
// console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms')
|
// console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms')
|
||||||
|
@ -120,12 +89,15 @@ export const generateTheme = (inputRuleset, callbacks, debug) => {
|
||||||
return { lazyProcessFunc: processChunk }
|
return { lazyProcessFunc: processChunk }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tryLoadCache = () => {
|
export const tryLoadCache = async () => {
|
||||||
const json = localStorage.getItem('pleroma-fe-theme-cache')
|
console.info('Trying to load compiled theme data from cache')
|
||||||
if (!json) return null
|
const data = await localforage.getItem('pleromafe-theme-cache')
|
||||||
|
if (!data) return null
|
||||||
let cache
|
let cache
|
||||||
try {
|
try {
|
||||||
cache = JSON.parse(json)
|
const decoded = new TextDecoder().decode(pako.inflate(data))
|
||||||
|
cache = JSON.parse(decoded)
|
||||||
|
console.info(`Loaded theme from cache, size=${cache}`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to decode theme cache:', e)
|
console.error('Failed to decode theme cache:', e)
|
||||||
return false
|
return false
|
||||||
|
@ -150,16 +122,28 @@ export const applyTheme = (input, onFinish = (data) => {}, debug) => {
|
||||||
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
|
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
|
||||||
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
|
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
|
||||||
|
|
||||||
|
const insertRule = (styles, rule) => {
|
||||||
|
if (rule.indexOf('webkit') >= 0) {
|
||||||
|
try {
|
||||||
|
styles.sheet.insertRule(rule, 'index-max')
|
||||||
|
styles.rules.push(rule)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Can\'t insert rule due to lack of support', e)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
styles.sheet.insertRule(rule, 'index-max')
|
||||||
|
styles.rules.push(rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { lazyProcessFunc } = generateTheme(
|
const { lazyProcessFunc } = generateTheme(
|
||||||
input,
|
input,
|
||||||
{
|
{
|
||||||
onNewRule (rule, isLazy) {
|
onNewRule (rule, isLazy) {
|
||||||
if (isLazy) {
|
if (isLazy) {
|
||||||
lazyStyles.sheet.insertRule(rule, 'index-max')
|
insertRule(lazyStyles, rule)
|
||||||
lazyStyles.rules.push(rule)
|
|
||||||
} else {
|
} else {
|
||||||
eagerStyles.sheet.insertRule(rule, 'index-max')
|
insertRule(eagerStyles, rule)
|
||||||
eagerStyles.rules.push(rule)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onEagerFinished () {
|
onEagerFinished () {
|
||||||
|
@ -169,16 +153,10 @@ export const applyTheme = (input, onFinish = (data) => {}, debug) => {
|
||||||
adoptStyleSheets([eagerStyles, lazyStyles])
|
adoptStyleSheets([eagerStyles, lazyStyles])
|
||||||
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
|
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
|
||||||
onFinish(cache)
|
onFinish(cache)
|
||||||
try {
|
const compress = (js) => {
|
||||||
localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache))
|
return pako.deflate(JSON.stringify(js))
|
||||||
} catch (e) {
|
|
||||||
localStorage.removeItem('pleroma-fe-theme-cache')
|
|
||||||
try {
|
|
||||||
localStorage.setItem('pleroma-fe-theme-cache', JSON.stringify(cache))
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('cannot save cache!', e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
localforage.setItem('pleromafe-theme-cache', compress(cache))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
debug
|
debug
|
||||||
|
@ -252,64 +230,66 @@ export const applyConfig = (input, i18n) => {
|
||||||
styleSheet.toString()
|
styleSheet.toString()
|
||||||
styleSheet.insertRule(`:root { ${rules} }`, 'index-max')
|
styleSheet.insertRule(`:root { ${rules} }`, 'index-max')
|
||||||
|
|
||||||
|
// TODO find a way to make this not apply to theme previews
|
||||||
if (Object.prototype.hasOwnProperty.call(config, 'forcedRoundness')) {
|
if (Object.prototype.hasOwnProperty.call(config, 'forcedRoundness')) {
|
||||||
styleSheet.insertRule(` * {
|
styleSheet.insertRule(` *:not(.preview-block) {
|
||||||
--roundness: var(--forcedRoundness) !important;
|
--roundness: var(--forcedRoundness) !important;
|
||||||
}`, 'index-max')
|
}`, 'index-max')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getThemes = () => {
|
export const getResourcesIndex = async (url, parser = JSON.parse) => {
|
||||||
const cache = 'no-store'
|
const cache = 'no-store'
|
||||||
|
const customUrl = url.replace(/\.(\w+)$/, '.custom.$1')
|
||||||
|
let builtin
|
||||||
|
let custom
|
||||||
|
|
||||||
return window.fetch('/static/styles.json', { cache })
|
const resourceTransform = (resources) => {
|
||||||
.then((data) => data.json())
|
return Object
|
||||||
.then((themes) => {
|
.entries(resources)
|
||||||
return Object.entries(themes).map(([k, v]) => {
|
.map(([k, v]) => {
|
||||||
let promise = null
|
|
||||||
if (typeof v === 'object') {
|
if (typeof v === 'object') {
|
||||||
promise = Promise.resolve(v)
|
return [k, () => Promise.resolve(v)]
|
||||||
} else if (typeof v === 'string') {
|
} else if (typeof v === 'string') {
|
||||||
promise = window.fetch(v, { cache })
|
return [
|
||||||
.then((data) => data.json())
|
k,
|
||||||
.catch((e) => {
|
() => window
|
||||||
console.error(e)
|
.fetch(v, { cache })
|
||||||
return null
|
.then(data => data.text())
|
||||||
})
|
.then(text => parser(text))
|
||||||
|
.catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
console.error(`Unknown resource format - ${k} is a ${typeof v}`)
|
||||||
|
return [k, null]
|
||||||
}
|
}
|
||||||
return [k, promise]
|
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
.then((promises) => {
|
|
||||||
return promises
|
try {
|
||||||
.reduce((acc, [k, v]) => {
|
const builtinData = await window.fetch(url, { cache })
|
||||||
acc[k] = v
|
const builtinResources = await builtinData.json()
|
||||||
return acc
|
builtin = resourceTransform(builtinResources)
|
||||||
}, {})
|
} catch (e) {
|
||||||
})
|
builtin = []
|
||||||
}
|
console.warn(`Builtin resources at ${url} unavailable`)
|
||||||
|
}
|
||||||
export const getPreset = (val) => {
|
|
||||||
return getThemes()
|
try {
|
||||||
.then((themes) => themes[val] ? themes[val] : themes['pleroma-dark'])
|
const customData = await window.fetch(customUrl, { cache })
|
||||||
.then((theme) => {
|
const customResources = await customData.json()
|
||||||
const isV1 = Array.isArray(theme)
|
custom = resourceTransform(customResources)
|
||||||
const data = isV1 ? {} : theme.theme
|
} catch (e) {
|
||||||
|
custom = []
|
||||||
if (isV1) {
|
console.warn(`Custom resources at ${customUrl} unavailable`)
|
||||||
const bg = hex2rgb(theme[1])
|
}
|
||||||
const fg = hex2rgb(theme[2])
|
|
||||||
const text = hex2rgb(theme[3])
|
const total = [...custom, ...builtin]
|
||||||
const link = hex2rgb(theme[4])
|
if (total.length === 0) {
|
||||||
|
return Promise.reject(new Error(`Resource at ${url} and ${customUrl} completely unavailable. Panicking`))
|
||||||
const cRed = hex2rgb(theme[5] || '#FF0000')
|
}
|
||||||
const cGreen = hex2rgb(theme[6] || '#00FF00')
|
return Promise.resolve(Object.fromEntries(total))
|
||||||
const cBlue = hex2rgb(theme[7] || '#0000FF')
|
|
||||||
const cOrange = hex2rgb(theme[8] || '#E3FF00')
|
|
||||||
|
|
||||||
data.colors = { bg, fg, text, link, cRed, cBlue, cGreen, cOrange }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { theme: data, source: theme.source }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,25 +2,6 @@ import { convert } from 'chromatism'
|
||||||
|
|
||||||
import { hex2rgb, rgba2css } from '../color_convert/color_convert.js'
|
import { hex2rgb, rgba2css } from '../color_convert/color_convert.js'
|
||||||
|
|
||||||
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 getCssColorString = (color, alpha = 1) => rgba2css({ ...convert(color).rgb, a: alpha })
|
||||||
|
|
||||||
export const getCssShadow = (input, usesDropShadow) => {
|
export const getCssShadow = (input, usesDropShadow) => {
|
||||||
|
@ -84,6 +65,9 @@ export const getCssRules = (rules, debug) => rules.map(rule => {
|
||||||
].join(';\n ')
|
].join(';\n ')
|
||||||
}
|
}
|
||||||
case 'shadow': {
|
case 'shadow': {
|
||||||
|
if (!rule.dynamicVars.shadow) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
return ' ' + [
|
return ' ' + [
|
||||||
'--shadow: ' + getCssShadow(rule.dynamicVars.shadow),
|
'--shadow: ' + getCssShadow(rule.dynamicVars.shadow),
|
||||||
'--shadowFilter: ' + getCssShadowFilter(rule.dynamicVars.shadow),
|
'--shadowFilter: ' + getCssShadowFilter(rule.dynamicVars.shadow),
|
||||||
|
@ -98,7 +82,7 @@ export const getCssRules = (rules, debug) => rules.map(rule => {
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
if (v === 'transparent') {
|
if (v === 'transparent') {
|
||||||
if (rule.component === 'Root') return []
|
if (rule.component === 'Root') return null
|
||||||
return [
|
return [
|
||||||
rule.directives.backgroundNoCssColor !== 'yes' ? ('background-color: ' + v) : '',
|
rule.directives.backgroundNoCssColor !== 'yes' ? ('background-color: ' + v) : '',
|
||||||
' --background: ' + v
|
' --background: ' + v
|
||||||
|
@ -130,7 +114,7 @@ export const getCssRules = (rules, debug) => rules.map(rule => {
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if (k.startsWith('--')) {
|
if (k.startsWith('--')) {
|
||||||
const [type, value] = v.split('|').map(x => x.trim()) // woah, Extreme!
|
const [type, value] = v.split('|').map(x => x.trim())
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'color': {
|
case 'color': {
|
||||||
const color = rule.dynamicVars[k]
|
const color = rule.dynamicVars[k]
|
||||||
|
@ -143,21 +127,20 @@ export const getCssRules = (rules, debug) => rules.map(rule => {
|
||||||
case 'generic':
|
case 'generic':
|
||||||
return k + ': ' + value
|
return k + ': ' + value
|
||||||
default:
|
default:
|
||||||
return ''
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ''
|
return null
|
||||||
}
|
}
|
||||||
}).filter(x => x).map(x => ' ' + x).join(';\n')
|
}).filter(x => x).map(x => ' ' + x + ';').join('\n')
|
||||||
|
|
||||||
return [
|
return [
|
||||||
header,
|
header,
|
||||||
directives + ';',
|
directives,
|
||||||
(rule.component === 'Text' && rule.state.indexOf('faint') < 0 && rule.directives.textNoCssColor !== 'yes') ? ' color: var(--text);' : '',
|
(rule.component === 'Text' && rule.state.indexOf('faint') < 0 && rule.directives.textNoCssColor !== 'yes') ? ' color: var(--text);' : '',
|
||||||
'',
|
|
||||||
virtualDirectives,
|
virtualDirectives,
|
||||||
footer
|
footer
|
||||||
].join('\n')
|
].filter(x => x).join('\n')
|
||||||
}).filter(x => x)
|
}).filter(x => x)
|
||||||
|
|
||||||
export const getScopedVersion = (rules, newScope) => {
|
export const getScopedVersion = (rules, newScope) => {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { flattenDeep } from 'lodash'
|
import { flattenDeep } from 'lodash'
|
||||||
|
|
||||||
const parseShadow = string => {
|
export const deserializeShadow = string => {
|
||||||
const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha']
|
const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha', 'name']
|
||||||
const regexPrep = [
|
const regexPrep = [
|
||||||
// inset keyword (optional)
|
// inset keyword (optional)
|
||||||
'^(?:(inset)\\s+)?',
|
'^',
|
||||||
|
'(?:(inset)\\s+)?',
|
||||||
// x
|
// x
|
||||||
'(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)',
|
'(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)',
|
||||||
// y
|
// y
|
||||||
|
@ -14,19 +15,31 @@ const parseShadow = string => {
|
||||||
// spread (optional)
|
// spread (optional)
|
||||||
'(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?',
|
'(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?',
|
||||||
// either hex, variable or function
|
// either hex, variable or function
|
||||||
'(#[0-9a-f]{6}|--[a-z\\-_]+|\\$[a-z\\-()_]+)',
|
'(#[0-9a-f]{6}|--[a-z0-9\\-_]+|\\$[a-z0-9\\-()_ ]+)',
|
||||||
// opacity (optional)
|
// opacity (optional)
|
||||||
'(?:\\s+\\/\\s+([0-9]+(?:\\.[0-9]+)?)\\s*)?$'
|
'(?:\\s+\\/\\s+([0-9]+(?:\\.[0-9]+)?)\\s*)?',
|
||||||
|
// name
|
||||||
|
'(?:\\s+#(\\w+)\\s*)?',
|
||||||
|
'$'
|
||||||
].join('')
|
].join('')
|
||||||
const regex = new RegExp(regexPrep, 'gis') // global, (stable) indices, single-string
|
const regex = new RegExp(regexPrep, 'gis') // global, (stable) indices, single-string
|
||||||
const result = regex.exec(string)
|
const result = regex.exec(string)
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return string
|
if (string.startsWith('$') || string.startsWith('--')) {
|
||||||
|
return string
|
||||||
|
} else {
|
||||||
|
throw new Error(`Invalid shadow definition: '${string}'`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha'])
|
const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha'])
|
||||||
const { x, y, blur, spread, alpha, inset, color } = Object.fromEntries(modes.map((mode, i) => {
|
const { x, y, blur, spread, alpha, inset, color, name } = Object.fromEntries(modes.map((mode, i) => {
|
||||||
if (numeric.has(mode)) {
|
if (numeric.has(mode)) {
|
||||||
return [mode, Number(result[i])]
|
const number = Number(result[i])
|
||||||
|
if (Number.isNaN(number)) {
|
||||||
|
if (mode === 'alpha') return [mode, 1]
|
||||||
|
return [mode, 0]
|
||||||
|
}
|
||||||
|
return [mode, number]
|
||||||
} else if (mode === 'inset') {
|
} else if (mode === 'inset') {
|
||||||
return [mode, !!result[i]]
|
return [mode, !!result[i]]
|
||||||
} else {
|
} else {
|
||||||
|
@ -34,7 +47,7 @@ const parseShadow = string => {
|
||||||
}
|
}
|
||||||
}).filter(([k, v]) => v !== false).slice(1))
|
}).filter(([k, v]) => v !== false).slice(1))
|
||||||
|
|
||||||
return { x, y, blur, spread, color, alpha, inset }
|
return { x, y, blur, spread, color, alpha, inset, name }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// this works nearly the same as HTML tree converter
|
// this works nearly the same as HTML tree converter
|
||||||
|
@ -136,12 +149,12 @@ export const deserialize = (input) => {
|
||||||
|
|
||||||
output.directives = Object.fromEntries(content.map(d => {
|
output.directives = Object.fromEntries(content.map(d => {
|
||||||
const [property, value] = d.split(':')
|
const [property, value] = d.split(':')
|
||||||
let realValue = value.trim()
|
let realValue = (value || '').trim()
|
||||||
if (property === 'shadow') {
|
if (property === 'shadow') {
|
||||||
if (realValue === 'none') {
|
if (realValue === 'none') {
|
||||||
realValue = []
|
realValue = []
|
||||||
} else {
|
} else {
|
||||||
realValue = value.split(',').map(v => parseShadow(v.trim()))
|
realValue = value.split(',').map(v => deserializeShadow(v.trim()))
|
||||||
}
|
}
|
||||||
} if (!Number.isNaN(Number(value))) {
|
} if (!Number.isNaN(Number(value))) {
|
||||||
realValue = Number(value)
|
realValue = Number(value)
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
import { unroll } from './iss_utils.js'
|
import { unroll } from './iss_utils.js'
|
||||||
|
import { deserializeShadow } from './iss_deserializer.js'
|
||||||
|
|
||||||
const serializeShadow = s => {
|
export const serializeShadow = (s, throwOnInvalid) => {
|
||||||
if (typeof s === 'object') {
|
if (typeof s === 'object') {
|
||||||
return `${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}`
|
const inset = s.inset ? 'inset ' : ''
|
||||||
|
const name = s.name ? ` #${s.name} ` : ''
|
||||||
|
const result = `${inset}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}${name}`
|
||||||
|
deserializeShadow(result) // Verify that output is valid and parseable
|
||||||
|
return result
|
||||||
} else {
|
} else {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,43 +56,74 @@ export const getAllPossibleCombinations = (array) => {
|
||||||
*
|
*
|
||||||
* @returns {String} CSS selector (or path)
|
* @returns {String} CSS selector (or path)
|
||||||
*/
|
*/
|
||||||
export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, isParent) => {
|
export const genericRuleToSelector = components => (rule, ignoreOutOfTreeSelector, liteMode, children) => {
|
||||||
|
const isParent = !!children
|
||||||
if (!rule && !isParent) return null
|
if (!rule && !isParent) return null
|
||||||
const component = components[rule.component]
|
const component = components[rule.component]
|
||||||
const { states = {}, variants = {}, selector, outOfTreeSelector } = component
|
const { states = {}, variants = {}, outOfTreeSelector } = component
|
||||||
|
|
||||||
const applicableStates = ((rule.state || []).filter(x => x !== 'normal')).map(state => states[state])
|
const expand = (array = [], subArray = []) => {
|
||||||
|
if (array.length === 0) return subArray.map(x => [x])
|
||||||
|
if (subArray.length === 0) return array.map(x => [x])
|
||||||
|
return array.map(a => {
|
||||||
|
return subArray.map(b => [a, b])
|
||||||
|
}).flat()
|
||||||
|
}
|
||||||
|
|
||||||
|
let componentSelectors = Array.isArray(component.selector) ? component.selector : [component.selector]
|
||||||
|
if (ignoreOutOfTreeSelector || liteMode) componentSelectors = [componentSelectors[0]]
|
||||||
|
componentSelectors = componentSelectors.map(selector => {
|
||||||
|
if (selector === ':root') {
|
||||||
|
return ''
|
||||||
|
} else if (isParent) {
|
||||||
|
return selector
|
||||||
|
} else {
|
||||||
|
if (outOfTreeSelector && !ignoreOutOfTreeSelector) return outOfTreeSelector
|
||||||
|
return selector
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const applicableVariantName = (rule.variant || 'normal')
|
const applicableVariantName = (rule.variant || 'normal')
|
||||||
let applicableVariant = ''
|
let variantSelectors = null
|
||||||
if (applicableVariantName !== 'normal') {
|
if (applicableVariantName !== 'normal') {
|
||||||
applicableVariant = variants[applicableVariantName]
|
variantSelectors = variants[applicableVariantName]
|
||||||
} else {
|
} else {
|
||||||
applicableVariant = variants?.normal ?? ''
|
variantSelectors = variants?.normal ?? ''
|
||||||
}
|
}
|
||||||
|
variantSelectors = Array.isArray(variantSelectors) ? variantSelectors : [variantSelectors]
|
||||||
|
if (ignoreOutOfTreeSelector || liteMode) variantSelectors = [variantSelectors[0]]
|
||||||
|
|
||||||
let realSelector
|
const applicableStates = (rule.state || []).filter(x => x !== 'normal')
|
||||||
if (selector === ':root') {
|
// const applicableStates = (rule.state || [])
|
||||||
realSelector = ''
|
const statesSelectors = applicableStates.map(state => {
|
||||||
} else if (isParent) {
|
const selector = states[state] || ''
|
||||||
realSelector = selector
|
let arraySelector = Array.isArray(selector) ? selector : [selector]
|
||||||
} else {
|
if (ignoreOutOfTreeSelector || liteMode) arraySelector = [arraySelector[0]]
|
||||||
if (outOfTreeSelector && !ignoreOutOfTreeSelector) realSelector = outOfTreeSelector
|
arraySelector
|
||||||
else realSelector = selector
|
.sort((a, b) => {
|
||||||
}
|
if (a.startsWith(':')) return 1
|
||||||
|
if (/^[a-z]/.exec(a)) return -1
|
||||||
|
else return 0
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
return arraySelector
|
||||||
|
})
|
||||||
|
|
||||||
const selectors = [realSelector, applicableVariant, ...applicableStates]
|
const statesSelectorsFlat = statesSelectors.reduce((acc, s) => {
|
||||||
.sort((a, b) => {
|
return expand(acc, s).map(st => st.join(''))
|
||||||
if (a.startsWith(':')) return 1
|
}, [])
|
||||||
if (/^[a-z]/.exec(a)) return -1
|
|
||||||
else return 0
|
const componentVariant = expand(componentSelectors, variantSelectors).map(cv => cv.join(''))
|
||||||
})
|
const componentVariantStates = expand(componentVariant, statesSelectorsFlat).map(cvs => cvs.join(''))
|
||||||
.join('')
|
const selectors = expand(componentVariantStates, children).map(cvsc => cvsc.join(' '))
|
||||||
|
/*
|
||||||
|
*/
|
||||||
|
|
||||||
if (rule.parent) {
|
if (rule.parent) {
|
||||||
return (genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, true) + ' ' + selectors).trim()
|
return genericRuleToSelector(components)(rule.parent, ignoreOutOfTreeSelector, liteMode, selectors)
|
||||||
}
|
}
|
||||||
return selectors.trim()
|
|
||||||
|
return selectors.join(', ').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -354,10 +354,6 @@ export const convertTheme2To3 = (data) => {
|
||||||
newRules.push({ ...rule, state: ['toggled'] })
|
newRules.push({ ...rule, state: ['toggled'] })
|
||||||
newRules.push({ ...rule, state: ['toggled', 'focus'] })
|
newRules.push({ ...rule, state: ['toggled', 'focus'] })
|
||||||
newRules.push({ ...rule, state: ['pressed', '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: ['toggled', 'focus', 'hover'] })
|
||||||
newRules.push({ ...rule, state: ['pressed', 'focus', 'hover'] })
|
newRules.push({ ...rule, state: ['pressed', 'focus', 'hover'] })
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { alphaBlend, getTextColor, relativeLuminance } from '../color_convert/co
|
||||||
|
|
||||||
export const process = (text, functions, { findColor, findShadow }, { dynamicVars, staticVars }) => {
|
export const process = (text, functions, { findColor, findShadow }, { dynamicVars, staticVars }) => {
|
||||||
const { funcName, argsString } = /\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-,.'"\s]*)\)/.exec(text).groups
|
const { funcName, argsString } = /\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-,.'"\s]*)\)/.exec(text).groups
|
||||||
const args = argsString.split(/,/g).map(a => a.trim())
|
const args = argsString.split(/ /g).map(a => a.trim())
|
||||||
|
|
||||||
const func = functions[funcName]
|
const func = functions[funcName]
|
||||||
if (args.length < func.argsNeeded) {
|
if (args.length < func.argsNeeded) {
|
||||||
|
@ -15,6 +15,11 @@ export const process = (text, functions, { findColor, findShadow }, { dynamicVar
|
||||||
export const colorFunctions = {
|
export const colorFunctions = {
|
||||||
alpha: {
|
alpha: {
|
||||||
argsNeeded: 2,
|
argsNeeded: 2,
|
||||||
|
documentation: 'Changes alpha value of the color only to be used for CSS variables',
|
||||||
|
args: [
|
||||||
|
'color: source color used',
|
||||||
|
'amount: alpha value'
|
||||||
|
],
|
||||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||||
const [color, amountArg] = args
|
const [color, amountArg] = args
|
||||||
|
|
||||||
|
@ -23,8 +28,32 @@ export const colorFunctions = {
|
||||||
return { ...colorArg, a: amount }
|
return { ...colorArg, a: amount }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
brightness: {
|
||||||
|
argsNeeded: 2,
|
||||||
|
document: 'Changes brightness/lightness of color in HSL colorspace',
|
||||||
|
args: [
|
||||||
|
'color: source color used',
|
||||||
|
'amount: lightness value'
|
||||||
|
],
|
||||||
|
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||||
|
const [color, amountArg] = args
|
||||||
|
|
||||||
|
const colorArg = convert(findColor(color, { dynamicVars, staticVars })).hsl
|
||||||
|
colorArg.l += Number(amountArg)
|
||||||
|
return { ...convert(colorArg).rgb }
|
||||||
|
}
|
||||||
|
},
|
||||||
textColor: {
|
textColor: {
|
||||||
argsNeeded: 2,
|
argsNeeded: 2,
|
||||||
|
documentation: 'Get text color with adequate contrast for given background and intended text color. Same function is used internally',
|
||||||
|
args: [
|
||||||
|
'background: color of backdrop where text will be shown',
|
||||||
|
'foreground: intended text color',
|
||||||
|
`[preserve]: (optional) intended color preservation:
|
||||||
|
'preserve' - try to preserve the color
|
||||||
|
'no-preserve' - if can't get adequate color - fall back to black or white
|
||||||
|
'no-auto' - don't do anything (useless as a color function)`
|
||||||
|
],
|
||||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||||
const [backgroundArg, foregroundArg, preserve = 'preserve'] = args
|
const [backgroundArg, foregroundArg, preserve = 'preserve'] = args
|
||||||
|
|
||||||
|
@ -36,6 +65,12 @@ export const colorFunctions = {
|
||||||
},
|
},
|
||||||
blend: {
|
blend: {
|
||||||
argsNeeded: 3,
|
argsNeeded: 3,
|
||||||
|
documentation: 'Alpha blending between two colors',
|
||||||
|
args: [
|
||||||
|
'background: bottom layer color',
|
||||||
|
'amount: opacity of top layer',
|
||||||
|
'foreground: upper layer color'
|
||||||
|
],
|
||||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||||
const [backgroundArg, amountArg, foregroundArg] = args
|
const [backgroundArg, amountArg, foregroundArg] = args
|
||||||
|
|
||||||
|
@ -48,6 +83,11 @@ export const colorFunctions = {
|
||||||
},
|
},
|
||||||
mod: {
|
mod: {
|
||||||
argsNeeded: 2,
|
argsNeeded: 2,
|
||||||
|
documentation: 'Old function that increases or decreases brightness depending if color is dark or light. Advised against using it as it might give unexpected results.',
|
||||||
|
args: [
|
||||||
|
'color: source color',
|
||||||
|
'amount: how much darken/brighten the color'
|
||||||
|
],
|
||||||
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
|
||||||
const [colorArg, amountArg] = args
|
const [colorArg, amountArg] = args
|
||||||
|
|
||||||
|
@ -65,6 +105,13 @@ export const colorFunctions = {
|
||||||
export const shadowFunctions = {
|
export const shadowFunctions = {
|
||||||
borderSide: {
|
borderSide: {
|
||||||
argsNeeded: 3,
|
argsNeeded: 3,
|
||||||
|
documentation: 'Simulate a border on a side with a shadow, best works on inset border',
|
||||||
|
args: [
|
||||||
|
'color: border color',
|
||||||
|
'side: string indicating on which side border should be, takes either one word or two words joined by dash (i.e. "left" or "bottom-right")',
|
||||||
|
'[alpha]: (Optional) border opacity, defaults to 1 (fully opaque)',
|
||||||
|
'[inset]: (Optional) whether border should be on the inside or outside, defaults to inside'
|
||||||
|
],
|
||||||
exec: (args, { findColor }) => {
|
exec: (args, { findColor }) => {
|
||||||
const [color, side, alpha = '1', widthArg = '1', inset = 'inset'] = args
|
const [color, side, alpha = '1', widthArg = '1', inset = 'inset'] = args
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ import {
|
||||||
normalizeCombination,
|
normalizeCombination,
|
||||||
findRules
|
findRules
|
||||||
} from './iss_utils.js'
|
} from './iss_utils.js'
|
||||||
import { parseCssShadow } from './css_utils.js'
|
import { deserializeShadow } from './iss_deserializer.js'
|
||||||
|
|
||||||
// Ensuring the order of components
|
// Ensuring the order of components
|
||||||
const components = {
|
const components = {
|
||||||
|
@ -37,18 +37,18 @@ const components = {
|
||||||
ChatMessage: null
|
ChatMessage: null
|
||||||
}
|
}
|
||||||
|
|
||||||
const findShadow = (shadows, { dynamicVars, staticVars }) => {
|
export const findShadow = (shadows, { dynamicVars, staticVars }) => {
|
||||||
return (shadows || []).map(shadow => {
|
return (shadows || []).map(shadow => {
|
||||||
let targetShadow
|
let targetShadow
|
||||||
if (typeof shadow === 'string') {
|
if (typeof shadow === 'string') {
|
||||||
if (shadow.startsWith('$')) {
|
if (shadow.startsWith('$')) {
|
||||||
targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars })
|
targetShadow = process(shadow, shadowFunctions, { findColor, findShadow }, { dynamicVars, staticVars })
|
||||||
} else if (shadow.startsWith('--')) {
|
} else if (shadow.startsWith('--')) {
|
||||||
const [variable] = shadow.split(/,/g).map(str => str.trim()) // discarding modifier since it's not supported
|
// modifiers are completely unsupported here
|
||||||
const variableSlot = variable.substring(2)
|
const variableSlot = shadow.substring(2)
|
||||||
return findShadow(staticVars[variableSlot], { dynamicVars, staticVars })
|
return findShadow(staticVars[variableSlot], { dynamicVars, staticVars })
|
||||||
} else {
|
} else {
|
||||||
targetShadow = parseCssShadow(shadow)
|
targetShadow = deserializeShadow(shadow)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targetShadow = shadow
|
targetShadow = shadow
|
||||||
|
@ -62,54 +62,63 @@ const findShadow = (shadows, { dynamicVars, staticVars }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const findColor = (color, { dynamicVars, staticVars }) => {
|
export const findColor = (color, { dynamicVars, staticVars }) => {
|
||||||
if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
|
try {
|
||||||
let targetColor = null
|
if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
|
||||||
if (color.startsWith('--')) {
|
let targetColor = null
|
||||||
const [variable, modifier] = color.split(/,/g).map(str => str.trim())
|
if (color.startsWith('--')) {
|
||||||
const variableSlot = variable.substring(2)
|
// Modifier support is pretty much for v2 themes only
|
||||||
if (variableSlot === 'stack') {
|
const [variable, modifier] = color.split(/,/g).map(str => str.trim())
|
||||||
const { r, g, b } = dynamicVars.stacked
|
const variableSlot = variable.substring(2)
|
||||||
targetColor = { r, g, b }
|
if (variableSlot === 'stack') {
|
||||||
} else if (variableSlot.startsWith('parent')) {
|
const { r, g, b } = dynamicVars.stacked
|
||||||
if (variableSlot === 'parent') {
|
|
||||||
const { r, g, b } = dynamicVars.lowerLevelBackground
|
|
||||||
targetColor = { r, g, b }
|
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 {
|
} else {
|
||||||
const virtualSlot = variableSlot.replace(/^parent/, '')
|
switch (variableSlot) {
|
||||||
targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb
|
case 'inheritedBackground':
|
||||||
|
targetColor = convert(dynamicVars.inheritedBackground).rgb
|
||||||
|
break
|
||||||
|
case 'background':
|
||||||
|
targetColor = convert(dynamicVars.background).rgb
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
targetColor = convert(staticVars[variableSlot]).rgb
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
switch (variableSlot) {
|
if (modifier) {
|
||||||
case 'inheritedBackground':
|
const effectiveBackground = dynamicVars.lowerLevelBackground ?? targetColor
|
||||||
targetColor = convert(dynamicVars.inheritedBackground).rgb
|
const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
|
||||||
break
|
const mod = isLightOnDark ? 1 : -1
|
||||||
case 'background':
|
targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
|
||||||
targetColor = convert(dynamicVars.background).rgb
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
targetColor = convert(staticVars[variableSlot]).rgb
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modifier) {
|
if (color.startsWith('$')) {
|
||||||
const effectiveBackground = dynamicVars.lowerLevelBackground ?? targetColor
|
try {
|
||||||
const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
|
targetColor = process(color, colorFunctions, { findColor }, { dynamicVars, staticVars })
|
||||||
const mod = isLightOnDark ? 1 : -1
|
} catch (e) {
|
||||||
targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
|
console.error('Failure executing color function', e)
|
||||||
|
targetColor = '#FF00FF'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Color references other color
|
||||||
|
return targetColor
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Couldn't find color "${color}", variables are:
|
||||||
|
Static:
|
||||||
|
${JSON.stringify(staticVars, null, 2)}
|
||||||
|
Dynamic:
|
||||||
|
${JSON.stringify(dynamicVars, null, 2)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVars) => {
|
||||||
|
@ -164,19 +173,19 @@ export const getEngineChecksum = () => engineChecksum
|
||||||
* @param {boolean} onlyNormalState - only use components 'normal' states, meant for generating theme
|
* @param {boolean} onlyNormalState - only use components 'normal' states, meant for generating theme
|
||||||
* previews since states are the biggest factor for compilation time and are completely unnecessary
|
* previews since states are the biggest factor for compilation time and are completely unnecessary
|
||||||
* when previewing multiple themes at same time
|
* when previewing multiple themes at same time
|
||||||
* @param {string} rootComponentName - [UNTESTED] which component to start from, meant for previewing a
|
|
||||||
* part of the theme (i.e. just the button) for themes 3 editor.
|
|
||||||
*/
|
*/
|
||||||
export const init = ({
|
export const init = ({
|
||||||
inputRuleset,
|
inputRuleset,
|
||||||
ultimateBackgroundColor,
|
ultimateBackgroundColor,
|
||||||
debug = false,
|
debug = false,
|
||||||
liteMode = false,
|
liteMode = false,
|
||||||
|
editMode = false,
|
||||||
onlyNormalState = false,
|
onlyNormalState = false,
|
||||||
rootComponentName = 'Root'
|
initialStaticVars = {}
|
||||||
}) => {
|
}) => {
|
||||||
|
const rootComponentName = 'Root'
|
||||||
if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
|
if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
|
||||||
const staticVars = {}
|
const staticVars = { ...initialStaticVars }
|
||||||
const stacked = {}
|
const stacked = {}
|
||||||
const computed = {}
|
const computed = {}
|
||||||
|
|
||||||
|
@ -218,8 +227,8 @@ export const init = ({
|
||||||
bScore += b.component === 'Text' ? 1 : 0
|
bScore += b.component === 'Text' ? 1 : 0
|
||||||
|
|
||||||
// Debug
|
// Debug
|
||||||
a.specifityScore = aScore
|
a._specificityScore = aScore
|
||||||
b.specifityScore = bScore
|
b._specificityScore = bScore
|
||||||
|
|
||||||
if (aScore === bScore) {
|
if (aScore === bScore) {
|
||||||
return ai - bi
|
return ai - bi
|
||||||
|
@ -228,211 +237,227 @@ export const init = ({
|
||||||
})
|
})
|
||||||
.map(({ data }) => data)
|
.map(({ data }) => data)
|
||||||
|
|
||||||
|
if (!ultimateBackgroundColor) {
|
||||||
|
console.warn('No ultimate background color provided, falling back to panel color')
|
||||||
|
const rootRule = ruleset.findLast((x) => (x.component === 'Root' && x.directives?.['--bg']))
|
||||||
|
ultimateBackgroundColor = rootRule.directives['--bg'].split('|')[1].trim()
|
||||||
|
}
|
||||||
|
|
||||||
const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
|
const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
|
||||||
|
const nonEditableComponents = new Set(Object.values(components).filter(c => c.notEditable).map(c => c.name))
|
||||||
|
|
||||||
const processCombination = (combination) => {
|
const processCombination = (combination) => {
|
||||||
const selector = ruleToSelector(combination, true)
|
try {
|
||||||
const cssSelector = ruleToSelector(combination)
|
const selector = ruleToSelector(combination, true)
|
||||||
|
const cssSelector = ruleToSelector(combination)
|
||||||
|
|
||||||
const parentSelector = selector.split(/ /g).slice(0, -1).join(' ')
|
const parentSelector = selector.split(/ /g).slice(0, -1).join(' ')
|
||||||
const soloSelector = selector.split(/ /g).slice(-1)[0]
|
const soloSelector = selector.split(/ /g).slice(-1)[0]
|
||||||
|
|
||||||
const lowerLevelSelector = parentSelector
|
const lowerLevelSelector = parentSelector
|
||||||
const lowerLevelBackground = computed[lowerLevelSelector]?.background
|
let lowerLevelBackground = computed[lowerLevelSelector]?.background
|
||||||
const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives
|
if (editMode && !lowerLevelBackground) {
|
||||||
const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw
|
// FIXME hack for editor until it supports handling component backgrounds
|
||||||
|
lowerLevelBackground = '#00FFFF'
|
||||||
|
}
|
||||||
|
const lowerLevelVirtualDirectives = computed[lowerLevelSelector]?.virtualDirectives
|
||||||
|
const lowerLevelVirtualDirectivesRaw = computed[lowerLevelSelector]?.virtualDirectivesRaw
|
||||||
|
|
||||||
const dynamicVars = computed[selector] || {
|
const dynamicVars = computed[selector] || {
|
||||||
lowerLevelBackground,
|
lowerLevelBackground,
|
||||||
lowerLevelVirtualDirectives,
|
lowerLevelVirtualDirectives,
|
||||||
lowerLevelVirtualDirectivesRaw
|
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(),
|
|
||||||
...sortBy(combination.state.filter(x => x !== 'normal')).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 = {
|
// Inheriting all of the applicable rules
|
||||||
...computedRule,
|
const existingRules = ruleset.filter(findRules(combination))
|
||||||
directives: {
|
const computedDirectives =
|
||||||
...computedRule.directives,
|
existingRules
|
||||||
textColor: inheritedTextColor,
|
.map(r => r.directives)
|
||||||
textAuto: inheritedTextAuto ?? 'preserve',
|
.reduce((acc, directives) => ({ ...acc, ...directives }), {})
|
||||||
textOpacity: inheritedTextOpacity,
|
const computedRule = {
|
||||||
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,
|
...combination,
|
||||||
directives: computedDirectives
|
directives: computedDirectives
|
||||||
}
|
}
|
||||||
|
|
||||||
return rule
|
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(),
|
||||||
|
...sortBy(combination.state.filter(x => x !== 'normal')).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, { dynamicVars, staticVars })
|
||||||
|
dynamicVars[k] = color
|
||||||
|
if (combination.component === rootComponentName) {
|
||||||
|
staticVars[k.substring(2)] = color
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'shadow': {
|
||||||
|
const shadow = value.split(/,/g).map(s => s.trim()).filter(x => x)
|
||||||
|
dynamicVars[k] = shadow
|
||||||
|
if (combination.component === rootComponentName) {
|
||||||
|
staticVars[k.substring(2)] = shadow
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'generic': {
|
||||||
|
dynamicVars[k] = value
|
||||||
|
if (combination.component === rootComponentName) {
|
||||||
|
staticVars[k.substring(2)] = value
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const rule = {
|
||||||
|
dynamicVars,
|
||||||
|
selector: cssSelector,
|
||||||
|
...combination,
|
||||||
|
directives: computedDirectives
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const { component, variant, state } = combination
|
||||||
|
throw new Error(`Error processing combination ${component}.${variant}:${state.join(':')}: ${e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -443,11 +468,15 @@ export const init = ({
|
||||||
variants: originalVariants = {}
|
variants: originalVariants = {}
|
||||||
} = component
|
} = component
|
||||||
|
|
||||||
const validInnerComponents = (
|
let validInnerComponents
|
||||||
liteMode
|
if (editMode) {
|
||||||
? (component.validInnerComponentsLite || component.validInnerComponents)
|
const temp = (component.validInnerComponentsLite || component.validInnerComponents || [])
|
||||||
: component.validInnerComponents
|
validInnerComponents = temp.filter(c => virtualComponents.has(c) && !nonEditableComponents.has(c))
|
||||||
) || []
|
} else if (liteMode) {
|
||||||
|
validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || [])
|
||||||
|
} else {
|
||||||
|
validInnerComponents = component.validInnerComponents || []
|
||||||
|
}
|
||||||
|
|
||||||
// Normalizing states and variants to always include "normal"
|
// Normalizing states and variants to always include "normal"
|
||||||
const states = { normal: '', ...originalStates }
|
const states = { normal: '', ...originalStates }
|
||||||
|
@ -489,7 +518,7 @@ export const init = ({
|
||||||
combination.component = component.name
|
combination.component = component.name
|
||||||
combination.lazy = component.lazy || parent?.lazy
|
combination.lazy = component.lazy || parent?.lazy
|
||||||
combination.parent = parent
|
combination.parent = parent
|
||||||
if (combination.state.indexOf('hover') >= 0) {
|
if (!liteMode && combination.state.indexOf('hover') >= 0) {
|
||||||
combination.lazy = true
|
combination.lazy = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -538,6 +567,7 @@ export const init = ({
|
||||||
lazy,
|
lazy,
|
||||||
eager,
|
eager,
|
||||||
staticVars,
|
staticVars,
|
||||||
engineChecksum
|
engineChecksum,
|
||||||
|
themeChecksum: sum([lazy, eager])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
static/.gitignore
vendored
Normal file
1
static/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*.custom.*
|
|
@ -24,6 +24,8 @@
|
||||||
"showInstanceSpecificPanel": false,
|
"showInstanceSpecificPanel": false,
|
||||||
"sidebarRight": false,
|
"sidebarRight": false,
|
||||||
"subjectLineBehavior": "email",
|
"subjectLineBehavior": "email",
|
||||||
"theme": "pleroma-dark",
|
"theme": null,
|
||||||
|
"style": null,
|
||||||
|
"palette": null,
|
||||||
"webPushNotifications": false
|
"webPushNotifications": false
|
||||||
}
|
}
|
||||||
|
|
32
static/palettes/index.json
Normal file
32
static/palettes/index.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
||||||
|
"pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
||||||
|
"classic-dark": {
|
||||||
|
"name": "Classic Dark",
|
||||||
|
"bg": "#161c20",
|
||||||
|
"fg": "#282e32",
|
||||||
|
"text": "#b9b9b9",
|
||||||
|
"link": "#baaa9c",
|
||||||
|
"cRed": "#d31014",
|
||||||
|
"cGreen": "#0fa00f",
|
||||||
|
"cBlue": "#0095ff",
|
||||||
|
"cOrange": "#ffa500"
|
||||||
|
},
|
||||||
|
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
|
||||||
|
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
|
||||||
|
"tomorrow-night": {
|
||||||
|
"name": "Tomorrow Night",
|
||||||
|
"bg": "#1d1f21",
|
||||||
|
"fg": "#373b41",
|
||||||
|
"link": "#81a2be",
|
||||||
|
"text": "#c5c8c6",
|
||||||
|
"cRed": "#cc6666",
|
||||||
|
"cBlue": "#8abeb7",
|
||||||
|
"cGreen": "#b5bd68",
|
||||||
|
"cOrange": "#de935f",
|
||||||
|
"_cYellow": "#f0c674",
|
||||||
|
"_cPurple": "#b294bb"
|
||||||
|
},
|
||||||
|
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
|
||||||
|
"monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ]
|
||||||
|
}
|
|
@ -1,12 +1,6 @@
|
||||||
{
|
{
|
||||||
"pleroma-dark": "/static/themes/pleroma-dark.json",
|
"pleroma-dark": "/static/themes/pleroma-dark.json",
|
||||||
"pleroma-light": "/static/themes/pleroma-light.json",
|
"pleroma-light": "/static/themes/pleroma-light.json",
|
||||||
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
|
|
||||||
"classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
|
||||||
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
|
|
||||||
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
|
|
||||||
"monokai": [ "Monokai", "#272822", "#383830", "#f8f8f2", "#f92672", "#F92672", "#a6e22e", "#66d9ef", "#f4bf75" ],
|
|
||||||
|
|
||||||
"redmond-xx": "/static/themes/redmond-xx.json",
|
"redmond-xx": "/static/themes/redmond-xx.json",
|
||||||
"redmond-xx-se": "/static/themes/redmond-xx-se.json",
|
"redmond-xx-se": "/static/themes/redmond-xx-se.json",
|
||||||
"redmond-xxi": "/static/themes/redmond-xxi.json",
|
"redmond-xxi": "/static/themes/redmond-xxi.json",
|
||||||
|
|
80
static/styles/Breezy DX.piss
Normal file
80
static/styles/Breezy DX.piss
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
@meta {
|
||||||
|
name: Breezy DX;
|
||||||
|
author: HJ;
|
||||||
|
license: WTFPL;
|
||||||
|
website: ebin.club;
|
||||||
|
}
|
||||||
|
|
||||||
|
@palette.Dark {
|
||||||
|
bg: #292C32;
|
||||||
|
fg: #292C32;
|
||||||
|
text: #ffffff;
|
||||||
|
link: #1CA4F3;
|
||||||
|
accent: #1CA4F3;
|
||||||
|
cRed: #f41a51;
|
||||||
|
cBlue: #1CA4F3;
|
||||||
|
cGreen: #1af46e;
|
||||||
|
cOrange: #f4af1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
@palette.Light {
|
||||||
|
bg: #EFF0F2;
|
||||||
|
fg: #EFF0F2;
|
||||||
|
text: #1B1F22;
|
||||||
|
underlay: #5d6086;
|
||||||
|
accent: #1CA4F3;
|
||||||
|
cBlue: #1CA4F3;
|
||||||
|
cRed: #f41a51;
|
||||||
|
cGreen: #1af46e;
|
||||||
|
cOrange: #f4af1a;
|
||||||
|
border: #d8e6f9;
|
||||||
|
link: #1CA4F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
Root {
|
||||||
|
--badgeNotification: color | --cRed;
|
||||||
|
--buttonDefaultHoverGlow: shadow | inset 0 0 0 1 --accent / 1;
|
||||||
|
--buttonDefaultFocusGlow: shadow | inset 0 0 0 1 --accent / 1;
|
||||||
|
--buttonDefaultShadow: shadow | inset 0 0 0 1 --text / 0.35, 0 5 5 -5 #000000 / 0.35;
|
||||||
|
--buttonDefaultBevel: shadow | inset 0 14 14 -14 #FFFFFF / 0.1;
|
||||||
|
--buttonPressedBevel: shadow | inset 0 -20 20 -20 #000000 / 0.05;
|
||||||
|
--defaultInputBevel: shadow | inset 0 0 0 1 --text / 0.35;
|
||||||
|
--defaultInputHoverGlow: shadow | 0 0 0 1 --accent / 1;
|
||||||
|
--defaultInputFocusGlow: shadow | 0 0 0 1 --link / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:disabled {
|
||||||
|
shadow: --buttonDefaultBevel, --buttonDefaultShadow
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:hover {
|
||||||
|
shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:toggled {
|
||||||
|
background: $blend(--bg 0.3 --accent)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:pressed {
|
||||||
|
background: $blend(--bg 0.8 --accent)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:pressed:toggled {
|
||||||
|
background: $blend(--bg 0.2 --accent)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:toggled:hover {
|
||||||
|
background: $blend(--bg 0.3 --accent)
|
||||||
|
}
|
||||||
|
|
||||||
|
Input {
|
||||||
|
shadow: --defaultInputBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelHeader {
|
||||||
|
shadow: inset 0 30 30 -30 #ffffff / 0.25
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab:hover {
|
||||||
|
shadow: --buttonDefaultHoverGlow, --buttonDefaultBevel, --buttonDefaultShadow
|
||||||
|
}
|
169
static/styles/Redmond DX.piss
Normal file
169
static/styles/Redmond DX.piss
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
@meta {
|
||||||
|
name: Redmond DX;
|
||||||
|
author: HJ;
|
||||||
|
license: WTFPL;
|
||||||
|
website: ebin.club;
|
||||||
|
}
|
||||||
|
|
||||||
|
@palette.Modern {
|
||||||
|
bg: #D3CFC7;
|
||||||
|
fg: #092369;
|
||||||
|
text: #000000;
|
||||||
|
link: #0000FF;
|
||||||
|
accent: #A5C9F0;
|
||||||
|
cRed: #FF3000;
|
||||||
|
cBlue: #009EFF;
|
||||||
|
cGreen: #309E00;
|
||||||
|
cOrange: #FFCE00;
|
||||||
|
}
|
||||||
|
|
||||||
|
@palette.Classic {
|
||||||
|
bg: #BFBFBF;
|
||||||
|
fg: #000180;
|
||||||
|
text: #000000;
|
||||||
|
link: #0000FF;
|
||||||
|
accent: #A5C9F0;
|
||||||
|
cRed: #FF0000;
|
||||||
|
cBlue: #2E2ECE;
|
||||||
|
cGreen: #007E00;
|
||||||
|
cOrange: #CE8F5F;
|
||||||
|
}
|
||||||
|
|
||||||
|
@palette.Vapor {
|
||||||
|
bg: #F0ADCD;
|
||||||
|
fg: #bca4ee;
|
||||||
|
text: #602040;
|
||||||
|
link: #064745;
|
||||||
|
accent: #9DF7C8;
|
||||||
|
cRed: #86004a;
|
||||||
|
cBlue: #0e5663;
|
||||||
|
cGreen: #0a8b51;
|
||||||
|
cOrange: #787424;
|
||||||
|
}
|
||||||
|
|
||||||
|
Root {
|
||||||
|
--gradientColor: color | --accent;
|
||||||
|
--inputColor: color | #FFFFFF;
|
||||||
|
--bevelLight: color | $brightness(--bg 50);
|
||||||
|
--bevelDark: color | $brightness(--bg -20);
|
||||||
|
--bevelExtraDark: color | #404040;
|
||||||
|
--buttonDefaultBevel: shadow | $borderSide(--bevelExtraDark bottom-right 1 1), $borderSide(--bevelLight top-left 1 1), $borderSide(--bevelDark bottom-right 1 2);
|
||||||
|
--buttonPressedFocusedBevel: shadow | inset 0 0 0 1 #000000 / 1 #Outer , inset 0 0 0 2 --bevelExtraDark / 1 #inner;
|
||||||
|
--buttonPressedBevel: shadow | $borderSide(--bevelDark top-left 1 1), $borderSide(--bevelLight bottom-right 1 1), $borderSide(--bevelExtraDark top-left 1 2);
|
||||||
|
--defaultInputBevel: shadow | $borderSide(--bevelDark top-left 1 1), $borderSide(--bevelLight bottom-right 1 1), $borderSide(--bevelExtraDark top-left 1 2), $borderSide(--bg bottom-right 1 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:toggled {
|
||||||
|
background: --bg;
|
||||||
|
shadow: --buttonPressedBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:focused {
|
||||||
|
shadow: --buttonDefaultBevel, 0 0 0 1 #000000 / 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:pressed {
|
||||||
|
shadow: --buttonPressedBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:hover {
|
||||||
|
shadow: --buttonDefaultBevel;
|
||||||
|
background: --bg
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
shadow: --buttonDefaultBevel;
|
||||||
|
background: --bg;
|
||||||
|
roundness: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:pressed:hover {
|
||||||
|
shadow: --buttonPressedBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:hover:pressed:focused {
|
||||||
|
shadow: --buttonPressedFocusedBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:pressed:focused {
|
||||||
|
shadow: --buttonPressedFocusedBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Button:toggled:pressed {
|
||||||
|
shadow: --buttonPressedFocusedBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Input {
|
||||||
|
background: $mod(--bg -80);
|
||||||
|
shadow: --defaultInputBevel;
|
||||||
|
roundness: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Input:focused {
|
||||||
|
shadow: inset 0 0 0 1 #000000 / 1, --defaultInputBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Input:focused:hover {
|
||||||
|
shadow: --defaultInputBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Input:focused:hover:disabled {
|
||||||
|
shadow: --defaultInputBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Input:hover {
|
||||||
|
shadow: --defaultInputBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Input:disabled {
|
||||||
|
shadow: --defaultInputBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel {
|
||||||
|
shadow: --buttonDefaultBevel;
|
||||||
|
roundness: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
PanelHeader {
|
||||||
|
shadow: inset -1100 0 1000 -1000 --gradientColor / 1 #Gradient ;
|
||||||
|
background: --fg
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab:hover {
|
||||||
|
background: --bg;
|
||||||
|
shadow: --buttonDefaultBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab:active {
|
||||||
|
background: --bg
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab:active:hover {
|
||||||
|
background: --bg;
|
||||||
|
shadow: --defaultButtonBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab:active:hover:disabled {
|
||||||
|
background: --bg
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab:hover:disabled {
|
||||||
|
background: --bg
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab:disabled {
|
||||||
|
background: --bg
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab {
|
||||||
|
background: --bg;
|
||||||
|
shadow: --buttonDefaultBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
Tab:hover:active {
|
||||||
|
shadow: --buttonDefaultBevel
|
||||||
|
}
|
||||||
|
|
||||||
|
TopBar Link {
|
||||||
|
textColor: #ffffff
|
||||||
|
}
|
4
static/styles/index.json
Normal file
4
static/styles/index.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"RedmondDX": "/static/styles/Redmond DX.piss",
|
||||||
|
"BreezyDX": "/static/styles/Breezy DX.piss"
|
||||||
|
}
|
|
@ -7201,6 +7201,11 @@ p-try@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
|
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.0.0.tgz#85080bb87c64688fa47996fe8f7dfbe8211760b1"
|
||||||
|
|
||||||
|
pako@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86"
|
||||||
|
integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==
|
||||||
|
|
||||||
pako@~1.0.2:
|
pako@~1.0.2:
|
||||||
version "1.0.11"
|
version "1.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||||
|
|
Loading…
Reference in a new issue