some initial drafts of component editor

This commit is contained in:
Henry Jameson 2024-09-24 21:32:13 +03:00
parent d5549ac1ee
commit e1d3ebc943
6 changed files with 352 additions and 115 deletions

View file

@ -33,8 +33,11 @@
> >
<div <div
class="preview-block" class="preview-block"
:class="previewClass"
:style="previewStyle" :style="previewStyle"
/> >
TEST
</div>
</div> </div>
<input <input
v-show="shadowControl" v-show="shadowControl"
@ -59,7 +62,6 @@
<Checkbox <Checkbox
id="lightGrid" id="lightGrid"
v-model="lightGrid" v-model="lightGrid"
:disabled="shadow == null"
name="lightGrid" name="lightGrid"
class="input-light-grid" class="input-light-grid"
> >
@ -163,6 +165,9 @@
} }
.preview-block { .preview-block {
display: flex;
justify-content: center;
align-items: center;
width: 33%; width: 33%;
height: 33%; height: 33%;
border-radius: var(--roundness); border-radius: var(--roundness);

View file

@ -1,40 +1,183 @@
// import { import { ref, reactive, computed, watch } from 'vue'
// rgb2hex,
// hex2rgb,
// getContrastRatioLayers,
// relativeLuminance
// } from 'src/services/color_convert/color_convert.js'
// import {
// getThemes
// } from 'src/services/style_setter/style_setter.js'
// import {
// newImporter,
// newExporter
// } from 'src/services/export_import/export_import.js'
// import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
// import { init } from 'src/services/theme_data/theme_data_3.service.js'
// import {
// getCssRules,
// getScopedVersion
// } from 'src/services/theme_data/css_utils.js'
// import ColorInput from 'src/components/color_input/color_input.vue'
// import RangeInput from 'src/components/range_input/range_input.vue'
// import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
// import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
// import FontControl from 'src/components/font_control/font_control.vue'
// import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
// import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
// import Checkbox from 'src/components/checkbox/checkbox.vue'
/* eslint-disable no-unused-vars */
import Select from 'src/components/select/select.vue' import Select from 'src/components/select/select.vue'
import Preview from './theme_preview.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import ComponentPreview from 'src/components/component_preview/component_preview.vue'
import StringSetting from '../../helpers/string_setting.vue'
import { defineOptions, ref } from 'vue' import { init } from 'src/services/theme_data/theme_data_3.service.js'
const componentsContext = require.context('src', true, /\.style.js(on)?$/) import { getCssRules } from 'src/services/theme_data/css_utils.js'
const componentNames = componentsContext.keys()
const componentName = ref(componentNames[0]) import { library } from '@fortawesome/fontawesome-svg-core'
import { faFloppyDisk, faFolderOpen, faFile } from '@fortawesome/free-solid-svg-icons'
library.add(
faFile,
faFloppyDisk,
faFolderOpen
)
export default {
components: {
Select,
Checkbox,
StringSetting,
ComponentPreview
},
setup () {
const name = ref('')
const author = ref('')
const license = ref('')
const website = ref('')
const palette = {
// These are here just to establish order,
// themes should override those
bg: '#121a24',
fg: '#182230',
text: '#b9b9ba',
link: '#d8a070',
accent: '#d8a070',
cRed: '#FF0000',
cBlue: '#0095ff',
cGreen: '#0fa00f',
cOrange: '#ffa500'
}
const getI18nPath = (componentName) => `settings.style.themes3.editor.components.${componentName}`
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}`
}
// Getting existing components
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
const componentKeysRaw = componentsContext.keys()
const componentsMap = new Map(
componentKeysRaw
.map(
key => [key, componentsContext(key).default]
).filter(([key, component]) => !component.virtual)
)
const componentKeys = [...componentsMap.keys()]
// const componentValues = componentsMap.values()
// Initializing selected component and its computed descendants
const selectedComponentKey = ref(componentKeys[0])
const selectedComponentValue = computed(() => componentsMap.get(selectedComponentKey.value))
const selectedComponentName = computed(() => selectedComponentValue.value.name)
const selectedComponentVariants = computed(() => {
return new Set(['normal', ...(Object.keys(selectedComponentValue.value.variants || {}))])
})
const selectedComponentStates = computed(() => {
return new Set([...(Object.keys(selectedComponentValue.value.states || {}).filter(x => x !== 'normal'))])
})
const selectedVariant = ref('normal')
const selectedStates = reactive(new Set())
const updateSelectedStates = (state, v) => {
if (v) {
selectedStates.add(state)
} else {
selectedStates.delete(state)
}
}
const simulatePseudoSelectors = css => css
.replace(selectedComponentValue.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 = []
selectors.push(selectedComponentValue.value.variants[selectedVariant.value])
if (selectedStates.size > 0) {
selectedStates.forEach(state => {
const original = selectedComponentValue.value.states[state]
selectors.push(simulatePseudoSelectors(original))
})
}
console.log(selectors)
return selectors.map(x => x.substring(1)).join('')
})
const previewCss = computed(() => {
console.log(previewRules)
const scoped = getCssRules(previewRules).map(simulatePseudoSelectors)
return scoped.join('\n')
})
const updateSelectedComponent = () => {
selectedVariant.value = 'normal'
selectedStates.clear()
previewRules.splice(0, previewRules.length)
previewRules.push(...init({
inputRuleset: [{
component: selectedComponentName.value
}],
initialStaticVars: {
...palette
},
ultimateBackgroundColor: '#000000',
rootComponentName: selectedComponentName.value,
editMode: true,
debug: true
}).eager)
}
updateSelectedComponent()
watch(
selectedComponentName,
updateSelectedComponent
)
return {
name,
author,
license,
website,
componentKeys,
componentsMap,
selectedComponentValue,
selectedComponentName,
selectedComponentKey,
selectedComponentVariants,
selectedComponentStates,
updateSelectedStates,
selectedVariant,
selectedStates,
getFriendlyNamePath,
getStatePath,
getVariantPath,
previewCss,
previewClass,
fallbackI18n (translated, fallback) {
if (translated.startsWith('settings.style.themes3')) {
return fallback
}
return translated
}
}
}
}

View file

@ -40,11 +40,37 @@
.component-editor { .component-editor {
display: grid; display: grid;
grid-template-columns: 10em, 1fr, 2fr; grid-template-columns: 10em 2fr 3fr;
grid-template-rows: auto 1fr; grid-template-rows: auto auto 1fr;
grid-gap: 0.5em;
grid-template-areas: grid-template-areas:
"variant" "preview" "controls", "component-label component ."
"state" "preview" "controls"; "variant preview settings"
"state preview settings";
.compopnent-selector {
grid-area: component;
align-self: center;
}
.component-selector-label {
grid-area: component-label;
align-self: center;
text-align: right;
font-weight: bold;
}
.state-selector,
.variant-selector {
display: grid;
grid-template-rows: auto auto;
grid-auto-flow: rows;
grid-gap: 0.5em;
> label {
font-weight: bold;
}
}
.state-selector { .state-selector {
grid-area: state; grid-area: state;
@ -52,8 +78,23 @@
.variant-selector { .variant-selector {
grid-area: variant; grid-area: variant;
display: flex; }
flex-direction: column;
.state-selector-list {
display: grid;
list-style: none;
grid-template-rows: auto;
grid-gap: 0.5em;
padding: 0;
margin: 0;
}
.preview-container {
grid-area: preview;
}
.component-settings {
grid-area: settings;
} }
} }
} }

View file

@ -1,49 +1,4 @@
<script setup> <script src="./style_tab.js">
import { ref, computed } from 'vue'
import Select from 'src/components/select/select.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import StringSetting from '../../helpers/string_setting.vue'
// import Preview from '../theme_tab/theme_preview.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faFloppyDisk, faFolderOpen, faFile } from '@fortawesome/free-solid-svg-icons'
library.add(
faFile,
faFloppyDisk,
faFolderOpen
)
const name = ref('')
const author = ref('')
const license = ref('')
const website = ref('')
// Getting existing components
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
const componentKeys = componentsContext.keys()
const componentsMap = new Map(
componentKeys
.map(
key => [key, componentsContext(key).default]
)
)
// const componentValues = componentsMap.values()
// Initializing selected component and its computed descendants
const selectedComponentKey = ref(componentKeys[0])
const selectedComponentValue = computed(() => componentsMap.get(selectedComponentKey.value))
// const selectedComponentName = computed(() => selectedComponent.value.name)
const selectedComponentVariants = computed(() => {
return new Set([...(Object.keys(selectedComponentValue.value.variants || {})), 'normal'])
})
const selectedComponentStates = computed(() => {
return new Set([...(Object.keys(selectedComponentValue.value.states || {})), 'normal'])
})
</script> </script>
<template> <template>
@ -96,40 +51,91 @@ const selectedComponentStates = computed(() => {
</li> </li>
</ul> </ul>
</div> </div>
<div class="setting-item"> <div class="setting-item component-editor">
<Select v-model="selectedComponentKey"> <label
for="component-selector"
class="component-selector-label"
>
{{ $t('settings.style.themes3.editor.component_selector') }}
{{ ' ' }}
</label>
<Select
v-model="selectedComponentKey"
id="component-selector"
class="component-selector"
>
<option <option
v-for="key in componentKeys" v-for="key in componentKeys"
:key="'component-' + key" :key="'component-' + key"
:value="key" :value="key"
> >
{{ componentsMap.get(key).name }} {{ fallbackI18n($t(getFriendlyNamePath(componentsMap.get(key).name)), componentsMap.get(key).name) }}
</option> </option>
</Select> </Select>
<div class="component-editor"> <label
for="component-selector"
class="component-selector-label"
>
{{ $t('settings.style.themes3.editor.component_selector') }}
</label>
<div class="variant-selector">
<label for="variant-selector">
{{ $t('settings.style.themes3.editor.variant_selector') }}
</label>
<Select <Select
v-model="selectedComponentVariant" v-if="selectedComponentVariants.size > 1"
class="variant-selector" v-model="selectedVariant"
> >
<option <option
v-for="variant in selectedComponentVariants" v-for="variant in selectedComponentVariants"
:value="variant"
:key="'component-variant-' + variant" :key="'component-variant-' + variant"
> >
{{ variant }} {{ fallbackI18n($t(getVariantPath(selectedComponentName, variant)), variant) }}
</option> </option>
</Select> </Select>
<ul class="state-selector"> <p v-else>
{{ $t('settings.style.themes3.editor.only_variant') }}
</p>
</div>
<div class="state-selector">
<label for="variant-selector">
{{ $t('settings.style.themes3.editor.states_selector') }}
</label>
<ul
v-if="selectedComponentStates.size > 0"
class="state-selector-list"
>
<li <li
v-for="state in selectedComponentStates" v-for="state in selectedComponentStates"
:key="'component-variant-' + state" :key="'component-variant-' + state"
> >
<Checkbox <Checkbox
v-model="selectedComponentStates" :value="selectedStates.has(state)"
@update:modelValue="(v) => updateSelectedStates(state, v)"
> >
{{ state }} {{ fallbackI18n($t(getStatePath(selectedComponentName, state)), state) }}
</Checkbox> </Checkbox>
</li> </li>
</ul> </ul>
<p v-else>
{{ $t('settings.style.themes3.editor.only_state') }}
</p>
</div>
<div class="preview-container">
<!-- eslint-disable vue/no-v-html vue/no-v-text-v-html-on-component -->
<component
:is="'style'"
v-html="previewCss"
/>
<!-- eslint-enable vue/no-v-html vue/no-v-text-v-html-on-component -->
<ComponentPreview
class="component-preview"
:previewClass="previewClass"
@update:shadow="({ axis, value }) => updateProperty(axis, value)"
/>
</div>
<div class="component-setting">
</div> </div>
</div> </div>
</div> </div>

View file

@ -761,7 +761,43 @@
"style_name": "Stylesheet name", "style_name": "Stylesheet name",
"style_author": "Made by", "style_author": "Made by",
"style_license": "License", "style_license": "License",
"style_website": "Website" "style_website": "Website",
"component_selector": "Component",
"variant_selector": "Variant",
"states_selector": "States",
"only_variant": "Component doesn't have any variants",
"only_state": "Component only has default state",
"components": {
"normal": {
"normal": "Normal state",
"variant": "Default variant"
},
"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

@ -172,11 +172,13 @@ export const init = ({
ultimateBackgroundColor, ultimateBackgroundColor,
debug = false, debug = false,
liteMode = false, liteMode = false,
editMode = false,
onlyNormalState = false, onlyNormalState = false,
rootComponentName = 'Root' rootComponentName = 'Root',
initialStaticVars = {}
}) => { }) => {
if (!inputRuleset) throw new Error('Ruleset is null or undefined!') if (!inputRuleset) throw new Error('Ruleset is null or undefined!')
const staticVars = {} const staticVars = { ...initialStaticVars }
const stacked = {} const stacked = {}
const computed = {} const computed = {}
@ -402,7 +404,7 @@ export const init = ({
case 'color': { case 'color': {
const color = findColor(value[0], { dynamicVars, staticVars }) const color = findColor(value[0], { dynamicVars, staticVars })
dynamicVars[k] = color dynamicVars[k] = color
if (combination.component === 'Root') { if (combination.component === rootComponentName) {
staticVars[k.substring(2)] = color staticVars[k.substring(2)] = color
} }
break break
@ -410,14 +412,14 @@ export const init = ({
case 'shadow': { case 'shadow': {
const shadow = value const shadow = value
dynamicVars[k] = shadow dynamicVars[k] = shadow
if (combination.component === 'Root') { if (combination.component === rootComponentName) {
staticVars[k.substring(2)] = shadow staticVars[k.substring(2)] = shadow
} }
break break
} }
case 'generic': { case 'generic': {
dynamicVars[k] = value dynamicVars[k] = value
if (combination.component === 'Root') { if (combination.component === rootComponentName) {
staticVars[k.substring(2)] = value staticVars[k.substring(2)] = value
} }
break break
@ -443,11 +445,15 @@ export const init = ({
variants: originalVariants = {} variants: originalVariants = {}
} = component } = component
const validInnerComponents = ( let validInnerComponents
liteMode if (editMode) {
? (component.validInnerComponentsLite || component.validInnerComponents) const temp = (component.validInnerComponentsLite || component.validInnerComponents || [])
: component.validInnerComponents validInnerComponents = temp.filter(c => virtualComponents.has(c))
) || [] } else if (editMode) {
validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || [])
} else {
validInnerComponents = component.validInnerComponents || []
}
// Normalizing states and variants to always include "normal" // Normalizing states and variants to always include "normal"
const states = { normal: '', ...originalStates } const states = { normal: '', ...originalStates }