Compare commits

...

15 commits

Author SHA1 Message Date
Henry Jameson
f8b39faae7 Merge branch 'themes3-grand-finale-maybe' into shigusegubu-themes3 2024-10-07 12:25:29 +03:00
Henry Jameson
4d472e1d4b fix my phone 2024-10-07 12:21:36 +03:00
Henry Jameson
4f66731723 lookin' good 2024-10-07 02:16:24 +03:00
Henry Jameson
f0e5b0be1e cleanup 2024-10-07 01:33:31 +03:00
Henry Jameson
f75ea738ca better cValue logic in shadow-control 2024-10-07 01:31:22 +03:00
Henry Jameson
97c058ebda neater looks for shadow-related things 2024-10-07 01:30:53 +03:00
Henry Jameson
cfe52185f7 neat-looking variables tab (sans shadow editor) 2024-10-07 00:57:54 +03:00
Henry Jameson
756ea63b67 variables stuff seem to be at least somewhat working 2024-10-06 20:21:13 +03:00
Henry Jameson
8725de3e91 got it working again 2024-10-06 18:56:45 +03:00
Henry Jameson
a6863248bb variables + consistency in code 2024-10-06 15:19:30 +03:00
Henry Jameson
da2c016ab4 better vars nomenclature 2024-10-06 15:19:30 +03:00
Henry Jameson
7e684ea3ff proper exporter 2024-10-06 03:32:08 +03:00
Henry Jameson
0f2bd39db8 cleanup 2024-10-06 03:31:59 +03:00
Henry Jameson
c58ed1036f remove i18n stuff.
first of all - it's too much work for me AND for translators
second of all - providing support would be a living hell nightmare
trying to understand what component it is in users's language that isn't english
2024-10-06 03:30:52 +03:00
Henry Jameson
d31da2c300 theme v3 config.json fields 2024-10-06 02:48:21 +03:00
14 changed files with 475 additions and 310 deletions

View file

