diff --git a/src/app/atoms/badge/NotificationBadge.scss b/src/app/atoms/badge/NotificationBadge.scss index c876c37..c672b11 100644 --- a/src/app/atoms/badge/NotificationBadge.scss +++ b/src/app/atoms/badge/NotificationBadge.scss @@ -12,7 +12,7 @@ } &--alert { - background-color: var(--bg-danger); + background-color: var(--bg-positive); & .text { color: white } } diff --git a/src/app/molecules/room-selector/RoomSelector.jsx b/src/app/molecules/room-selector/RoomSelector.jsx index 7e7f277..47201a6 100644 --- a/src/app/molecules/room-selector/RoomSelector.jsx +++ b/src/app/molecules/room-selector/RoomSelector.jsx @@ -84,7 +84,10 @@ RoomSelector.propTypes = { iconSrc: PropTypes.string, isSelected: PropTypes.bool, isUnread: PropTypes.bool.isRequired, - notificationCount: PropTypes.number.isRequired, + notificationCount: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, isAlert: PropTypes.bool.isRequired, options: PropTypes.node, onClick: PropTypes.func.isRequired, diff --git a/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx b/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx index 8d57e9b..882c00c 100644 --- a/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx +++ b/src/app/molecules/sidebar-avatar/SidebarAvatar.jsx @@ -2,29 +2,22 @@ import React from 'react'; import PropTypes from 'prop-types'; import './SidebarAvatar.scss'; -import Tippy from '@tippyjs/react'; import Avatar from '../../atoms/avatar/Avatar'; import Text from '../../atoms/text/Text'; +import Tooltip from '../../atoms/tooltip/Tooltip'; import NotificationBadge from '../../atoms/badge/NotificationBadge'; import { blurOnBubbling } from '../../atoms/button/script'; const SidebarAvatar = React.forwardRef(({ tooltip, text, bgColor, imageSrc, - iconSrc, active, onClick, notifyCount, + iconSrc, active, onClick, isUnread, notificationCount, isAlert, }, ref) => { let activeClass = ''; if (active) activeClass = ' sidebar-avatar--active'; return ( - {tooltip}} - className="sidebar-avatar-tippy" - touch="hold" - arrow={false} placement="right" - maxWidth={200} - delay={[0, 0]} - duration={[100, 0]} - offset={[0, 0]} > - + ); }); SidebarAvatar.defaultProps = { @@ -52,7 +50,9 @@ SidebarAvatar.defaultProps = { imageSrc: null, active: false, onClick: null, - notifyCount: null, + isUnread: false, + notificationCount: 0, + isAlert: false, }; SidebarAvatar.propTypes = { @@ -63,10 +63,12 @@ SidebarAvatar.propTypes = { iconSrc: PropTypes.string, active: PropTypes.bool, onClick: PropTypes.func, - notifyCount: PropTypes.oneOfType([ + isUnread: PropTypes.bool, + notificationCount: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, ]), + isAlert: PropTypes.bool, }; export default SidebarAvatar; diff --git a/src/app/molecules/sidebar-avatar/SidebarAvatar.scss b/src/app/molecules/sidebar-avatar/SidebarAvatar.scss index 6191735..3f445df 100644 --- a/src/app/molecules/sidebar-avatar/SidebarAvatar.scss +++ b/src/app/molecules/sidebar-avatar/SidebarAvatar.scss @@ -1,28 +1,18 @@ - -.sidebar-avatar-tippy { - padding: var(--sp-extra-tight) var(--sp-normal); - background-color: var(--bg-tooltip); - border-radius: var(--bo-radius); - box-shadow: var(--bs-popup); - - .text { - color: var(--tc-tooltip); - } -} - .sidebar-avatar { position: relative; display: flex; justify-content: center; align-items: center; - width: 100%; cursor: pointer; & .notification-badge { position: absolute; - right: var(--sp-extra-tight); - top: calc(-1 * var(--sp-ultra-tight)); + right: 0; + top: 0; box-shadow: 0 0 0 2px var(--bg-surface-low); + transform: translate(20%, -20%); + + margin: 0 !important; } &:focus { outline: none; @@ -37,7 +27,7 @@ content: ""; display: block; position: absolute; - left: 0; + left: -11px; top: 50%; transform: translateY(-50%); @@ -48,7 +38,8 @@ transition: height 200ms linear; [dir=rtl] & { - right: 0; + left: unset; + right: -11px; border-radius: 4px 0 0 4px; } } diff --git a/src/app/organisms/navigation/Directs.jsx b/src/app/organisms/navigation/Directs.jsx index a907980..639f4cd 100644 --- a/src/app/organisms/navigation/Directs.jsx +++ b/src/app/organisms/navigation/Directs.jsx @@ -12,7 +12,7 @@ import { AtoZ } from './common'; const drawerPostie = new Postie(); function Directs() { - const { roomList } = initMatrix; + const { roomList, notifications } = initMatrix; const directIds = [...roomList.directs].sort(AtoZ); const [, forceUpdate] = useState({}); @@ -26,10 +26,11 @@ function Directs() { drawerPostie.post('selector-change', addresses, selectedRoomId); } - function unreadChanged(roomId) { - if (!drawerPostie.hasTopic('unread-change')) return; - if (!drawerPostie.hasSubscriber('unread-change', roomId)) return; - drawerPostie.post('unread-change', roomId); + function notiChanged(roomId, total, prevTotal) { + if (total === prevTotal) return; + if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) { + drawerPostie.post('unread-change', roomId); + } } function roomListUpdated() { @@ -47,13 +48,11 @@ function Directs() { useEffect(() => { roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); - roomList.on(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); - roomList.on(cons.events.roomList.EVENT_ARRIVED, unreadChanged); + notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged); return () => { roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); - roomList.removeListener(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); - roomList.removeListener(cons.events.roomList.EVENT_ARRIVED, unreadChanged); + notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged); }; }, []); diff --git a/src/app/organisms/navigation/DrawerBreadcrumb.jsx b/src/app/organisms/navigation/DrawerBreadcrumb.jsx index e784362..7eaae4e 100644 --- a/src/app/organisms/navigation/DrawerBreadcrumb.jsx +++ b/src/app/organisms/navigation/DrawerBreadcrumb.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import './DrawerBreadcrumb.scss'; @@ -6,51 +6,108 @@ import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import { selectSpace } from '../../../client/action/navigation'; import navigation from '../../../client/state/navigation'; +import { abbreviateNumber } from '../../../util/common'; import Text from '../../atoms/text/Text'; import RawIcon from '../../atoms/system-icons/RawIcon'; import Button from '../../atoms/button/Button'; import ScrollView from '../../atoms/scroll/ScrollView'; +import NotificationBadge from '../../atoms/badge/NotificationBadge'; import ChevronRightIC from '../../../../public/res/ic/outlined/chevron-right.svg'; function DrawerBreadcrumb({ spaceId }) { + const [, forceUpdate] = useState({}); const scrollRef = useRef(null); + const { roomList, notifications } = initMatrix; const mx = initMatrix.matrixClient; const spacePath = navigation.selectedSpacePath; + function onNotiChanged(roomId, total, prevTotal) { + if (total === prevTotal) return; + if (navigation.selectedSpacePath.includes(roomId)) { + forceUpdate({}); + } + if (navigation.selectedSpacePath[0] === cons.tabs.HOME) { + if (!roomList.isOrphan(roomId)) return; + if (roomList.directs.has(roomId)) return; + forceUpdate({}); + } + } + useEffect(() => { requestAnimationFrame(() => { if (scrollRef?.current === null) return; scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; }); + notifications.on(cons.events.notifications.NOTI_CHANGED, onNotiChanged); + return () => { + notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotiChanged); + }; }, [spaceId]); if (spacePath.length === 1) return null; + function getHomeNotiExcept(childId) { + const orphans = roomList.getOrphans(); + const childIndex = orphans.indexOf(childId); + if (childId !== -1) orphans.splice(childIndex, 1); + + let noti = null; + + orphans.forEach((roomId) => { + if (!notifications.hasNoti(roomId)) return; + if (noti === null) noti = { total: 0, highlight: 0 }; + const childNoti = notifications.getNoti(roomId); + noti.total += childNoti.total; + noti.highlight += childNoti.highlight; + }); + + return noti; + } + + function getNotiExcept(roomId, childId) { + if (!notifications.hasNoti(roomId)) return null; + + const noti = notifications.getNoti(roomId); + if (!notifications.hasNoti(childId)) return noti; + if (noti.from === null) return noti; + if (noti.from.has(childId) && noti.from.size === 1) return null; + + const childNoti = notifications.getNoti(childId); + + return { + total: noti.total - childNoti.total, + highlight: noti.highlight - childNoti.highlight, + }; + } + return (
{ spacePath.map((id, index) => { - if (index === 0) { - return ( - - ); - } + const noti = (id !== cons.tabs.HOME && index < spacePath.length) + ? getNotiExcept(id, (index === spacePath.length - 1) ? null : spacePath[index + 1]) + : getHomeNotiExcept((index === spacePath.length - 1) ? null : spacePath[index + 1]); + return ( - + { index !== 0 && } ); diff --git a/src/app/organisms/navigation/DrawerBreadcrumb.scss b/src/app/organisms/navigation/DrawerBreadcrumb.scss index 80262a9..60cd47f 100644 --- a/src/app/organisms/navigation/DrawerBreadcrumb.scss +++ b/src/app/organisms/navigation/DrawerBreadcrumb.scss @@ -51,6 +51,14 @@ overflow: hidden; text-overflow: ellipsis; } + + & .notification-badge { + margin-left: var(--sp-extra-tight); + [dir=rtl] & { + margin-left: 0; + margin-right: var(--sp-extra-tight); + } + } } &__btn--selected { diff --git a/src/app/organisms/navigation/Home.jsx b/src/app/organisms/navigation/Home.jsx index 120ceb7..2c505f7 100644 --- a/src/app/organisms/navigation/Home.jsx +++ b/src/app/organisms/navigation/Home.jsx @@ -15,7 +15,7 @@ import { AtoZ } from './common'; const drawerPostie = new Postie(); function Home({ spaceId }) { const [, forceUpdate] = useState({}); - const { roomList } = initMatrix; + const { roomList, notifications } = initMatrix; let spaceIds = []; let roomIds = []; let directIds = []; @@ -40,10 +40,11 @@ function Home({ spaceId }) { if (addresses.length === 0) return; drawerPostie.post('selector-change', addresses, selectedRoomId); } - function unreadChanged(roomId) { - if (!drawerPostie.hasTopic('unread-change')) return; - if (!drawerPostie.hasSubscriber('unread-change', roomId)) return; - drawerPostie.post('unread-change', roomId); + function notiChanged(roomId, total, prevTotal) { + if (total === prevTotal) return; + if (drawerPostie.hasTopicAndSubscriber('unread-change', roomId)) { + drawerPostie.post('unread-change', roomId); + } } function roomListUpdated() { @@ -61,13 +62,11 @@ function Home({ spaceId }) { useEffect(() => { roomList.on(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); - roomList.on(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); - roomList.on(cons.events.roomList.EVENT_ARRIVED, unreadChanged); + notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged); return () => { roomList.removeListener(cons.events.roomList.ROOMLIST_UPDATED, roomListUpdated); navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); - roomList.removeListener(cons.events.roomList.MY_RECEIPT_ARRIVED, unreadChanged); - roomList.removeListener(cons.events.roomList.EVENT_ARRIVED, unreadChanged); + notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged); }; }, []); diff --git a/src/app/organisms/navigation/Selector.jsx b/src/app/organisms/navigation/Selector.jsx index 1a47a57..7ec7c0b 100644 --- a/src/app/organisms/navigation/Selector.jsx +++ b/src/app/organisms/navigation/Selector.jsx @@ -3,11 +3,10 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; import initMatrix from '../../../client/initMatrix'; -import { doesRoomHaveUnread } from '../../../util/matrixUtil'; import navigation from '../../../client/state/navigation'; import { openRoomOptions } from '../../../client/action/navigation'; import { createSpaceShortcut, deleteSpaceShortcut } from '../../../client/action/room'; -import { getEventCords } from '../../../util/common'; +import { getEventCords, abbreviateNumber } from '../../../util/common'; import IconButton from '../../atoms/button/IconButton'; import RoomSelector from '../../molecules/room-selector/RoomSelector'; @@ -24,6 +23,7 @@ function Selector({ roomId, isDM, drawerPostie, onClick, }) { const mx = initMatrix.matrixClient; + const noti = initMatrix.notifications; const room = mx.getRoom(roomId); let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; @@ -54,9 +54,9 @@ function Selector({ name={room.name} roomId={roomId} iconSrc={room.getJoinRule() === 'invite' ? SpaceLockIC : SpaceIC} - isUnread={doesRoomHaveUnread(room)} - notificationCount={room.getUnreadNotificationCount('total') || 0} - isAlert={room.getUnreadNotificationCount('highlight') !== 0} + isUnread={noti.hasNoti(roomId)} + notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))} + isAlert={noti.getHighlightNoti(roomId) !== 0} onClick={onClick} options={( roomList.inviteRooms.size + roomList.inviteSpaces.size @@ -74,33 +75,83 @@ function SideBar() { function onSpaceShortcutUpdated() { forceUpdate({}); } + function onNotificationChanged(roomId, total, prevTotal) { + if (total === prevTotal) return; + forceUpdate({}); + } useEffect(() => { navigation.on(cons.events.navigation.TAB_SELECTED, onTabSelected); roomList.on(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated); - initMatrix.roomList.on( - cons.events.roomList.INVITELIST_UPDATED, - onInviteListChange, - ); + roomList.on(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange); + notifications.on(cons.events.notifications.NOTI_CHANGED, onNotificationChanged); return () => { navigation.removeListener(cons.events.navigation.TAB_SELECTED, onTabSelected); roomList.removeListener(cons.events.roomList.SPACE_SHORTCUT_UPDATED, onSpaceShortcutUpdated); - initMatrix.roomList.removeListener( - cons.events.roomList.INVITELIST_UPDATED, - onInviteListChange, - ); + roomList.removeListener(cons.events.roomList.INVITELIST_UPDATED, onInviteListChange); + notifications.removeListener(cons.events.notifications.NOTI_CHANGED, onNotificationChanged); }; }, []); + function getHomeNoti() { + const orphans = roomList.getOrphans(); + let noti = null; + + orphans.forEach((roomId) => { + if (!notifications.hasNoti(roomId)) return; + if (noti === null) noti = { total: 0, highlight: 0 }; + const childNoti = notifications.getNoti(roomId); + noti.total += childNoti.total; + noti.highlight += childNoti.highlight; + }); + + return noti; + } + function getDMsNoti() { + if (roomList.directs.size === 0) return null; + let noti = null; + + [...roomList.directs].forEach((roomId) => { + if (!notifications.hasNoti(roomId)) return; + if (noti === null) noti = { total: 0, highlight: 0 }; + const childNoti = notifications.getNoti(roomId); + noti.total += childNoti.total; + noti.highlight += childNoti.highlight; + }); + + return noti; + } + + // TODO: bellow operations are heavy. + // refactor this component into more smaller components. + const dmsNoti = getDMsNoti(); + const homeNoti = getHomeNoti(); + return (
- selectTab(cons.tabs.HOME)} tooltip="Home" iconSrc={HomeIC} /> - selectTab(cons.tabs.DIRECTS)} tooltip="People" iconSrc={UserIC} /> + selectTab(cons.tabs.HOME)} + tooltip="Home" + iconSrc={HomeIC} + isUnread={homeNoti !== null} + notificationCount={homeNoti !== null ? abbreviateNumber(homeNoti.total) : 0} + isAlert={homeNoti?.highlight > 0} + /> + selectTab(cons.tabs.DIRECTS)} + tooltip="People" + iconSrc={UserIC} + isUnread={dmsNoti !== null} + notificationCount={dmsNoti !== null ? abbreviateNumber(dmsNoti.total) : 0} + isAlert={dmsNoti?.highlight > 0} + /> openPublicRooms()} tooltip="Public rooms" iconSrc={HashSearchIC} />
@@ -117,6 +168,9 @@ function SideBar() { bgColor={colorMXID(room.roomId)} imageSrc={room.getAvatarUrl(initMatrix.matrixClient.baseUrl, 42, 42, 'crop') || null} text={room.name.slice(0, 1)} + isUnread={notifications.hasNoti(sRoomId)} + notificationCount={abbreviateNumber(notifications.getTotalNoti(sRoomId))} + isAlert={notifications.getHighlightNoti(sRoomId) !== 0} onClick={() => selectTab(shortcut)} /> ); @@ -131,7 +185,9 @@ function SideBar() {
{ totalInvites !== 0 && ( openInviteList()} tooltip="Invites" iconSrc={InviteIC} diff --git a/src/client/state/Notifications.js b/src/client/state/Notifications.js index f3b56ac..f5ecce2 100644 --- a/src/client/state/Notifications.js +++ b/src/client/state/Notifications.js @@ -58,11 +58,27 @@ class Notifications extends EventEmitter { return this.roomIdToNoti.get(roomId) || { total: 0, highlight: 0, from: null }; } + getTotalNoti(roomId) { + const { total } = this.getNoti(roomId); + return total; + } + + getHighlightNoti(roomId) { + const { highlight } = this.getNoti(roomId); + return highlight; + } + + getFromNoti(roomId) { + const { from } = this.getNoti(roomId); + return from; + } + hasNoti(roomId) { return this.roomIdToNoti.has(roomId); } _setNoti(roomId, total, highlight, childId) { + const prevTotal = this.roomIdToNoti.get(roomId)?.total ?? null; const noti = this.getNoti(roomId); noti.total += total; @@ -73,7 +89,7 @@ class Notifications extends EventEmitter { } this.roomIdToNoti.set(roomId, noti); - this.emit(cons.events.notification.NOTI_CHANGED, roomId); + this.emit(cons.events.notifications.NOTI_CHANGED, roomId, noti.total, prevTotal); const parentIds = this.roomList.roomIdToParents.get(roomId); if (typeof parentIds === 'undefined') return; @@ -84,6 +100,7 @@ class Notifications extends EventEmitter { if (this.roomIdToNoti.has(roomId) === false) return; const noti = this.getNoti(roomId); + const prevTotal = noti.total; noti.total -= total; noti.highlight -= highlight; if (childId && noti.from !== null) { @@ -91,10 +108,11 @@ class Notifications extends EventEmitter { } if (noti.from === null || noti.from.size === 0) { this.roomIdToNoti.delete(roomId); - this.emit(cons.events.notification.FULL_READ, roomId); + this.emit(cons.events.notifications.FULL_READ, roomId); + this.emit(cons.events.notifications.NOTI_CHANGED, roomId, null, prevTotal); } else { this.roomIdToNoti.set(roomId, noti); - this.emit(cons.events.notification.NOTI_CHANGED, roomId); + this.emit(cons.events.notifications.NOTI_CHANGED, roomId, noti.total, prevTotal); } const parentIds = this.roomList.roomIdToParents.get(roomId); @@ -120,8 +138,6 @@ class Notifications extends EventEmitter { this.matrixClient.on('Room.receipt', (mEvent, room) => { if (mEvent.getType() === 'm.receipt') { - if (typeof mEvent.event.room_id === 'string') return; - const content = mEvent.getContent(); const readedEventId = Object.keys(content)[0]; const readerUserId = Object.keys(content[readedEventId]['m.read'])[0]; diff --git a/src/client/state/RoomList.js b/src/client/state/RoomList.js index c6b6f86..b746a46 100644 --- a/src/client/state/RoomList.js +++ b/src/client/state/RoomList.js @@ -41,6 +41,15 @@ class RoomList extends EventEmitter { this.matrixClient.setAccountData(cons['in.cinny.spaces'], spaceContent); } + isOrphan(roomId) { + return !this.roomIdToParents.has(roomId); + } + + getOrphans() { + const rooms = [...this.spaces].concat([...this.rooms]); + return rooms.filter((roomId) => !this.roomIdToParents.has(roomId)); + } + getSpaceChildren(roomId) { const space = this.matrixClient.getRoom(roomId); const mSpaceChild = space?.currentState.getStateEvents('m.space.child'); @@ -254,15 +263,6 @@ class RoomList extends EventEmitter { this.matrixClient.on('Room.name', () => { this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); - this.matrixClient.on('Room.receipt', (event, room) => { - if (event.getType() === 'm.receipt') { - const content = event.getContent(); - const userReadEventId = Object.keys(content)[0]; - const eventReaderUserId = Object.keys(content[userReadEventId]['m.read'])[0]; - if (eventReaderUserId !== this.matrixClient.getUserId()) return; - this.emit(cons.events.roomList.MY_RECEIPT_ARRIVED, room.roomId); - } - }); this.matrixClient.on('RoomState.events', (mEvent) => { if (mEvent.getType() === 'm.space.child') { @@ -387,16 +387,6 @@ class RoomList extends EventEmitter { } this.emit(cons.events.roomList.ROOMLIST_UPDATED); }); - - this.matrixClient.on('Room.timeline', (event, room) => { - const supportEvents = ['m.room.message', 'm.room.encrypted', 'm.sticker']; - if (!supportEvents.includes(event.getType())) return; - - const lastTimelineEvent = room.timeline[room.timeline.length - 1]; - if (lastTimelineEvent.getId() !== event.getId()) return; - if (event.getSender() === this.matrixClient.getUserId()) return; - this.emit(cons.events.roomList.EVENT_ARRIVED, room.roomId); - }); } } export default RoomList; diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 6211348..fee81b5 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -67,11 +67,9 @@ const cons = { ROOM_JOINED: 'ROOM_JOINED', ROOM_LEAVED: 'ROOM_LEAVED', ROOM_CREATED: 'ROOM_CREATED', - MY_RECEIPT_ARRIVED: 'MY_RECEIPT_ARRIVED', - EVENT_ARRIVED: 'EVENT_ARRIVED', SPACE_SHORTCUT_UPDATED: 'SPACE_SHORTCUT_UPDATED', }, - notification: { + notifications: { NOTI_CHANGED: 'NOTI_CHANGED', FULL_READ: 'FULL_READ', }, diff --git a/src/index.scss b/src/index.scss index 552e9b0..77261e5 100644 --- a/src/index.scss +++ b/src/index.scss @@ -220,6 +220,8 @@ --bg-surface-low: hsl(64, 6%, 10%); --bg-surface-low-transparent: hsla(64, 6%, 14%, 0); + --bg-badge: #c4c1ab; + /* text color | --tc-[background type]-[priority]: value */ --tc-surface-high: rgb(255, 251, 222, 94%);