update, should inherit stuff properly now.

merge-requests/1931/head
Henry Jameson 8 months ago
parent d4795d2e3c
commit c34590c439

@ -371,8 +371,7 @@ nav {
border-radius: $fallback--btnRadius; border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius); border-radius: var(--btnRadius, $fallback--btnRadius);
cursor: pointer; cursor: pointer;
box-shadow: $fallback--buttonShadow; box-shadow: var(--shadow);
box-shadow: var(--buttonShadow);
font-size: 1em; font-size: 1em;
font-family: sans-serif; font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif); font-family: var(--interfaceFont, sans-serif);
@ -383,25 +382,14 @@ nav {
i[class*="icon-"], i[class*="icon-"],
.svg-inline--fa { .svg-inline--fa {
color: $fallback--text; color: var(--icon);
color: var(--btnText, $fallback--text);
} }
&::-moz-focus-inner { &::-moz-focus-inner {
border: none; border: none;
} }
&:hover {
box-shadow: 0 0 4px rgb(255 255 255 / 30%);
box-shadow: var(--buttonHoverShadow);
}
&:active { &:active {
box-shadow:
0 0 4px 0 rgb(255 255 255 / 30%),
0 1px 0 0 rgb(0 0 0 / 20%) inset,
0 -1px 0 0 rgb(255 255 255 / 20%) inset;
box-shadow: var(--buttonPressedShadow);
color: $fallback--text; color: $fallback--text;
color: var(--btnPressedText, $fallback--text); color: var(--btnPressedText, $fallback--text);
background-color: $fallback--fg; background-color: $fallback--fg;
@ -487,7 +475,12 @@ nav {
} }
input, input,
textarea, textarea {
border: none;
display: inline-block;
outline: none;
}
.input { .input {
&.unstyled { &.unstyled {
border-radius: 0; border-radius: 0;
@ -501,15 +494,7 @@ textarea,
border: none; border: none;
border-radius: $fallback--inputRadius; border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius); border-radius: var(--inputRadius, $fallback--inputRadius);
box-shadow: box-shadow: var(--shadow);
0 1px 0 0 rgb(0 0 0 / 20%) inset,
0 -1px 0 0 rgb(255 255 255 / 20%) inset,
0 0 2px 0 rgb(0 0 0 / 100%) inset;
box-shadow: var(--inputShadow);
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
color: $fallback--lightText;
color: var(--inputText, $fallback--lightText);
font-family: sans-serif; font-family: sans-serif;
font-family: var(--inputFont, sans-serif); font-family: var(--inputFont, sans-serif);
font-size: 1em; font-size: 1em;
@ -561,11 +546,9 @@ textarea,
width: 1.1em; width: 1.1em;
height: 1.1em; height: 1.1em;
border-radius: 100%; // Radio buttons should always be circle border-radius: 100%; // Radio buttons should always be circle
box-shadow: 0 0 2px black inset; background-color: var(--background);
box-shadow: var(--inputShadow); box-shadow: var(--shadow);
margin-right: 0.5em; margin-right: 0.5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
vertical-align: top; vertical-align: top;
text-align: center; text-align: center;
line-height: 1.1; line-height: 1.1;
@ -578,8 +561,9 @@ textarea,
&[type="checkbox"] { &[type="checkbox"] {
&:checked + label::before { &:checked + label::before {
color: $fallback--text; color: var(--text);
color: var(--inputText, $fallback--text); background-color: var(--background);
box-shadow: var(--shadow);
} }
&:disabled { &:disabled {

@ -1,11 +1,34 @@
const border = (top, shadow) => ({
x: 0,
y: top ? 1 : -1,
blur: 0,
spread: 0,
color: shadow ? '#000000' : '#FFFFFF',
alpha: 0.2,
inset: true
})
const buttonInsetFakeBorders = [border(true, false), border(false, true)]
const inputInsetFakeBorders = [border(true, true), border(false, false)]
const hoverGlow = {
x: 0,
y: 0,
blur: 4,
spread: 0,
color: '--text',
alpha: 1
}
export default { export default {
name: 'Button', name: 'Button',
selector: '.btn', selector: '.button-default',
states: { states: {
disabled: ':disabled', disabled: ':disabled',
toggled: '.toggled', toggled: '.toggled',
pressed: ':active', pressed: ':active',
hover: ':hover' hover: ':hover',
focused: ':focus-within'
}, },
variants: { variants: {
danger: '.danger', danger: '.danger',
@ -20,14 +43,49 @@ export default {
{ {
component: 'Button', component: 'Button',
directives: { directives: {
background: '--fg' background: '--fg',
shadow: [{
x: 0,
y: 0,
blur: 2,
spread: 0,
color: '#000000',
alpha: 1
}, ...buttonInsetFakeBorders]
} }
}, },
{ {
component: 'Button', component: 'Button',
state: ['hover'], state: ['hover'],
directives: { directives: {
background: '#FFFFFF' shadow: [hoverGlow, ...buttonInsetFakeBorders]
}
},
{
component: 'Button',
state: ['hover', 'pressed'],
directives: {
background: '--accent,-24.2',
shadow: [hoverGlow, ...inputInsetFakeBorders]
}
},
{
component: 'Button',
state: ['disabled'],
directives: {
background: '$blend(--background, 0.25, --parent)',
shadow: [...buttonInsetFakeBorders]
}
},
{
component: 'Text',
parent: {
component: 'Button',
state: ['disabled']
},
directives: {
textOpacity: 0.25,
textOpacityMode: 'blend'
} }
} }
] ]

@ -0,0 +1,19 @@
export default {
name: 'DropdownMenu',
selector: '.dropdown',
validInnerComponents: [
'Text',
'Icon',
'Input'
],
states: {
hover: ':hover'
},
defaultRules: [
{
directives: {
background: '--fg'
}
}
]
}

@ -0,0 +1,60 @@
const border = (top, shadow) => ({
x: 0,
y: top ? 1 : -1,
blur: 0,
spread: 0,
color: shadow ? '#000000' : '#FFFFFF',
alpha: 0.2,
inset: true
})
const inputInsetFakeBorders = [border(true, true), border(false, false)]
const hoverGlow = {
x: 0,
y: 0,
blur: 4,
spread: 0,
color: '--text',
alpha: 1
}
export default {
name: 'Input',
selector: '.input',
states: {
disabled: ':disabled',
pressed: ':active',
hover: ':hover',
focused: ':focus-within'
},
variants: {
danger: '.danger',
unstyled: '.unstyled',
sublime: '.sublime'
},
validInnerComponents: [
'Text'
],
defaultRules: [
{
directives: {
background: '--fg',
shadow: [{
x: 0,
y: 0,
blur: 2,
spread: 0,
color: '#000000',
alpha: 1
}, ...inputInsetFakeBorders]
}
},
{
state: ['hover'],
directives: {
shadow: [hoverGlow, ...inputInsetFakeBorders]
}
}
]
}

@ -6,13 +6,14 @@ export default {
'Link', 'Link',
'Icon', 'Icon',
'Button', 'Button',
'PanelHeader' 'Input',
'PanelHeader',
'DropdownMenu'
], ],
defaultRules: [ defaultRules: [
{ {
component: 'Panel',
directives: { directives: {
background: '--fg' background: '--bg'
} }
} }
] ]

@ -12,7 +12,6 @@ export default {
component: 'PanelHeader', component: 'PanelHeader',
directives: { directives: {
background: '--fg' background: '--fg'
// opacity: 0.9
} }
} }
] ]

@ -0,0 +1,20 @@
export default {
name: 'Popover',
selector: '.popover',
validInnerComponents: [
'Text',
'Link',
'Icon',
'Button',
'Input',
'PanelHeader',
'DropdownMenu'
],
defaultRules: [
{
directives: {
background: '--fg'
}
}
]
}

@ -0,0 +1,17 @@
export default {
name: 'Root',
selector: ':root',
validInnerComponents: [
'Underlay',
'TopBar',
'Popover'
],
defaultRules: [
{
directives: {
background: '--bg',
opacity: 0
}
}
]
}

@ -0,0 +1,18 @@
export default {
name: 'TopBar',
selector: 'nav',
validInnerComponents: [
'Link',
'Text',
'Icon',
'Button',
'Input'
],
defaultRules: [
{
directives: {
background: '--fg'
}
}
]
}

@ -1,6 +1,6 @@
export default { export default {
name: 'Underlay', name: 'Underlay',
selector: '#content', selector: 'body', // Should be '#content' but for now this is better for testing until I have proper popovers and such...
outOfTreeSelector: '.underlay', outOfTreeSelector: '.underlay',
validInnerComponents: [ validInnerComponents: [
'Panel' 'Panel'

@ -6,6 +6,10 @@
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg); background-color: var(--bg, $fallback--bg);
.panel-heading {
background-color: inherit;
}
&::after, &::after,
& { & {
border-radius: $fallback--panelRadius; border-radius: $fallback--panelRadius;
@ -131,12 +135,9 @@
align-items: start; align-items: start;
// panel theme // panel theme
color: var(--panelText); color: var(--panelText);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
&::after { &::after {
background-color: $fallback--fg; background-color: var(--background);
background-color: var(--panel, $fallback--fg);
z-index: -2; z-index: -2;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0; border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0; border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;

@ -20,8 +20,8 @@ export const applyTheme = (input) => {
styleSheet.toString() styleSheet.toString()
styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max') styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max') // styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max') // styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max') styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max')
themes3.css.forEach(rule => { themes3.css.forEach(rule => {
console.log(rule) console.log(rule)

@ -0,0 +1,176 @@
export default [
'bg',
'wallpaper',
'fg',
'text',
'underlay',
'link',
'accent',
'faint',
'faintLink',
'postFaintLink',
'cBlue',
'cRed',
'cGreen',
'cOrange',
'profileBg',
'profileTint',
'highlight',
'highlightLightText',
'highlightPostLink',
'highlightFaintText',
'highlightFaintLink',
'highlightPostFaintLink',
'highlightText',
'highlightLink',
'highlightIcon',
'popover',
'popoverLightText',
'popoverPostLink',
'popoverFaintText',
'popoverFaintLink',
'popoverPostFaintLink',
'popoverText',
'popoverLink',
'popoverIcon',
'selectedPost',
'selectedPostFaintText',
'selectedPostLightText',
'selectedPostPostLink',
'selectedPostFaintLink',
'selectedPostText',
'selectedPostLink',
'selectedPostIcon',
'selectedMenu',
'selectedMenuLightText',
'selectedMenuFaintText',
'selectedMenuFaintLink',
'selectedMenuText',
'selectedMenuLink',
'selectedMenuIcon',
'selectedMenuPopover',
'selectedMenuPopoverLightText',
'selectedMenuPopoverFaintText',
'selectedMenuPopoverFaintLink',
'selectedMenuPopoverText',
'selectedMenuPopoverLink',
'selectedMenuPopoverIcon',
'lightText',
'postLink',
'postGreentext',
'postCyantext',
'border',
'poll',
'pollText',
'icon',
// Foreground,
'fgText',
'fgLink',
// Panel header,
'panel',
'panelText',
'panelFaint',
'panelLink',
// Top bar,
'topBar',
'topBarLink',
// Tabs,
'tab',
'tabText',
'tabActiveText',
// Buttons,
'btn',
'btnText',
'btnPanelText',
'btnTopBarText',
// Buttons: pressed,
'btnPressed',
'btnPressedText',
'btnPressedPanel',
'btnPressedPanelText',
'btnPressedTopBar',
'btnPressedTopBarText',
// Buttons: toggled,
'btnToggled',
'btnToggledText',
'btnToggledPanelText',
'btnToggledTopBarText',
// Buttons: disabled,
'btnDisabled',
'btnDisabledText',
'btnDisabledPanelText',
'btnDisabledTopBarText',
// Input fields,
'input',
'inputText',
'inputPanelText',
'inputTopbarText',
'alertError',
'alertErrorText',
'alertErrorPanelText',
'alertWarning',
'alertWarningText',
'alertWarningPanelText',
'alertSuccess',
'alertSuccessText',
'alertSuccessPanelText',
'alertNeutral',
'alertNeutralText',
'alertNeutralPanelText',
'alertPopupError',
'alertPopupErrorText',
'alertPopupWarning',
'alertPopupWarningText',
'alertPopupSuccess',
'alertPopupSuccessText',
'alertPopupNeutral',
'alertPopupNeutralText',
'badgeNotification',
'badgeNotificationText',
'badgeNeutral',
'badgeNeutralText',
'chatBg',
'chatMessageIncomingBg',
'chatMessageIncomingText',
'chatMessageIncomingLink',
'chatMessageIncomingBorder',
'chatMessageOutgoingBg',
'chatMessageOutgoingText',
'chatMessageOutgoingLink',
'chatMessageOutgoingBorder'
]

@ -0,0 +1,58 @@
import allKeys from './theme2_keys'
// keys that are meant to be used globally, i.e. what's the rest of the theme is based upon.
const basePaletteKeys = new Set([
'bg',
'fg',
'text',
'link',
'accent',
'cBlue',
'cRed',
'cGreen',
'cOrange'
])
// Keys that are not available in editor and never meant to be edited
const hiddenKeys = new Set([
'profileBg',
'profileTint'
])
const extendedBasePrefixes = [
'border',
'icon',
'highlight',
'lightText',
'popover',
'panel',
'topBar',
'tab',
'btn',
'input',
'selectedMenu',
'alert',
'badge',
'post',
'selectedPost', // wrong nomenclature
'poll',
'chatBg',
'chatMessageIncoming',
'chatMessageOutgoing'
]
const extendedBaseKeys = Object.fromEntries(extendedBasePrefixes.map(prefix => [prefix, allKeys.filter(k => k.startsWith(prefix))]))
// Keysets that are only really used intermideately, i.e. to generate other colors
const temporary = new Set([
'border',
'highlight'
])
const temporaryColors = {}

@ -1,24 +1,89 @@
import { convert, brightness } from 'chromatism' import { convert, brightness } from 'chromatism'
import merge from 'lodash.merge' import merge from 'lodash.merge'
import { alphaBlend, getTextColor, rgba2css, mixrgb, relativeLuminance } from '../color_convert/color_convert.js' import {
alphaBlend,
getTextColor,
rgba2css,
mixrgb,
relativeLuminance
} from '../color_convert/color_convert.js'
import Root from 'src/components/root.style.js'
import TopBar from 'src/components/top_bar.style.js'
import Underlay from 'src/components/underlay.style.js' import Underlay from 'src/components/underlay.style.js'
import Popover from 'src/components/popover.style.js'
import DropdownMenu from 'src/components/dropdown_menu.style.js'
import Panel from 'src/components/panel.style.js' import Panel from 'src/components/panel.style.js'
import PanelHeader from 'src/components/panel_header.style.js' import PanelHeader from 'src/components/panel_header.style.js'
import Button from 'src/components/button.style.js' import Button from 'src/components/button.style.js'
import Input from 'src/components/input.style.js'
import Text from 'src/components/text.style.js' import Text from 'src/components/text.style.js'
import Link from 'src/components/link.style.js' import Link from 'src/components/link.style.js'
import Icon from 'src/components/icon.style.js' import Icon from 'src/components/icon.style.js'
const root = Underlay export const DEFAULT_SHADOWS = {
panel: [{
x: 1,
y: 1,
blur: 4,
spread: 0,
color: '#000000',
alpha: 0.6
}],
topBar: [{
x: 0,
y: 0,
blur: 4,
spread: 0,
color: '#000000',
alpha: 0.6
}],
popup: [{
x: 2,
y: 2,
blur: 3,
spread: 0,
color: '#000000',
alpha: 0.5
}],
avatar: [{
x: 0,
y: 1,
blur: 8,
spread: 0,
color: '#000000',
alpha: 0.7
}],
avatarStatus: [],
panelHeader: []
}
const components = { const components = {
Root,
Text,
Link,
Icon,
Underlay, Underlay,
Popover,
DropdownMenu,
Panel, Panel,
PanelHeader, PanelHeader,
TopBar,
Button, Button,
Text, Input
Link, }
Icon
// "Unrolls" a tree structure of item: { parent: { ...item2, parent: { ...item3, parent: {...} } }}
// into an array [item2, item3] for iterating
const unroll = (item) => {
const out = []
let currentParent = item.parent
while (currentParent) {
const { parent: newParent, ...rest } = currentParent
out.push(rest)
currentParent = newParent
}
return out
} }
// This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations // This gives you an array of arrays of all possible unique (i.e. order-insensitive) combinations
@ -38,7 +103,9 @@ export const getAllPossibleCombinations = (array) => {
return combos.reduce((acc, x) => [...acc, ...x], []) return combos.reduce((acc, x) => [...acc, ...x], [])
} }
export const ruleToSelector = (rule, isParent) => { // Converts rule, parents and their criteria into a CSS (or path if ignoreOutOfTreeSelector == true) selector
export const ruleToSelector = (rule, ignoreOutOfTreeSelector, isParent) => {
if (!rule && !isParent) return null
const component = components[rule.component] const component = components[rule.component]
const { states, variants, selector, outOfTreeSelector } = component const { states, variants, selector, outOfTreeSelector } = component
@ -51,10 +118,12 @@ export const ruleToSelector = (rule, isParent) => {
} }
let realSelector let realSelector
if (isParent) { if (selector === ':root') {
realSelector = ''
} else if (isParent) {
realSelector = selector realSelector = selector
} else { } else {
if (outOfTreeSelector) realSelector = outOfTreeSelector if (outOfTreeSelector && !ignoreOutOfTreeSelector) realSelector = outOfTreeSelector
else realSelector = selector else realSelector = selector
} }
@ -67,123 +136,133 @@ export const ruleToSelector = (rule, isParent) => {
.join('') .join('')
if (rule.parent) { if (rule.parent) {
return ruleToSelector(rule.parent, true) + ' ' + selectors return (ruleToSelector(rule.parent, ignoreOutOfTreeSelector, true) + ' ' + selectors).trim()
} }
return selectors return selectors.trim()
} }
export const init = (extraRuleset, palette) => { const combinationsMatch = (criteria, subject) => {
const rootName = root.name if (criteria.component !== subject.component) return false
const rules = []
const rulesByComponent = {}
const ruleset = [
...Object.values(components).map(c => c.defaultRules || []).reduce((acc, arr) => [...acc, ...arr], []),
...extraRuleset
]
const addRule = (rule) => { // All variants inherit from normal
rules.push(rule) const subjectVariant = Object.prototype.hasOwnProperty.call(subject, 'variant') ? subject.variant : 'normal'
rulesByComponent[rule.component] = rulesByComponent[rule.component] || [] if (subjectVariant !== 'normal') {
rulesByComponent[rule.component].push(rule) if (criteria.variant !== subject.variant) return false
} }
const findRules = (searchCombination, parent) => rule => { const subjectStatesSet = new Set(['normal', ...(subject.state || [])])
// inexact search const criteriaStatesSet = new Set(['normal', ...(criteria.state || [])])
const doesCombinationMatch = () => {
if (searchCombination.component !== rule.component) return false
const ruleVariant = Object.prototype.hasOwnProperty.call(rule, 'variant') ? rule.variant : 'normal'
if (ruleVariant !== 'normal') { // Subject states > 1 essentially means state is "normal" and therefore matches
if (searchCombination.variant !== rule.variant) return false if (subjectStatesSet.size > 1) {
} const setsAreEqual =
[...criteriaStatesSet].every(state => subjectStatesSet.has(state)) &&
[...subjectStatesSet].every(state => criteriaStatesSet.has(state))
const ruleHasStateDefined = Object.prototype.hasOwnProperty.call(rule, 'state') if (!setsAreEqual) return false
let ruleStateSet
if (ruleHasStateDefined) {
ruleStateSet = new Set(['normal', ...rule.state])
} else {
ruleStateSet = new Set(['normal'])
} }
if (ruleStateSet.size > 1) {
const ruleStatesSet = ruleStateSet
const combinationSet = new Set(['normal', ...searchCombination.state])
const setsAreEqual = searchCombination.state.every(state => ruleStatesSet.has(state)) &&
[...ruleStatesSet].every(state => combinationSet.has(state))
return setsAreEqual
} else {
return true return true
} }
}
const combinationMatches = doesCombinationMatch() const findRules = criteria => subject => {
if (!parent || !combinationMatches) return combinationMatches // If we searching for "general" rules - ignore "specific" ones
if (criteria.parent === null && !!subject.parent) return false
if (!combinationsMatch(criteria, subject)) return false
// exact search if (criteria.parent !== undefined && criteria.parent !== null) {
if (!subject.parent) return true
const pathCriteria = unroll(criteria)
const pathSubject = unroll(subject)
if (pathCriteria.length < pathSubject.length) return false
// unroll parents into array // Search: .a .b .c
const unroll = (item) => { // Matches: .a .b .c; .b .c; .c; .z .a .b .c
const out = [] // Does not match .a .b .c .d, .a .b .e
let currentParent = item.parent for (let i = 0; i < pathCriteria.length; i++) {
while (currentParent) { const criteriaParent = pathCriteria[i]
const { parent: newParent, ...rest } = currentParent const subjectParent = pathSubject[i]
out.push(rest) if (!subjectParent) return true
currentParent = newParent if (!combinationsMatch(criteriaParent, subjectParent)) return false
} }
return out
}
const { parent: _, ...rest } = parent
const pathSearch = [rest, ...unroll(parent)]
const pathRule = unroll(rule)
if (pathSearch.length !== pathRule.length) return false
const pathsMatch = pathSearch.every((searchRule, i) => {
const existingRule = pathRule[i]
if (existingRule.component !== searchRule.component) return false
if (existingRule.variant !== searchRule.variant) return false
const existingRuleStatesSet = new Set(['normal', ...(existingRule.state || [])])
const searchStatesSet = new Set(['normal', ...(searchRule.state || [])])
const setsAreEqual = existingRule.state.every(state => searchStatesSet.has(state)) &&
[...searchStatesSet].every(state => existingRuleStatesSet.has(state))
return setsAreEqual
})
return pathsMatch
} }
return true
}
const findLowerLevelRule = (parent, filter = () => true) => { export const init = (extraRuleset, palette) => {
let lowerLevelComponent = null const cache = {}
let currentParent = parent const computed = {}
while (currentParent) {
const rulesParent = ruleset.filter(findRules(currentParent)) const rules = []
rulesParent > 1 && console.warn('OOPS')
lowerLevelComponent = rulesParent[rulesParent.length - 1] const ruleset = [
currentParent = currentParent.parent ...Object.values(components).map(c => c.defaultRules.map(r => ({ component: c.name, ...r })) || []).reduce((acc, arr) => [...acc, ...arr], []),
if (lowerLevelComponent && filter(lowerLevelComponent)) currentParent = null ...extraRuleset
} ]
return filter(lowerLevelComponent) ? lowerLevelComponent : null
const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
const addRule = (rule) => {
rules.push(rule)
} }
const findColor = (color, background) => { const findColor = (color, inheritedBackground, lowerLevelBackground) => {
if (typeof color !== 'string' || !color.startsWith('--')) return color if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
let targetColor = null let targetColor = null
// Color references other color if (color.startsWith('--')) {
const [variable, modifier] = color.split(/,/g).map(str => str.trim()) const [variable, modifier] = color.split(/,/g).map(str => str.trim())
const variableSlot = variable.substring(2) const variableSlot = variable.substring(2)
if (variableSlot.startsWith('parent')) {
// TODO support more than just background?
if (variableSlot === 'parent') {
targetColor = lowerLevelBackground
}
} else {
switch (variableSlot) {
case 'background':
targetColor = inheritedBackground
break
default:
targetColor = palette[variableSlot] targetColor = palette[variableSlot]
}
}
if (modifier) { if (modifier) {
const effectiveBackground = background ?? targetColor const effectiveBackground = lowerLevelBackground ?? targetColor
const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5 const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
const mod = isLightOnDark ? 1 : -1 const mod = isLightOnDark ? 1 : -1
targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
} }
}
if (color.startsWith('$')) {
try {
const { funcName, argsString } = /\$(?<funcName>\w+)\((?<argsString>[a-zA-Z0-9-,.'"\s]*)\)/.exec(color).groups
const args = argsString.split(/,/g).map(a => a.trim())
switch (funcName) {
case 'blend': {
if (args.length !== 3) {
throw new Error(`$blend requires 3 arguments, ${args.length} were provided`)
}
const backgroundArg = findColor(args[2], inheritedBackground, lowerLevelBackground)
const foregroundArg = findColor(args[0], inheritedBackground, lowerLevelBackground)
const amount = Number(args[1])
targetColor = alphaBlend(backgroundArg, amount, foregroundArg)
break
}
}
} catch (e) {
console.error('Failure executing color function', e)
targetColor = '#FF00FF'
}
}
// Color references other color
return targetColor return targetColor
} }
const getTextColorAlpha = (rule, lowerRule, value) => { const cssColorString = (color, alpha) => rgba2css({ ...convert(color).rgb, a: alpha })
const getTextColorAlpha = (rule, lowerColor, value) => {
const opacity = rule.directives.textOpacity const opacity = rule.directives.textOpacity
const backgroundColor = convert(lowerRule.cache.background).rgb const backgroundColor = convert(lowerColor).rgb
const textColor = convert(findColor(value, backgroundColor)).rgb const textColor = convert(findColor(value, backgroundColor)).rgb
if (opacity === null || opacity === undefined || opacity >= 1) { if (opacity === null || opacity === undefined || opacity >= 1) {
return convert(textColor).hex return convert(textColor).hex
@ -202,6 +281,44 @@ export const init = (extraRuleset, palette) => {
} }
} }
const getCssShadow = (input, usesDropShadow) => {
if (input.length === 0) {
return 'none'
}
return input
.filter(_ => usesDropShadow ? _.inset : _)
.map((shad) => [
shad.x,
shad.y,
shad.blur,
shad.spread
].map(_ => _ + 'px ').concat([
cssColorString(findColor(shad.color), shad.alpha),
shad.inset ? 'inset' : ''
]).join(' ')).join(', ')
}
const getCssShadowFilter = (input) => {
if (input.length === 0) {
return 'none'
}
return input
// drop-shadow doesn't support inset or spread
.filter((shad) => !shad.inset && Number(shad.spread) === 0)
.map((shad) => [
shad.x,
shad.y,
// drop-shadow's blur is twice as strong compared to box-shadow
shad.blur / 2
].map(_ => _ + 'px').concat([
cssColorString(findColor(shad.color), shad.alpha)
]).join(' '))
.map(_ => `drop-shadow(${_})`)
.join(' ')
}
const processInnerComponent = (component, parent) => { const processInnerComponent = (component, parent) => {
const { const {
validInnerComponents = [], validInnerComponents = [],
@ -210,6 +327,7 @@ export const init = (extraRuleset, palette) => {
name name
} = component } = component
// Normalizing states and variants to always include "normal"
const states = { normal: '', ...originalStates } const states = { normal: '', ...originalStates }
const variants = { normal: '', ...originalVariants } const variants = { normal: '', ...originalVariants }
const innerComponents = validInnerComponents.map(name => components[name]) const innerComponents = validInnerComponents.map(name => components[name])
@ -219,13 +337,24 @@ export const init = (extraRuleset, palette) => {
return stateCombinations.map(state => ({ variant, state })) return stateCombinations.map(state => ({ variant, state }))
}).reduce((acc, x) => [...acc, ...x], []) }).reduce((acc, x) => [...acc, ...x], [])
const VIRTUAL_COMPONENTS = new Set(['Text', 'Link', 'Icon'])
stateVariantCombination.forEach(combination => { stateVariantCombination.forEach(combination => {
let needRuleAdd = false const soloSelector = ruleToSelector({ component: component.name, ...combination }, true)
const selector = ruleToSelector({ component: component.name, ...combination, parent }, true)
// Inheriting all of the applicable rules
const existingRules = ruleset.filter(findRules({ component: component.name, ...combination, parent }))
const { directives: computedDirectives } = existingRules.reduce((acc, rule) => merge(acc, rule), {})
const computedRule = {
component: component.name,
...combination,
parent,
directives: computedDirectives
}
computed[selector] = computed[selector] || {}
computed[selector].computedRule = computedRule
if (VIRTUAL_COMPONENTS.has(component.name)) { if (virtualComponents.has(component.name)) {
const selector = component.name + ruleToSelector({ component: component.name, ...combination })
const virtualName = [ const virtualName = [
'--', '--',
component.name.toLowerCase(), component.name.toLowerCase(),
@ -235,52 +364,46 @@ export const init = (extraRuleset, palette) => {
...combination.state.filter(x => x !== 'normal').toSorted().map(state => state[0].toUpperCase() + state.slice(1).toLowerCase()) ...combination.state.filter(x => x !== 'normal').toSorted().map(state => state[0].toUpperCase() + state.slice(1).toLowerCase())
].join('') ].join('')
const lowerLevel = findLowerLevelRule(parent, (r) => { let inheritedTextColor = computedDirectives.textColor
if (!r) return false let inheritedTextOpacity = computedDirectives.textOpacity
if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false let inheritedTextOpacityMode = computedDirectives.textOpacityMode
if (r.cache.background === undefined) return false const lowerLevelTextSelector = [...selector.split(/ /g).slice(0, -1), soloSelector].join(' ')
if (r.cache.textDefined) { const lowerLevelTextRule = computed[lowerLevelTextSelector]
return !r.cache.textDefined[selector]
}
return true
})
if (!lowerLevel) return
let inheritedTextColorRule if (inheritedTextColor == null || inheritedTextOpacity == null || inheritedTextOpacityMode == null) {
const inheritedTextColorRules = findLowerLevelRule(parent, (r) => { inheritedTextColor = computedDirectives.textColor ?? lowerLevelTextRule.textColor
return r.cache?.textDefined?.[selector] inheritedTextOpacity = computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
}) inheritedTextOpacityMode = computedDirectives.textOpacityMode ?? lowerLevelTextRule.textOpacityMode
if (!inheritedTextColorRule) {
const generalTextColorRules = ruleset.filter(findRules({ component: component.name, ...combination }, null, true))
inheritedTextColorRule = generalTextColorRules.reduce((acc, rule) => merge(acc, rule), {})
} else {
inheritedTextColorRule = inheritedTextColorRules.reduce((acc, rule) => merge(acc, rule), {})
} }
let inheritedTextColor const newTextRule = {
let inheritedTextOpacity = {} ...computedRule,
if (inheritedTextColorRule) { directives: {
inheritedTextColor = findColor(inheritedTextColorRule.directives.textColor, convert(lowerLevel.cache.background).rgb) ...computedRule.directives,
// also inherit opacity settings textColor: inheritedTextColor,
const { textOpacity, textOpacityMode } = inheritedTextColorRule.directives textOpacity: inheritedTextOpacity,
inheritedTextOpacity = { textOpacity, textOpacityMode } textOpacityMode: inheritedTextOpacityMode
} else { }
// Emergency fallback
inheritedTextColor = '#000000'
} }
const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ')
const lowerLevelBackground = cache[lowerLevelSelector].background
const textColor = getTextColor( const textColor = getTextColor(
convert(lowerLevel.cache.background).rgb, convert(lowerLevelBackground).rgb,
convert(inheritedTextColor).rgb, // TODO properly provide "parent" text color?
component.name === 'Link' // make it configurable? convert(findColor(inheritedTextColor, null, lowerLevelBackground)).rgb,
true // component.name === 'Link' || combination.variant === 'greentext' // make it configurable?
) )
lowerLevel.cache.textDefined = lowerLevel.cache.textDefined || {} // Storing color data in lower layer to use as custom css properties
lowerLevel.cache.textDefined[selector] = textColor cache[lowerLevelSelector].textDefined = cache[lowerLevelSelector].textDefined || {}
lowerLevel.virtualDirectives = lowerLevel.virtualDirectives || {} cache[lowerLevelSelector].textDefined[selector] = textColor
lowerLevel.virtualDirectives[virtualName] = getTextColorAlpha(inheritedTextColorRule, lowerLevel, textColor)
const virtualDirectives = {}
virtualDirectives[virtualName] = getTextColorAlpha(newTextRule, lowerLevelBackground, textColor)
// lastRule.computed = lastRule.computed || {}
const directives = { const directives = {
textColor, textColor,
@ -288,45 +411,54 @@ export const init = (extraRuleset, palette) => {
} }
// Debug: lets you see what it think background color should be // Debug: lets you see what it think background color should be
directives.background = convert(lowerLevel.cache.background).hex // directives.background = convert(cache[lowerLevelSelector].background).hex
addRule({ addRule({
parent, parent,
virtual: true, virtual: true,
component: component.name, component: component.name,
...combination, ...combination,
cache: { background: lowerLevel.cache.background }, directives,
directives virtualDirectives
}) })
} else { } else {
const existingGlobalRules = ruleset.filter(findRules({ component: component.name, ...combination }, null)) cache[selector] = cache[selector] || {}
const existingRules = ruleset.filter(findRules({ component: component.name, ...combination }, parent)) computed[selector] = computed[selector] || {}
// Global (general) rules if (computedDirectives.background) {
if (existingGlobalRules.length !== 0) { let inheritRule = null
const totalRule = existingGlobalRules.reduce((acc, rule) => merge(acc, rule), {}) const variantRules = ruleset.filter(findRules({ component: component.name, variant: combination.variant, parent }))
const { directives } = totalRule const lastVariantRule = variantRules[variantRules.length - 1]
if (lastVariantRule) {
// last rule is used as a cache inheritRule = lastVariantRule
const lastRule = existingGlobalRules[existingGlobalRules.length - 1] } else {
lastRule.cache = lastRule.cache || {} const normalRules = ruleset.filter(findRules({ component: component.name, parent }))
const lastNormalRule = normalRules[normalRules.length - 1]
inheritRule = lastNormalRule
}
if (directives.background) { const inheritSelector = ruleToSelector({ ...inheritRule, parent }, true)
const rgb = convert(findColor(directives.background)).rgb const inheritedBackground = cache[inheritSelector].background
const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ')
// TODO: DEFAULT TEXT COLOR // TODO: DEFAULT TEXT COLOR
const bg = findLowerLevelRule(parent)?.cache.background || convert('#FFFFFF').rgb const bg = cache[lowerLevelSelector]?.background || convert('#FFFFFF').rgb
if (!lastRule.cache.background) { console.log('SELECTOR', lowerLevelSelector)
const blend = directives.opacity < 1 ? alphaBlend(rgb, directives.opacity, bg) : rgb
lastRule.cache.background = blend
needRuleAdd = true const rgb = convert(findColor(computedDirectives.background, inheritedBackground, cache[lowerLevelSelector].background)).rgb
}
}
if (needRuleAdd) { if (!cache[selector].background) {
addRule(lastRule) const blend = computedDirectives.opacity < 1 ? alphaBlend(rgb, computedDirectives.opacity, bg) : rgb
cache[selector].background = blend
computed[selector].background = rgb
addRule({
component: component.name,
...combination,
parent,
directives: computedDirectives
})
} }
} }
@ -338,12 +470,12 @@ export const init = (extraRuleset, palette) => {
}) })
} }
processInnerComponent(components[rootName]) processInnerComponent(components.Root, { component: 'Root' })
return { return {
raw: rules, raw: rules,
css: rules.map(rule => { css: rules.map(rule => {
if (rule.virtual) return '' // if (rule.virtual) return ''
let selector = ruleToSelector(rule).replace(/\/\*.*\*\//g, '') let selector = ruleToSelector(rule).replace(/\/\*.*\*\//g, '')
if (!selector) { if (!selector) {
@ -356,10 +488,23 @@ export const init = (extraRuleset, palette) => {
return ' ' + k + ': ' + v return ' ' + k + ': ' + v
}).join(';\n') }).join(';\n')
const directives = Object.entries(rule.directives).map(([k, v]) => { let directives
if (rule.component !== 'Root') {
directives = Object.entries(rule.directives).map(([k, v]) => {
switch (k) { switch (k) {
case 'shadow': {
return ' ' + [
'--shadow: ' + getCssShadow(v),
'--shadowFilter: ' + getCssShadowFilter(v),
'--shadowInset: ' + getCssShadow(v, true)
].join(';\n ')
}
case 'background': { case 'background': {
return 'background-color: ' + rgba2css({ ...convert(findColor(v)).rgb, a: rule.directives.opacity ?? 1 }) const color = cssColorString(computed[ruleToSelector(rule, true)].background, rule.directives.opacity)
return [
'background-color: ' + color,
' --background: ' + color
].join(';\n')
} }
case 'textColor': { case 'textColor': {
return 'color: ' + v return 'color: ' + v
@ -367,11 +512,14 @@ export const init = (extraRuleset, palette) => {
default: return '' default: return ''
} }
}).filter(x => x).map(x => ' ' + x).join(';\n') }).filter(x => x).map(x => ' ' + x).join(';\n')
} else {
directives = {}
}
return [ return [
header, header,
directives + ';', directives + ';',
' color: var(--text);', !rule.virtual ? ' color: var(--text);' : '',
'', '',
virtualDirectives, virtualDirectives,
footer footer

@ -0,0 +1,9 @@
export default {
name: 'TopBar',
selector: 'nav',
validInnerComponents: [
'Link',
'Text',
'Icon'
]
}
Loading…
Cancel
Save