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'
},
computedColor () {
return this.modelValue && this.modelValue.startsWith('--')
return this.modelValue && (this.modelValue.startsWith('--') || this.modelValue.startsWith('$'))
}
},
methods: {

View file

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

View file

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

View file

@ -49,6 +49,7 @@ label.Select {
option {
background-color: transparent;
&:checked,
&.-active {
color: var(--selectionText);
background-color: var(--selectionBackground);

View file

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

View file

@ -5,6 +5,7 @@
>
<label
:for="path"
class="setting-label"
:class="{ 'faint': shouldBeDisabled }"
>
<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 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 { 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 { parseShadow /* , deserialize */ } from 'src/services/theme_data/iss_deserializer.js'
import { init, findColor } from 'src/services/theme_data/theme_data_3.service.js'
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,
getContrastRatio
} from 'src/services/color_convert/color_convert.js'
import {
// newImporter,
newImporter,
newExporter
} from 'src/services/export_import/export_import.js'
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
// eslint-disable-next-line no-unused-vars
@ -41,7 +51,9 @@ const normalizeStates = (states) => ['normal', ...(states?.filter(x => x !== 'no
library.add(
faFile,
faFloppyDisk,
faFolderOpen
faFolderOpen,
faArrowsRotate,
faCheck
)
export default {
@ -57,25 +69,27 @@ export default {
ColorInput,
PaletteEditor,
OpacityInput,
ContrastRatio
ContrastRatio,
Preview
},
setup () {
const exports = {}
// All rules that are made by editor
const allEditedRules = reactive({})
// ## Meta stuff
const name = ref('')
const author = ref('')
const license = ref('')
const website = ref('')
exports.name = ref('')
exports.author = ref('')
exports.license = ref('')
exports.website = ref('')
const metaOut = computed(() => {
return [
'@meta {',
` name: ${name.value};`,
` author: ${author.value};`,
` license: ${license.value};`,
` website: ${website.value};`,
` name: ${exports.name.value};`,
` author: ${exports.author.value};`,
` license: ${exports.license.value};`,
` website: ${exports.website.value};`,
'}'
].join('\n')
})
@ -108,6 +122,20 @@ export default {
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 () {
@ -117,7 +145,10 @@ export default {
palettes[selectedPaletteId.value] = newPalette
}
})
const getNewPalette = () => ({
exports.selectedPaletteId = selectedPaletteId
exports.selectedPalette = selectedPalette
exports.getNewPalette = () => ({
name: 'new palette',
bg: '#121a24',
fg: '#182230',
@ -151,26 +182,31 @@ export default {
key => [key, componentsContext(key).default]
).filter(([key, component]) => !component.virtual && !component.notEditable)
)
exports.componentsMap = componentsMap
const componentKeys = [...componentsMap.keys()]
exports.componentKeys = componentKeys
// selection basis
const selectedComponentKey = ref(componentsMap.keys().next().value)
exports.selectedComponentKey = selectedComponentKey
const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value))
const selectedComponentName = computed(() => selectedComponent.value.name)
const selectedComponentVariants = computed(() => {
exports.selectedComponentVariants = computed(() => {
return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) })
})
const selectedComponentStatesAll = computed(() => {
return Object.keys({ normal: null, ...(selectedComponent.value.states || {}) })
})
const selectedComponentStates = computed(() => {
exports.selectedComponentStates = computed(() => {
return selectedComponentStatesAll.value.filter(x => x !== 'normal')
})
// selection
const selectedVariant = ref('normal')
exports.selectedVariant = selectedVariant
const selectedState = reactive(new Set())
const updateSelectedStates = (state, v) => {
exports.selectedState = selectedState
exports.updateSelectedStates = (state, v) => {
if (v) {
selectedState.add(state)
} else {
@ -182,47 +218,53 @@ export default {
// 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) {
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 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
}
})
const { defaultRules, name } = componentValue
rulesToEditorFriendly(
defaultRules.map((rule) => ({ ...rule, component: name })),
root
)
})
return root
@ -230,7 +272,7 @@ export default {
// Checkging whether component can support some "directives" which
// are actually virtual subcomponents, i.e. Text, Link etc
const componentHas = (subComponent) => {
exports.componentHas = (subComponent) => {
return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent)
}
@ -283,21 +325,24 @@ export default {
})
// All the editable stuff for the component
const editedBackgroundColor = getEditedElement(null, 'background')
const isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
const editedOpacity = getEditedElement(null, 'opacity')
const isOpacityPresent = isElementPresent(null, 'opacity', 1)
const editedTextColor = getEditedElement('Text', 'textColor')
const isTextColorPresent = isElementPresent('Text', 'textColor', '#000000')
const editedTextAuto = getEditedElement('Text', 'textAuto')
const isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000')
const editedLinkColor = getEditedElement('Link', 'textColor')
const isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080')
const editedIconColor = getEditedElement('Icon', 'textColor')
const isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090')
exports.editedBackgroundColor = getEditedElement(null, 'background')
exports.isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
exports.editedOpacity = getEditedElement(null, 'opacity')
exports.isOpacityPresent = isElementPresent(null, 'opacity', 1)
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')
// TODO this is VERY primitive right now, need to make it
// support variables, fallbacks etc.
const getContrast = (bg, text) => {
exports.getContrast = (bg, text) => {
try {
const bgRgb = hex2rgb(bg)
const textRgb = hex2rgb(text)
@ -321,7 +366,6 @@ export default {
}
const normalizeShadows = (shadows) => {
console.log('NORMALIZE')
return shadows?.map(shadow => {
if (typeof shadow === 'object') {
return shadow
@ -336,20 +380,23 @@ export default {
// 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]
})
const isShadowPresent = isElementPresent(null, 'shadow', [])
const onSubShadow = (id) => {
exports.editedSubShadow = editedSubShadow
exports.isShadowPresent = isElementPresent(null, 'shadow', [])
exports.onSubShadow = (id) => {
if (id != null) {
editedSubShadowId.value = id
} else {
editedSubShadow.value = null
}
}
const updateSubShadow = (axis, value) => {
exports.updateSubShadow = (axis, value) => {
if (!editedSubShadow.value || editedSubShadowId.value == null) return
const newEditedShadow = [...editedShadow.value]
@ -360,13 +407,13 @@ export default {
editedShadow.value = newEditedShadow
}
const isShadowTabOpen = ref(false)
const onTabSwitch = (tab) => {
isShadowTabOpen.value = tab === 'shadow'
exports.isShadowTabOpen = ref(false)
exports.onTabSwitch = (tab) => {
exports.isShadowTabOpen.value = tab === 'shadow'
}
// component preview
const editorHintStyle = computed(() => {
exports.editorHintStyle = computed(() => {
const editorHint = selectedComponent.value.editor
const styles = []
if (editorHint && Object.keys(editorHint).length > 0) {
@ -389,7 +436,7 @@ export default {
.replace(':focus', '.preview-focus')
.replace(':focus-within', '.preview-focus-within')
.replace(':disabled', '.preview-disabled')
const previewClass = computed(() => {
exports.previewClass = computed(() => {
const selectors = []
if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') {
selectors.push(selectedComponent.value.variants[selectedVariant.value])
@ -403,7 +450,8 @@ export default {
return selectors.map(x => x.substring(1)).join('')
})
const previewRules = reactive([])
const previewCss = computed(() => {
exports.previewRules = previewRules
exports.previewCss = computed(() => {
try {
const scoped = getCssRules(previewRules).map(simulatePseudoSelectors)
return scoped.join('\n')
@ -523,9 +571,43 @@ export default {
}
})
const virtualDirectives = reactive(allCustomVirtualDirectives)
exports.virtualDirectives = virtualDirectives
exports.onVirtualDirectivesUpdate = (e) => {
virtualDirectives.splice(0, virtualDirectives.length)
virtualDirectives.push(...e)
}
const selectedVirtualDirectiveId = ref(0)
const selectedVirtualDirective = computed(() => virtualDirectives[selectedVirtualDirectiveId.value])
const selectedVirtualDirectiveParsed = computed({
exports.selectedVirtualDirectiveId = selectedVirtualDirectiveId
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 () {
switch (selectedVirtualDirective.value.valType) {
case 'shadow': {
@ -537,25 +619,109 @@ export default {
return normalizeShadows(splitShadow)
}
}
case 'color':
return selectedVirtualDirective.value.value
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',
valType: 'generic',
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
const styleExporter = newExporter({
filename: name.value || 'pleroma_theme',
filename: () => exports.name.value ?? 'pleroma_theme',
mime: 'text/plain',
extension: 'piss',
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(() => {
return [
metaOut.value,
@ -563,80 +729,15 @@ export default {
serialize(editorFriendlyToOriginal.value)
].join('\n\n')
})
const exportStyle = () => {
exports.exportStyle = () => {
styleExporter.exportData()
}
return {
// ## Meta
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
exports.importStyle = () => {
styleImporter.importData()
}
return exports
}
}

View file

@ -65,24 +65,60 @@
&.heading {
display: grid;
align-items: baseline;
grid-template-columns: 1fr auto auto auto;
grid-template:
"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-template-columns: min-content min-content 6fr max-content;
grid-template-rows: repeat(4, min-content) repeat(2, 2em);
h2 {
flex: 1 0 auto;
}
}
ul.setting-list {
padding: 0;
margin: 0;
display: grid;
grid-template-rows: subgrid;
grid-area: meta;
&.metadata {
display: flex;
> li {
margin: 0;
}
.setting-item {
flex: 2 0 auto;
.meta-field {
margin: 0;
.setting-label {
display: inline-block;
margin-bottom: 0.5em;
}
}
}
li {
text-align: right;
#edited-style-preview {
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 {
display: grid;
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>
<div class="StyleTab">
<div class="setting-item heading">
<!-- TODO: This needs to go -->
<h2>{{ $t('settings.style.themes3.editor.title') }}</h2>
<!-- eslint-disable vue/no-v-text-v-html-on-component -->
<component
:is="'style'"
v-html="overallPreviewRules"
/>
<!-- eslint-enable vue/no-v-text-v-html-on-component -->
<Preview id="edited-style-preview" />
<button
class="btn button-default"
class="btn button-default button-new"
@click="clearTheme"
>
<FAIcon icon="file" />
{{ $t('settings.style.themes3.editor.new_style') }}
</button>
<button
class="btn button-default"
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"
class="btn button-default button-save"
@click="exportStyle"
>
<FAIcon icon="floppy-disk" />
{{ $t('settings.style.themes3.editor.save_style') }}
</button>
</div>
<div class="setting-item metadata">
<ul class="setting-list">
<button
class="btn button-default button-refresh"
@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>
<StringSetting v-model="name">
<StringSetting class="meta-field" v-model="name">
{{ $t('settings.style.themes3.editor.style_name') }}
</StringSetting>
</li>
<li>
<StringSetting v-model="author">
<StringSetting class="meta-field" v-model="author">
{{ $t('settings.style.themes3.editor.style_author') }}
</StringSetting>
</li>
<li>
<StringSetting v-model="license">
<StringSetting class="meta-field" v-model="license">
{{ $t('settings.style.themes3.editor.style_license') }}
</StringSetting>
</li>
<li>
<StringSetting v-model="website">
<StringSetting class="meta-field" v-model="website">
{{ $t('settings.style.themes3.editor.style_website') }}
</StringSetting>
</li>
@ -148,6 +165,7 @@
>
<ColorInput
v-model="editedBackgroundColor"
:fallback="computeColor(editedBackgroundColor)"
:disabled="!isBackgroundColorPresent"
:label="$t('settings.style.themes3.editor.background')"
/>
@ -165,6 +183,7 @@
<ColorInput
v-if="componentHas('Text')"
v-model="editedTextColor"
:fallback="computeColor(editedTextColor)"
:label="$t('settings.style.themes3.editor.text_color')"
:disabled="!isTextColorPresent"
/>
@ -213,6 +232,7 @@
<ColorInput
v-if="componentHas('Link')"
v-model="editedLinkColor"
:fallback="computeColor(editedLinkColor)"
:label="$t('settings.style.themes3.editor.link_color')"
:disabled="!isLinkColorPresent"
/>
@ -225,6 +245,7 @@
<ColorInput
v-if="componentHas('Icon')"
v-model="editedIconColor"
:fallback="computeColor(editedIconColor)"
:label="$t('settings.style.themes3.editor.icon_color')"
:disabled="!isIconColorPresent"
/>
@ -234,6 +255,19 @@
>
<Checkbox v-model="isIconColorPresent" />
</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
key="shadow"
@ -262,38 +296,47 @@
:label="$t('settings.style.themes3.editor.palette_tab')"
class="setting-item list-editor palette-editor"
>
<label
class="list-select-label"
for="palette-selector"
<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"
>
{{ $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"
v-model="palettes"
:get-add-value="getNewPalette"
:selected-id="selectedPaletteId"
@update:selectedId="e => selectedPaletteId = e"
/>
<PaletteEditor
class="list-edit-area"
v-model="selectedPalette"
{{ 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>
<div
key="variables"
@ -304,14 +347,14 @@
class="list-select-label"
for="variables-selector"
>
{{ $t('settings.style.themes3.variables.label') }}
{{ $t('settings.style.themes3.editor.variables.label') }}
{{ ' ' }}
</label>
<Select
id="variables-selector"
v-model="selectedVirtualDirectiveId"
class="list-select"
size="9"
size="20"
>
<option
v-for="(p, index) in virtualDirectives"
@ -323,7 +366,8 @@
</Select>
<SelectMotion
class="list-select-movement"
v-model="virtualDirectives"
:modelValue="virtualDirectives"
@update:modelValue="onVirtualDirectivesUpdate"
:selected-id="selectedVirtualDirectiveId"
:get-add-value="getNewVirtualDirective"
@update:selectedId="e => selectedVirtualDirectiveId = e"
@ -334,7 +378,7 @@
class="variable-name-label"
for="variables-selector"
>
{{ $t('settings.style.themes3.variables.name_label') }}
{{ $t('settings.style.themes3.editor.variables.name_label') }}
{{ ' ' }}
</label>
<input
@ -345,25 +389,36 @@
class="variable-type-label"
for="variables-selector"
>
{{ $t('settings.style.themes3.variables.type_label') }}
{{ $t('settings.style.themes3.editor.variables.type_label') }}
{{ ' ' }}
</label>
<Select
v-model="selectedVirtualDirective.valType"
v-model="selectedVirtualDirectiveValType"
>
<option value='shadow'>
{{ $t('settings.style.themes3.variables.type_label') }}
shadow</option>
<option value='shadow'>color</option>
<option value='shadow'>generic</option>
{{ $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="selectedVirtualDirective.valType === 'shadow'"
v-if="selectedVirtualDirectiveValType === 'shadow'"
v-model="selectedVirtualDirectiveParsed"
:computeColor="computeColor"
: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>
</tab-switcher>
</div>

View file

@ -46,6 +46,7 @@ export default {
'separateInset',
'noPreview',
'disabled',
'computeColor',
'compact'
],
emits: ['update:modelValue', 'subShadowSelected'],
@ -107,6 +108,13 @@ export default {
usingFallback () {
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 () {
try {
if (this.separateInset) {

View file

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

View file

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

View file

@ -757,7 +757,8 @@
"themes3": {
"define": "Override",
"palette": {
"label": "Palettes",
"label": "Color schemes",
"name_label": "Color scheme name",
"import": "Import",
"export": "Export",
"bg": "Panel background",
@ -774,11 +775,6 @@
"extra3": "Extra 3",
"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": {
"title": "Style",
"new_style": "New",
@ -798,6 +794,9 @@
"icon_color": "Icon color",
"link_color": "Link color",
"include_in_rule": "Add to rule",
"test_string": "TEST",
"refresh_preview": "Refresh preview",
"apply_preview": "Apply",
"text_auto": {
"label": "Auto-contrast",
"no-preserve": "Black or White",
@ -805,8 +804,17 @@
"no-auto": "Disabled"
},
"component_tab": "Components style",
"palette_tab": "Color presets",
"variables_tab": "Variables (Advanced)"
"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": {
"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
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.style.display = 'none'
@ -28,6 +29,7 @@ export const newExporter = ({
export const newImporter = ({
accept = '.json',
parser = (string) => JSON.parse(string),
onImport,
onImportFailure,
validator = () => true
@ -44,7 +46,7 @@ export const newImporter = ({
const reader = new FileReader()
reader.onload = ({ target }) => {
try {
const parsed = JSON.parse(target.result)
const parsed = parser(target.result)
const validationResult = validator(parsed, filename)
if (validationResult === true) {
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 result = regex.exec(string)
if (result == null) {
return string
if (string.startsWith('$') || string.startsWith('--')) {
return string
} else {
throw new Error(`Invalid shadow definition: ${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) => {

View file

@ -1,6 +1,6 @@
import { unroll } from './iss_utils.js'
const serializeShadow = s => {
export const serializeShadow = (s, throwOnInvalid) => {
if (typeof s === 'object') {
return `${s.inset ? 'inset ' : ''}${s.x} ${s.y} ${s.blur} ${s.spread} ${s.color} / ${s.alpha}`
} 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
let targetColor = null
if (color.startsWith('--')) {