@ -35,11 +35,11 @@ export default {
{ {
component: 'Root', component: 'Root',
directives: { directives: {
'--defaultButtonHoverGlow': 'shadow | 0 0 4 --text / 0.5', '--buttonDefaultHoverGlow': 'shadow | 0 0 4 --text / 0.5',
'--defaultButtonFocusGlow': 'shadow | 0 0 4 4 --link / 0.5', '--buttonDefaultFocusGlow': 'shadow | 0 0 4 4 --link / 0.5',
'--defaultButtonShadow': 'shadow | 0 0 2 #000000', '--buttonDefaultShadow': 'shadow | 0 0 2 #000000',
'--defaultButtonBevel': 'shadow | $borderSide(#FFFFFF top 0.2 2), $borderSide(#000000 bottom 0.2 2)', '--buttonDefaultBevel': 'shadow | $borderSide(#FFFFFF top 0.2 2), $borderSide(#000000 bottom 0.2 2)',
'--pressedButtonBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2 2), $borderSide(#000000 top 0.2 2)' '--buttonPressedBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2 2), $borderSide(#000000 top 0.2 2)'
} }
}, },
{ {
@ -47,53 +47,53 @@ export default {
// like within it // like within it
directives: { directives: {
background: '--fg', background: '--fg',
shadow: ['--defaultButtonShadow', '--defaultButtonBevel'], shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
roundness: 3 roundness: 3
} }
}, },
{ {
state: ['hover'], state: ['hover'],
directives: { directives: {
shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel'] shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel']
} }
}, },
{ {
state: ['focused'], state: ['focused'],
directives: { directives: {
shadow: ['--defaultButtonFocusGlow', '--defaultButtonBevel'] shadow: ['--buttonDefaultFocusGlow', '--buttonDefaultBevel']
} }
}, },
{ {
state: ['pressed'], state: ['pressed'],
directives: { directives: {
shadow: ['--defaultButtonShadow', '--pressedButtonBevel'] shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
} }
}, },
{ {
state: ['pressed', 'hover'], state: ['pressed', 'hover'],
directives: { directives: {
shadow: ['--pressedButtonBevel', '--defaultButtonHoverGlow'] shadow: ['--buttonPressedBevel', '--buttonDefaultHoverGlow']
} }
}, },
{ {
state: ['toggled'], state: ['toggled'],
directives: { directives: {
background: '--inheritedBackground,-14.2', background: '--inheritedBackground,-14.2',
shadow: ['--defaultButtonShadow', '--pressedButtonBevel'] shadow: ['--buttonDefaultShadow', '--buttonPressedBevel']
} }
}, },
{ {
state: ['toggled', 'hover'], state: ['toggled', 'hover'],
directives: { directives: {
background: '--inheritedBackground,-14.2', background: '--inheritedBackground,-14.2',
shadow: ['--defaultButtonHoverGlow', '--pressedButtonBevel'] shadow: ['--buttonDefaultHoverGlow', '--buttonPressedBevel']
} }
}, },
{ {
state: ['disabled'], state: ['disabled'],
directives: { directives: {
background: '$blend(--inheritedBackground 0.25 --parent)', background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--defaultButtonBevel'] shadow: ['--buttonDefaultBevel']
} }
}, },
{ {

View file

@ -111,6 +111,8 @@ export default {
"x-num x-slide . " "x-num x-slide . "
"options options options"; "options options options";
grid-gap: 0.5em; grid-gap: 0.5em;
max-width: 25em;
max-height: 25em;
.header { .header {
grid-area: header; grid-area: header;

View file

@ -107,7 +107,7 @@ const updatePalette = (paletteKey, value) => {
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, 1fr) auto; grid-template-rows: repeat(3, 1fr) auto;
grid-gap: 0.5em; grid-gap: 0.5em;
align-items: space-between; align-items: baseline;
.palette-import-button { .palette-import-button {
grid-column: 1 / span 2; grid-column: 1 / span 2;

View file

@ -82,9 +82,7 @@ const moveDn = () => {
} }
const add = () => { const add = () => {
const newModel = [...props.modelValue] const newModel = [...props.modelValue, props.getAddValue()]
newModel.push(props.getAddValue())
console.log(newModel)
emit('update:modelValue', newModel) emit('update:modelValue', newModel)
emit('update:selectedId', Math.max(newModel.length - 1, 0)) emit('update:selectedId', Math.max(newModel.length - 1, 0))

View file

@ -17,12 +17,16 @@ import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
import { init } from 'src/services/theme_data/theme_data_3.service.js' import { init } from 'src/services/theme_data/theme_data_3.service.js'
import { getCssRules } from 'src/services/theme_data/css_utils.js' import { getCssRules } from 'src/services/theme_data/css_utils.js'
import { serialize } from 'src/services/theme_data/iss_serializer.js' import { serialize } from 'src/services/theme_data/iss_serializer.js'
import { deserialize } from 'src/services/theme_data/iss_deserializer.js' import { parseShadow /* , deserialize */ } from 'src/services/theme_data/iss_deserializer.js'
import { import {
// rgb2hex, // rgb2hex,
hex2rgb, hex2rgb,
getContrastRatio getContrastRatio
} from 'src/services/color_convert/color_convert.js' } from 'src/services/color_convert/color_convert.js'
import {
// newImporter,
newExporter
} from 'src/services/export_import/export_import.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faFloppyDisk, faFolderOpen, faFile } from '@fortawesome/free-solid-svg-icons' import { faFloppyDisk, faFolderOpen, faFile } from '@fortawesome/free-solid-svg-icons'
@ -56,23 +60,40 @@ export default {
ContrastRatio ContrastRatio
}, },
setup () { setup () {
// ### Meta stuff // All rules that are made by editor
const allEditedRules = reactive({})
// ## Meta stuff
const name = ref('') const name = ref('')
const author = ref('') const author = ref('')
const license = ref('') const license = ref('')
const website = ref('') const website = ref('')
const metaOut = computed(() => { const metaOut = computed(() => {
return `@meta { return [
name: ${name.value}; '@meta {',
author: ${author.value}; ` name: ${name.value};`,
license: ${license.value}; ` author: ${author.value};`,
website: ${website.value}; ` license: ${license.value};`,
}` ` website: ${website.value};`,
'}'
].join('\n')
}) })
// ### Palette stuff // ## Palette stuff
const palettes = reactive([ const palettes = reactive([
{
name: 'dark',
bg: '#121a24',
fg: '#182230',
text: '#b9b9ba',
link: '#d8a070',
accent: '#d8a070',
cRed: '#FF0000',
cBlue: '#0095ff',
cGreen: '#0fa00f',
cOrange: '#ffa500'
},
{ {
name: 'light', name: 'light',
bg: '#f2f6f9', bg: '#f2f6f9',
@ -85,44 +106,17 @@ export default {
cGreen: '#0fa00f', cGreen: '#0fa00f',
cOrange: '#ffa500', cOrange: '#ffa500',
border: '#d8e6f9' border: '#d8e6f9'
},
{
name: 'dark',
bg: '#121a24',
fg: '#182230',
text: '#b9b9ba',
link: '#d8a070',
accent: '#d8a070',
cRed: '#FF0000',
cBlue: '#0095ff',
cGreen: '#0fa00f',
cOrange: '#ffa500'
} }
]) ])
const selectedPaletteId = ref(0)
const palettesOut = computed(() => { const selectedPalette = computed({
console.log('WORK DAMN', palettes)
return 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(0)
const palette = computed({
get () { get () {
console.log(palettes, editedPalette.value) return palettes[selectedPaletteId.value]
return palettes[editedPalette.value]
}, },
set (newPalette) { set (newPalette) {
palettes[editedPalette.value] = newPalette palettes[selectedPaletteId.value] = newPalette
} }
}) })
const getNewPalette = () => ({ const getNewPalette = () => ({
name: 'new palette', name: 'new palette',
bg: '#121a24', bg: '#121a24',
@ -136,34 +130,21 @@ export default {
cOrange: '#ffa500' cOrange: '#ffa500'
}) })
// ### I18n stuff const palettesOut = computed(() => {
// The paths in i18n are getting ridicously long, this effectively shortens them return palettes.map(({ name, ...palette }) => {
const getI18nPath = (componentName) => `settings.style.themes3.editor.components.${componentName}` const entries = Object
// vue i18n doesn't seem to have (working) mechanic to have a fallback so we have to .entries(palette)
// make do ourselves .map(([slot, data]) => ` ${slot}: ${data};`)
const fallbackI18n = (translated, fallback) => { .join('\n')
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 return `@palette.${name} {\n${entries}\n}`
}).join('\n\n')
})
// ## Components stuff
// Getting existing components // Getting existing components
const componentsContext = require.context('src', true, /\.style.js(on)?$/) const componentsContext = require.context('src', true, /\.style.js(on)?$/)
const componentKeysAll = componentsContext.keys() const componentKeysAll = componentsContext.keys()
const componentsMap = new Map( const componentsMap = new Map(
componentKeysAll componentKeysAll
.map( .map(
@ -172,23 +153,23 @@ export default {
) )
const componentKeys = [...componentsMap.keys()] const componentKeys = [...componentsMap.keys()]
// Initializing selected component and its computed descendants // selection basis
const selectedComponentKey = ref(componentsMap.keys().next().value) const selectedComponentKey = ref(componentsMap.keys().next().value)
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(() => {
const selectedVariant = ref('normal')
const selectedComponentVariantsAll = computed(() => {
return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) }) return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) })
}) })
const selectedState = reactive(new Set())
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(() => { const selectedComponentStates = computed(() => {
return selectedComponentStatesAll.value.filter(x => x !== 'normal') return selectedComponentStatesAll.value.filter(x => x !== 'normal')
}) })
// selection
const selectedVariant = ref('normal')
const selectedState = reactive(new Set())
const updateSelectedStates = (state, v) => { const updateSelectedStates = (state, v) => {
if (v) { if (v) {
selectedState.add(state) selectedState.add(state)
@ -197,56 +178,6 @@ export default {
} }
} }
// ### 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(() => {
try {
const scoped = getCssRules(previewRules).map(simulatePseudoSelectors)
return scoped.join('\n')
} catch (e) {
console.error('Invalid ruleset', e)
return null
}
})
// ### Rules stuff aka meat and potatoes // ### Rules stuff aka meat and potatoes
// 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
@ -297,9 +228,6 @@ export default {
return root return root
}) })
// All rules that are made by editor
const allEditedRules = reactive({})
// 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) => { const componentHas = (subComponent) => {
@ -331,7 +259,7 @@ export default {
} }
}) })
const getEditedElement = (component, directive) => computed({ const getEditedElement = (component, directive, postProcess = x => x) => computed({
get () { get () {
let usedRule let usedRule
const fallback = editorFriendlyFallbackStructure.value const fallback = editorFriendlyFallbackStructure.value
@ -343,7 +271,11 @@ export default {
usedRule = get(fallback, path) usedRule = get(fallback, path)
} }
return usedRule if (directive === 'shadow') {
console.log('EDITED', usedRule)
console.log('PP', postProcess(usedRule))
}
return postProcess(usedRule)
}, },
set (value) { set (value) {
set(allEditedRules, getPath(component, directive), value) set(allEditedRules, getPath(component, directive), value)
@ -352,21 +284,71 @@ export default {
// All the editable stuff for the component // All the editable stuff for the component
const editedBackgroundColor = getEditedElement(null, 'background') const editedBackgroundColor = getEditedElement(null, 'background')
const isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
const editedOpacity = getEditedElement(null, 'opacity') const editedOpacity = getEditedElement(null, 'opacity')
const isOpacityPresent = isElementPresent(null, 'opacity', 1)
const editedTextColor = getEditedElement('Text', 'textColor') const editedTextColor = getEditedElement('Text', 'textColor')
const isTextColorPresent = isElementPresent('Text', 'textColor', '#000000')
const editedTextAuto = getEditedElement('Text', 'textAuto') const editedTextAuto = getEditedElement('Text', 'textAuto')
const isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000')
const editedLinkColor = getEditedElement('Link', 'textColor') const editedLinkColor = getEditedElement('Link', 'textColor')
const isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080')
const editedIconColor = getEditedElement('Icon', 'textColor') const editedIconColor = getEditedElement('Icon', 'textColor')
const editedShadow = getEditedElement(null, 'shadow') const isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090')
// 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 normalizeShadows = (shadows) => {
console.log('NORMALIZE')
return shadows?.map(shadow => {
if (typeof shadow === 'object') {
return shadow
}
if (typeof shadow === 'string') {
return parseShadow(shadow)
}
return null
})
}
// 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 editedSubShadowId = ref(null) const editedSubShadowId = ref(null)
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', [])
const onSubShadow = (id) => {
if (id != null) {
editedSubShadowId.value = id
} else {
editedSubShadow.value = null
}
}
const updateSubShadow = (axis, value) => { const 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]
@ -378,26 +360,58 @@ export default {
editedShadow.value = newEditedShadow editedShadow.value = newEditedShadow
} }
const isShadowTabOpen = ref(false)
const onSubShadow = (id) => { const onTabSwitch = (tab) => {
if (id != null) { isShadowTabOpen.value = tab === 'shadow'
editedSubShadowId.value = id
} else {
editedSubShadow.value = null
}
} }
// Whether specific directives present in the edited rule or not // component preview
// Somewhat serves double-duty as it creates/removes the directive const editorHintStyle = computed(() => {
// when set const editorHint = selectedComponent.value.editor
const isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF') const styles = []
const isOpacityPresent = isElementPresent(null, 'opacity', 1) if (editorHint && Object.keys(editorHint).length > 0) {
const isTextColorPresent = isElementPresent('Text', 'textColor', '#000000') if (editorHint.aspect != null) {
const isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000') styles.push(`aspect-ratio: ${editorHint.aspect} !important;`)
const isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080') }
const isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090') if (editorHint.border != null) {
const isShadowPresent = isElementPresent(null, 'shadow', []) 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 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 previewRules = reactive([])
const previewCss = computed(() => {
try {
const scoped = getCssRules(previewRules).map(simulatePseudoSelectors)
return scoped.join('\n')
} catch (e) {
console.error('Invalid ruleset', e)
return null
}
})
const editorFriendlyToOriginal = computed(() => { const editorFriendlyToOriginal = computed(() => {
const resultRules = [] const resultRules = []
@ -439,8 +453,11 @@ export default {
const updatePreview = () => { const updatePreview = () => {
try { try {
const { name, ...paletteData } = palette.value const { name, ...paletteData } = selectedPalette.value
console.log('WORK', paletteData) // This normally would be handled by Root but since we pass something
// else we have to make do ourselves
paletteData.accent = paletteData.accent || paletteData.link
paletteData.link = paletteData.link || paletteData.accent
const rules = init({ const rules = init({
inputRuleset: editorFriendlyToOriginal.value, inputRuleset: editorFriendlyToOriginal.value,
initialStaticVars: { initialStaticVars: {
@ -465,6 +482,7 @@ export default {
} }
updateSelectedComponent() updateSelectedComponent()
// export and import
watch( watch(
allEditedRules, allEditedRules,
updatePreview updatePreview
@ -476,7 +494,7 @@ export default {
) )
watch( watch(
editedPalette, selectedPalette,
updatePreview updatePreview
) )
@ -485,97 +503,139 @@ export default {
updateSelectedComponent updateSelectedComponent
) )
// TODO this is VERY primitive right now, need to make it // ## Variables
// support variables, fallbacks etc. const allCustomVirtualDirectives = [...componentsMap.values()]
const getContrast = (bg, text) => { .map(c => {
try { return c
const bgRgb = hex2rgb(bg) .defaultRules
const textRgb = hex2rgb(text) .filter(c => c.component === 'Root')
.map(x => Object.entries(x.directives))
const ratio = getContrastRatio(bgRgb, textRgb) .flat()
})
.filter(x => x)
.flat()
.map(([name, value]) => {
const [valType, valVal] = value.split('|')
return { return {
// TODO this ideally should be part of <ContractRatio /> name: name.substring(2),
ratio, valType: valType.trim(),
text: ratio.toPrecision(3) + ':1', value: valVal.trim()
// AA level, AAA level }
aa: ratio >= 4.5, })
aaa: ratio >= 7, const virtualDirectives = reactive(allCustomVirtualDirectives)
// same but for 18pt+ texts const selectedVirtualDirectiveId = ref(0)
laa: ratio >= 3, const selectedVirtualDirective = computed(() => virtualDirectives[selectedVirtualDirectiveId.value])
laaa: ratio >= 4.5 const selectedVirtualDirectiveParsed = computed({
get () {
switch (selectedVirtualDirective.value.valType) {
case 'shadow': {
const directiveValue = selectedVirtualDirective.value.value
if (Array.isArray(directiveValue)) {
return normalizeShadows(directiveValue)
} else {
const splitShadow = directiveValue.split(/,/g).map(x => x.trim())
return normalizeShadows(splitShadow)
}
}
default:
return null
} }
} catch (e) {
console.warn('Failure computing contrast', e)
return { error: e }
} }
} })
const isShadowTabOpen = ref(false) const getNewDirective = () => ({
const onTabSwitch = (tab) => { name: 'newDirective',
isShadowTabOpen.value = tab === 'shadow' valType: 'generic',
} value: 'foobar'
})
const exportStyle = () => { // ## Export and Import
console.log('ORIG', toValue(editorFriendlyToOriginal.value)) const styleExporter = newExporter({
console.log('SERI', serialize(editorFriendlyToOriginal.value)) filename: name.value || 'pleroma_theme',
mime: 'text/plain',
const result = [ extension: 'piss',
getExportedObject: () => exportStyleData.value
})
const exportStyleData = computed(() => {
return [
metaOut.value, metaOut.value,
palettesOut.value, palettesOut.value,
serialize(editorFriendlyToOriginal.value) serialize(editorFriendlyToOriginal.value)
].join('\n\n') ].join('\n\n')
})
console.log('RESULT', result) const exportStyle = () => {
console.log('DESERI', deserialize(result)) styleExporter.exportData()
} }
return { return {
// ## Meta
name, name,
author, author,
license, license,
website, website,
palette,
// ## Palette
palettes, palettes,
editedPalette, selectedPalette,
selectedPaletteId,
getNewPalette, getNewPalette,
// ## Components
componentKeys, componentKeys,
componentsMap, componentsMap,
// selection basis
selectedComponent, selectedComponent,
selectedComponentName, selectedComponentName,
selectedComponentKey, selectedComponentKey,
selectedComponentVariantsAll, selectedComponentVariants,
selectedComponentStates, selectedComponentStates,
// selection
selectedVariant, selectedVariant,
selectedState, selectedState,
updateSelectedStates, updateSelectedStates,
// component directives
componentHas,
// component colors
editedBackgroundColor, editedBackgroundColor,
isBackgroundColorPresent,
editedOpacity, editedOpacity,
isOpacityPresent,
editedTextColor, editedTextColor,
isTextColorPresent,
editedTextAuto, editedTextAuto,
isTextAutoPresent,
editedLinkColor, editedLinkColor,
isLinkColorPresent,
editedIconColor, editedIconColor,
isIconColorPresent,
getContrast,
// component shadow
editedShadow, editedShadow,
editedSubShadow, editedSubShadow,
isShadowPresent,
onSubShadow, onSubShadow,
updateSubShadow, updateSubShadow,
getContrast,
isBackgroundColorPresent,
isOpacityPresent,
isTextColorPresent,
isTextAutoPresent,
isLinkColorPresent,
isIconColorPresent,
isShadowPresent,
previewCss,
previewClass,
editorHintStyle,
getFriendlyNamePath,
fallbackI18n,
getVariantPath,
getStatePath,
componentHas,
isShadowTabOpen, isShadowTabOpen,
onTabSwitch, onTabSwitch,
// component preview
editorHintStyle,
previewCss,
previewClass,
// ## Variables
virtualDirectives,
selectedVirtualDirective,
selectedVirtualDirectiveId,
selectedVirtualDirectiveParsed,
getNewDirective,
// ## Export and Import
exportStyle exportStyle
} }
} }

View file

@ -87,48 +87,72 @@
} }
} }
.palette-editor { .list-editor {
display: grid; display: grid;
grid-template-areas: grid-template-areas:
"label editor" "label editor"
"selector editor" "selector editor"
"motion editor"; "movement editor";
grid-template-columns: auto 1fr; grid-template-columns: 10em 1fr;
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr auto;
grid-gap: 0.5em; grid-gap: 0.5em;
.palette-editor-edit { .list-edit-area {
grid-area: editor; grid-area: editor;
} }
.palette-selector { .list-select {
grid-area: selector;
margin: 0;
&-label { &-label {
font-weight: bold; font-weight: bold;
grid-area: label; grid-area: label;
margin: 0; margin: 0;
align-self: baseline;
} }
}
.palette-list {
grid-area: selector;
margin: 0;
&-movement { &-movement {
grid-area: motion; grid-area: movement;
margin: 0; margin: 0;
} }
} }
} }
.palettes-editor {
.list-edit-area {
align-self: baseline;
}
}
.variables-editor {
.variable-selector {
display: grid;
grid-template-columns: auto 1fr auto 10em;
grid-template-rows: subgrid;
align-items: baseline;
grid-gap: 0 0.5em;
}
.list-edit-area {
display: grid;
grid-template-rows: subgrid;
}
.shadow-control {
grid-row: 2 / span 2;
}
}
.component-editor { .component-editor {
display: grid; display: grid;
grid-template-columns: 6fr 3fr 4fr; grid-template-columns: 6fr 3fr 4fr;
grid-template-rows: auto auto 1fr; grid-template-rows: auto auto 1fr;
grid-gap: 0.5em; grid-gap: 0.5em;
grid-template-areas: grid-template-areas:
"component component variant" "component component variant"
"state state state" "state state state"
"preview settings settings"; "preview settings settings";
.component-selector { .component-selector {
grid-area: component; grid-area: component;

View file

@ -4,6 +4,7 @@
<template> <template>
<div class="StyleTab"> <div class="StyleTab">
<div class="setting-item heading"> <div class="setting-item heading">
<!-- TODO: This needs to go -->
<h2>{{ $t('settings.style.themes3.editor.title') }}</h2> <h2>{{ $t('settings.style.themes3.editor.title') }}</h2>
<button <button
class="btn button-default" class="btn button-default"
@ -71,12 +72,12 @@
:key="'component-' + key" :key="'component-' + key"
:value="key" :value="key"
> >
{{ fallbackI18n($t(getFriendlyNamePath(componentsMap.get(key).name)), componentsMap.get(key).name) }} {{ componentsMap.get(key).name }}
</option> </option>
</Select> </Select>
</div> </div>
<div <div
v-if="selectedComponentVariantsAll.length > 1" v-if="selectedComponentVariants.length > 1"
class="variant-selector" class="variant-selector"
> >
<label for="variant-selector"> <label for="variant-selector">
@ -86,11 +87,11 @@
v-model="selectedVariant" v-model="selectedVariant"
> >
<option <option
v-for="variant in selectedComponentVariantsAll" v-for="variant in selectedComponentVariants"
:key="'component-variant-' + variant" :key="'component-variant-' + variant"
:value="variant" :value="variant"
> >
{{ fallbackI18n($t(getVariantPath(selectedComponentName, variant)), variant) }} {{ variant }}
</option> </option>
</Select> </Select>
</div> </div>
@ -112,7 +113,7 @@
:value="selectedState.has(state)" :value="selectedState.has(state)"
@update:modelValue="(v) => updateSelectedStates(state, v)" @update:modelValue="(v) => updateSelectedStates(state, v)"
> >
{{ fallbackI18n($t(getStatePath(selectedComponentName, state)), state) }} {{ state }}
</Checkbox> </Checkbox>
</li> </li>
</ul> </ul>
@ -249,6 +250,7 @@
v-model="editedShadow" v-model="editedShadow"
:disabled="!isShadowPresent" :disabled="!isShadowPresent"
:no-preview="true" :no-preview="true"
:compact="true"
:separate-inset="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'" :separate-inset="shadowSelected === 'avatar' || shadowSelected === 'avatarStatus'"
@subShadowSelected="onSubShadow" @subShadowSelected="onSubShadow"
/> />
@ -258,10 +260,10 @@
<div <div
key="palette" key="palette"
:label="$t('settings.style.themes3.editor.palette_tab')" :label="$t('settings.style.themes3.editor.palette_tab')"
class="setting-item palette-editor" class="setting-item list-editor palette-editor"
> >
<label <label
class="palette-selector-label" class="list-select-label"
for="palette-selector" for="palette-selector"
> >
{{ $t('settings.style.themes3.palette.label') }} {{ $t('settings.style.themes3.palette.label') }}
@ -269,9 +271,9 @@
</label> </label>
<Select <Select
id="palette-selector" id="palette-selector"
v-model="editedPalette" v-model="selectedPaletteId"
class="palette-list" class="list-select"
size="9" size="4"
> >
<option <option
v-for="(p, index) in palettes" v-for="(p, index) in palettes"
@ -282,17 +284,87 @@
</option> </option>
</Select> </Select>
<SelectMotion <SelectMotion
class="palette-list-movement" class="list-select-movement"
v-model="palettes" v-model="palettes"
:selected-id="editedPalette"
:get-add-value="getNewPalette" :get-add-value="getNewPalette"
@update:selectedId="e => editedPalette = e" :selected-id="selectedPaletteId"
@update:selectedId="e => selectedPaletteId = e"
/> />
<PaletteEditor <PaletteEditor
class="palette-editor-edit" class="list-edit-area"
v-model="palette" v-model="selectedPalette"
/> />
</div> </div>
<div
key="variables"
:label="$t('settings.style.themes3.editor.variables_tab')"
class="setting-item list-editor variables-editor"
>
<label
class="list-select-label"
for="variables-selector"
>
{{ $t('settings.style.themes3.variables.label') }}
{{ ' ' }}
</label>
<Select
id="variables-selector"
v-model="selectedVirtualDirectiveId"
class="list-select"
size="9"
>
<option
v-for="(p, index) in virtualDirectives"
:key="p.name"
:value="index"
>
{{ p.name }}
</option>
</Select>
<SelectMotion
class="list-select-movement"
v-model="virtualDirectives"
:selected-id="selectedVirtualDirectiveId"
:get-add-value="getNewVirtualDirective"
@update:selectedId="e => selectedVirtualDirectiveId = e"
/>
<div class="list-edit-area">
<div class="variable-selector">
<label
class="variable-name-label"
for="variables-selector"
>
{{ $t('settings.style.themes3.variables.name_label') }}
{{ ' ' }}
</label>
<input
class="input"
v-model="selectedVirtualDirective.name"
>
<label
class="variable-type-label"
for="variables-selector"
>
{{ $t('settings.style.themes3.variables.type_label') }}
{{ ' ' }}
</label>
<Select
v-model="selectedVirtualDirective.valType"
>
<option value='shadow'>
{{ $t('settings.style.themes3.variables.type_label') }}
shadow</option>
<option value='shadow'>color</option>
<option value='shadow'>generic</option>
</Select>
</div>
<ShadowControl
v-if="selectedVirtualDirective.valType === 'shadow'"
v-model="selectedVirtualDirectiveParsed"
:compact="true"
/>
</div>
</div>
</tab-switcher> </tab-switcher>
</div> </div>
</template> </template>

View file

@ -41,14 +41,17 @@ const toModel = (input) => {
export default { export default {
props: [ props: [
'modelValue', 'fallback', 'separateInset', 'noPreview', 'disabled' 'modelValue',
'fallback',
'separateInset',
'noPreview',
'disabled',
'compact'
], ],
emits: ['update:modelValue', 'subShadowSelected'], emits: ['update:modelValue', 'subShadowSelected'],
data () { data () {
return { return {
selectedId: 0, selectedId: 0
// TODO there are some bugs regarding display of array (it's not getting updated when deleting for some reason)
cValue: (this.modelValue ?? this.fallback ?? []).map(toModel)
} }
}, },
components: { components: {
@ -61,6 +64,14 @@ export default {
ComponentPreview ComponentPreview
}, },
computed: { computed: {
cValue: {
get () {
return (this.modelValue ?? this.fallback ?? []).map(toModel)
},
set (newVal) {
this.$emit('update:modelValue', newVal)
}
},
selectedType: { selectedType: {
get () { get () {
return typeof this.selected return typeof this.selected
@ -115,9 +126,6 @@ export default {
} }
}, },
watch: { watch: {
modelValue (value) {
if (!value) this.cValue = (this.modelValue ?? this.fallback ?? []).map(toModel)
},
selected (value) { selected (value) {
this.$emit('subShadowSelected', this.selectedId) this.$emit('subShadowSelected', this.selectedId)
} }

View file

@ -1,12 +1,31 @@
.settings-modal .settings-modal-panel .shadow-control { .ShadowControl {
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: 10em 1fr 1fr;
grid-template-rows: 1fr;
grid-template-areas: "selector preview tweak";
grid-gap: 0.5em;
justify-content: stretch; justify-content: stretch;
grid-gap: 0.25em;
margin-bottom: 1em; margin-bottom: 1em;
width: 100%; width: 100%;
&.-compact {
grid-template-columns: 10em 1fr;
grid-template-rows: auto auto;
grid-template-areas:
"selector preview"
"tweak tweak";
&.-no-preview {
grid-template-columns: 1fr;
grid-template-rows: 10em 1fr;
grid-template-areas:
"selector"
"tweak";
}
}
.shadow-switcher { .shadow-switcher {
grid-area: selector;
order: 1; order: 1;
flex: 1 0 6em; flex: 1 0 6em;
min-width: 6em; min-width: 6em;
@ -20,6 +39,7 @@
} }
.shadow-tweak { .shadow-tweak {
grid-area: tweak;
order: 3; order: 3;
flex: 2 0 10em; flex: 2 0 10em;
min-width: 10em; min-width: 10em;
@ -65,6 +85,10 @@
} }
&.-no-preview { &.-no-preview {
grid-template-columns: 10em 1fr;
grid-template-rows: 1fr;
grid-template-areas: "selector tweak";
.shadow-tweak { .shadow-tweak {
order: 0; order: 0;
flex: 2 0 8em; flex: 2 0 8em;
@ -87,11 +111,11 @@
} }
.shadow-preview { .shadow-preview {
order: 2; grid-area: preview;
flex: 3 3 15em;
min-width: 10em; min-width: 10em;
margin-left: 0.125em; margin-left: 0.125em;
align-self: start; align-self: start;
justify-self: center;
} }
} }

View file

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="label shadow-control" class="ShadowControl label shadow-control"
:class="{ disabled: disabled || !present, '-no-preview': noPreview }" :class="{ disabled: disabled || !present, '-no-preview': noPreview, '-compact': compact }"
> >
<ComponentPreview <ComponentPreview
v-if="!noPreview" v-if="!noPreview"
@ -16,7 +16,7 @@
id="shadow-list" id="shadow-list"
v-model="selectedId" v-model="selectedId"
class="shadow-list" class="shadow-list"
size="10" size="4"
:disabled="disabled || shadowsAreNull" :disabled="disabled || shadowsAreNull"
> >
<option <option

View file

@ -14,14 +14,14 @@ export default {
{ {
directives: { directives: {
background: '--fg', background: '--fg',
shadow: ['--defaultButtonShadow', '--defaultButtonBevel'], shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel'],
roundness: 3 roundness: 3
} }
}, },
{ {
state: ['hover'], state: ['hover'],
directives: { directives: {
shadow: ['--defaultButtonHoverGlow', '--defaultButtonBevel'] shadow: ['--buttonDefaultHoverGlow', '--buttonDefaultBevel']
} }
}, },
{ {
@ -33,14 +33,14 @@ export default {
{ {
state: ['hover', 'active'], state: ['hover', 'active'],
directives: { directives: {
shadow: ['--defaultButtonShadow', '--defaultButtonBevel'] shadow: ['--buttonDefaultShadow', '--buttonDefaultBevel']
} }
}, },
{ {
state: ['disabled'], state: ['disabled'],
directives: { directives: {
background: '$blend(--inheritedBackground 0.25 --parent)', background: '$blend(--inheritedBackground 0.25 --parent)',
shadow: ['--defaultButtonBevel'] shadow: ['--buttonDefaultBevel']
} }
}, },
{ {

View file

@ -757,7 +757,7 @@
"themes3": { "themes3": {
"define": "Override", "define": "Override",
"palette": { "palette": {
"label": "Palette", "label": "Palettes",
"import": "Import", "import": "Import",
"export": "Export", "export": "Export",
"bg": "Panel background", "bg": "Panel background",
@ -774,6 +774,11 @@
"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",
@ -801,45 +806,7 @@
}, },
"component_tab": "Components style", "component_tab": "Components style",
"palette_tab": "Color presets", "palette_tab": "Color presets",
"components": { "variables_tab": "Variables (Advanced)"
"normal": {
"state": "Normal",
"variant": "Default"
},
"Alert": {
"friendlyName": "Alert",
"variants": {
"error": "Error",
"warning": "Warning",
"success": "Success"
}
},
"Button": {
"friendlyName": "Button",
"variants": {
"danger": "Dangerous"
},
"states": {
"toggled": "Toggled",
"pressed": "Pressed",
"hover": "Hovered",
"focused": "Has focus",
"disabled": "Disabled"
}
},
"Input": {
"friendlyName": "Input fields",
"variants": {
"checkbox": "Checkbox",
"radio": "Radio"
},
"states": {
"hover": "Hovered",
"focus": "Focused",
"disabled": "Disabled"
}
}
}
}, },
"hacks": { "hacks": {
"underlay_overrides": "Change underlay", "underlay_overrides": "Change underlay",

View file

@ -627,7 +627,10 @@ export const normalizeThemeData = (input) => {
// We got passed a full theme file // We got passed a full theme file
themeData = input.theme themeData = input.theme
themeSource = input.source themeSource = input.source
} else if (Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion')) { } else if (
Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion') ||
Object.prototype.hasOwnProperty.call(input, 'bg')
) {
// We got passed a source/snapshot // We got passed a source/snapshot
themeData = input themeData = input
themeSource = input themeSource = input

View file

@ -2,15 +2,22 @@ import utf8 from 'utf8'
export const newExporter = ({ export const newExporter = ({
filename = 'data', filename = 'data',
mime = 'application/json',
extension = '.json',
getExportedObject getExportedObject
}) => ({ }) => ({
exportData () { exportData () {
const stringified = utf8.encode(JSON.stringify(getExportedObject(), null, 2)) // Pretty-print and indent with 2 spaces let stringified
if (mime === 'application/json') {
stringified = utf8.encode(JSON.stringify(getExportedObject(), null, 2)) // Pretty-print and indent with 2 spaces
} else {
stringified = utf8.encode(getExportedObject()) // Pretty-print and indent with 2 spaces
}
// Create an invisible link with a data url and simulate a click // Create an invisible link with a data url and simulate a click
const e = document.createElement('a') const e = document.createElement('a')
e.setAttribute('download', `${filename}.json`) e.setAttribute('download', `${filename}.${extension}`)
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified)) e.setAttribute('href', `data:${mime};base64, ${window.btoa(stringified)}`)
e.style.display = 'none' e.style.display = 'none'
document.body.appendChild(e) document.body.appendChild(e)