昨天用 map 與 @each 組合出各個主題的 CSS 顏色變數,再用這些變數來寫主題樣式。正當我們高高興興地要回城,卻被設計稿的更動 Gank 了:部分主題樣式的用色規律跟其他主題不同,有些甚至用了新顏色。
當然,可以把特例直接寫在 .vue、.jsx 元件內。但主題樣式在專案中很多地方都會用到,一個個設定的話,特例會散落在各個元件裡,顏色也不再集中在 map。
今天,我們接著擴寫主題檔,讓有特例的元件不再讀「什麼顏色」,而是讀「用在哪裡」。那麼開始吧!
處理跳色問題,我的想法是:在主題 map 裡,為跳色的元件增加以「用途」為名的 key。這樣從名稱就能看出哪些元件的用色不同,也能同時處理兩種跳色:
以選項按鈕 btn-option-bg、卡片背景 card-bg 為例:
$themes: (
coffee: (
primary: (100: #ffe9d2, 500: #ff7300),
accent: (100: #fff1db),
btn-option-bg: #ff7300,
card-bg: #fff1db,
),
matcha: (
primary: (100: #ddfee7, 500: #10c659),
accent: (100: #ecffcf),
btn-option-bg: #10c659,
card-bg: #fcefbd, // 跳色:色階外的顏色
),
);
// 要輸出成 --theme-<key> 的語意色票
$var-keys: (btn-option-bg, card-bg);
$theme-default 也要補上這兩個 key。接著在 theme-vars 的最後,把語意色票輸出成變數:
@mixin theme-vars($name, $theme) {
// ...Day 14 的必填色階檢查
@each $key in $var-keys {
--theme-#{$key}: #{map.get($theme, $key)};
}
}
編譯後(節錄 matcha):
.theme-container.theme-matcha {
/* ...色階變數 */
--theme-btn-option-bg: #10c659;
--theme-card-bg: #fcefbd;
}
跳色元件改讀語意變數:
.theme-container {
.theme-btn-option { background-color: var(--theme-btn-option-bg); }
.theme-card { background-color: var(--theme-card-bg); }
}
語意色票解決了跳色,但產生了新問題:語意色票得再手寫一次色階的色碼,例如 btn-option-bg: #10c659 其實就是 matcha 的 primary 500。同一個顏色有兩個來源,之後要改色時,還得記得兩邊一起改,嘖嘖。
同樣的事不幹第二次,所以要讓語意色票也能接受「色系 色階」的寫法,讓它參照自己主題的色階:
matcha: (
primary: (100: #ddfee7, 500: #10c659),
// ...
btn-option-bg: primary 500,
// ...
),
💡
primary 500中間用空格隔開,在 Sass 裡會被視為一個 list。
接下來將 $theme 傳入自訂的轉換函式 resolve-theme(),一一檢查每個 value,如果遇到 list 就換成對應的實際色碼,其他值則直接寫入:
@use "sass:list";
@use "sass:meta";
@function resolve-theme($name, $theme) {
$resolved: (); // 用來儲存解析結果的 map
@each $key, $value in $theme {
@if meta.type-of($value) == list {
// 從該主題的 map 中取得對應的色碼
$value: map.get($theme, list.nth($value, 1), list.nth($value, 2));
}
$resolved: map.set($resolved, $key, $value);
}
@return $resolved;
}
💡 SCSS 內建函式回傳了什麼?
meta.type-of()回傳類型:#ff7300會回傳color;primary 500則會回傳listlist.nth(list, 順序)回傳指定順序的值:list.nth(primary 500, 1)會回傳primary;list.nth(primary 500, 2)則會回傳500
回到 theme-vars 裡,檢查完必填色階後,先把主題解析一次再輸出:
@mixin theme-vars($name, $theme) {
// ...Day 14 的必填色階檢查
$theme: resolve-theme($name, $theme);
@each $key in $var-keys {
// ...
}
}
Day 14 已經檢查了必填色階,現在多了語意色票和參照,也一起加入檢查:
@mixin theme-vars($name, $theme) {
// ...Day 14 的必填色階檢查
// 必填色票
@each $key in $var-keys {
@if not map.has-key($theme, $key) {
@error "主題 #{$name} 缺少色票 #{$key}";
}
}
$theme: resolve-theme($name, $theme);
// ...輸出
}
參照是否存在,則是在 resolve-theme() 裡、換成色碼之前檢查:
@if meta.type-of($value) == list {
@if not map.has-key($theme, list.nth($value, 1), list.nth($value, 2)) {
@error "主題#{$name} 的 #{$key} 參照了不存在的色階:#{$value}";
}
$value: map.get($theme, list.nth($value, 1), list.nth($value, 2));
}
實際觸發的錯誤訊息:
| 情境 | 錯誤訊息 |
|---|---|
coffee 沒寫 btn-option-bg |
主題 coffee 缺少色票 btn-option-bg |
matcha 寫成 card-bg: accent 5000 |
主題 matcha 的 card-bg 參照了不存在的色階:accent 5000 |
.
.
.
在這個架構下:
$themes 色票設定。.theme- 開頭的主題樣式只要寫一次,之後基本上不用再改。一天又平安地過去了,感謝 SCSS 的努力!
純 CSS 能解決的主題樣式,介紹到這邊差不多告一段落。明天,來看看那些不得不用圖片處理的樣式吧!