commit: c34590c43983711152a843a2e36b991a5fe3bcdf
parent d4795d2e3c363065319a978fd2d9237fb62f2fe6
Author: Henry Jameson <me@hjkos.com>
Date: Wed, 7 Feb 2024 15:53:34 +0200
update, should inherit stuff properly now.
Diffstat:
16 files changed, 794 insertions(+), 226 deletions(-)
diff --git a/src/App.scss b/src/App.scss
@@ -371,8 +371,7 @@ nav {
border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius);
cursor: pointer;
- box-shadow: $fallback--buttonShadow;
- box-shadow: var(--buttonShadow);
+ box-shadow: var(--shadow);
font-size: 1em;
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
@@ -383,25 +382,14 @@ nav {
i[class*="icon-"],
.svg-inline--fa {
- color: $fallback--text;
- color: var(--btnText, $fallback--text);
+ color: var(--icon);
}
&::-moz-focus-inner {
border: none;
}
- &:hover {
- box-shadow: 0 0 4px rgb(255 255 255 / 30%);
- box-shadow: var(--buttonHoverShadow);
- }
-
&: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: var(--btnPressedText, $fallback--text);
background-color: $fallback--fg;
@@ -487,7 +475,12 @@ nav {
}
input,
-textarea,
+textarea {
+ border: none;
+ display: inline-block;
+ outline: none;
+}
+
.input {
&.unstyled {
border-radius: 0;
@@ -501,15 +494,7 @@ textarea,
border: none;
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
- box-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);
+ box-shadow: var(--shadow);
font-family: sans-serif;
font-family: var(--inputFont, sans-serif);
font-size: 1em;
@@ -561,11 +546,9 @@ textarea,
width: 1.1em;
height: 1.1em;
border-radius: 100%; // Radio buttons should always be circle
- box-shadow: 0 0 2px black inset;
- box-shadow: var(--inputShadow);
+ background-color: var(--background);
+ box-shadow: var(--shadow);
margin-right: 0.5em;
- background-color: $fallback--fg;
- background-color: var(--input, $fallback--fg);
vertical-align: top;
text-align: center;
line-height: 1.1;
@@ -578,8 +561,9 @@ textarea,
&[type="checkbox"] {
&:checked + label::before {
- color: $fallback--text;
- color: var(--inputText, $fallback--text);
+ color: var(--text);
+ background-color: var(--background);
+ box-shadow: var(--shadow);
}
&:disabled {
diff --git a/src/components/button.style.js b/src/components/button.style.js
@@ -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 {
name: 'Button',
- selector: '.btn',
+ selector: '.button-default',
states: {
disabled: ':disabled',
toggled: '.toggled',
pressed: ':active',
- hover: ':hover'
+ hover: ':hover',
+ focused: ':focus-within'
},
variants: {
danger: '.danger',
@@ -20,14 +43,49 @@ export default {
{
component: 'Button',
directives: {
- background: '--fg'
+ background: '--fg',
+ shadow: [{
+ x: 0,
+ y: 0,
+ blur: 2,
+ spread: 0,
+ color: '#000000',
+ alpha: 1
+ }, ...buttonInsetFakeBorders]
}
},
{
component: 'Button',
state: ['hover'],
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'
}
}
]
diff --git a/src/components/dropdown_menu.style.js b/src/components/dropdown_menu.style.js
@@ -0,0 +1,19 @@
+export default {
+ name: 'DropdownMenu',
+ selector: '.dropdown',
+ validInnerComponents: [
+ 'Text',
+ 'Icon',
+ 'Input'
+ ],
+ states: {
+ hover: ':hover'
+ },
+ defaultRules: [
+ {
+ directives: {
+ background: '--fg'
+ }
+ }
+ ]
+}
diff --git a/src/components/input.style.js b/src/components/input.style.js
@@ -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]
+ }
+ }
+ ]
+}
diff --git a/src/components/panel.style.js b/src/components/panel.style.js
@@ -6,13 +6,14 @@ export default {
'Link',
'Icon',
'Button',
- 'PanelHeader'
+ 'Input',
+ 'PanelHeader',
+ 'DropdownMenu'
],
defaultRules: [
{
- component: 'Panel',
directives: {
- background: '--fg'
+ background: '--bg'
}
}
]
diff --git a/src/components/panel_header.style.js b/src/components/panel_header.style.js
@@ -12,7 +12,6 @@ export default {
component: 'PanelHeader',
directives: {
background: '--fg'
- // opacity: 0.9
}
}
]
diff --git a/src/components/popover.style.js b/src/components/popover.style.js
@@ -0,0 +1,20 @@
+export default {
+ name: 'Popover',
+ selector: '.popover',
+ validInnerComponents: [
+ 'Text',
+ 'Link',
+ 'Icon',
+ 'Button',
+ 'Input',
+ 'PanelHeader',
+ 'DropdownMenu'
+ ],
+ defaultRules: [
+ {
+ directives: {
+ background: '--fg'
+ }
+ }
+ ]
+}
diff --git a/src/components/root.style.js b/src/components/root.style.js
@@ -0,0 +1,17 @@
+export default {
+ name: 'Root',
+ selector: ':root',
+ validInnerComponents: [
+ 'Underlay',
+ 'TopBar',
+ 'Popover'
+ ],
+ defaultRules: [
+ {
+ directives: {
+ background: '--bg',
+ opacity: 0
+ }
+ }
+ ]
+}
diff --git a/src/components/top_bar.style.js b/src/components/top_bar.style.js
@@ -0,0 +1,18 @@
+export default {
+ name: 'TopBar',
+ selector: 'nav',
+ validInnerComponents: [
+ 'Link',
+ 'Text',
+ 'Icon',
+ 'Button',
+ 'Input'
+ ],
+ defaultRules: [
+ {
+ directives: {
+ background: '--fg'
+ }
+ }
+ ]
+}
diff --git a/src/components/underlay.style.js b/src/components/underlay.style.js
@@ -1,6 +1,6 @@
export default {
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',
validInnerComponents: [
'Panel'
diff --git a/src/panel.scss b/src/panel.scss
@@ -6,6 +6,10 @@
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
+ .panel-heading {
+ background-color: inherit;
+ }
+
&::after,
& {
border-radius: $fallback--panelRadius;
@@ -131,12 +135,9 @@
align-items: start;
// panel theme
color: var(--panelText);
- background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg);
&::after {
- background-color: $fallback--fg;
- background-color: var(--panel, $fallback--fg);
+ background-color: var(--background);
z-index: -2;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
@@ -20,8 +20,8 @@ export const applyTheme = (input) => {
styleSheet.toString()
styleSheet.insertRule(`:root { ${rules.radii} }`, 'index-max')
- styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max')
- styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max')
+ // styleSheet.insertRule(`:root { ${rules.colors} }`, 'index-max')
+ // styleSheet.insertRule(`:root { ${rules.shadows} }`, 'index-max')
styleSheet.insertRule(`:root { ${rules.fonts} }`, 'index-max')
themes3.css.forEach(rule => {
console.log(rule)
diff --git a/src/services/theme_data/theme2_keys.js b/src/services/theme_data/theme2_keys.js
@@ -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'
+]
diff --git a/src/services/theme_data/theme2_to_theme3.js b/src/services/theme_data/theme2_to_theme3.js
@@ -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 = {}
diff --git a/src/services/theme_data/theme_data_3.service.js b/src/services/theme_data/theme_data_3.service.js
@@ -1,24 +1,89 @@
import { convert, brightness } from 'chromatism'
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 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 PanelHeader from 'src/components/panel_header.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 Link from 'src/components/link.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 = {
+ Root,
+ Text,
+ Link,
+ Icon,
Underlay,
+ Popover,
+ DropdownMenu,
Panel,
PanelHeader,
+ TopBar,
Button,
- Text,
- Link,
- Icon
+ 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
@@ -38,7 +103,9 @@ export const getAllPossibleCombinations = (array) => {
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 { states, variants, selector, outOfTreeSelector } = component
@@ -51,10 +118,12 @@ export const ruleToSelector = (rule, isParent) => {
}
let realSelector
- if (isParent) {
+ if (selector === ':root') {
+ realSelector = ''
+ } else if (isParent) {
realSelector = selector
} else {
- if (outOfTreeSelector) realSelector = outOfTreeSelector
+ if (outOfTreeSelector && !ignoreOutOfTreeSelector) realSelector = outOfTreeSelector
else realSelector = selector
}
@@ -67,123 +136,133 @@ export const ruleToSelector = (rule, isParent) => {
.join('')
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) => {
- const rootName = root.name
+ const cache = {}
+ const computed = {}
+
const rules = []
- const rulesByComponent = {}
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
]
+ const virtualComponents = new Set(Object.values(components).filter(c => c.virtual).map(c => c.name))
+
const addRule = (rule) => {
rules.push(rule)
- rulesByComponent[rule.component] = rulesByComponent[rule.component] || []
- rulesByComponent[rule.component].push(rule)
}
- const findRules = (searchCombination, parent) => rule => {
- // inexact search
- 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])
+ const findColor = (color, inheritedBackground, lowerLevelBackground) => {
+ if (typeof color !== 'string' || (!color.startsWith('--') && !color.startsWith('$'))) return color
+ let targetColor = null
+ if (color.startsWith('--')) {
+ const [variable, modifier] = color.split(/,/g).map(str => str.trim())
+ const variableSlot = variable.substring(2)
+ if (variableSlot.startsWith('parent')) {
+ // TODO support more than just background?
+ if (variableSlot === 'parent') {
+ targetColor = lowerLevelBackground
+ }
} else {
- ruleStateSet = new Set(['normal'])
+ switch (variableSlot) {
+ case 'background':
+ targetColor = inheritedBackground
+ break
+ default:
+ targetColor = palette[variableSlot]
+ }
}
- 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
+ if (modifier) {
+ const effectiveBackground = lowerLevelBackground ?? targetColor
+ const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
+ const mod = isLightOnDark ? 1 : -1
+ targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
}
}
-
- 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
+ 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'
}
- 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
// Color references other color
- const [variable, modifier] = color.split(/,/g).map(str => str.trim())
- const variableSlot = variable.substring(2)
- targetColor = palette[variableSlot]
-
- if (modifier) {
- const effectiveBackground = background ?? targetColor
- const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
- const mod = isLightOnDark ? 1 : -1
- targetColor = brightness(Number.parseFloat(modifier) * mod, targetColor).rgb
- }
-
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 backgroundColor = convert(lowerRule.cache.background).rgb
+ const backgroundColor = convert(lowerColor).rgb
const textColor = convert(findColor(value, backgroundColor)).rgb
if (opacity === null || opacity === undefined || opacity >= 1) {
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 {
validInnerComponents = [],
@@ -210,6 +327,7 @@ export const init = (extraRuleset, palette) => {
name
} = component
+ // Normalizing states and variants to always include "normal"
const states = { normal: '', ...originalStates }
const variants = { normal: '', ...originalVariants }
const innerComponents = validInnerComponents.map(name => components[name])
@@ -219,13 +337,24 @@ export const init = (extraRuleset, palette) => {
return stateCombinations.map(state => ({ variant, state }))
}).reduce((acc, x) => [...acc, ...x], [])
- const VIRTUAL_COMPONENTS = new Set(['Text', 'Link', 'Icon'])
-
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
+ }
- if (VIRTUAL_COMPONENTS.has(component.name)) {
- const selector = component.name + ruleToSelector({ component: component.name, ...combination })
+ computed[selector] = computed[selector] || {}
+ computed[selector].computedRule = computedRule
+
+ if (virtualComponents.has(component.name)) {
const virtualName = [
'--',
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())
].join('')
- const lowerLevel = findLowerLevelRule(parent, (r) => {
- if (!r) return false
- if (components[r.component].validInnerComponents.indexOf(component.name) < 0) return false
- if (r.cache.background === undefined) return false
- if (r.cache.textDefined) {
- return !r.cache.textDefined[selector]
- }
- return true
- })
-
- if (!lowerLevel) return
+ let inheritedTextColor = computedDirectives.textColor
+ let inheritedTextOpacity = computedDirectives.textOpacity
+ let inheritedTextOpacityMode = computedDirectives.textOpacityMode
+ const lowerLevelTextSelector = [...selector.split(/ /g).slice(0, -1), soloSelector].join(' ')
+ const lowerLevelTextRule = computed[lowerLevelTextSelector]
- let inheritedTextColorRule
- const inheritedTextColorRules = findLowerLevelRule(parent, (r) => {
- 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), {})
+ if (inheritedTextColor == null || inheritedTextOpacity == null || inheritedTextOpacityMode == null) {
+ inheritedTextColor = computedDirectives.textColor ?? lowerLevelTextRule.textColor
+ inheritedTextOpacity = computedDirectives.textOpacity ?? lowerLevelTextRule.textOpacity
+ inheritedTextOpacityMode = computedDirectives.textOpacityMode ?? lowerLevelTextRule.textOpacityMode
}
- let inheritedTextColor
- let inheritedTextOpacity = {}
- if (inheritedTextColorRule) {
- inheritedTextColor = findColor(inheritedTextColorRule.directives.textColor, convert(lowerLevel.cache.background).rgb)
- // also inherit opacity settings
- const { textOpacity, textOpacityMode } = inheritedTextColorRule.directives
- inheritedTextOpacity = { textOpacity, textOpacityMode }
- } else {
- // Emergency fallback
- inheritedTextColor = '#000000'
+ const newTextRule = {
+ ...computedRule,
+ directives: {
+ ...computedRule.directives,
+ textColor: inheritedTextColor,
+ textOpacity: inheritedTextOpacity,
+ textOpacityMode: inheritedTextOpacityMode
+ }
}
+ const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ')
+ const lowerLevelBackground = cache[lowerLevelSelector].background
+
const textColor = getTextColor(
- convert(lowerLevel.cache.background).rgb,
- convert(inheritedTextColor).rgb,
- component.name === 'Link' // make it configurable?
+ convert(lowerLevelBackground).rgb,
+ // TODO properly provide "parent" text color?
+ convert(findColor(inheritedTextColor, null, lowerLevelBackground)).rgb,
+ true // component.name === 'Link' || combination.variant === 'greentext' // make it configurable?
)
- lowerLevel.cache.textDefined = lowerLevel.cache.textDefined || {}
- lowerLevel.cache.textDefined[selector] = textColor
- lowerLevel.virtualDirectives = lowerLevel.virtualDirectives || {}
- lowerLevel.virtualDirectives[virtualName] = getTextColorAlpha(inheritedTextColorRule, lowerLevel, textColor)
+ // Storing color data in lower layer to use as custom css properties
+ cache[lowerLevelSelector].textDefined = cache[lowerLevelSelector].textDefined || {}
+ cache[lowerLevelSelector].textDefined[selector] = textColor
+
+ const virtualDirectives = {}
+ virtualDirectives[virtualName] = getTextColorAlpha(newTextRule, lowerLevelBackground, textColor)
+
+ // lastRule.computed = lastRule.computed || {}
const directives = {
textColor,
@@ -288,45 +411,54 @@ export const init = (extraRuleset, palette) => {
}
// 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({
parent,
virtual: true,
component: component.name,
...combination,
- cache: { background: lowerLevel.cache.background },
- directives
+ directives,
+ virtualDirectives
})
} else {
- const existingGlobalRules = ruleset.filter(findRules({ component: component.name, ...combination }, null))
- const existingRules = ruleset.filter(findRules({ component: component.name, ...combination }, parent))
+ cache[selector] = cache[selector] || {}
+ computed[selector] = computed[selector] || {}
+
+ if (computedDirectives.background) {
+ let inheritRule = null
+ const variantRules = ruleset.filter(findRules({ component: component.name, variant: combination.variant, parent }))
+ 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
+ }
- // Global (general) rules
- if (existingGlobalRules.length !== 0) {
- const totalRule = existingGlobalRules.reduce((acc, rule) => merge(acc, rule), {})
- const { directives } = totalRule
+ const inheritSelector = ruleToSelector({ ...inheritRule, parent }, true)
+ const inheritedBackground = cache[inheritSelector].background
+ const lowerLevelSelector = selector.split(/ /g).slice(0, -1).join(' ')
- // last rule is used as a cache
- const lastRule = existingGlobalRules[existingGlobalRules.length - 1]
- lastRule.cache = lastRule.cache || {}
+ // TODO: DEFAULT TEXT COLOR
+ const bg = cache[lowerLevelSelector]?.background || convert('#FFFFFF').rgb
- if (directives.background) {
- const rgb = convert(findColor(directives.background)).rgb
+ console.log('SELECTOR', lowerLevelSelector)
- // TODO: DEFAULT TEXT COLOR
- const bg = findLowerLevelRule(parent)?.cache.background || convert('#FFFFFF').rgb
+ const rgb = convert(findColor(computedDirectives.background, inheritedBackground, cache[lowerLevelSelector].background)).rgb
- if (!lastRule.cache.background) {
- const blend = directives.opacity < 1 ? alphaBlend(rgb, directives.opacity, bg) : rgb
- lastRule.cache.background = blend
+ if (!cache[selector].background) {
+ const blend = computedDirectives.opacity < 1 ? alphaBlend(rgb, computedDirectives.opacity, bg) : rgb
+ cache[selector].background = blend
+ computed[selector].background = rgb
- needRuleAdd = true
- }
- }
-
- if (needRuleAdd) {
- addRule(lastRule)
+ 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 {
raw: rules,
css: rules.map(rule => {
- if (rule.virtual) return ''
+ // if (rule.virtual) return ''
let selector = ruleToSelector(rule).replace(/\/\*.*\*\//g, '')
if (!selector) {
@@ -356,22 +488,38 @@ export const init = (extraRuleset, palette) => {
return ' ' + k + ': ' + v
}).join(';\n')
- const directives = Object.entries(rule.directives).map(([k, v]) => {
- switch (k) {
- case 'background': {
- return 'background-color: ' + rgba2css({ ...convert(findColor(v)).rgb, a: rule.directives.opacity ?? 1 })
- }
- case 'textColor': {
- return 'color: ' + v
+ let directives
+ if (rule.component !== 'Root') {
+ directives = Object.entries(rule.directives).map(([k, v]) => {
+ switch (k) {
+ case 'shadow': {
+ return ' ' + [
+ '--shadow: ' + getCssShadow(v),
+ '--shadowFilter: ' + getCssShadowFilter(v),
+ '--shadowInset: ' + getCssShadow(v, true)
+ ].join(';\n ')
+ }
+ case 'background': {
+ const color = cssColorString(computed[ruleToSelector(rule, true)].background, rule.directives.opacity)
+ return [
+ 'background-color: ' + color,
+ ' --background: ' + color
+ ].join(';\n')
+ }
+ case 'textColor': {
+ return 'color: ' + v
+ }
+ default: return ''
}
- default: return ''
- }
- }).filter(x => x).map(x => ' ' + x).join(';\n')
+ }).filter(x => x).map(x => ' ' + x).join(';\n')
+ } else {
+ directives = {}
+ }
return [
header,
directives + ';',
- ' color: var(--text);',
+ !rule.virtual ? ' color: var(--text);' : '',
'',
virtualDirectives,
footer
diff --git a/top_bar.style.js b/top_bar.style.js
@@ -0,0 +1,9 @@
+export default {
+ name: 'TopBar',
+ selector: 'nav',
+ validInnerComponents: [
+ 'Link',
+ 'Text',
+ 'Icon'
+ ]
+}