logo

pleroma-fe

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

theme_data.service.js (21637B)


  1. import { convert, brightness, contrastRatio } from 'chromatism'
  2. import { rgb2hex, rgba2css, alphaBlendLayers, getTextColor, relativeLuminance, getCssColor } from '../color_convert/color_convert.js'
  3. import { LAYERS, DEFAULT_OPACITY, SLOT_INHERITANCE } from './pleromafe.js'
  4. /*
  5. * # What's all this?
  6. * Here be theme engine for pleromafe. All of this supposed to ease look
  7. * and feel customization, making widget styles and make developer's life
  8. * easier when it comes to supporting themes. Like many other theme systems
  9. * it operates on color definitions, or "slots" - for example you define
  10. * "button" color slot and then in UI component Button's CSS you refer to
  11. * it as a CSS3 Variable.
  12. *
  13. * Some applications allow you to customize colors for certain things.
  14. * Some UI toolkits allow you to define colors for each type of widget.
  15. * Most of them are pretty barebones and have no assistance for common
  16. * problems and cases, and in general themes themselves are very hard to
  17. * maintain in all aspects. This theme engine tries to solve all of the
  18. * common problems with themes.
  19. *
  20. * You don't have redefine several similar colors if you just want to
  21. * change one color - all color slots are derived from other ones, so you
  22. * can have at least one or two "basic" colors defined and have all other
  23. * components inherit and modify basic ones.
  24. *
  25. * You don't have to test contrast ratio for colors or pick text color for
  26. * each element even if you have light-on-dark elements in dark-on-light
  27. * theme.
  28. *
  29. * You don't have to maintain order of code for inheriting slots from othet
  30. * slots - dependency graph resolving does it for you.
  31. */
  32. /* This indicates that this version of code outputs similar theme data and
  33. * should be incremented if output changes - for instance if getTextColor
  34. * function changes and older themes no longer render text colors as
  35. * author intended previously.
  36. */
  37. export const CURRENT_VERSION = 3
  38. export const getLayersArray = (layer, data = LAYERS) => {
  39. const array = [layer]
  40. let parent = data[layer]
  41. while (parent) {
  42. array.unshift(parent)
  43. parent = data[parent]
  44. }
  45. return array
  46. }
  47. export const getLayers = (layer, variant = layer, opacitySlot, colors, opacity) => {
  48. return getLayersArray(layer).map((currentLayer) => ([
  49. currentLayer === layer
  50. ? colors[variant]
  51. : colors[currentLayer],
  52. currentLayer === layer
  53. ? opacity[opacitySlot] || 1
  54. : opacity[currentLayer]
  55. ]))
  56. }
  57. const getDependencies = (key, inheritance) => {
  58. const data = inheritance[key]
  59. if (typeof data === 'string' && data.startsWith('--')) {
  60. return [data.substring(2)]
  61. } else {
  62. if (data === null) return []
  63. const { depends, layer, variant } = data
  64. const layerDeps = layer
  65. ? getLayersArray(layer).map(currentLayer => {
  66. return currentLayer === layer
  67. ? variant || layer
  68. : currentLayer
  69. })
  70. : []
  71. if (Array.isArray(depends)) {
  72. return [...depends, ...layerDeps]
  73. } else {
  74. return [...layerDeps]
  75. }
  76. }
  77. }
  78. /**
  79. * Sorts inheritance object topologically - dependant slots come after
  80. * dependencies
  81. *
  82. * @property {Object} inheritance - object defining the nodes
  83. * @property {Function} getDeps - function that returns dependencies for
  84. * given value and inheritance object.
  85. * @returns {String[]} keys of inheritance object, sorted in topological
  86. * order. Additionally, dependency-less nodes will always be first in line
  87. */
  88. export const topoSort = (
  89. inheritance = SLOT_INHERITANCE,
  90. getDeps = getDependencies
  91. ) => {
  92. // This is an implementation of https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
  93. const allKeys = Object.keys(inheritance)
  94. const whites = new Set(allKeys)
  95. const grays = new Set()
  96. const blacks = new Set()
  97. const unprocessed = [...allKeys]
  98. const output = []
  99. const step = (node) => {
  100. if (whites.has(node)) {
  101. // Make node "gray"
  102. whites.delete(node)
  103. grays.add(node)
  104. // Do step for each node connected to it (one way)
  105. getDeps(node, inheritance).forEach(step)
  106. // Make node "black"
  107. grays.delete(node)
  108. blacks.add(node)
  109. // Put it into the output list
  110. output.push(node)
  111. } else if (grays.has(node)) {
  112. output.push(node)
  113. } else if (blacks.has(node)) {
  114. // do nothing
  115. } else {
  116. throw new Error('Unintended condition in topoSort!')
  117. }
  118. }
  119. while (unprocessed.length > 0) {
  120. step(unprocessed.pop())
  121. }
  122. // The index thing is to make sorting stable on browsers
  123. // where Array.sort() isn't stable
  124. return output.map((data, index) => ({ data, index })).sort(({ data: a, index: ai }, { data: b, index: bi }) => {
  125. const depsA = getDeps(a, inheritance).length
  126. const depsB = getDeps(b, inheritance).length
  127. if (depsA === depsB || (depsB !== 0 && depsA !== 0)) return ai - bi
  128. if (depsA === 0 && depsB !== 0) return -1
  129. if (depsB === 0 && depsA !== 0) return 1
  130. return 0 // failsafe, shouldn't happen?
  131. }).map(({ data }) => data)
  132. }
  133. const expandSlotValue = (value) => {
  134. if (typeof value === 'object') return value
  135. return {
  136. depends: value.startsWith('--') ? [value.substring(2)] : [],
  137. default: value.startsWith('#') ? value : undefined
  138. }
  139. }
  140. /**
  141. * retrieves opacity slot for given slot. This goes up the depenency graph
  142. * to find which parent has opacity slot defined for it.
  143. * TODO refactor this
  144. */
  145. export const getOpacitySlot = (
  146. k,
  147. inheritance = SLOT_INHERITANCE,
  148. getDeps = getDependencies
  149. ) => {
  150. const value = expandSlotValue(inheritance[k])
  151. if (value.opacity === null) return
  152. if (value.opacity) return value.opacity
  153. const findInheritedOpacity = (key, visited = [k]) => {
  154. const depSlot = getDeps(key, inheritance)[0]
  155. if (depSlot === undefined) return
  156. const dependency = inheritance[depSlot]
  157. if (dependency === undefined) return
  158. if (dependency.opacity || dependency === null) {
  159. return dependency.opacity
  160. } else if (dependency.depends && visited.includes(depSlot)) {
  161. return findInheritedOpacity(depSlot, [...visited, depSlot])
  162. } else {
  163. return null
  164. }
  165. }
  166. if (value.depends) {
  167. return findInheritedOpacity(k)
  168. }
  169. }
  170. /**
  171. * retrieves layer slot for given slot. This goes up the depenency graph
  172. * to find which parent has opacity slot defined for it.
  173. * this is basically copypaste of getOpacitySlot except it checks if key is
  174. * in LAYERS
  175. * TODO refactor this
  176. */
  177. export const getLayerSlot = (
  178. k,
  179. inheritance = SLOT_INHERITANCE,
  180. getDeps = getDependencies
  181. ) => {
  182. const value = expandSlotValue(inheritance[k])
  183. if (LAYERS[k]) return k
  184. if (value.layer === null) return
  185. if (value.layer) return value.layer
  186. const findInheritedLayer = (key, visited = [k]) => {
  187. const depSlot = getDeps(key, inheritance)[0]
  188. if (depSlot === undefined) return
  189. const dependency = inheritance[depSlot]
  190. if (dependency === undefined) return
  191. if (dependency.layer || dependency === null) {
  192. return dependency.layer
  193. } else if (dependency.depends) {
  194. return findInheritedLayer(dependency, [...visited, depSlot])
  195. } else {
  196. return null
  197. }
  198. }
  199. if (value.depends) {
  200. return findInheritedLayer(k)
  201. }
  202. }
  203. /**
  204. * topologically sorted SLOT_INHERITANCE
  205. */
  206. export const SLOT_ORDERED = topoSort(
  207. Object.entries(SLOT_INHERITANCE)
  208. .sort(([aK, aV], [bK, bV]) => ((aV && aV.priority) || 0) - ((bV && bV.priority) || 0))
  209. .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {})
  210. )
  211. /**
  212. * All opacity slots used in color slots, their default values and affected
  213. * color slots.
  214. */
  215. export const OPACITIES = Object.entries(SLOT_INHERITANCE).reduce((acc, [k, v]) => {
  216. const opacity = getOpacitySlot(k, SLOT_INHERITANCE, getDependencies)
  217. if (opacity) {
  218. return {
  219. ...acc,
  220. [opacity]: {
  221. defaultValue: DEFAULT_OPACITY[opacity] || 1,
  222. affectedSlots: [...((acc[opacity] && acc[opacity].affectedSlots) || []), k]
  223. }
  224. }
  225. } else {
  226. return acc
  227. }
  228. }, {})
  229. /**
  230. * Handle dynamic color
  231. */
  232. export const computeDynamicColor = (sourceColor, getColor, mod) => {
  233. if (typeof sourceColor !== 'string' || !sourceColor.startsWith('--')) return sourceColor
  234. let targetColor = null
  235. // Color references other color
  236. const [variable, modifier] = sourceColor.split(/,/g).map(str => str.trim())
  237. const variableSlot = variable.substring(2)
  238. targetColor = getColor(variableSlot)
  239. if (modifier) {
  240. targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
  241. }
  242. return targetColor
  243. }
  244. /**
  245. * THE function you want to use. Takes provided colors and opacities
  246. * value and uses inheritance data to figure out color needed for the slot.
  247. */
  248. export const getColors = (sourceColors, sourceOpacity) => SLOT_ORDERED.reduce(({ colors, opacity }, key) => {
  249. const sourceColor = sourceColors[key]
  250. const value = expandSlotValue(SLOT_INHERITANCE[key])
  251. const deps = getDependencies(key, SLOT_INHERITANCE)
  252. const isTextColor = !!value.textColor
  253. const variant = value.variant || value.layer
  254. let backgroundColor = null
  255. if (isTextColor) {
  256. backgroundColor = alphaBlendLayers(
  257. { ...(colors[deps[0]] || convert(sourceColors[key] || '#FF00FF').rgb) },
  258. getLayers(
  259. getLayerSlot(key) || 'bg',
  260. variant || 'bg',
  261. getOpacitySlot(variant),
  262. colors,
  263. opacity
  264. )
  265. )
  266. } else if (variant && variant !== key) {
  267. backgroundColor = colors[variant] || convert(sourceColors[variant]).rgb
  268. } else {
  269. backgroundColor = colors.bg || convert(sourceColors.bg)
  270. }
  271. const isLightOnDark = relativeLuminance(backgroundColor) < 0.5
  272. const mod = isLightOnDark ? 1 : -1
  273. let outputColor = null
  274. if (sourceColor) {
  275. // Color is defined in source color
  276. let targetColor = sourceColor
  277. if (targetColor === 'transparent') {
  278. // We take only layers below current one
  279. const layers = getLayers(
  280. getLayerSlot(key),
  281. key,
  282. getOpacitySlot(key) || key,
  283. colors,
  284. opacity
  285. ).slice(0, -1)
  286. targetColor = {
  287. ...alphaBlendLayers(
  288. convert('#FF00FF').rgb,
  289. layers
  290. ),
  291. a: 0
  292. }
  293. } else if (typeof sourceColor === 'string' && sourceColor.startsWith('--')) {
  294. targetColor = computeDynamicColor(
  295. sourceColor,
  296. variableSlot => colors[variableSlot] || sourceColors[variableSlot],
  297. mod
  298. )
  299. } else if (typeof sourceColor === 'string' && sourceColor.startsWith('#')) {
  300. targetColor = convert(targetColor).rgb
  301. }
  302. outputColor = { ...targetColor }
  303. } else if (value.default) {
  304. // same as above except in object form
  305. outputColor = convert(value.default).rgb
  306. } else {
  307. // calculate color
  308. const defaultColorFunc = (mod, dep) => ({ ...dep })
  309. const colorFunc = value.color || defaultColorFunc
  310. if (value.textColor) {
  311. if (value.textColor === 'bw') {
  312. outputColor = contrastRatio(backgroundColor).rgb
  313. } else {
  314. let color = { ...colors[deps[0]] }
  315. if (value.color) {
  316. color = colorFunc(mod, ...deps.map((dep) => ({ ...colors[dep] })))
  317. }
  318. outputColor = getTextColor(
  319. backgroundColor,
  320. { ...color },
  321. value.textColor === 'preserve'
  322. )
  323. }
  324. } else {
  325. // background color case
  326. outputColor = colorFunc(
  327. mod,
  328. ...deps.map((dep) => ({ ...colors[dep] }))
  329. )
  330. }
  331. }
  332. if (!outputColor) {
  333. throw new Error('Couldn\'t generate color for ' + key)
  334. }
  335. const opacitySlot = value.opacity || getOpacitySlot(key)
  336. const ownOpacitySlot = value.opacity
  337. if (ownOpacitySlot === null) {
  338. outputColor.a = 1
  339. } else if (sourceColor === 'transparent') {
  340. outputColor.a = 0
  341. } else {
  342. const opacityOverriden = ownOpacitySlot && sourceOpacity[opacitySlot] !== undefined
  343. const dependencySlot = deps[0]
  344. const dependencyColor = dependencySlot && colors[dependencySlot]
  345. if (!ownOpacitySlot && dependencyColor && !value.textColor && ownOpacitySlot !== null) {
  346. // Inheriting color from dependency (weird, i know)
  347. // except if it's a text color or opacity slot is set to 'null'
  348. outputColor.a = dependencyColor.a
  349. } else if (!dependencyColor && !opacitySlot) {
  350. // Remove any alpha channel if no dependency and no opacitySlot found
  351. delete outputColor.a
  352. } else {
  353. // Otherwise try to assign opacity
  354. if (dependencyColor && dependencyColor.a === 0) {
  355. // transparent dependency shall make dependents transparent too
  356. outputColor.a = 0
  357. } else {
  358. // Otherwise check if opacity is overriden and use that or default value instead
  359. outputColor.a = Number(
  360. opacityOverriden
  361. ? sourceOpacity[opacitySlot]
  362. : (OPACITIES[opacitySlot] || {}).defaultValue
  363. )
  364. }
  365. }
  366. }
  367. if (Number.isNaN(outputColor.a) || outputColor.a === undefined) {
  368. outputColor.a = 1
  369. }
  370. if (opacitySlot) {
  371. return {
  372. colors: { ...colors, [key]: outputColor },
  373. opacity: { ...opacity, [opacitySlot]: outputColor.a }
  374. }
  375. } else {
  376. return {
  377. colors: { ...colors, [key]: outputColor },
  378. opacity
  379. }
  380. }
  381. }, { colors: {}, opacity: {} })
  382. export const composePreset = (colors, radii, shadows, fonts) => {
  383. return {
  384. rules: {
  385. ...shadows.rules,
  386. ...colors.rules,
  387. ...radii.rules,
  388. ...fonts.rules
  389. },
  390. theme: {
  391. ...shadows.theme,
  392. ...colors.theme,
  393. ...radii.theme,
  394. ...fonts.theme
  395. }
  396. }
  397. }
  398. export const generatePreset = (input) => {
  399. const colors = generateColors(input)
  400. return composePreset(
  401. colors,
  402. generateRadii(input),
  403. generateShadows(input, colors.theme.colors, colors.mod),
  404. generateFonts(input)
  405. )
  406. }
  407. export const getCssShadow = (input, usesDropShadow) => {
  408. if (input.length === 0) {
  409. return 'none'
  410. }
  411. return input
  412. .filter(_ => usesDropShadow ? _.inset : _)
  413. .map((shad) => [
  414. shad.x,
  415. shad.y,
  416. shad.blur,
  417. shad.spread
  418. ].map(_ => _ + 'px').concat([
  419. getCssColor(shad.color, shad.alpha),
  420. shad.inset ? 'inset' : ''
  421. ]).join(' ')).join(', ')
  422. }
  423. const getCssShadowFilter = (input) => {
  424. if (input.length === 0) {
  425. return 'none'
  426. }
  427. return input
  428. // drop-shadow doesn't support inset or spread
  429. .filter((shad) => !shad.inset && Number(shad.spread) === 0)
  430. .map((shad) => [
  431. shad.x,
  432. shad.y,
  433. // drop-shadow's blur is twice as strong compared to box-shadow
  434. shad.blur / 2
  435. ].map(_ => _ + 'px').concat([
  436. getCssColor(shad.color, shad.alpha)
  437. ]).join(' '))
  438. .map(_ => `drop-shadow(${_})`)
  439. .join(' ')
  440. }
  441. export const generateColors = (themeData) => {
  442. const sourceColors = !themeData.themeEngineVersion
  443. ? colors2to3(themeData.colors || themeData)
  444. : themeData.colors || themeData
  445. const { colors, opacity } = getColors(sourceColors, themeData.opacity || {})
  446. const htmlColors = Object.entries(colors)
  447. .reduce((acc, [k, v]) => {
  448. if (!v) return acc
  449. acc.solid[k] = rgb2hex(v)
  450. acc.complete[k] = typeof v.a === 'undefined' ? rgb2hex(v) : rgba2css(v)
  451. return acc
  452. }, { complete: {}, solid: {} })
  453. return {
  454. rules: {
  455. colors: Object.entries(htmlColors.complete)
  456. .filter(([k, v]) => v)
  457. .map(([k, v]) => `--${k}: ${v}`)
  458. .join(';')
  459. },
  460. theme: {
  461. colors: htmlColors.solid,
  462. opacity
  463. }
  464. }
  465. }
  466. export const generateRadii = (input) => {
  467. let inputRadii = input.radii || {}
  468. // v1 -> v2
  469. if (typeof input.btnRadius !== 'undefined') {
  470. inputRadii = Object
  471. .entries(input)
  472. .filter(([k, v]) => k.endsWith('Radius'))
  473. .reduce((acc, e) => { acc[e[0].split('Radius')[0]] = e[1]; return acc }, {})
  474. }
  475. const radii = Object.entries(inputRadii).filter(([k, v]) => v).reduce((acc, [k, v]) => {
  476. acc[k] = v
  477. return acc
  478. }, {
  479. btn: 4,
  480. input: 4,
  481. checkbox: 2,
  482. panel: 10,
  483. avatar: 5,
  484. avatarAlt: 50,
  485. tooltip: 2,
  486. attachment: 5,
  487. chatMessage: inputRadii.panel
  488. })
  489. return {
  490. rules: {
  491. radii: Object.entries(radii).filter(([k, v]) => v).map(([k, v]) => `--${k}Radius: ${v}px`).join(';')
  492. },
  493. theme: {
  494. radii
  495. }
  496. }
  497. }
  498. export const generateFonts = (input) => {
  499. const fonts = Object.entries(input.fonts || {}).filter(([k, v]) => v).reduce((acc, [k, v]) => {
  500. acc[k] = Object.entries(v).filter(([k, v]) => v).reduce((acc, [k, v]) => {
  501. acc[k] = v
  502. return acc
  503. }, acc[k])
  504. return acc
  505. }, {
  506. interface: {
  507. family: 'sans-serif'
  508. },
  509. input: {
  510. family: 'inherit'
  511. },
  512. post: {
  513. family: 'inherit'
  514. },
  515. postCode: {
  516. family: 'monospace'
  517. }
  518. })
  519. return {
  520. rules: {
  521. fonts: Object
  522. .entries(fonts)
  523. .filter(([k, v]) => v)
  524. .map(([k, v]) => `--${k}Font: ${v.family}`).join(';')
  525. },
  526. theme: {
  527. fonts
  528. }
  529. }
  530. }
  531. const border = (top, shadow) => ({
  532. x: 0,
  533. y: top ? 1 : -1,
  534. blur: 0,
  535. spread: 0,
  536. color: shadow ? '#000000' : '#FFFFFF',
  537. alpha: 0.2,
  538. inset: true
  539. })
  540. const buttonInsetFakeBorders = [border(true, false), border(false, true)]
  541. const inputInsetFakeBorders = [border(true, true), border(false, false)]
  542. const hoverGlow = {
  543. x: 0,
  544. y: 0,
  545. blur: 4,
  546. spread: 0,
  547. color: '--faint',
  548. alpha: 1
  549. }
  550. export const DEFAULT_SHADOWS = {
  551. panel: [{
  552. x: 1,
  553. y: 1,
  554. blur: 4,
  555. spread: 0,
  556. color: '#000000',
  557. alpha: 0.6
  558. }],
  559. topBar: [{
  560. x: 0,
  561. y: 0,
  562. blur: 4,
  563. spread: 0,
  564. color: '#000000',
  565. alpha: 0.6
  566. }],
  567. popup: [{
  568. x: 2,
  569. y: 2,
  570. blur: 3,
  571. spread: 0,
  572. color: '#000000',
  573. alpha: 0.5
  574. }],
  575. avatar: [{
  576. x: 0,
  577. y: 1,
  578. blur: 8,
  579. spread: 0,
  580. color: '#000000',
  581. alpha: 0.7
  582. }],
  583. avatarStatus: [],
  584. panelHeader: [],
  585. button: [{
  586. x: 0,
  587. y: 0,
  588. blur: 2,
  589. spread: 0,
  590. color: '#000000',
  591. alpha: 1
  592. }, ...buttonInsetFakeBorders],
  593. buttonHover: [hoverGlow, ...buttonInsetFakeBorders],
  594. buttonPressed: [hoverGlow, ...inputInsetFakeBorders],
  595. input: [...inputInsetFakeBorders, {
  596. x: 0,
  597. y: 0,
  598. blur: 2,
  599. inset: true,
  600. spread: 0,
  601. color: '#000000',
  602. alpha: 1
  603. }]
  604. }
  605. export const generateShadows = (input, colors) => {
  606. // TODO this is a small hack for `mod` to work with shadows
  607. // this is used to get the "context" of shadow, i.e. for `mod` properly depend on background color of element
  608. const hackContextDict = {
  609. button: 'btn',
  610. panel: 'bg',
  611. top: 'topBar',
  612. popup: 'popover',
  613. avatar: 'bg',
  614. panelHeader: 'panel',
  615. input: 'input'
  616. }
  617. const cleanInputShadows = Object.fromEntries(
  618. Object.entries(input.shadows || {})
  619. .map(([name, shadowSlot]) => [
  620. name,
  621. // defaulting color to black to avoid potential problems
  622. shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef }))
  623. ])
  624. )
  625. const inputShadows = cleanInputShadows && !input.themeEngineVersion
  626. ? shadows2to3(cleanInputShadows, input.opacity)
  627. : cleanInputShadows || {}
  628. const shadows = Object.entries({
  629. ...DEFAULT_SHADOWS,
  630. ...inputShadows
  631. }).reduce((shadowsAcc, [slotName, shadowDefs]) => {
  632. const slotFirstWord = slotName.replace(/[A-Z].*$/, '')
  633. const colorSlotName = hackContextDict[slotFirstWord]
  634. const isLightOnDark = relativeLuminance(convert(colors[colorSlotName]).rgb) < 0.5
  635. const mod = isLightOnDark ? 1 : -1
  636. const newShadow = shadowDefs.reduce((shadowAcc, def) => [
  637. ...shadowAcc,
  638. {
  639. ...def,
  640. color: rgb2hex(computeDynamicColor(
  641. def.color,
  642. (variableSlot) => convert(colors[variableSlot]).rgb,
  643. mod
  644. ))
  645. }
  646. ], [])
  647. return { ...shadowsAcc, [slotName]: newShadow }
  648. }, {})
  649. return {
  650. rules: {
  651. shadows: Object
  652. .entries(shadows)
  653. // TODO for v2.2: if shadow doesn't have non-inset shadows with spread > 0 - optionally
  654. // convert all non-inset shadows into filter: drop-shadow() to boost performance
  655. .map(([k, v]) => [
  656. `--${k}Shadow: ${getCssShadow(v)}`,
  657. `--${k}ShadowFilter: ${getCssShadowFilter(v)}`,
  658. `--${k}ShadowInset: ${getCssShadow(v, true)}`
  659. ].join(';'))
  660. .join(';')
  661. },
  662. theme: {
  663. shadows
  664. }
  665. }
  666. }
  667. /**
  668. * This handles compatibility issues when importing v2 theme's shadows to current format
  669. *
  670. * Back in v2 shadows allowed you to use dynamic colors however those used pure CSS3 variables
  671. */
  672. export const shadows2to3 = (shadows, opacity) => {
  673. return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => {
  674. const isDynamic = ({ color = '#000000' }) => color.startsWith('--')
  675. const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])]
  676. const newShadow = shadowDefs.reduce((shadowAcc, def) => [
  677. ...shadowAcc,
  678. {
  679. ...def,
  680. alpha: isDynamic(def) ? getOpacity(def) || 1 : def.alpha
  681. }
  682. ], [])
  683. return { ...shadowsAcc, [slotName]: newShadow }
  684. }, {})
  685. }
  686. export const colors2to3 = (colors) => {
  687. return Object.entries(colors).reduce((acc, [slotName, color]) => {
  688. const btnPositions = ['', 'Panel', 'TopBar']
  689. switch (slotName) {
  690. case 'lightBg':
  691. return { ...acc, highlight: color }
  692. case 'btnText':
  693. return {
  694. ...acc,
  695. ...btnPositions
  696. .reduce(
  697. (statePositionAcc, position) =>
  698. ({ ...statePositionAcc, ['btn' + position + 'Text']: color })
  699. , {}
  700. )
  701. }
  702. default:
  703. return { ...acc, [slotName]: color }
  704. }
  705. }, {})
  706. }