Merge branch 'themes3-grand-finale-maybe' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2024-10-13 17:09:57 +03:00
commit eac63e0c57
17 changed files with 477 additions and 254 deletions

View file

@ -130,7 +130,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: {

View file

@ -39,7 +39,7 @@
:class="previewClass" :class="previewClass"
:style="previewStyle" :style="previewStyle"
> >
TEST {{ $t('settings.style.themes3.editor.test_string') }}
</div> </div>
</div> </div>
<input <input

View file

@ -28,7 +28,7 @@
<script setup> <script setup>
import ColorInput from 'src/components/color_input/color_input.vue' import ColorInput from 'src/components/color_input/color_input.vue'
import { import {
// newImporter, newImporter,
newExporter newExporter
} from 'src/services/export_import/export_import.js' } from 'src/services/export_import/export_import.js'
@ -46,23 +46,23 @@ library.add(
const props = defineProps(['modelValue']) const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const paletteExporter = newExporter({ const paletteExporter = newExporter({
filename: 'pleroma.palette.json', filename: 'pleroma_palette',
extension: 'json',
getExportedObject: () => props.modelValue getExportedObject: () => props.modelValue
}) })
/* const paletteImporter = newImporter({
const themeImporter = newImporter({ accept: '.json',
validator: importValidator, onImport (parsed, filename) {
onImport, emit('update:modelValue', parsed)
onImportFailure, }
}) })
*/
const exportPalette = () => { const exportPalette = () => {
paletteExporter.exportData() paletteExporter.exportData()
} }
const importPalette = () => { const importPalette = () => {
// TODO paletteImporter.importData()
} }
const paletteKeys = [ const paletteKeys = [

View file

@ -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);

View file

@ -170,7 +170,7 @@ export default {
}, },
configSink () { configSink () {
if (this.path == null) { if (this.path == null) {
return (k, v) => this.$emit('modelValue:update', v) return (k, v) => this.$emit('update:modelValue', v)
} }
switch (this.realSource) { switch (this.realSource) {
case 'profile': case 'profile':

View file

@ -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">

View file

@ -13,23 +13,33 @@ import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Tooltip from 'src/components/tooltip/tooltip.vue' import Tooltip from 'src/components/tooltip/tooltip.vue'
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue' import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
import Preview from '../theme_tab/theme_preview.vue'
import { init } from 'src/services/theme_data/theme_data_3.service.js' import { init, findColor } 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 { parseShadow /* , deserialize */ } from 'src/services/theme_data/iss_deserializer.js'
import { import {
// rgb2hex, getCssRules,
getScopedVersion
} from 'src/services/theme_data/css_utils.js'
import { serializeShadow, serialize } from 'src/services/theme_data/iss_serializer.js'
import { parseShadow, deserialize } from 'src/services/theme_data/iss_deserializer.js'
import {
rgb2hex,
hex2rgb, hex2rgb,
getContrastRatio getContrastRatio
} from 'src/services/color_convert/color_convert.js' } from 'src/services/color_convert/color_convert.js'
import { import {
// newImporter, newImporter,
newExporter newExporter
} from 'src/services/export_import/export_import.js' } from 'src/services/export_import/export_import.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faFloppyDisk, faFolderOpen, faFile } from '@fortawesome/free-solid-svg-icons' import {
faFloppyDisk,
faFolderOpen,
faFile,
faArrowsRotate,
faCheck
} from '@fortawesome/free-solid-svg-icons'
// helper for debugging // helper for debugging
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
@ -41,7 +51,9 @@ const normalizeStates = (states) => ['normal', ...(states?.filter(x => x !== 'no
library.add( library.add(
faFile, faFile,
faFloppyDisk, faFloppyDisk,
faFolderOpen faFolderOpen,
faArrowsRotate,
faCheck
) )
export default { export default {
@ -57,25 +69,27 @@ export default {
ColorInput, ColorInput,
PaletteEditor, PaletteEditor,
OpacityInput, OpacityInput,
ContrastRatio ContrastRatio,
Preview
}, },
setup () { setup () {
const exports = {}
// All rules that are made by editor // All rules that are made by editor
const allEditedRules = reactive({}) const allEditedRules = reactive({})
// ## Meta stuff // ## Meta stuff
const name = ref('') exports.name = ref('')
const author = ref('') exports.author = ref('')
const license = ref('') exports.license = ref('')
const website = ref('') exports.website = ref('')
const metaOut = computed(() => { const metaOut = computed(() => {
return [ return [
'@meta {', '@meta {',
` name: ${name.value};`, ` name: ${exports.name.value};`,
` author: ${author.value};`, ` author: ${exports.author.value};`,
` license: ${license.value};`, ` license: ${exports.license.value};`,
` website: ${website.value};`, ` website: ${exports.website.value};`,
'}' '}'
].join('\n') ].join('\n')
}) })
@ -108,6 +122,20 @@ export default {
border: '#d8e6f9' 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 selectedPaletteId = ref(0)
const selectedPalette = computed({ const selectedPalette = computed({
get () { get () {
@ -117,7 +145,10 @@ export default {
palettes[selectedPaletteId.value] = newPalette palettes[selectedPaletteId.value] = newPalette
} }
}) })
const getNewPalette = () => ({ exports.selectedPaletteId = selectedPaletteId
exports.selectedPalette = selectedPalette
exports.getNewPalette = () => ({
name: 'new palette', name: 'new palette',
bg: '#121a24', bg: '#121a24',
fg: '#182230', fg: '#182230',
@ -151,26 +182,31 @@ export default {
key => [key, componentsContext(key).default] key => [key, componentsContext(key).default]
).filter(([key, component]) => !component.virtual && !component.notEditable) ).filter(([key, component]) => !component.virtual && !component.notEditable)
) )
exports.componentsMap = componentsMap
const componentKeys = [...componentsMap.keys()] const componentKeys = [...componentsMap.keys()]
exports.componentKeys = componentKeys
// selection basis // selection basis
const selectedComponentKey = ref(componentsMap.keys().next().value) const selectedComponentKey = ref(componentsMap.keys().next().value)
exports.selectedComponentKey = selectedComponentKey
const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value)) const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value))
const selectedComponentName = computed(() => selectedComponent.value.name) const selectedComponentName = computed(() => selectedComponent.value.name)
const selectedComponentVariants = computed(() => { exports.selectedComponentVariants = computed(() => {
return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) }) return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) })
}) })
const selectedComponentStatesAll = computed(() => { const selectedComponentStatesAll = computed(() => {
return Object.keys({ normal: null, ...(selectedComponent.value.states || {}) }) return Object.keys({ normal: null, ...(selectedComponent.value.states || {}) })
}) })
const selectedComponentStates = computed(() => { exports.selectedComponentStates = computed(() => {
return selectedComponentStatesAll.value.filter(x => x !== 'normal') return selectedComponentStatesAll.value.filter(x => x !== 'normal')
}) })
// selection // selection
const selectedVariant = ref('normal') const selectedVariant = ref('normal')
exports.selectedVariant = selectedVariant
const selectedState = reactive(new Set()) const selectedState = reactive(new Set())
const updateSelectedStates = (state, v) => { exports.selectedState = selectedState
exports.updateSelectedStates = (state, v) => {
if (v) { if (v) {
selectedState.add(state) selectedState.add(state)
} else { } else {
@ -182,47 +218,53 @@ export default {
// The native structure of separate rules and the child -> parent // The native structure of separate rules and the child -> parent
// relation isn't very convenient for editor, we replace the array // relation isn't very convenient for editor, we replace the array
// and child -> parent structure with map and parent -> child structure // 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) {
acc._children = acc._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 editorFriendlyFallbackStructure = computed(() => {
const root = {} const root = {}
componentKeys.forEach((componentKey) => { componentKeys.forEach((componentKey) => {
const componentValue = componentsMap.get(componentKey) const componentValue = componentsMap.get(componentKey)
const { defaultRules } = componentValue const { defaultRules, name } = componentValue
defaultRules.forEach((rule) => { rulesToEditorFriendly(
const { parent: rParent } = rule defaultRules.map((rule) => ({ ...rule, component: name })),
const parent = rParent ?? rule root
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 return root
@ -230,7 +272,7 @@ export default {
// Checkging whether component can support some "directives" which // Checkging whether component can support some "directives" which
// are actually virtual subcomponents, i.e. Text, Link etc // are actually virtual subcomponents, i.e. Text, Link etc
const componentHas = (subComponent) => { exports.componentHas = (subComponent) => {
return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent) return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent)
} }
@ -283,21 +325,24 @@ export default {
}) })
// All the editable stuff for the component // All the editable stuff for the component
const editedBackgroundColor = getEditedElement(null, 'background') exports.editedBackgroundColor = getEditedElement(null, 'background')
const isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF') exports.isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
const editedOpacity = getEditedElement(null, 'opacity') exports.editedOpacity = getEditedElement(null, 'opacity')
const isOpacityPresent = isElementPresent(null, 'opacity', 1) exports.isOpacityPresent = isElementPresent(null, 'opacity', 1)
const editedTextColor = getEditedElement('Text', 'textColor') exports.editedTextColor = getEditedElement('Text', 'textColor')
const isTextColorPresent = isElementPresent('Text', 'textColor', '#000000') exports.isTextColorPresent = isElementPresent('Text', 'textColor', '#000000')
const editedTextAuto = getEditedElement('Text', 'textAuto') exports.editedTextAuto = getEditedElement('Text', 'textAuto')
const isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000') exports.isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000')
const editedLinkColor = getEditedElement('Link', 'textColor') exports.editedLinkColor = getEditedElement('Link', 'textColor')
const isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080') exports.isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080')
const editedIconColor = getEditedElement('Icon', 'textColor') exports.editedIconColor = getEditedElement('Icon', 'textColor')
const isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090') exports.isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090')
exports.editedBorderColor = getEditedElement('Border', 'textColor')
exports.isBorderColorPresent = isElementPresent('Border', 'textColor', '#909090')
// TODO this is VERY primitive right now, need to make it // TODO this is VERY primitive right now, need to make it
// support variables, fallbacks etc. // support variables, fallbacks etc.
const getContrast = (bg, text) => { exports.getContrast = (bg, text) => {
try { try {
const bgRgb = hex2rgb(bg) const bgRgb = hex2rgb(bg)
const textRgb = hex2rgb(text) const textRgb = hex2rgb(text)
@ -321,7 +366,6 @@ export default {
} }
const normalizeShadows = (shadows) => { const normalizeShadows = (shadows) => {
console.log('NORMALIZE')
return shadows?.map(shadow => { return shadows?.map(shadow => {
if (typeof shadow === 'object') { if (typeof shadow === 'object') {
return shadow return shadow
@ -336,20 +380,23 @@ export default {
// Shadow is partially edited outside the ShadowControl // Shadow is partially edited outside the ShadowControl
// for better space utilization // for better space utilization
const editedShadow = getEditedElement(null, 'shadow', normalizeShadows) const editedShadow = getEditedElement(null, 'shadow', normalizeShadows)
exports.editedShadow = editedShadow
const editedSubShadowId = ref(null) const editedSubShadowId = ref(null)
exports.editedSubShadowId = editedSubShadowId
const editedSubShadow = computed(() => { const editedSubShadow = computed(() => {
if (editedShadow.value == null || editedSubShadowId.value == null) return null if (editedShadow.value == null || editedSubShadowId.value == null) return null
return editedShadow.value[editedSubShadowId.value] return editedShadow.value[editedSubShadowId.value]
}) })
const isShadowPresent = isElementPresent(null, 'shadow', []) exports.editedSubShadow = editedSubShadow
const onSubShadow = (id) => { exports.isShadowPresent = isElementPresent(null, 'shadow', [])
exports.onSubShadow = (id) => {
if (id != null) { if (id != null) {
editedSubShadowId.value = id editedSubShadowId.value = id
} else { } else {
editedSubShadow.value = null editedSubShadow.value = null
} }
} }
const updateSubShadow = (axis, value) => { exports.updateSubShadow = (axis, value) => {
if (!editedSubShadow.value || editedSubShadowId.value == null) return if (!editedSubShadow.value || editedSubShadowId.value == null) return
const newEditedShadow = [...editedShadow.value] const newEditedShadow = [...editedShadow.value]
@ -360,13 +407,13 @@ export default {
editedShadow.value = newEditedShadow editedShadow.value = newEditedShadow
} }
const isShadowTabOpen = ref(false) exports.isShadowTabOpen = ref(false)
const onTabSwitch = (tab) => { exports.onTabSwitch = (tab) => {
isShadowTabOpen.value = tab === 'shadow' exports.isShadowTabOpen.value = tab === 'shadow'
} }
// component preview // component preview
const editorHintStyle = computed(() => { exports.editorHintStyle = computed(() => {
const editorHint = selectedComponent.value.editor const editorHint = selectedComponent.value.editor
const styles = [] const styles = []
if (editorHint && Object.keys(editorHint).length > 0) { if (editorHint && Object.keys(editorHint).length > 0) {
@ -389,7 +436,7 @@ export default {
.replace(':focus', '.preview-focus') .replace(':focus', '.preview-focus')
.replace(':focus-within', '.preview-focus-within') .replace(':focus-within', '.preview-focus-within')
.replace(':disabled', '.preview-disabled') .replace(':disabled', '.preview-disabled')
const previewClass = computed(() => { exports.previewClass = computed(() => {
const selectors = [] const selectors = []
if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') { if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') {
selectors.push(selectedComponent.value.variants[selectedVariant.value]) selectors.push(selectedComponent.value.variants[selectedVariant.value])
@ -403,7 +450,8 @@ export default {
return selectors.map(x => x.substring(1)).join('') return selectors.map(x => x.substring(1)).join('')
}) })
const previewRules = reactive([]) const previewRules = reactive([])
const previewCss = computed(() => { exports.previewRules = previewRules
exports.previewCss = computed(() => {
try { try {
const scoped = getCssRules(previewRules).map(simulatePseudoSelectors) const scoped = getCssRules(previewRules).map(simulatePseudoSelectors)
return scoped.join('\n') return scoped.join('\n')
@ -523,9 +571,43 @@ export default {
} }
}) })
const virtualDirectives = reactive(allCustomVirtualDirectives) const virtualDirectives = reactive(allCustomVirtualDirectives)
exports.virtualDirectives = virtualDirectives
exports.onVirtualDirectivesUpdate = (e) => {
virtualDirectives.splice(0, virtualDirectives.length)
virtualDirectives.push(...e)
}
const selectedVirtualDirectiveId = ref(0) const selectedVirtualDirectiveId = ref(0)
const selectedVirtualDirective = computed(() => virtualDirectives[selectedVirtualDirectiveId.value]) exports.selectedVirtualDirectiveId = selectedVirtualDirectiveId
const selectedVirtualDirectiveParsed = computed({ const selectedVirtualDirective = computed({
get () {
return virtualDirectives[selectedVirtualDirectiveId.value]
},
set (value) {
virtualDirectives[selectedVirtualDirectiveId.value].value = value
}
})
exports.selectedVirtualDirective = selectedVirtualDirective
exports.selectedVirtualDirectiveValType = computed({
get () {
return virtualDirectives[selectedVirtualDirectiveId.value].valType
},
set (value) {
virtualDirectives[selectedVirtualDirectiveId.value].valType = value
switch (value) {
case 'shadow':
virtualDirectives[selectedVirtualDirectiveId.value].value = '0 0 0 #000000'
break
case 'color':
virtualDirectives[selectedVirtualDirectiveId.value].value = '#000000'
break
default:
virtualDirectives[selectedVirtualDirectiveId.value].value = 'none'
}
}
})
exports.selectedVirtualDirectiveParsed = computed({
get () { get () {
switch (selectedVirtualDirective.value.valType) { switch (selectedVirtualDirective.value.valType) {
case 'shadow': { case 'shadow': {
@ -537,25 +619,109 @@ export default {
return normalizeShadows(splitShadow) return normalizeShadows(splitShadow)
} }
} }
case 'color':
return selectedVirtualDirective.value.value
default: default:
return null return selectedVirtualDirective.value.value
}
},
set (value) {
switch (selectedVirtualDirective.value.valType) {
case 'shadow': {
virtualDirectives[selectedVirtualDirectiveId.value].value = value.map(x => serializeShadow(x)).join(', ')
break
}
default:
virtualDirectives[selectedVirtualDirectiveId.value].value = value
} }
} }
}) })
const getNewDirective = () => ({ exports.getNewVirtualDirective = () => ({
name: 'newDirective', name: 'newDirective',
valType: 'generic', valType: 'generic',
value: 'foobar' value: 'foobar'
}) })
exports.computeColor = (color) => {
const computedColor = findColor(color, { dynamicVars: {}, staticVars: selectedPalette.value })
if (computedColor) {
return rgb2hex(computedColor)
}
return null
}
const overallPreviewRules = ref()
exports.overallPreviewRules = overallPreviewRules
exports.updateOverallPreview = () => {
try {
// This normally would be handled by Root but since we pass something
// else we have to make do ourselves
const { name, ...rest } = selectedPalette.value
const paletteRule = {
component: 'Root',
directives: Object
.entries(rest)
.map(([k, v]) => ['--' + k, v])
.reduce((acc, [k, v]) => ({ ...acc, [k]: `color | ${v}` }), {})
}
const rules = init({
inputRuleset: [
...editorFriendlyToOriginal.value,
paletteRule
],
ultimateBackgroundColor: '#000000',
liteMode: true,
debug: true
}).eager
overallPreviewRules.value = getScopedVersion(
getCssRules(rules),
'#edited-style-preview'
).join('\n')
} catch (e) {
console.error('Could not compile preview theme', e)
}
}
// ## Export and Import // ## Export and Import
const styleExporter = newExporter({ const styleExporter = newExporter({
filename: name.value || 'pleroma_theme', filename: () => exports.name.value ?? 'pleroma_theme',
mime: 'text/plain', mime: 'text/plain',
extension: 'piss', extension: 'piss',
getExportedObject: () => exportStyleData.value getExportedObject: () => exportStyleData.value
}) })
const styleImporter = newImporter({
accept: '.piss',
parser: (string) => deserialize(string),
onImport (parsed, filename) {
const editorComponents = parsed.filter(x => x.component.startsWith('@'))
const rules = parsed.filter(x => !x.component.startsWith('@'))
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
onPalettesUpdate(palettesIn.map(x => ({ name: x.variant, ...x.directives })))
console.log('PALETTES', palettesIn)
Object.keys(allEditedRules).forEach((k) => delete allEditedRules[k])
rules.forEach(rule => {
rulesToEditorFriendly(
[rule],
allEditedRules
)
})
}
})
const exportStyleData = computed(() => { const exportStyleData = computed(() => {
return [ return [
metaOut.value, metaOut.value,
@ -563,80 +729,15 @@ export default {
serialize(editorFriendlyToOriginal.value) serialize(editorFriendlyToOriginal.value)
].join('\n\n') ].join('\n\n')
}) })
const exportStyle = () => {
exports.exportStyle = () => {
styleExporter.exportData() styleExporter.exportData()
} }
return { exports.importStyle = () => {
// ## Meta styleImporter.importData()
name,
author,
license,
website,
// ## Palette
palettes,
selectedPalette,
selectedPaletteId,
getNewPalette,
// ## Components
componentKeys,
componentsMap,
// selection basis
selectedComponent,
selectedComponentName,
selectedComponentKey,
selectedComponentVariants,
selectedComponentStates,
// selection
selectedVariant,
selectedState,
updateSelectedStates,
// component directives
componentHas,
// component colors
editedBackgroundColor,
isBackgroundColorPresent,
editedOpacity,
isOpacityPresent,
editedTextColor,
isTextColorPresent,
editedTextAuto,
isTextAutoPresent,
editedLinkColor,
isLinkColorPresent,
editedIconColor,
isIconColorPresent,
getContrast,
// component shadow
editedShadow,
editedSubShadow,
isShadowPresent,
onSubShadow,
updateSubShadow,
isShadowTabOpen,
onTabSwitch,
// component preview
editorHintStyle,
previewCss,
previewClass,
// ## Variables
virtualDirectives,
selectedVirtualDirective,
selectedVirtualDirectiveId,
selectedVirtualDirectiveParsed,
getNewDirective,
// ## Export and Import
exportStyle
} }
return exports
} }
} }

View file

@ -65,24 +65,60 @@
&.heading { &.heading {
display: grid; display: grid;
align-items: baseline; grid-template:
grid-template-columns: 1fr auto auto auto; "meta meta preview preview"
"meta meta preview preview"
"meta meta preview preview"
"meta meta preview preview"
"new new preview preview"
"load save refresh apply";
grid-gap: 0.5em; grid-gap: 0.5em;
grid-template-columns: min-content min-content 6fr max-content;
grid-template-rows: repeat(4, min-content) repeat(2, 2em);
h2 { ul.setting-list {
flex: 1 0 auto; padding: 0;
} margin: 0;
} display: grid;
grid-template-rows: subgrid;
grid-area: meta;
&.metadata { > li {
display: flex; margin: 0;
}
.setting-item { .meta-field {
flex: 2 0 auto; margin: 0;
.setting-label {
display: inline-block;
margin-bottom: 0.5em;
}
}
} }
li { #edited-style-preview {
text-align: right; grid-area: preview;
}
.button-save {
grid-area: save;
}
.button-load {
grid-area: load;
}
.button-new {
grid-area: new;
}
.button-refresh {
grid-area: refresh;
}
.button-apply {
grid-area: apply;
} }
} }
} }
@ -119,9 +155,18 @@
} }
} }
.palettes-editor { .palette-editor {
width: min-content;
.list-edit-area { .list-edit-area {
display: grid;
align-self: baseline; align-self: baseline;
grid-template-rows: subgrid;
grid-template-columns: 1fr;
}
.palette-editor-single {
grid-row: 2 / span 2;
} }
} }

View file

@ -4,49 +4,66 @@
<template> <template>
<div class="StyleTab"> <div class="StyleTab">
<div class="setting-item heading"> <div class="setting-item heading">
<!-- TODO: This needs to go --> <!-- eslint-disable vue/no-v-text-v-html-on-component -->
<h2>{{ $t('settings.style.themes3.editor.title') }}</h2> <component
:is="'style'"
v-html="overallPreviewRules"
/>
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
<Preview id="edited-style-preview" />
<button <button
class="btn button-default" class="btn button-default button-new"
@click="clearTheme" @click="clearTheme"
> >
<FAIcon icon="file" /> <FAIcon icon="file" />
{{ $t('settings.style.themes3.editor.new_style') }} {{ $t('settings.style.themes3.editor.new_style') }}
</button> </button>
<button <button
class="btn button-default" class="btn button-default button-load"
@click="importStyle" @click="importStyle"
> >
<FAIcon icon="folder-open" /> <FAIcon icon="folder-open" />
{{ $t('settings.style.themes3.editor.load_style') }} {{ $t('settings.style.themes3.editor.load_style') }}
</button> </button>
<button <button
class="btn button-default" class="btn button-default button-save"
@click="exportStyle" @click="exportStyle"
> >
<FAIcon icon="floppy-disk" /> <FAIcon icon="floppy-disk" />
{{ $t('settings.style.themes3.editor.save_style') }} {{ $t('settings.style.themes3.editor.save_style') }}
</button> </button>
</div> <button
<div class="setting-item metadata"> class="btn button-default button-refresh"
<ul class="setting-list"> @click="updateOverallPreview"
>
<FAIcon icon="arrows-rotate" />
{{ $t('settings.style.themes3.editor.refresh_preview') }}
</button>
<button
class="btn button-default button-apply"
@click="applyTheme"
>
<FAIcon icon="check" />
{{ $t('settings.style.themes3.editor.apply_preview') }}
</button>
<ul class="setting-list style-metadata">
<li> <li>
<StringSetting v-model="name"> <StringSetting class="meta-field" v-model="name">
{{ $t('settings.style.themes3.editor.style_name') }} {{ $t('settings.style.themes3.editor.style_name') }}
</StringSetting> </StringSetting>
</li> </li>
<li> <li>
<StringSetting v-model="author"> <StringSetting class="meta-field" v-model="author">
{{ $t('settings.style.themes3.editor.style_author') }} {{ $t('settings.style.themes3.editor.style_author') }}
</StringSetting> </StringSetting>
</li> </li>
<li> <li>
<StringSetting v-model="license"> <StringSetting class="meta-field" v-model="license">
{{ $t('settings.style.themes3.editor.style_license') }} {{ $t('settings.style.themes3.editor.style_license') }}
</StringSetting> </StringSetting>
</li> </li>
<li> <li>
<StringSetting v-model="website"> <StringSetting class="meta-field" v-model="website">
{{ $t('settings.style.themes3.editor.style_website') }} {{ $t('settings.style.themes3.editor.style_website') }}
</StringSetting> </StringSetting>
</li> </li>
@ -148,6 +165,7 @@
> >
<ColorInput <ColorInput
v-model="editedBackgroundColor" v-model="editedBackgroundColor"
:fallback="computeColor(editedBackgroundColor)"
:disabled="!isBackgroundColorPresent" :disabled="!isBackgroundColorPresent"
:label="$t('settings.style.themes3.editor.background')" :label="$t('settings.style.themes3.editor.background')"
/> />
@ -165,6 +183,7 @@
<ColorInput <ColorInput
v-if="componentHas('Text')" v-if="componentHas('Text')"
v-model="editedTextColor" v-model="editedTextColor"
:fallback="computeColor(editedTextColor)"
:label="$t('settings.style.themes3.editor.text_color')" :label="$t('settings.style.themes3.editor.text_color')"
:disabled="!isTextColorPresent" :disabled="!isTextColorPresent"
/> />
@ -213,6 +232,7 @@
<ColorInput <ColorInput
v-if="componentHas('Link')" v-if="componentHas('Link')"
v-model="editedLinkColor" v-model="editedLinkColor"
:fallback="computeColor(editedLinkColor)"
:label="$t('settings.style.themes3.editor.link_color')" :label="$t('settings.style.themes3.editor.link_color')"
:disabled="!isLinkColorPresent" :disabled="!isLinkColorPresent"
/> />
@ -225,6 +245,7 @@
<ColorInput <ColorInput
v-if="componentHas('Icon')" v-if="componentHas('Icon')"
v-model="editedIconColor" v-model="editedIconColor"
:fallback="computeColor(editedIconColor)"
:label="$t('settings.style.themes3.editor.icon_color')" :label="$t('settings.style.themes3.editor.icon_color')"
:disabled="!isIconColorPresent" :disabled="!isIconColorPresent"
/> />
@ -234,6 +255,19 @@
> >
<Checkbox v-model="isIconColorPresent" /> <Checkbox v-model="isIconColorPresent" />
</Tooltip> </Tooltip>
<ColorInput
v-if="componentHas('Border')"
v-model="editedBorderColor"
:fallback="computeColor(editedBorderColor)"
:label="$t('settings.style.themes3.editor.Border_color')"
:disabled="!isBorderColorPresent"
/>
<Tooltip
v-if="componentHas('Border')"
:text="$t('settings.style.themes3.editor.include_in_rule')"
>
<Checkbox v-model="isBorderColorPresent" />
</Tooltip>
</div> </div>
<div <div
key="shadow" key="shadow"
@ -262,38 +296,47 @@
:label="$t('settings.style.themes3.editor.palette_tab')" :label="$t('settings.style.themes3.editor.palette_tab')"
class="setting-item list-editor palette-editor" class="setting-item list-editor palette-editor"
> >
<label <label
class="list-select-label" class="list-select-label"
for="palette-selector" 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"
> >
{{ $t('settings.style.themes3.palette.label') }} {{ p.name }}
{{ ' ' }} </option>
</label> </Select>
<Select <SelectMotion
id="palette-selector" class="list-select-movement"
v-model="selectedPaletteId" :modelValue="palettes"
class="list-select" @update:modelValue="onPalettesUpdate"
size="4" :selected-id="selectedPaletteId"
> :get-add-value="getNewPalette"
<option @update:selectedId="e => selectedPaletteId = e"
v-for="(p, index) in palettes"
:key="p.name"
:value="index"
>
{{ p.name }}
</option>
</Select>
<SelectMotion
class="list-select-movement"
v-model="palettes"
:get-add-value="getNewPalette"
:selected-id="selectedPaletteId"
@update:selectedId="e => selectedPaletteId = e"
/>
<PaletteEditor
class="list-edit-area"
v-model="selectedPalette"
/> />
<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> </div>
<div <div
key="variables" key="variables"
@ -304,14 +347,14 @@
class="list-select-label" class="list-select-label"
for="variables-selector" for="variables-selector"
> >
{{ $t('settings.style.themes3.variables.label') }} {{ $t('settings.style.themes3.editor.variables.label') }}
{{ ' ' }} {{ ' ' }}
</label> </label>
<Select <Select
id="variables-selector" id="variables-selector"
v-model="selectedVirtualDirectiveId" v-model="selectedVirtualDirectiveId"
class="list-select" class="list-select"
size="9" size="20"
> >
<option <option
v-for="(p, index) in virtualDirectives" v-for="(p, index) in virtualDirectives"
@ -323,7 +366,8 @@
</Select> </Select>
<SelectMotion <SelectMotion
class="list-select-movement" class="list-select-movement"
v-model="virtualDirectives" :modelValue="virtualDirectives"
@update:modelValue="onVirtualDirectivesUpdate"
:selected-id="selectedVirtualDirectiveId" :selected-id="selectedVirtualDirectiveId"
:get-add-value="getNewVirtualDirective" :get-add-value="getNewVirtualDirective"
@update:selectedId="e => selectedVirtualDirectiveId = e" @update:selectedId="e => selectedVirtualDirectiveId = e"
@ -334,7 +378,7 @@
class="variable-name-label" class="variable-name-label"
for="variables-selector" for="variables-selector"
> >
{{ $t('settings.style.themes3.variables.name_label') }} {{ $t('settings.style.themes3.editor.variables.name_label') }}
{{ ' ' }} {{ ' ' }}
</label> </label>
<input <input
@ -345,25 +389,36 @@
class="variable-type-label" class="variable-type-label"
for="variables-selector" for="variables-selector"
> >
{{ $t('settings.style.themes3.variables.type_label') }} {{ $t('settings.style.themes3.editor.variables.type_label') }}
{{ ' ' }} {{ ' ' }}
</label> </label>
<Select <Select
v-model="selectedVirtualDirective.valType" v-model="selectedVirtualDirectiveValType"
> >
<option value='shadow'> <option value='shadow'>
{{ $t('settings.style.themes3.variables.type_label') }} {{ $t('settings.style.themes3.editor.variables.type_shadow') }}
shadow</option> </option>
<option value='shadow'>color</option> <option value='color'>
<option value='shadow'>generic</option> {{ $t('settings.style.themes3.editor.variables.type_color') }}
</option>
<option value='generic'>
{{ $t('settings.style.themes3.editor.variables.type_generic') }}
</option>
</Select> </Select>
</div> </div>
<ShadowControl <ShadowControl
v-if="selectedVirtualDirective.valType === 'shadow'" v-if="selectedVirtualDirectiveValType === 'shadow'"
v-model="selectedVirtualDirectiveParsed" v-model="selectedVirtualDirectiveParsed"
:computeColor="computeColor"
:compact="true" :compact="true"
/> />
</div> <ColorInput
v-if="selectedVirtualDirectiveValType === 'color'"
v-model="selectedVirtualDirectiveParsed"
:fallback="computeColor(selectedVirtualDirectiveParsed)"
:label="$t('settings.style.themes3.editor.variables.virtual_color')"
/>
</div>
</div> </div>
</tab-switcher> </tab-switcher>
</div> </div>

View file

@ -46,6 +46,7 @@ export default {
'separateInset', 'separateInset',
'noPreview', 'noPreview',
'disabled', 'disabled',
'computeColor',
'compact' 'compact'
], ],
emits: ['update:modelValue', 'subShadowSelected'], emits: ['update:modelValue', 'subShadowSelected'],
@ -107,6 +108,13 @@ export default {
usingFallback () { usingFallback () {
return this.modelValue == null return this.modelValue == null
}, },
getFallback () {
if (typeof this.computeColor === 'function' && this.selected?.color) {
return this.computeColor(this.selected.color)
} else {
return this.currentFallback?.color
}
},
style () { style () {
try { try {
if (this.separateInset) { if (this.separateInset) {

View file

@ -5,8 +5,6 @@
grid-template-areas: "selector preview tweak"; grid-template-areas: "selector preview tweak";
grid-gap: 0.5em; grid-gap: 0.5em;
justify-content: stretch; justify-content: stretch;
margin-bottom: 1em;
width: 100%;
&.-compact { &.-compact {
grid-template-columns: 10em 1fr; grid-template-columns: 10em 1fr;
@ -112,7 +110,7 @@
.shadow-preview { .shadow-preview {
grid-area: preview; grid-area: preview;
min-width: 10em; min-width: 25em;
margin-left: 0.125em; margin-left: 0.125em;
align-self: start; align-self: start;
justify-self: center; justify-self: center;

View file

@ -165,7 +165,7 @@
:model-value="selected?.color" :model-value="selected?.color"
:disabled="disabled || !present" :disabled="disabled || !present"
:label="$t('settings.style.common.color')" :label="$t('settings.style.common.color')"
:fallback="currentFallback?.color" :fallback="getFallback"
:show-optional-tickbox="false" :show-optional-tickbox="false"
name="shadow" name="shadow"
@update:modelValue="e => updateProperty('color', e)" @update:modelValue="e => updateProperty('color', e)"

View file

@ -757,7 +757,8 @@
"themes3": { "themes3": {
"define": "Override", "define": "Override",
"palette": { "palette": {
"label": "Palettes", "label": "Color schemes",
"name_label": "Color scheme name",
"import": "Import", "import": "Import",
"export": "Export", "export": "Export",
"bg": "Panel background", "bg": "Panel background",
@ -774,11 +775,6 @@
"extra3": "Extra 3", "extra3": "Extra 3",
"v2_unsupported": "Older v2 themes don't support palettes. Switch to v3 theme to make use of palettes" "v2_unsupported": "Older v2 themes don't support palettes. Switch to v3 theme to make use of palettes"
}, },
"variables": {
"label": "Variables",
"name_label": "Name:",
"type_label": "Type:"
},
"editor": { "editor": {
"title": "Style", "title": "Style",
"new_style": "New", "new_style": "New",
@ -798,6 +794,9 @@
"icon_color": "Icon color", "icon_color": "Icon color",
"link_color": "Link color", "link_color": "Link color",
"include_in_rule": "Add to rule", "include_in_rule": "Add to rule",
"test_string": "TEST",
"refresh_preview": "Refresh preview",
"apply_preview": "Apply",
"text_auto": { "text_auto": {
"label": "Auto-contrast", "label": "Auto-contrast",
"no-preserve": "Black or White", "no-preserve": "Black or White",
@ -805,8 +804,17 @@
"no-auto": "Disabled" "no-auto": "Disabled"
}, },
"component_tab": "Components style", "component_tab": "Components style",
"palette_tab": "Color presets", "palette_tab": "Color schemes",
"variables_tab": "Variables (Advanced)" "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",

View file

@ -16,7 +16,8 @@ export const newExporter = ({
// 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}.${extension}`) const realFilename = typeof filename === 'function' ? filename() : filename
e.setAttribute('download', `${realFilename}.${extension}`)
e.setAttribute('href', `data:${mime};base64, ${window.btoa(stringified)}`) e.setAttribute('href', `data:${mime};base64, ${window.btoa(stringified)}`)
e.style.display = 'none' e.style.display = 'none'
@ -28,6 +29,7 @@ export const newExporter = ({
export const newImporter = ({ export const newImporter = ({
accept = '.json', accept = '.json',
parser = (string) => JSON.parse(string),
onImport, onImport,
onImportFailure, onImportFailure,
validator = () => true validator = () => true
@ -44,7 +46,7 @@ export const newImporter = ({
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)
const validationResult = validator(parsed, filename) const validationResult = validator(parsed, filename)
if (validationResult === true) { if (validationResult === true) {
onImport(parsed, filename) onImport(parsed, filename)

View file

@ -21,7 +21,11 @@ export const parseShadow = string => {
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 } = Object.fromEntries(modes.map((mode, i) => {

View file

@ -1,6 +1,6 @@
import { unroll } from './iss_utils.js' import { unroll } from './iss_utils.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}` return `${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}`
} else { } else {

View file

@ -62,7 +62,7 @@ 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 if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
let targetColor = null let targetColor = null
if (color.startsWith('--')) { if (color.startsWith('--')) {