update, should inherit stuff properly now.

This commit is contained in:
Henry Jameson 2024-02-07 15:53:34 +02:00
parent d4795d2e3c
commit c34590c439
16 changed files with 804 additions and 236 deletions

View File

@ -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 {

View File

@ -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'
} }
} }
] ]

View File

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

View File

@ -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]
}
}
]
}

View File

@ -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'
} }
} }
] ]

View File

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

View File

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

View File

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

View File

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

View File

@ -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'

View File

@ -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;

View File

@ -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)

View File

@ -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'
]

View File

@ -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 = {}

View File

@ -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 = {
Underlay, Root,
Panel,
PanelHeader,
Button,
Text, Text,
Link, Link,
Icon Icon,
Underlay,
Popover,
DropdownMenu,
Panel,
PanelHeader,
TopBar,
Button,
Input
}
// "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()
}
const combinationsMatch = (criteria, subject) => {
if (criteria.component !== subject.component) return false
// All variants inherit from normal
const subjectVariant = Object.prototype.hasOwnProperty.call(subject, 'variant') ? subject.variant : 'normal'
if (subjectVariant !== 'normal') {
if (criteria.variant !== subject.variant) return false
}
const subjectStatesSet = new Set(['normal', ...(subject.state || [])])
const criteriaStatesSet = new Set(['normal', ...(criteria.state || [])])
// Subject states > 1 essentially means state is "normal" and therefore matches
if (subjectStatesSet.size > 1) {
const setsAreEqual =
[...criteriaStatesSet].every(state => subjectStatesSet.has(state)) &&
[...subjectStatesSet].every(state => criteriaStatesSet.has(state))
if (!setsAreEqual) return false
}
return true
}
const findRules = criteria => subject => {
// If we searching for "general" rules - ignore "specific" ones
if (criteria.parent === null && !!subject.parent) return false
if (!combinationsMatch(criteria, subject)) return false
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
// Search: .a .b .c
// Matches: .a .b .c; .b .c; .c; .z .a .b .c
// Does not match .a .b .c .d, .a .b .e
for (let i = 0; i < pathCriteria.length; i++) {
const criteriaParent = pathCriteria[i]
const subjectParent = pathSubject[i]
if (!subjectParent) return true
if (!combinationsMatch(criteriaParent, subjectParent)) return false
}
}
return true
} }
export const init = (extraRuleset, palette) => { export const init = (extraRuleset, palette) => {
const rootName = root.name const cache = {}
const computed = {}
const rules = [] const rules = []
const rulesByComponent = {}
const ruleset = [ const ruleset = [
...Object.values(components).map(c => c.defaultRules || []).reduce((acc, arr) => [...acc, ...arr], []), ...Object.values(components).map(c => c.defaultRules.map(r => ({ component: c.name, ...r })) || []).reduce((acc, arr) => [...acc, ...arr], []),
...extraRuleset ...extraRuleset
] ]
const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
const addRule = (rule) => { const addRule = (rule) => {
rules.push(rule) rules.push(rule)
rulesByComponent[rule.component] = rulesByComponent[rule.component] || []
rulesByComponent[rule.component].push(rule)
} }
const findRules = (searchCombination, parent) => rule => { const findColor = (color, inheritedBackground, lowerLevelBackground) => {
// inexact search if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
const doesCombinationMatch = () => {
if (searchCombination.component !== rule.component) return false
const ruleVariant = Object.prototype.hasOwnProperty.call(rule, 'variant') ? rule.variant : 'normal'
if (ruleVariant !== 'normal') {
if (searchCombination.variant !== rule.variant) return false
}
const ruleHasStateDefined = Object.prototype.hasOwnProperty.call(rule, 'state')
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
}
}
const combinationMatches = doesCombinationMatch()
if (!parent || !combinationMatches) return combinationMatches
// exact search
// unroll parents into array
const unroll = (item) => {
const out = []
let currentParent = item.parent
while (currentParent) {
const { parent: newParent, ...rest } = currentParent
out.push(rest)
currentParent = newParent
}
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
}
const findLowerLevelRule = (parent, filter = () => true) => {
let lowerLevelComponent = null
let currentParent = parent
while (currentParent) {
const rulesParent = ruleset.filter(findRules(currentParent))
rulesParent > 1 && console.warn('OOPS')
lowerLevelComponent = rulesParent[rulesParent.length - 1]
currentParent = currentParent.parent
if (lowerLevelComponent && filter(lowerLevelComponent)) currentParent = null
}
return filter(lowerLevelComponent) ? lowerLevelComponent : null
}
const findColor = (color, background) => {
if (typeof color !== 'string' || !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)
if (VIRTUAL_COMPONENTS.has(component.name)) { // Inheriting all of the applicable rules
const selector = component.name + ruleToSelector({ component: component.name, ...combination }) 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 (virtualComponents.has(component.name)) {
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 if (inheritedTextColor == null || inheritedTextOpacity == null || inheritedTextOpacityMode == null) {
inheritedTextColor = computedDirectives.textColor ?? lowerLevelTextRule.textColor
let inheritedTextColorRule inheritedTextOpacity = computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
const inheritedTextColorRules = findLowerLevelRule(parent, (r) => { inheritedTextOpacityMode = computedDirectives.textOpacityMode ?? lowerLevelTextRule.textOpacityMode
return r.cache?.textDefined?.[selector]
})
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) {
inheritRule = lastVariantRule
} else {
const normalRules = ruleset.filter(findRules({ component: component.name, parent }))
const lastNormalRule = normalRules[normalRules.length - 1]
inheritRule = lastNormalRule
}
// last rule is used as a cache const inheritSelector = ruleToSelector({ ...inheritRule, parent }, true)
const lastRule = existingGlobalRules[existingGlobalRules.length - 1] const inheritedBackground = cache[inheritSelector].background
lastRule.cache = lastRule.cache || {} const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ')
if (directives.background) {
const rgb = convert(findColor(directives.background)).rgb
// 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

9
top_bar.style.js Normal file
View File

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