master-co / css

The CSS Language and Framework

Home Page:https://css.master.co

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

✨ Layers

1aron opened this issue · comments

commented

Description

🚧 Under internal discussion

開發 Master UI 第一個 Button 元件後我意識到沒有透過 CSS Layers 管理優先級會導致未預期的樣式覆蓋並使 config.styles 無法支援選取器及媒體查詢。

假設 master.css.js:

export default {
    styles: {
        btn: 'inline-flex p:4x bg:white:focus bg:black:hover h:32@sm'
    }
}

預期重構後的 style.sheet

@layer base, preset, theme, style, utility;
@layer theme {
    :root { --text-strong: 0 0 0 }
    .light { --text-strong: 24 32 48 }
    .light { --text-light: 95 115 149 }
    .dark { --text-strong: 239 238 240 }
    .dark { --text-light: 137 136 138 }
}
@layer style {
    .btn { display: inline-flex }               /* inline-flex */
    .btn { padding: 1rem }                      /* p:4x */
    .btn:hover { background-color: #000000 }    /* bg:black:hover */
    .btn:focus { background-color: #ffffff }    /* bg:white:focus */
    @media (min-width: 834px) {                 /* h:32@sm */
        .btn { height: 2rem; }
    }
}
@layer utility {
    .p\:4x { padding: 1rem }
    .pt\:8x { padding-top: 2rem }
}
/* anonymous layer */
@keyframes fade-in {}

產生的 style.sheet.cssRules

[
    CSSLayerStatementRule,
    CSSLayerBlockRule { name: 'theme'  },
    CSSLayerBlockRule { name: 'style'  },
    CSSLayerBlockRule { name: 'utility'  },
    CSSKeyframesRule { name: 'fade'  },
    CSSKeyframesRule { name: 'flash'  },
]
  • @layer base: 無需操作,通常給 @master/normal.css 使用
  • @layer theme: variables
  • @layer style: styles
  • @layer utility: rules
  • CSSLayerBlockRule.cssRules 可以直接插入

實作

  • @keyframes 隨意插入至匿名層
  • css.hasKeyframesRule 相關邏輯不再需要,包含 variable
  • enum Layer 應改為 enum SyntaxType,它壓根就不是 layer,相關變數也要跟著改
  • config.rules 重新命名為 config.syntaxes
  • 固定插入第一個陳述 @layer base, preset, theme, style, utility;
  • css.Rules 可以重命名為 css.syntaxes

全新 class LayerRule

現在引入 Layer 概念就必須創建 class Layer 形成多個封閉區來排序各自的 rules

// core.ts
class MasterCSS {
    layerStatementRule = new Rule('layer-statement', [
        { text: '@layer base,preset,theme,style,utility' }
    ], this);
    themeLayer = new Layer('theme', this);
    styleLayer = new SyntaxLayer('style', this);
    utilityLayer = new SyntaxLayer('utility', this);
    keyframeLayer = new AnonymousLayer('keyframe', this);
    sheet = new AnonymousLayer('', this);
    constructor() {
        this.sheet.rules = [
            this.layerStatementRule,
            this.themeLayer,
            this.styleLayer,
            this.utilityLayer,
            this.keyframeLayer
        ]
    }
    get text(): string {
        return this.sheet.text
    }
}
// layer.ts
class Layer {
    native?: CSSLayerBlockRule | CSSStyleSheet
    rules: Rule[] = []
    usages: Record<string, number>
    constructor(
        public name: string,
        public css: MasterCSS
    ) {}
    insert(rule: Rule, index?: number) {}
    delete(rule: Rule) {}
    get text(): string {
        return '@layer ' + this.name + '{' + this.rules.map((eachRule) => eachRule.text).join('') + '}'
    }
}
  • layer.insert() 實作簡易的 this.rules.push(rule)
    • 透過 index 可以插入指定的位置,實際會用到的像是在 anonymousLayer 的頂部插入 CSSLayerStatementRule
  • layer.delete() 實作簡易的 this.rules.splice(this.rules.indexOf(rule), 1)
  • 執行插入及刪除時必須記錄 usages
    • 舉例 themeLayer.usages 的 keys 可能是 --text-purple
    • 舉例 styleLayer.usages 的 keys 可能是 btn
    • 舉例 utilityLayer.usages 的 keys 可能是 fg:white
    • 舉例 keyframeLayer.usages 的 keys 可能是 fade
// anonymous-layer.ts
class AnonymousLayer extends Layer {
    native?: CSSStyleSheet
    rules: (Rule | Layer)[] = []
    constructor(
        public name: string,
        public css: MasterCSS
    ) {}
    get text(): string {
        return this.rules.map((eachRule) => eachRule.text).join('')
    }
}
  • 匿名層主要用來連接 CSSStyleSheet 並方便統一操縱
// syntax-layer.ts
class SyntaxLayer extends Layer {
    native?: CSSLayerBlockRule
    constructor(
        public name: string,
        public css: MasterCSS
    ) {
        super()
    }
    insert(rule: Rule) {}
    delete(rule: Rule) {}
}
  • css.insert() 應改寫為 syntaxLayer.insert()
  • css.delete() 內的 deleteRule 函數應改寫為 syntaxLayer.delete()
  • css.delete() 重新命名為 css.remove() 以對應 css.add()
  • css.add() 時判斷該 className 屬於哪個 style 還是 utility layer

重構 Rule

// rule.ts
class Rule {
    constructor(
        public readonly name: string,
        public natives: NativeRule[] = [],
        public css: MasterCSS
    ) {}
    get text(): string {
        return this.natives.map((eachNative) => eachNative.text).join('')
    }
}
// syntax-rule.ts
class SyntaxRule extends Rule {
    constructor(
        public readonly name: string,
        public natives: NativeRule[] = [],
        public css: MasterCSS,
        public readonly RegisteredRule: RegisteredRule,
    ) {
        super(name, layer, natives)
    }
}
  • Rule 重新命名為 SyntaxRule 並調整 constructor 傳入參數
  • Rule 改為一個共通類
    • SyntaxRule 擴展
    • 創建 VariableRule extends Rule 並將相關的 methods 整併
    • 創建 KeyframeRule extends Rule 並將相關的 methods 整併

@layer theme

  • 主題變數統一放在 @layer theme {},這樣可以更容易地與其他規則分開。
  • 特別注意的是 :root 相關變數必須從 0 插入,避免出現在 .light .dark 之後
  • 目前 --variable 以單個 .light 包含所有變數,一個 class/host/media 只能包含一個 --variable,這才能對單一變數進行新增/刪除,而不是 PUT 整個規則導致額外的開銷。

@layer style

  • 一個 style class 會對應多個 layers[].rules
  • 建立 rule.fixedClass 用來固定為目標 class 名稱。像原本是 .bg\:black\:hover:hover {} 要固定為 .btn:hover {}

解決問題

  • 使用 utility 修飾 style 不需要再加 !btn font:semibold! -> btn font:semibold
  • config.styles 支援 at-rules。btn-sm@<sm