diff --git a/src/components/color_input/color_input.scss b/src/components/color_input/color_input.scss index 19c88a690..3128381f5 100644 --- a/src/components/color_input/color_input.scss +++ b/src/components/color_input/color_input.scss @@ -5,6 +5,10 @@ flex: 1 1 auto; } + .opt { + margin-right: 0.5em; + } + &-field.input { display: inline-flex; flex: 0 0 0; diff --git a/src/components/component_preview/component_preview.vue b/src/components/component_preview/component_preview.vue index fc8b3f4fb..6795d393a 100644 --- a/src/components/component_preview/component_preview.vue +++ b/src/components/component_preview/component_preview.vue @@ -103,6 +103,7 @@ > { const moveDnValid = computed(() => { return props.selectedId < props.modelValue.length - 1 }) + const moveDn = () => { const newModel = [...props.modelValue] const movable = newModel.splice(props.selectedId.value, 1)[0] diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.js b/src/components/settings_modal/tabs/style_tab/style_tab.js index c5ac8d06c..2605e8d8b 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.js +++ b/src/components/settings_modal/tabs/style_tab/style_tab.js @@ -1,5 +1,6 @@ -import { ref, reactive, computed, watch } from 'vue' -import { get, set } from 'lodash' +import { ref, reactive, computed, watch, provide } from 'vue' +import { useStore } from 'vuex' +import { get, set, unset } from 'lodash' import Select from 'src/components/select/select.vue' import SelectMotion from 'src/components/select/select_motion.vue' @@ -15,12 +16,14 @@ import Tooltip from 'src/components/tooltip/tooltip.vue' import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue' import Preview from '../theme_tab/theme_preview.vue' +import VirtualDirectivesTab from './virtual_directives_tab.vue' + import { init, findColor } from 'src/services/theme_data/theme_data_3.service.js' import { getCssRules, getScopedVersion } from 'src/services/theme_data/css_utils.js' -import { serializeShadow, serialize } from 'src/services/theme_data/iss_serializer.js' +import { serialize } from 'src/services/theme_data/iss_serializer.js' import { deserializeShadow, deserialize } from 'src/services/theme_data/iss_deserializer.js' import { rgb2hex, @@ -70,10 +73,12 @@ export default { PaletteEditor, OpacityInput, ContrastRatio, - Preview + Preview, + VirtualDirectivesTab }, - setup () { + setup (props, context) { const exports = {} + const store = useStore() // All rules that are made by editor const allEditedRules = reactive({}) @@ -97,7 +102,7 @@ export default { // ## Palette stuff const palettes = reactive([ { - name: 'dark', + name: 'default', bg: '#121a24', fg: '#182230', text: '#b9b9ba', @@ -147,6 +152,7 @@ export default { }) exports.selectedPaletteId = selectedPaletteId exports.selectedPalette = selectedPalette + provide('selectedPalette', selectedPalette) exports.getNewPalette = () => ({ name: 'new palette', @@ -186,19 +192,20 @@ export default { const componentKeys = [...componentsMap.keys()] exports.componentKeys = componentKeys - // selection basis + // Component list and selection const selectedComponentKey = ref(componentsMap.keys().next().value) exports.selectedComponentKey = selectedComponentKey + const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value)) const selectedComponentName = computed(() => selectedComponent.value.name) + + // Selection basis exports.selectedComponentVariants = computed(() => { return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) }) }) - const selectedComponentStatesAll = computed(() => { - return Object.keys({ normal: null, ...(selectedComponent.value.states || {}) }) - }) exports.selectedComponentStates = computed(() => { - return selectedComponentStatesAll.value.filter(x => x !== 'normal') + const all = Object.keys({ normal: null, ...(selectedComponent.value.states || {}) }) + return all.filter(x => x !== 'normal') }) // selection @@ -214,6 +221,17 @@ export default { } } + // Reset variant and state on component change + const updateSelectedComponent = () => { + selectedVariant.value = 'normal' + selectedState.clear() + } + + watch( + selectedComponentName, + updateSelectedComponent + ) + // ### Rules stuff aka meat and potatoes // The native structure of separate rules and the child -> parent // relation isn't very convenient for editor, we replace the array @@ -270,13 +288,13 @@ export default { return root }) - // Checkging whether component can support some "directives" which + // Checking whether component can support some "directives" which // are actually virtual subcomponents, i.e. Text, Link etc exports.componentHas = (subComponent) => { return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent) } - // Path is path for lodash's get and set + // Path for lodash's get and set const getPath = (component, directive) => { const pathSuffix = component ? `._children.${component}.normal.normal` : '' const path = `${selectedComponentName.value}.${selectedVariant.value}.${normalizeStates([...selectedState])}${pathSuffix}.directives.${directive}` @@ -296,7 +314,7 @@ export default { ) set(allEditedRules, getPath(component, directive), fallback ?? defaultValue) } else { - set(allEditedRules, getPath(component, directive), null) + unset(allEditedRules, getPath(component, directive)) } } }) @@ -313,14 +331,14 @@ export default { usedRule = get(fallback, path) } - if (directive === 'shadow') { - console.log('EDITED', usedRule) - console.log('PP', postProcess(usedRule)) - } return postProcess(usedRule) }, set (value) { - set(allEditedRules, getPath(component, directive), value) + if (value) { + set(allEditedRules, getPath(component, directive), value) + } else { + unset(allEditedRules, getPath(component, directive)) + } } }) @@ -378,6 +396,7 @@ export default { return null }) } + provide('normalizeShadows', normalizeShadows) // Shadow is partially edited outside the ShadowControl // for better space utilization @@ -428,57 +447,6 @@ export default { } 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') - exports.previewClass = computed(() => { - const selectors = [] - if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') { - selectors.push(selectedComponent.value.variants[selectedVariant.value]) - } - if (selectedState.size > 0) { - selectedState.forEach(state => { - const original = selectedComponent.value.states[state] - selectors.push(simulatePseudoSelectors(original)) - }) - } - return selectors.map(x => x.substring(1)).join('') - }) - const previewRules = reactive([]) - exports.previewRules = previewRules - exports.previewCss = computed(() => { - try { - const scoped = getCssRules(previewRules).map(simulatePseudoSelectors) - return scoped.join('\n') - } catch (e) { - console.error('Invalid ruleset', e) - return null - } - }) - - const applicablePreviewRules = computed(() => { - return previewRules.filter(rule => { - const filterable = rule.parent ? rule.parent : rule - const variantMatches = filterable.variant === selectedVariant.value - const stateMatches = filterable.state.filter(x => x !== 'normal').every(x => selectedState.has(x)) - return variantMatches && stateMatches - }) - }) - const previewColors = computed(() => ({ - text: applicablePreviewRules.value.find(r => r.component === 'Text')?.virtualDirectives['--text'], - link: applicablePreviewRules.value.find(r => r.component === 'Link')?.virtualDirectives['--link'], - border: applicablePreviewRules.value.find(r => r.component === 'Border')?.virtualDirectives['--border'], - icon: applicablePreviewRules.value.find(r => r.component === 'Icon')?.virtualDirectives['--icon'], - background: applicablePreviewRules.value.find(r => r.parent == null)?.dynamicVars.stacked - })) - exports.previewColors = previewColors const editorFriendlyToOriginal = computed(() => { const resultRules = [] @@ -514,64 +482,13 @@ export default { }) } - convert(selectedComponentName.value, allEditedRules[selectedComponentName.value]) + componentsMap.values().forEach(({ name }) => { + convert(name, allEditedRules[name]) + }) return resultRules }) - const updatePreview = () => { - try { - const { name, ...paletteData } = selectedPalette.value - // 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({ - inputRuleset: editorFriendlyToOriginal.value, - initialStaticVars: { - ...paletteData - }, - ultimateBackgroundColor: '#000000', - rootComponentName: selectedComponentName.value, - editMode: true, - debug: true - }).eager - previewRules.splice(0, previewRules.length) - previewRules.push(...rules) - } catch (e) { - console.error('Could not compile preview theme', e) - } - } - - const updateSelectedComponent = () => { - selectedVariant.value = 'normal' - selectedState.clear() - updatePreview() - } - updateSelectedComponent() - - // export and import - watch( - allEditedRules, - updatePreview - ) - - watch( - palettes, - updatePreview - ) - - watch( - selectedPalette, - updatePreview - ) - - watch( - selectedComponentName, - updateSelectedComponent - ) - - // ## Variables const allCustomVirtualDirectives = [...componentsMap.values()] .map(c => { return c @@ -590,124 +507,29 @@ export default { value: valVal.trim() } }) - const virtualDirectives = reactive(allCustomVirtualDirectives) + + const virtualDirectives = ref(allCustomVirtualDirectives) exports.virtualDirectives = virtualDirectives - - exports.onVirtualDirectivesUpdate = (e) => { - virtualDirectives.splice(0, virtualDirectives.length) - virtualDirectives.push(...e) + exports.updateVirtualDirectives = (value) => { + virtualDirectives.value = value } - const selectedVirtualDirectiveId = ref(0) - 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) { - const newValType = value - let newValue - switch (value) { - case 'shadow': - newValue = '0 0 0 #000000 / 1' - break - case 'color': - newValue = '#000000' - break - default: - newValue = 'none' - } - const newName = virtualDirectives[selectedVirtualDirectiveId.value].name - virtualDirectives[selectedVirtualDirectiveId.value] = { - name: newName, - value: newValue, - valType: newValType - } - } - }) - - const draftVirtualDirectiveValid = ref(true) - const draftVirtualDirective = ref({}) - exports.draftVirtualDirective = draftVirtualDirective - - watch( - selectedVirtualDirective, - (directive) => { - switch (directive.valType) { - case 'shadow': { - if (Array.isArray(directive.value)) { - draftVirtualDirective.value = normalizeShadows(directive.value) - } else { - const splitShadow = directive.value.split(/,/g).map(x => x.trim()) - draftVirtualDirective.value = normalizeShadows(splitShadow) - } - break - } - case 'color': - draftVirtualDirective.value = directive.value - break - default: - draftVirtualDirective.value = directive.value - break - } - }, - { immediate: true } - ) - - watch( - draftVirtualDirective, - (directive) => { - try { - switch (selectedVirtualDirective.value.valType) { - case 'shadow': { - virtualDirectives[selectedVirtualDirectiveId.value].value = - directive.map(x => serializeShadow(x)).join(', ') - break - } - default: - virtualDirectives[selectedVirtualDirectiveId.value].value = directive - } - draftVirtualDirectiveValid.value = true - } catch (e) { - console.error('Invalid virtual directive value', e) - draftVirtualDirectiveValid.value = false - } - }, - { immediate: true } - ) - const virtualDirectivesOut = computed(() => { return [ 'Root {', - ...virtualDirectives.map(vd => ` --${vd.name}: ${vd.valType} | ${vd.value};`), + ...virtualDirectives.value.map(vd => ` --${vd.name}: ${vd.valType} | ${vd.value};`), '}' ].join('\n') }) - exports.getNewVirtualDirective = () => ({ - name: 'newDirective', - valType: 'generic', - value: 'foobar' - }) - exports.computeColor = (color) => { - const computedColor = findColor(color, { dynamicVars: {}, staticVars: selectedPalette.value }) + const computedColor = findColor(color, { dynamicVars: dynamicVars.value, staticVars: selectedPalette.value }) if (computedColor) { return rgb2hex(computedColor) } return null } + provide('computeColor', exports.computeColor) exports.contrast = computed(() => { return getContrast( @@ -716,48 +538,24 @@ export default { ) }) - 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 virtualDirectivesRule = { - component: 'Root', - directives: Object.fromEntries( - virtualDirectives.map(vd => [`--${vd.name}`, `${vd.valType} | ${vd.value}`]) - ) - } - - const rules = init({ - inputRuleset: [ - paletteRule, - virtualDirectivesRule, - ...editorFriendlyToOriginal.value - ], - 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) + const paletteRule = computed(() => { + const { name, ...rest } = selectedPalette.value + return { + component: 'Root', + directives: Object + .entries(rest) + .filter(([k, v]) => v) + .map(([k, v]) => ['--' + k, v]) + .reduce((acc, [k, v]) => ({ ...acc, [k]: `color | ${v}` }), {}) } - } + }) + + const virtualDirectivesRule = computed(() => ({ + component: 'Root', + directives: Object.fromEntries( + virtualDirectives.value.map(vd => [`--${vd.name}`, `${vd.valType} | ${vd.value}`]) + ) + })) // ## Export and Import const styleExporter = newExporter({ @@ -782,14 +580,13 @@ export default { exports.author.value = metaIn.author exports.website.value = metaIn.website - virtualDirectives.splice(0, virtualDirectives.length) const newVirtualDirectives = Object .entries(rootComponent.directives) .map(([name, value]) => { const [valType, valVal] = value.split('|').map(x => x.trim()) return { name: name.substring(2), valType, value: valVal } }) - virtualDirectives.push(...newVirtualDirectives) + virtualDirectives.value = newVirtualDirectives onPalettesUpdate(palettesIn.map(x => ({ name: x.variant, ...x.directives }))) @@ -801,6 +598,8 @@ export default { allEditedRules ) }) + + exports.updateOverallPreview() } }) @@ -821,6 +620,133 @@ export default { styleImporter.importData() } + const exportRules = computed(() => [ + paletteRule.value, + virtualDirectivesRule.value, + ...editorFriendlyToOriginal.value + ]) + + exports.applyStyle = () => { + store.dispatch('setStyleCustom', exportRules.value) + } + + const overallPreviewRules = ref([]) + exports.overallPreviewRules = overallPreviewRules + + const overallPreviewCssRules = computed(() => getScopedVersion( + getCssRules(overallPreviewRules.value), + '#edited-style-preview' + ).join('\n')) + exports.overallPreviewCssRules = overallPreviewCssRules + + const updateOverallPreview = () => { + try { + overallPreviewRules.value = init({ + inputRuleset: exportRules.value, + ultimateBackgroundColor: '#000000', + liteMode: true, + debug: true + }).eager + } catch (e) { + console.error('Could not compile preview theme', e) + return null + } + } + // + // Apart from "hover" we can't really show how component looks like in + // certain states, so we have to fake them. + const simulatePseudoSelectors = (css, prefix) => css + .replace(prefix, '.component-preview .preview-block') + .replace(':active', '.preview-active') + .replace(':hover', '.preview-hover') + .replace(':active', '.preview-active') + .replace(':focus', '.preview-focus') + .replace(':focus-within', '.preview-focus-within') + .replace(':disabled', '.preview-disabled') + + const previewRules = computed(() => { + const filtered = overallPreviewRules.value.filter(r => { + const componentMatch = r.component === selectedComponentName.value + const parentComponentMatch = r.parent?.component === selectedComponentName.value + if (!componentMatch && !parentComponentMatch) return false + const rule = parentComponentMatch ? r.parent : r + if (rule.component !== selectedComponentName.value) return false + if (rule.variant !== selectedVariant.value) return false + const ruleState = new Set(rule.state.filter(x => x !== 'normal')) + const differenceA = [...ruleState].filter(x => !selectedState.has(x)) + const differenceB = [...selectedState].filter(x => !ruleState.has(x)) + return (differenceA.length + differenceB.length) === 0 + }) + const sorted = [...filtered] + .filter(x => x.component === selectedComponentName.value) + .sort((a, b) => { + const aSelectorLength = a.selector.split(/ /g).length + const bSelectorLength = b.selector.split(/ /g).length + return aSelectorLength - bSelectorLength + }) + + const prefix = sorted[0].selector + + return filtered.filter(x => x.selector.startsWith(prefix)) + }) + + exports.previewClass = computed(() => { + const selectors = [] + if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') { + selectors.push(selectedComponent.value.variants[selectedVariant.value]) + } + if (selectedState.size > 0) { + selectedState.forEach(state => { + const original = selectedComponent.value.states[state] + console.log('ORIG', original) + selectors.push(simulatePseudoSelectors(original)) + }) + } + return selectors.map(x => x.substring(1)).join('') + }) + + exports.previewCss = computed(() => { + try { + const prefix = previewRules.value[0].selector + const scoped = getCssRules(previewRules.value).map(x => simulatePseudoSelectors(x, prefix)) + return scoped.join('\n') + } catch (e) { + console.error('Invalid ruleset', e) + return null + } + }) + + const dynamicVars = computed(() => { + return previewRules.value[0].dynamicVars + }) + + const previewColors = computed(() => { + const stacked = dynamicVars.value.stacked + const background = typeof stacked === 'string' ? stacked : rgb2hex(stacked) + return { + text: previewRules.value.find(r => r.component === 'Text')?.virtualDirectives['--text'], + link: previewRules.value.find(r => r.component === 'Link')?.virtualDirectives['--link'], + border: previewRules.value.find(r => r.component === 'Border')?.virtualDirectives['--border'], + icon: previewRules.value.find(r => r.component === 'Icon')?.virtualDirectives['--icon'], + background + } + }) + exports.previewColors = previewColors + exports.updateOverallPreview = updateOverallPreview + + updateOverallPreview() + + watch( + [ + allEditedRules, + palettes, + selectedPalette, + selectedState, + selectedVariant + ], + updateOverallPreview + ) + return exports } } diff --git a/src/components/settings_modal/tabs/style_tab/style_tab.vue b/src/components/settings_modal/tabs/style_tab/style_tab.vue index 5ca274c35..ccba2c7dc 100644 --- a/src/components/settings_modal/tabs/style_tab/style_tab.vue +++ b/src/components/settings_modal/tabs/style_tab/style_tab.vue @@ -7,7 +7,7 @@ @@ -41,7 +41,7 @@