Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85830280
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
40 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/settings_modal/settings_modal_user_content.scss b/src/components/settings_modal/settings_modal_user_content.scss
index a5314fe126..74172c82cf 100644
--- a/src/components/settings_modal/settings_modal_user_content.scss
+++ b/src/components/settings_modal/settings_modal_user_content.scss
@@ -1,45 +1,44 @@
.settings_tab-switcher {
height: 100%;
.setting-item {
border-bottom: 2px solid var(--border);
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
> div,
> label {
- display: block;
margin-bottom: 0.5em;
&:last-child {
margin-bottom: 0;
}
}
.select-multiple {
display: flex;
.option-list {
margin: 0;
padding-left: 0.5em;
}
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1em;
}
textarea {
width: 100%;
max-width: 100%;
height: 100px;
}
.unavailable,
.unavailable svg {
color: var(--cRed);
}
}
}
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.js b/src/components/settings_modal/tabs/style_tab/style_tab.js
index e4a5f84134..7be1446ed9 100644
--- a/src/components/settings_modal/tabs/style_tab/style_tab.js
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.js
@@ -1,508 +1,550 @@
import { ref, reactive, computed, watch } from 'vue'
import { get, set } from 'lodash'
import Select from 'src/components/select/select.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 TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Popover from 'src/components/popover/popover.vue'
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
import { init } from 'src/services/theme_data/theme_data_3.service.js'
import { getCssRules } from 'src/services/theme_data/css_utils.js'
+import { serialize } from 'src/services/theme_data/iss_serializer.js'
+import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
import {
// rgb2hex,
hex2rgb,
getContrastRatio
} from 'src/services/color_convert/color_convert.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faFloppyDisk, faFolderOpen, faFile } 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
)
export default {
components: {
Select,
Checkbox,
Popover,
StringSetting,
ComponentPreview,
TabSwitcher,
ShadowControl,
ColorInput,
+ PaletteEditor,
OpacityInput,
ContrastRatio
},
setup () {
// ### Meta stuff
const name = ref('')
const author = ref('')
const license = ref('')
const website = ref('')
+ const metaOut = computed(() => {
+ return `@meta {
+ name: ${name.value};
+ author: ${author.value};
+ license: ${license.value};
+ website: ${website.value};
+}`
+ })
+
// ### Palette stuff
const palettes = reactive({
light: {
bg: '#f2f6f9',
fg: '#d6dfed',
text: '#304055',
underlay: '#5d6086',
accent: '#f55b1b',
cBlue: '#0095ff',
cRed: '#d31014',
cGreen: '#0fa00f',
cOrange: '#ffa500',
border: '#d8e6f9'
},
dark: {
bg: '#121a24',
fg: '#182230',
text: '#b9b9ba',
link: '#d8a070',
accent: '#d8a070',
cRed: '#FF0000',
cBlue: '#0095ff',
cGreen: '#0fa00f',
cOrange: '#ffa500'
}
})
+ const palettesOut = computed(() => {
+ return Object.entries(palettes).map(([name, palette]) => {
+ const entries = Object
+ .entries(palette)
+ .map(([slot, data]) => ` ${slot}: ${data};`)
+ .join('\n')
+
+ return `@palette.${name} {\n${entries}\n}`
+ }).join('\n\n')
+ })
+
const editedPalette = ref('dark')
const palette = computed({
get () {
return palettes[editedPalette.value]
},
set (newPalette) {
- console.log(newPalette)
palettes[editedPalette.value] = newPalette
}
})
// ### I18n stuff
// The paths in i18n are getting ridicously long, this effectively shortens them
const getI18nPath = (componentName) => `settings.style.themes3.editor.components.${componentName}`
+ // vue i18n doesn't seem to have (working) mechanic to have a fallback so we have to
+ // make do ourselves
+ const fallbackI18n = (translated, fallback) => {
+ if (translated.startsWith('settings.style.themes3')) {
+ return fallback
+ }
+ return translated
+ }
const getFriendlyNamePath = (componentName) => getI18nPath(componentName) + '.friendlyName'
const getVariantPath = (componentName, variant) => {
return variant === 'normal'
? 'settings.style.themes3.editor.components.normal.variant'
: `${getI18nPath(componentName)}.variants.${variant}`
}
const getStatePath = (componentName, state) => {
return state === 'normal'
? 'settings.style.themes3.editor.components.normal.state'
: `${getI18nPath(componentName)}.states.${state}`
}
// ### Initialization 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)
)
const componentKeys = [...componentsMap.keys()]
// Initializing selected component and its computed descendants
const selectedComponentKey = ref(componentsMap.keys().next().value)
const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value))
const selectedComponentName = computed(() => selectedComponent.value.name)
const selectedVariant = ref('normal')
const selectedComponentVariantsAll = computed(() => {
return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) })
})
const selectedState = reactive(new Set())
const selectedComponentStatesAll = computed(() => {
return Object.keys({ normal: null, ...(selectedComponent.value.states || {}) })
})
const selectedComponentStates = computed(() => {
return selectedComponentStatesAll.value.filter(x => x !== 'normal')
})
+ const updateSelectedStates = (state, v) => {
+ if (v) {
+ selectedState.add(state)
+ } else {
+ selectedState.delete(state)
+ }
+ }
// ### Preview stuff
const 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('; ')
})
// Apart from "hover" we can't really show how component looks like in
// certain states, so we have to fake them.
const simulatePseudoSelectors = css => css
.replace(selectedComponent.value.selector, '.ComponentPreview .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 = reactive([])
const 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('')
})
const previewCss = computed(() => {
const scoped = getCssRules(previewRules)
.map(simulatePseudoSelectors)
return scoped.join('\n')
})
// ### 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 editorFriendlyFallbackStructure = computed(() => {
const root = {}
componentKeys.forEach((componentKey) => {
const componentValue = componentsMap.get(componentKey)
const { defaultRules } = componentValue
defaultRules.forEach((rule) => {
const { parent: rParent } = 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 : componentValue.name}.${pVariant}.${normalizeStates(pState)}`
let output = get(root, pPath)
if (!output) {
set(root, pPath, {})
output = get(root, 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 root
})
// All rules that are made by editor
const allEditedRules = reactive({})
// Checkging whether component can support some "directives" which
// are actually virtual subcomponents, i.e. Text, Link etc
const componentHas = (subComponent) => {
return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent)
}
// Path is 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, getPath(component, directive)) != null
},
set (value) {
if (value) {
const fallback = get(
editorFriendlyFallbackStructure.value,
getPath(component, directive)
)
set(allEditedRules, getPath(component, directive), fallback ?? defaultValue)
} else {
set(allEditedRules, getPath(component, directive), null)
}
}
})
const getEditedElement = (component, directive) => computed({
get () {
let usedRule
const fallback = editorFriendlyFallbackStructure.value
const real = allEditedRules
const path = getPath(component, directive)
usedRule = get(real, path) // get real
if (!usedRule) {
usedRule = get(fallback, path)
}
return usedRule
},
set (value) {
set(allEditedRules, getPath(component, directive), value)
}
})
// All the editable stuff for the component
const editedBackgroundColor = getEditedElement(null, 'background')
const editedOpacity = getEditedElement(null, 'opacity')
const editedTextColor = getEditedElement('Text', 'textColor')
const editedTextAuto = getEditedElement('Text', 'textAuto')
const editedLinkColor = getEditedElement('Link', 'textColor')
const editedIconColor = getEditedElement('Icon', 'textColor')
const editedShadow = getEditedElement(null, 'shadow')
// Shadow is partially edited outside the ShadowControl
// for better space utilization
const editedSubShadowId = ref(null)
const editedSubShadow = computed(() => {
if (editedShadow.value == null || editedSubShadowId.value == null) return null
return editedShadow.value[editedSubShadowId.value]
})
const 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
}
const onSubShadow = (id) => {
if (id != null) {
editedSubShadowId.value = id
} else {
editedSubShadow.value = null
}
}
// Whether specific directives present in the edited rule or not
// Somewhat serves double-duty as it creates/removes the directive
// when set
const isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
const isOpacityPresent = isElementPresent(null, 'opacity', 1)
const isTextColorPresent = isElementPresent('Text', 'textColor', '#000000')
const isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000')
const isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080')
const isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090')
const isShadowPresent = isElementPresent(null, 'shadow', [])
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))
}
})
})
}
convert(selectedComponentName.value, allEditedRules[selectedComponentName.value])
return resultRules
})
const updatePreview = () => {
previewRules.splice(0, previewRules.length)
previewRules.push(...init({
inputRuleset: editorFriendlyToOriginal.value,
initialStaticVars: {
...palette.value
},
ultimateBackgroundColor: '#000000',
rootComponentName: selectedComponentName.value,
editMode: true,
debug: true
}).eager)
}
const updateSelectedComponent = () => {
selectedVariant.value = 'normal'
selectedState.clear()
updatePreview()
}
updateSelectedComponent()
watch(
allEditedRules,
updatePreview
)
watch(
palettes,
updatePreview
)
watch(
editedPalette,
updatePreview
)
watch(
selectedComponentName,
updateSelectedComponent
)
// TODO this is VERY primitive right now, need to make it
// support variables, fallbacks etc.
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 isShadowTabOpen = ref(false)
const onTabSwitch = (tab) => {
isShadowTabOpen.value = tab === 'shadow'
}
+ const exportStyle = () => {
+ console.log('ORIG', toValue(editorFriendlyToOriginal.value))
+ console.log('SERI', serialize(editorFriendlyToOriginal.value))
+
+ const result = [
+ metaOut.value,
+ palettesOut.value,
+ serialize(editorFriendlyToOriginal.value)
+ ].join('\n\n')
+
+ console.log('RESULT', result)
+ console.log('DESERI', deserialize(result))
+ }
+
return {
name,
author,
license,
website,
palette,
editedPalette,
componentKeys,
componentsMap,
selectedComponent,
selectedComponentName,
selectedComponentKey,
selectedComponentVariantsAll,
selectedComponentStates,
selectedVariant,
selectedState,
- updateSelectedStates (state, v) {
- if (v) {
- selectedState.add(state)
- } else {
- selectedState.delete(state)
- }
- },
+ updateSelectedStates,
editedBackgroundColor,
editedOpacity,
editedTextColor,
editedTextAuto,
editedLinkColor,
editedIconColor,
editedShadow,
editedSubShadow,
onSubShadow,
updateSubShadow,
getContrast,
isBackgroundColorPresent,
isOpacityPresent,
isTextColorPresent,
isTextAutoPresent,
isLinkColorPresent,
isIconColorPresent,
isShadowPresent,
previewCss,
previewClass,
editorHintStyle,
getFriendlyNamePath,
+ fallbackI18n,
getVariantPath,
getStatePath,
componentHas,
isShadowTabOpen,
onTabSwitch,
- fallbackI18n (translated, fallback) {
- if (translated.startsWith('settings.style.themes3')) {
- return fallback
- }
- return translated
- }
+ exportStyle
}
}
}
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.scss b/src/components/settings_modal/tabs/style_tab/style_tab.scss
index f93c8ae337..0306454fc7 100644
--- a/src/components/settings_modal/tabs/style_tab/style_tab.scss
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.scss
@@ -1,186 +1,178 @@
.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;
}
}
}
.setting-item {
padding-bottom: 0;
.btn {
padding: 0 0.5em;
}
&:not(:first-child) {
margin-top: 0.5em;
}
&:not(:last-child) {
margin-bottom: 0.5em;
}
&.heading {
display: grid;
align-items: baseline;
grid-template-columns: 1fr auto auto auto;
grid-gap: 0.5em;
h2 {
flex: 1 0 auto;
}
}
&.metadata {
display: flex;
.setting-item {
flex: 2 0 auto;
}
li {
text-align: right;
}
}
}
.palette-editor {
- > label:not(.Select) {
+ > .label:not(.Select) {
font-weight: bold;
justify-self: right;
}
-
- .colors {
- display: grid;
- justify-content: space-around;
- grid-template-columns: repeat(4, min-content);
- grid-template-rows: repeat(auto-fit, min-content);
- grid-gap: 0.5em;
- }
}
.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;
}
}
}
diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.vue b/src/components/settings_modal/tabs/style_tab/style_tab.vue
index 4674d093e8..7184d33a1f 100644
--- a/src/components/settings_modal/tabs/style_tab/style_tab.vue
+++ b/src/components/settings_modal/tabs/style_tab/style_tab.vue
@@ -1,360 +1,306 @@
<script src="./style_tab.js">
</script>
<template>
<div class="StyleTab">
<div class="setting-item heading">
<h2>{{ $t('settings.style.themes3.editor.title') }}</h2>
<button
class="btn button-default"
@click="clearTheme"
>
<FAIcon icon="file" />
{{ $t('settings.style.themes3.editor.new_style') }}
</button>
<button
class="btn button-default"
@click="importStyle"
>
<FAIcon icon="folder-open" />
{{ $t('settings.style.themes3.editor.load_style') }}
</button>
<button
class="btn button-default"
- @click="exportTheme"
+ @click="exportStyle"
>
<FAIcon icon="floppy-disk" />
{{ $t('settings.style.themes3.editor.save_style') }}
</button>
</div>
<div class="setting-item metadata">
<ul class="setting-list">
<li>
<StringSetting v-model="name">
{{ $t('settings.style.themes3.editor.style_name') }}
</StringSetting>
</li>
<li>
<StringSetting v-model="author">
{{ $t('settings.style.themes3.editor.style_author') }}
</StringSetting>
</li>
<li>
<StringSetting v-model="license">
{{ $t('settings.style.themes3.editor.style_license') }}
</StringSetting>
</li>
<li>
<StringSetting v-model="website">
{{ $t('settings.style.themes3.editor.style_website') }}
</StringSetting>
</li>
</ul>
</div>
<div class="setting-item palette-editor">
<div class="label">
<label for="palette-selector">
{{ $t('settings.style.themes3.editor.palette.label') }}
{{ ' ' }}
</label>
<Select
v-model="editedPalette"
id="palette-selector"
>
<option key="dark" value="dark">
{{ $t('settings.style.themes3.editor.palette.dark') }}
</option>
<option key="light" value="light">
{{ $t('settings.style.themes3.editor.palette.light') }}
</option>
</Select>
</div>
- <div class="colors">
- <ColorInput
- v-model="palette.bg"
- :label="$t('settings.style.themes3.editor.palette.bg')"
- />
- <ColorInput
- v-model="palette.fg"
- :label="$t('settings.style.themes3.editor.palette.fg')"
- />
- <ColorInput
- v-model="palette.text"
- :label="$t('settings.style.themes3.editor.palette.text')"
- />
- <ColorInput
- v-model="palette.link"
- :label="$t('settings.style.themes3.editor.palette.link')"
- :fallback="palette.accent"
- />
- <ColorInput
- v-model="palette.accent"
- :label="$t('settings.style.themes3.editor.palette.accent')"
- :fallback="palette.link"
- />
- <ColorInput
- v-model="palette.cRed"
- :label="$t('settings.style.themes3.editor.palette.cRed')"
- />
- <ColorInput
- v-model="palette.cBlue"
- :label="$t('settings.style.themes3.editor.palette.cBlue')"
- />
- <ColorInput
- v-model="palette.cGreen"
- :label="$t('settings.style.themes3.editor.palette.cGreen')"
- />
- <ColorInput
- v-model="palette.cOrange"
- :label="$t('settings.style.themes3.editor.palette.cOrange')"
- />
- <ColorInput
- v-model="palette.extra1"
- fallback="#000000"
- :label="$t('settings.style.themes3.editor.palette.extra1')"
- />
- <ColorInput
- v-model="palette.extra2"
- fallback="#000000"
- :label="$t('settings.style.themes3.editor.palette.extra2')"
- />
- <ColorInput
- v-model="palette.extra3"
- fallback="#000000"
- :label="$t('settings.style.themes3.editor.palette.extra3')"
- />
- </div>
+ <PaletteEditor v-model="palette" />
</div>
<div class="setting-item component-editor">
<div class="component-selector">
<label for="component-selector">
{{ $t('settings.style.themes3.editor.component_selector') }}
{{ ' ' }}
</label>
<Select
v-model="selectedComponentKey"
id="component-selector"
>
<option
v-for="key in componentKeys"
:key="'component-' + key"
:value="key"
>
{{ fallbackI18n($t(getFriendlyNamePath(componentsMap.get(key).name)), componentsMap.get(key).name) }}
</option>
</Select>
</div>
<div
class="variant-selector"
v-if="selectedComponentVariantsAll.length > 1"
>
<label for="variant-selector">
{{ $t('settings.style.themes3.editor.variant_selector') }}
</label>
<Select
v-model="selectedVariant"
>
<option
v-for="variant in selectedComponentVariantsAll"
:value="variant"
:key="'component-variant-' + variant"
>
{{ fallbackI18n($t(getVariantPath(selectedComponentName, variant)), variant) }}
</option>
</Select>
</div>
<div
class="state-selector"
v-if="selectedComponentStates.length > 0"
>
<label>
{{ $t('settings.style.themes3.editor.states_selector') }}
</label>
<ul
class="state-selector-list"
>
<li
v-for="state in selectedStates"
:key="'component-variant-' + state"
>
<Checkbox
:value="selectedStates.has(state)"
@update:modelValue="(v) => updateSelectedStates(state, v)"
>
{{ fallbackI18n($t(getStatePath(selectedComponentName, state)), 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"
:showText="componentHas('Text')"
:shadowControl="isShadowTabOpen"
:previewClass="previewClass"
:previewStyle="editorHintStyle"
:disabled="!editedSubShadow"
:shadow="editedSubShadow"
@update:shadow="({ axis, value }) => updateSubShadow(axis, value)"
/>
</div>
<tab-switcher
ref="tabSwitcher"
class="component-settings"
:on-switch="onTabSwitch"
>
<div
class="editor-tab"
:label="$t('settings.style.themes3.editor.main_tab')"
key="main"
>
<ColorInput
v-model="editedBackgroundColor"
:disabled="!isBackgroundColorPresent"
:label="$t('settings.style.themes3.editor.background')"
/>
<Popover trigger="hover">
<template #trigger>
<Checkbox v-model="isBackgroundColorPresent" />
</template>
<template #content>
{{ $t('settings.style.themes3.editor.include_in_rule') }}
</template>
</Popover>
<OpacityInput
v-model="editedOpacity"
:disabled="!isOpacityPresent"
:label="$t('settings.style.themes3.editor.opacity')"
/>
<Popover trigger="hover">
<template #trigger>
<Checkbox v-model="isOpacityPresent" />
</template>
<template #content>
{{ $t('settings.style.themes3.editor.include_in_rule') }}
</template>
</Popover>
<ColorInput
v-model="editedTextColor"
:label="$t('settings.style.themes3.editor.text_color')"
:disabled="!isTextColorPresent"
v-if="componentHas('Text')"
/>
<Popover
trigger="hover"
v-if="componentHas('Text')"
>
<template #trigger>
<Checkbox v-model="isTextColorPresent" />
</template>
<template #content>
{{ $t('settings.style.themes3.editor.include_in_rule') }}
</template>
</Popover>
<div 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>
<Popover
trigger="hover"
v-if="componentHas('Text')"
>
<template #trigger>
<Checkbox v-model="isTextAutoPresent" />
</template>
<template #content>
{{ $t('settings.style.themes3.editor.include_in_rule') }}
</template>
</Popover>
<div>
<ContrastRatio :contrast="getContrast(editedBackgroundColor, editedTextColor)" />
</div>
<div>
<!-- spacer for missing checkbox -->
</div>
<ColorInput
v-model="editedLinkColor"
:label="$t('settings.style.themes3.editor.link_color')"
:disabled="!isLinkColorPresent"
v-if="componentHas('Link')"
/>
<Popover
trigger="hover"
v-if="componentHas('Link')"
>
<template #trigger>
<Checkbox v-model="isLinkColorPresent" />
</template>
<template #content>
{{ $t('settings.style.themes3.editor.include_in_rule') }}
</template>
</Popover>
<ColorInput
v-model="editedIconColor"
:label="$t('settings.style.themes3.editor.icon_color')"
:disabled="!isIconColorPresent"
v-if="componentHas('Icon')"
/>
<Popover
trigger="hover"
v-if="componentHas('Icon')"
>
<template #trigger>
<Checkbox v-model="isIconColorPresent" />
</template>
<template #content>
{{ $t('settings.style.themes3.editor.include_in_rule') }}
</template>
</Popover>
</div>
<div
class="editor-tab shadow-tab"
:label="$t('settings.style.themes3.editor.shadows_tab')"
key="shadow"
>
<Checkbox
class="style-control"
v-model="isShadowPresent"
>
{{ $t('settings.style.themes3.editor.include_in_rule') }}
</checkbox>
<ShadowControl
v-model="editedShadow"
:disabled="!isShadowPresent"
:no-preview="true"
:separate-inset="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"
@subShadowSelected="onSubShadow"
/>
</div>
</tab-switcher>
</div>
</div>
</template>
<style src="./style_tab.scss" lang="scss"></style>
diff --git a/src/services/theme_data/iss_deserializer.js b/src/services/theme_data/iss_deserializer.js
index 909e9411a6..976d577623 100644
--- a/src/services/theme_data/iss_deserializer.js
+++ b/src/services/theme_data/iss_deserializer.js
@@ -1,157 +1,157 @@
import { flattenDeep } from 'lodash'
const parseShadow = string => {
const modes = ['_full', 'inset', 'x', 'y', 'blur', 'spread', 'color', 'alpha']
const regexPrep = [
// inset keyword (optional)
'^(?:(inset)\\s+)?',
// x
'(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)',
// y
'(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)',
// blur (optional)
'(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?',
// spread (optional)
'(?:(-?[0-9]+(?:\\.[0-9]+)?)\\s+)?',
// either hex, variable or function
'(#[0-9a-f]{6}|--[a-z\\-_]+|\\$[a-z\\-()_]+)',
// opacity (optional)
'(?:\\s+\\/\\s+([0-9]+(?:\\.[0-9]+)?)\\s*)?$'
].join('')
const regex = new RegExp(regexPrep, 'gis') // global, (stable) indices, single-string
const result = regex.exec(string)
if (result == null) {
return string
} else {
const numeric = new Set(['x', 'y', 'blur', 'spread', 'alpha'])
const { x, y, blur, spread, alpha, inset, color } = Object.fromEntries(modes.map((mode, i) => {
if (numeric.has(mode)) {
return [mode, Number(result[i])]
} else if (mode === 'inset') {
return [mode, !!result[i]]
} else {
return [mode, result[i]]
}
}).filter(([k, v]) => v !== false).slice(1))
return { x, y, blur, spread, color, alpha, inset }
}
}
// this works nearly the same as HTML tree converter
const parseIss = (input) => {
const buffer = [{ selector: null, content: [] }]
let textBuffer = ''
const getCurrentBuffer = () => {
let current = buffer[buffer.length - 1]
if (current == null) {
current = { selector: null, content: [] }
}
return current
}
// Processes current line buffer, adds it to output buffer and clears line buffer
const flushText = (kind) => {
if (textBuffer === '') return
if (kind === 'content') {
getCurrentBuffer().content.push(textBuffer.trim())
} else {
getCurrentBuffer().selector = textBuffer.trim()
}
textBuffer = ''
}
for (let i = 0; i < input.length; i++) {
const char = input[i]
if (char === ';') {
flushText('content')
} else if (char === '{') {
flushText('header')
} else if (char === '}') {
flushText('content')
buffer.push({ selector: null, content: [] })
textBuffer = ''
} else {
textBuffer += char
}
}
return buffer
}
export const deserialize = (input) => {
const ast = parseIss(input)
const finalResult = ast.filter(i => i.selector != null).map(item => {
const { selector, content } = item
let stateCount = 0
const selectors = selector.split(/,/g)
const result = selectors.map(selector => {
const output = { component: '' }
let currentDepth = null
selector.split(/ /g).reverse().forEach((fragment, index, arr) => {
const fragmentObject = { component: '' }
let mode = 'component'
for (let i = 0; i < fragment.length; i++) {
const char = fragment[i]
switch (char) {
case '.': {
mode = 'variant'
fragmentObject.variant = ''
break
}
case ':': {
mode = 'state'
fragmentObject.state = fragmentObject.state || []
stateCount++
break
}
default: {
if (mode === 'state') {
const currentState = fragmentObject.state[stateCount - 1]
if (currentState == null) {
fragmentObject.state.push('')
}
fragmentObject.state[stateCount - 1] += char
} else {
fragmentObject[mode] += char
}
}
}
}
if (currentDepth !== null) {
currentDepth.parent = { ...fragmentObject }
currentDepth = currentDepth.parent
} else {
Object.keys(fragmentObject).forEach(key => {
output[key] = fragmentObject[key]
})
if (index !== (arr.length - 1)) {
output.parent = { component: '' }
}
currentDepth = output
}
})
output.directives = Object.fromEntries(content.map(d => {
const [property, value] = d.split(':')
- let realValue = value.trim()
+ let realValue = (value || '').trim()
if (property === 'shadow') {
if (realValue === 'none') {
realValue = []
} else {
realValue = value.split(',').map(v => parseShadow(v.trim()))
}
} if (!Number.isNaN(Number(value))) {
realValue = Number(value)
}
return [property, realValue]
}))
return output
})
return result
})
return flattenDeep(finalResult)
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 1, 3:21 AM (15 h, 44 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1802325
Default Alt Text
(40 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline