logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://anongit.hacktivis.me/git/pleroma-fe.git/

style_tab.js (27330B)


  1. import { ref, reactive, computed, watch, watchEffect, provide, getCurrentInstance } from 'vue'
  2. import { useStore } from 'vuex'
  3. import { get, set, unset, throttle } from 'lodash'
  4. import Select from 'src/components/select/select.vue'
  5. import SelectMotion from 'src/components/select/select_motion.vue'
  6. import Checkbox from 'src/components/checkbox/checkbox.vue'
  7. import ComponentPreview from 'src/components/component_preview/component_preview.vue'
  8. import StringSetting from '../../helpers/string_setting.vue'
  9. import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
  10. import ColorInput from 'src/components/color_input/color_input.vue'
  11. import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
  12. import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
  13. import RoundnessInput from 'src/components/roundness_input/roundness_input.vue'
  14. import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
  15. import Tooltip from 'src/components/tooltip/tooltip.vue'
  16. import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
  17. import Preview from '../theme_tab/theme_preview.vue'
  18. import VirtualDirectivesTab from './virtual_directives_tab.vue'
  19. import { init, findColor } from 'src/services/theme_data/theme_data_3.service.js'
  20. import {
  21. getCssRules,
  22. getScopedVersion
  23. } from 'src/services/theme_data/css_utils.js'
  24. import { serialize } from 'src/services/theme_data/iss_serializer.js'
  25. import { deserializeShadow, deserialize } from 'src/services/theme_data/iss_deserializer.js'
  26. import {
  27. rgb2hex,
  28. hex2rgb,
  29. getContrastRatio
  30. } from 'src/services/color_convert/color_convert.js'
  31. import {
  32. newImporter,
  33. newExporter
  34. } from 'src/services/export_import/export_import.js'
  35. import { library } from '@fortawesome/fontawesome-svg-core'
  36. import {
  37. faFloppyDisk,
  38. faFolderOpen,
  39. faFile,
  40. faArrowsRotate,
  41. faCheck
  42. } from '@fortawesome/free-solid-svg-icons'
  43. // helper for debugging
  44. // eslint-disable-next-line no-unused-vars
  45. const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x))
  46. // helper to make states comparable
  47. const normalizeStates = (states) => ['normal', ...(states?.filter(x => x !== 'normal') || [])].join(':')
  48. library.add(
  49. faFile,
  50. faFloppyDisk,
  51. faFolderOpen,
  52. faArrowsRotate,
  53. faCheck
  54. )
  55. export default {
  56. components: {
  57. Select,
  58. SelectMotion,
  59. Checkbox,
  60. Tooltip,
  61. StringSetting,
  62. ComponentPreview,
  63. TabSwitcher,
  64. ShadowControl,
  65. ColorInput,
  66. PaletteEditor,
  67. OpacityInput,
  68. RoundnessInput,
  69. ContrastRatio,
  70. Preview,
  71. VirtualDirectivesTab
  72. },
  73. setup (props, context) {
  74. const exports = {}
  75. const store = useStore()
  76. // All rules that are made by editor
  77. const allEditedRules = ref(store.state.interface.styleDataUsed || {})
  78. const styleDataUsed = computed(() => store.state.interface.styleDataUsed)
  79. watch([styleDataUsed], (value) => {
  80. onImport(store.state.interface.styleDataUsed)
  81. }, { once: true })
  82. exports.isActive = computed(() => {
  83. const tabSwitcher = getCurrentInstance().parent.ctx
  84. return tabSwitcher ? tabSwitcher.isActive('style') : false
  85. })
  86. // ## Meta stuff
  87. exports.name = ref('')
  88. exports.author = ref('')
  89. exports.license = ref('')
  90. exports.website = ref('')
  91. const metaOut = computed(() => {
  92. return [
  93. '@meta {',
  94. ` name: ${exports.name.value};`,
  95. ` author: ${exports.author.value};`,
  96. ` license: ${exports.license.value};`,
  97. ` website: ${exports.website.value};`,
  98. '}'
  99. ].join('\n')
  100. })
  101. const metaRule = computed(() => ({
  102. component: '@meta',
  103. directives: {
  104. name: exports.name.value,
  105. author: exports.author.value,
  106. license: exports.license.value,
  107. website: exports.website.value
  108. }
  109. }))
  110. // ## Palette stuff
  111. const palettes = reactive([
  112. {
  113. name: 'default',
  114. bg: '#121a24',
  115. fg: '#182230',
  116. text: '#b9b9ba',
  117. link: '#d8a070',
  118. accent: '#d8a070',
  119. cRed: '#FF0000',
  120. cBlue: '#0095ff',
  121. cGreen: '#0fa00f',
  122. cOrange: '#ffa500'
  123. },
  124. {
  125. name: 'light',
  126. bg: '#f2f6f9',
  127. fg: '#d6dfed',
  128. text: '#304055',
  129. underlay: '#5d6086',
  130. accent: '#f55b1b',
  131. cBlue: '#0095ff',
  132. cRed: '#d31014',
  133. cGreen: '#0fa00f',
  134. cOrange: '#ffa500',
  135. border: '#d8e6f9'
  136. }
  137. ])
  138. exports.palettes = palettes
  139. // This is kinda dumb but you cannot "replace" reactive() object
  140. // and so v-model simply fails when you try to chage (increase only?)
  141. // length of the array. Since linter complains about mutating modelValue
  142. // inside SelectMotion, the next best thing is to just wipe existing array
  143. // and replace it with new one.
  144. const onPalettesUpdate = (e) => {
  145. palettes.splice(0, palettes.length)
  146. palettes.push(...e)
  147. }
  148. exports.onPalettesUpdate = onPalettesUpdate
  149. const selectedPaletteId = ref(0)
  150. const selectedPalette = computed({
  151. get () {
  152. return palettes[selectedPaletteId.value]
  153. },
  154. set (newPalette) {
  155. palettes[selectedPaletteId.value] = newPalette
  156. }
  157. })
  158. exports.selectedPaletteId = selectedPaletteId
  159. exports.selectedPalette = selectedPalette
  160. provide('selectedPalette', selectedPalette)
  161. watch([selectedPalette], () => updateOverallPreview())
  162. exports.getNewPalette = () => ({
  163. name: 'new palette',
  164. bg: '#121a24',
  165. fg: '#182230',
  166. text: '#b9b9ba',
  167. link: '#d8a070',
  168. accent: '#d8a070',
  169. cRed: '#FF0000',
  170. cBlue: '#0095ff',
  171. cGreen: '#0fa00f',
  172. cOrange: '#ffa500'
  173. })
  174. // Raw format
  175. const palettesRule = computed(() => {
  176. return palettes.map(palette => {
  177. const { name, ...rest } = palette
  178. return {
  179. component: '@palette',
  180. variant: name,
  181. directives: Object
  182. .entries(rest)
  183. .filter(([k, v]) => v && k)
  184. .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
  185. }
  186. })
  187. })
  188. // Text format
  189. const palettesOut = computed(() => {
  190. return palettes.map(({ name, ...palette }) => {
  191. const entries = Object
  192. .entries(palette)
  193. .filter(([k, v]) => v && k)
  194. .map(([slot, data]) => ` ${slot}: ${data};`)
  195. .join('\n')
  196. return `@palette.${name} {\n${entries}\n}`
  197. }).join('\n\n')
  198. })
  199. // ## Components stuff
  200. // Getting existing components
  201. const componentsContext = require.context('src', true, /\.style.js(on)?$/)
  202. const componentKeysAll = componentsContext.keys()
  203. const componentsMap = new Map(
  204. componentKeysAll
  205. .map(
  206. key => [key, componentsContext(key).default]
  207. ).filter(([key, component]) => !component.virtual && !component.notEditable)
  208. )
  209. exports.componentsMap = componentsMap
  210. const componentKeys = [...componentsMap.keys()]
  211. exports.componentKeys = componentKeys
  212. // Component list and selection
  213. const selectedComponentKey = ref(componentsMap.keys().next().value)
  214. exports.selectedComponentKey = selectedComponentKey
  215. const selectedComponent = computed(() => componentsMap.get(selectedComponentKey.value))
  216. const selectedComponentName = computed(() => selectedComponent.value.name)
  217. // Selection basis
  218. exports.selectedComponentVariants = computed(() => {
  219. return Object.keys({ normal: null, ...(selectedComponent.value.variants || {}) })
  220. })
  221. exports.selectedComponentStates = computed(() => {
  222. const all = Object.keys({ normal: null, ...(selectedComponent.value.states || {}) })
  223. return all.filter(x => x !== 'normal')
  224. })
  225. // selection
  226. const selectedVariant = ref('normal')
  227. exports.selectedVariant = selectedVariant
  228. const selectedState = reactive(new Set())
  229. exports.selectedState = selectedState
  230. exports.updateSelectedStates = (state, v) => {
  231. if (v) {
  232. selectedState.add(state)
  233. } else {
  234. selectedState.delete(state)
  235. }
  236. }
  237. // Reset variant and state on component change
  238. const updateSelectedComponent = () => {
  239. selectedVariant.value = 'normal'
  240. selectedState.clear()
  241. }
  242. watch(
  243. selectedComponentName,
  244. updateSelectedComponent
  245. )
  246. // ### Rules stuff aka meat and potatoes
  247. // The native structure of separate rules and the child -> parent
  248. // relation isn't very convenient for editor, we replace the array
  249. // and child -> parent structure with map and parent -> child structure
  250. const rulesToEditorFriendly = (rules, root = {}) => rules.reduce((acc, rule) => {
  251. const { parent: rParent, component: rComponent } = rule
  252. const parent = rParent ?? rule
  253. const hasChildren = !!rParent
  254. const child = hasChildren ? rule : null
  255. const {
  256. component: pComponent,
  257. variant: pVariant = 'normal',
  258. state: pState = [] // no relation to Intel CPUs whatsoever
  259. } = parent
  260. const pPath = `${hasChildren ? pComponent : rComponent}.${pVariant}.${normalizeStates(pState)}`
  261. let output = get(acc, pPath)
  262. if (!output) {
  263. set(acc, pPath, {})
  264. output = get(acc, pPath)
  265. }
  266. if (hasChildren) {
  267. output._children = output._children ?? {}
  268. const {
  269. component: cComponent,
  270. variant: cVariant = 'normal',
  271. state: cState = [],
  272. directives
  273. } = child
  274. const cPath = `${cComponent}.${cVariant}.${normalizeStates(cState)}`
  275. set(output._children, cPath, { directives })
  276. } else {
  277. output.directives = parent.directives
  278. }
  279. return acc
  280. }, root)
  281. const editorFriendlyFallbackStructure = computed(() => {
  282. const root = {}
  283. componentKeys.forEach((componentKey) => {
  284. const componentValue = componentsMap.get(componentKey)
  285. const { defaultRules, name } = componentValue
  286. rulesToEditorFriendly(
  287. defaultRules.map((rule) => ({ ...rule, component: name })),
  288. root
  289. )
  290. })
  291. return root
  292. })
  293. // Checking whether component can support some "directives" which
  294. // are actually virtual subcomponents, i.e. Text, Link etc
  295. exports.componentHas = (subComponent) => {
  296. return !!selectedComponent.value.validInnerComponents?.find(x => x === subComponent)
  297. }
  298. // Path for lodash's get and set
  299. const getPath = (component, directive) => {
  300. const pathSuffix = component ? `._children.${component}.normal.normal` : ''
  301. const path = `${selectedComponentName.value}.${selectedVariant.value}.${normalizeStates([...selectedState])}${pathSuffix}.directives.${directive}`
  302. return path
  303. }
  304. // Templates for directives
  305. const isElementPresent = (component, directive, defaultValue = '') => computed({
  306. get () {
  307. return get(allEditedRules.value, getPath(component, directive)) != null
  308. },
  309. set (value) {
  310. if (value) {
  311. const fallback = get(
  312. editorFriendlyFallbackStructure.value,
  313. getPath(component, directive)
  314. )
  315. set(allEditedRules.value, getPath(component, directive), fallback ?? defaultValue)
  316. } else {
  317. unset(allEditedRules.value, getPath(component, directive))
  318. }
  319. exports.updateOverallPreview()
  320. }
  321. })
  322. const getEditedElement = (component, directive, postProcess = x => x) => computed({
  323. get () {
  324. let usedRule
  325. const fallback = editorFriendlyFallbackStructure.value
  326. const real = allEditedRules.value
  327. const path = getPath(component, directive)
  328. usedRule = get(real, path) // get real
  329. if (!usedRule) {
  330. usedRule = get(fallback, path)
  331. }
  332. return postProcess(usedRule)
  333. },
  334. set (value) {
  335. if (value) {
  336. set(allEditedRules.value, getPath(component, directive), value)
  337. } else {
  338. unset(allEditedRules.value, getPath(component, directive))
  339. }
  340. exports.updateOverallPreview()
  341. }
  342. })
  343. // All the editable stuff for the component
  344. exports.editedBackgroundColor = getEditedElement(null, 'background')
  345. exports.isBackgroundColorPresent = isElementPresent(null, 'background', '#FFFFFF')
  346. exports.editedOpacity = getEditedElement(null, 'opacity')
  347. exports.isOpacityPresent = isElementPresent(null, 'opacity', 1)
  348. exports.editedRoundness = getEditedElement(null, 'roundness')
  349. exports.isRoundnessPresent = isElementPresent(null, 'roundness', '0')
  350. exports.editedTextColor = getEditedElement('Text', 'textColor')
  351. exports.isTextColorPresent = isElementPresent('Text', 'textColor', '#000000')
  352. exports.editedTextAuto = getEditedElement('Text', 'textAuto')
  353. exports.isTextAutoPresent = isElementPresent('Text', 'textAuto', '#000000')
  354. exports.editedLinkColor = getEditedElement('Link', 'textColor')
  355. exports.isLinkColorPresent = isElementPresent('Link', 'textColor', '#000080')
  356. exports.editedIconColor = getEditedElement('Icon', 'textColor')
  357. exports.isIconColorPresent = isElementPresent('Icon', 'textColor', '#909090')
  358. exports.editedBorderColor = getEditedElement('Border', 'textColor')
  359. exports.isBorderColorPresent = isElementPresent('Border', 'textColor', '#909090')
  360. const getContrast = (bg, text) => {
  361. try {
  362. const bgRgb = hex2rgb(bg)
  363. const textRgb = hex2rgb(text)
  364. const ratio = getContrastRatio(bgRgb, textRgb)
  365. return {
  366. // TODO this ideally should be part of <ContractRatio />
  367. ratio,
  368. text: ratio.toPrecision(3) + ':1',
  369. // AA level, AAA level
  370. aa: ratio >= 4.5,
  371. aaa: ratio >= 7,
  372. // same but for 18pt+ texts
  373. laa: ratio >= 3,
  374. laaa: ratio >= 4.5
  375. }
  376. } catch (e) {
  377. console.warn('Failure computing contrast', e)
  378. return { error: e }
  379. }
  380. }
  381. const normalizeShadows = (shadows) => {
  382. return shadows?.map(shadow => {
  383. if (typeof shadow === 'object') {
  384. return shadow
  385. }
  386. if (typeof shadow === 'string') {
  387. try {
  388. return deserializeShadow(shadow)
  389. } catch (e) {
  390. console.warn(e)
  391. return shadow
  392. }
  393. }
  394. return null
  395. })
  396. }
  397. provide('normalizeShadows', normalizeShadows)
  398. // Shadow is partially edited outside the ShadowControl
  399. // for better space utilization
  400. const editedShadow = getEditedElement(null, 'shadow', normalizeShadows)
  401. exports.editedShadow = editedShadow
  402. const editedSubShadowId = ref(null)
  403. exports.editedSubShadowId = editedSubShadowId
  404. const editedSubShadow = computed(() => {
  405. if (editedShadow.value == null || editedSubShadowId.value == null) return null
  406. return editedShadow.value[editedSubShadowId.value]
  407. })
  408. exports.editedSubShadow = editedSubShadow
  409. exports.isShadowPresent = isElementPresent(null, 'shadow', [])
  410. exports.onSubShadow = (id) => {
  411. if (id != null) {
  412. editedSubShadowId.value = id
  413. } else {
  414. editedSubShadow.value = null
  415. }
  416. }
  417. exports.updateSubShadow = (axis, value) => {
  418. if (!editedSubShadow.value || editedSubShadowId.value == null) return
  419. const newEditedShadow = [...editedShadow.value]
  420. newEditedShadow[editedSubShadowId.value] = {
  421. ...newEditedShadow[editedSubShadowId.value],
  422. [axis]: value
  423. }
  424. editedShadow.value = newEditedShadow
  425. }
  426. exports.isShadowTabOpen = ref(false)
  427. exports.onTabSwitch = (tab) => {
  428. exports.isShadowTabOpen.value = tab === 'shadow'
  429. }
  430. // component preview
  431. exports.editorHintStyle = computed(() => {
  432. const editorHint = selectedComponent.value.editor
  433. const styles = []
  434. if (editorHint && Object.keys(editorHint).length > 0) {
  435. if (editorHint.aspect != null) {
  436. styles.push(`aspect-ratio: ${editorHint.aspect} !important;`)
  437. }
  438. if (editorHint.border != null) {
  439. styles.push(`border-width: ${editorHint.border}px !important;`)
  440. }
  441. }
  442. return styles.join('; ')
  443. })
  444. const editorFriendlyToOriginal = computed(() => {
  445. const resultRules = []
  446. const convert = (component, data = {}, parent) => {
  447. const variants = Object.entries(data || {})
  448. variants.forEach(([variant, variantData]) => {
  449. const states = Object.entries(variantData)
  450. states.forEach(([jointState, stateData]) => {
  451. const state = jointState.split(/:/g)
  452. const result = {
  453. component,
  454. variant,
  455. state,
  456. directives: stateData.directives || {}
  457. }
  458. if (parent) {
  459. result.parent = {
  460. component: parent
  461. }
  462. }
  463. resultRules.push(result)
  464. // Currently we only support single depth for simplicity's sake
  465. if (!parent) {
  466. Object.entries(stateData._children || {}).forEach(([cName, child]) => convert(cName, child, component))
  467. }
  468. })
  469. })
  470. }
  471. [...componentsMap.values()].forEach(({ name }) => {
  472. convert(name, allEditedRules.value[name])
  473. })
  474. return resultRules
  475. })
  476. const allCustomVirtualDirectives = [...componentsMap.values()]
  477. .map(c => {
  478. return c
  479. .defaultRules
  480. .filter(c => c.component === 'Root')
  481. .map(x => Object.entries(x.directives))
  482. .flat()
  483. })
  484. .filter(x => x)
  485. .flat()
  486. .map(([name, value]) => {
  487. const [valType, valVal] = value.split('|')
  488. return {
  489. name: name.substring(2),
  490. valType: valType?.trim(),
  491. value: valVal?.trim()
  492. }
  493. })
  494. const virtualDirectives = ref(allCustomVirtualDirectives)
  495. exports.virtualDirectives = virtualDirectives
  496. exports.updateVirtualDirectives = (value) => {
  497. virtualDirectives.value = value
  498. }
  499. // Raw format
  500. const virtualDirectivesRule = computed(() => ({
  501. component: 'Root',
  502. directives: Object.fromEntries(
  503. virtualDirectives.value.map(vd => [`--${vd.name}`, `${vd.valType} | ${vd.value}`])
  504. )
  505. }))
  506. // Text format
  507. const virtualDirectivesOut = computed(() => {
  508. return [
  509. 'Root {',
  510. ...virtualDirectives.value
  511. .filter(vd => vd.name && vd.valType && vd.value)
  512. .map(vd => ` --${vd.name}: ${vd.valType} | ${vd.value};`),
  513. '}'
  514. ].join('\n')
  515. })
  516. exports.computeColor = (color) => {
  517. let computedColor
  518. try {
  519. computedColor = findColor(color, { dynamicVars: dynamicVars.value, staticVars: staticVars.value })
  520. if (computedColor) {
  521. return rgb2hex(computedColor)
  522. }
  523. } catch (e) {
  524. console.warn(e)
  525. }
  526. return null
  527. }
  528. provide('computeColor', exports.computeColor)
  529. exports.contrast = computed(() => {
  530. return getContrast(
  531. exports.computeColor(previewColors.value.background),
  532. exports.computeColor(previewColors.value.text)
  533. )
  534. })
  535. // ## Export and Import
  536. const styleExporter = newExporter({
  537. filename: () => exports.name.value ?? 'pleroma_theme',
  538. mime: 'text/plain',
  539. extension: 'iss',
  540. getExportedObject: () => exportStyleData.value
  541. })
  542. const onImport = parsed => {
  543. const editorComponents = parsed.filter(x => x.component.startsWith('@'))
  544. const rootComponent = parsed.find(x => x.component === 'Root')
  545. const rules = parsed.filter(x => !x.component.startsWith('@') && x.component !== 'Root')
  546. const metaIn = editorComponents.find(x => x.component === '@meta').directives
  547. const palettesIn = editorComponents.filter(x => x.component === '@palette')
  548. exports.name.value = metaIn.name
  549. exports.license.value = metaIn.license
  550. exports.author.value = metaIn.author
  551. exports.website.value = metaIn.website
  552. const newVirtualDirectives = Object
  553. .entries(rootComponent.directives)
  554. .map(([name, value]) => {
  555. const [valType, valVal] = value.split('|').map(x => x.trim())
  556. return { name: name.substring(2), valType, value: valVal }
  557. })
  558. virtualDirectives.value = newVirtualDirectives
  559. onPalettesUpdate(palettesIn.map(x => ({ name: x.variant, ...x.directives })))
  560. allEditedRules.value = rulesToEditorFriendly(rules)
  561. exports.updateOverallPreview()
  562. }
  563. const styleImporter = newImporter({
  564. accept: '.iss',
  565. parser (string) { return deserialize(string) },
  566. onImportFailure (result) {
  567. console.error('Failure importing style:', result)
  568. this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
  569. },
  570. onImport
  571. })
  572. // Raw format
  573. const exportRules = computed(() => [
  574. metaRule.value,
  575. ...palettesRule.value,
  576. virtualDirectivesRule.value,
  577. ...editorFriendlyToOriginal.value
  578. ])
  579. // Text format
  580. const exportStyleData = computed(() => {
  581. return [
  582. metaOut.value,
  583. palettesOut.value,
  584. virtualDirectivesOut.value,
  585. serialize(editorFriendlyToOriginal.value)
  586. ].join('\n\n')
  587. })
  588. exports.clearStyle = () => {
  589. onImport(store.state.interface.styleDataUsed)
  590. }
  591. exports.exportStyle = () => {
  592. styleExporter.exportData()
  593. }
  594. exports.importStyle = () => {
  595. styleImporter.importData()
  596. }
  597. exports.applyStyle = () => {
  598. store.dispatch('setStyleCustom', exportRules.value)
  599. }
  600. const overallPreviewRules = ref([])
  601. exports.overallPreviewRules = overallPreviewRules
  602. const overallPreviewCssRules = ref([])
  603. watchEffect(throttle(() => {
  604. try {
  605. overallPreviewCssRules.value = getScopedVersion(
  606. getCssRules(overallPreviewRules.value),
  607. '#edited-style-preview'
  608. ).join('\n')
  609. } catch (e) {
  610. console.error(e)
  611. }
  612. }, 500))
  613. exports.overallPreviewCssRules = overallPreviewCssRules
  614. const updateOverallPreview = throttle(() => {
  615. try {
  616. overallPreviewRules.value = init({
  617. inputRuleset: [
  618. ...exportRules.value,
  619. {
  620. component: 'Root',
  621. directives: Object.fromEntries(
  622. Object
  623. .entries(selectedPalette.value)
  624. .filter(([k, v]) => k && v && k !== 'name')
  625. .map(([k, v]) => [`--${k}`, `color | ${v}`])
  626. )
  627. }
  628. ],
  629. ultimateBackgroundColor: '#000000',
  630. debug: true
  631. }).eager
  632. } catch (e) {
  633. console.error('Could not compile preview theme', e)
  634. return null
  635. }
  636. }, 5000)
  637. //
  638. // Apart from "hover" we can't really show how component looks like in
  639. // certain states, so we have to fake them.
  640. const simulatePseudoSelectors = (css, prefix) => css
  641. .replace(prefix, '.component-preview .preview-block')
  642. .replace(':active', '.preview-active')
  643. .replace(':hover', '.preview-hover')
  644. .replace(':active', '.preview-active')
  645. .replace(':focus', '.preview-focus')
  646. .replace(':focus-within', '.preview-focus-within')
  647. .replace(':disabled', '.preview-disabled')
  648. const previewRules = computed(() => {
  649. const filtered = overallPreviewRules.value.filter(r => {
  650. const componentMatch = r.component === selectedComponentName.value
  651. const parentComponentMatch = r.parent?.component === selectedComponentName.value
  652. if (!componentMatch && !parentComponentMatch) return false
  653. const rule = parentComponentMatch ? r.parent : r
  654. if (rule.component !== selectedComponentName.value) return false
  655. if (rule.variant !== selectedVariant.value) return false
  656. const ruleState = new Set(rule.state.filter(x => x !== 'normal'))
  657. const differenceA = [...ruleState].filter(x => !selectedState.has(x))
  658. const differenceB = [...selectedState].filter(x => !ruleState.has(x))
  659. return (differenceA.length + differenceB.length) === 0
  660. })
  661. const sorted = [...filtered]
  662. .filter(x => x.component === selectedComponentName.value)
  663. .sort((a, b) => {
  664. const aSelectorLength = a.selector.split(/ /g).length
  665. const bSelectorLength = b.selector.split(/ /g).length
  666. return aSelectorLength - bSelectorLength
  667. })
  668. const prefix = sorted[0].selector
  669. return filtered.filter(x => x.selector.startsWith(prefix))
  670. })
  671. exports.previewClass = computed(() => {
  672. const selectors = []
  673. if (!!selectedComponent.value.variants?.normal || selectedVariant.value !== 'normal') {
  674. selectors.push(selectedComponent.value.variants[selectedVariant.value])
  675. }
  676. if (selectedState.size > 0) {
  677. selectedState.forEach(state => {
  678. const original = selectedComponent.value.states[state]
  679. selectors.push(simulatePseudoSelectors(original))
  680. })
  681. }
  682. return selectors.map(x => x.substring(1)).join('')
  683. })
  684. exports.previewCss = computed(() => {
  685. try {
  686. const prefix = previewRules.value[0].selector
  687. const scoped = getCssRules(previewRules.value).map(x => simulatePseudoSelectors(x, prefix))
  688. return scoped.join('\n')
  689. } catch (e) {
  690. console.error('Invalid ruleset', e)
  691. return null
  692. }
  693. })
  694. const dynamicVars = computed(() => {
  695. return previewRules.value[0].dynamicVars
  696. })
  697. const staticVars = computed(() => {
  698. const rootComponent = overallPreviewRules.value.find(r => {
  699. return r.component === 'Root'
  700. })
  701. const rootDirectivesEntries = Object.entries(rootComponent.directives)
  702. const directives = {}
  703. rootDirectivesEntries
  704. .filter(([k, v]) => k.startsWith('--') && v.startsWith('color | '))
  705. .map(([k, v]) => [k.substring(2), v.substring('color | '.length)])
  706. .forEach(([k, v]) => {
  707. directives[k] = findColor(v, { dynamicVars: {}, staticVars: directives })
  708. })
  709. return directives
  710. })
  711. provide('staticVars', staticVars)
  712. exports.staticVars = staticVars
  713. const previewColors = computed(() => {
  714. const stacked = dynamicVars.value.stacked
  715. const background = typeof stacked === 'string' ? stacked : rgb2hex(stacked)
  716. return {
  717. text: previewRules.value.find(r => r.component === 'Text')?.virtualDirectives['--text'],
  718. link: previewRules.value.find(r => r.component === 'Link')?.virtualDirectives['--link'],
  719. border: previewRules.value.find(r => r.component === 'Border')?.virtualDirectives['--border'],
  720. icon: previewRules.value.find(r => r.component === 'Icon')?.virtualDirectives['--icon'],
  721. background
  722. }
  723. })
  724. exports.previewColors = previewColors
  725. exports.updateOverallPreview = updateOverallPreview
  726. updateOverallPreview()
  727. watch(
  728. [
  729. allEditedRules.value,
  730. palettes,
  731. selectedPalette,
  732. selectedState,
  733. selectedVariant
  734. ],
  735. updateOverallPreview
  736. )
  737. return exports
  738. }
  739. }