refactor: zen modal implemention and fix macos traffic light overlay

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2024-11-05 21:17:28 +08:00
parent eaa5699661
commit 51a4bd273e
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
22 changed files with 251 additions and 106 deletions

View File

@ -1,3 +1,4 @@
import { callWindowExpose } from "@follow/shared/bridge"
import { dispatchEventOnWindow } from "@follow/shared/event"
import { name } from "@pkg"
import type { BrowserWindow, MenuItem, MenuItemConstructorOptions } from "electron"
@ -130,6 +131,18 @@ export const registerAppMenu = () => {
{ role: "zoomIn", label: t("menu.zoomIn") },
{ role: "zoomOut", label: t("menu.zoomOut") },
{ type: "separator" },
{
type: "normal",
label: t("menu.zenMode"),
accelerator: "Ctrl+Shift+Z",
click: () => {
const mainWindow = getMainWindow()
if (!mainWindow) return
const caller = callWindowExpose(mainWindow)
caller.zenMode()
},
},
{ role: "togglefullscreen", label: t("menu.toggleFullScreen") },
],
},

View File

@ -1,8 +1,8 @@
import { createSettingAtom } from "@follow/atoms/helper/setting.js"
import type { UISettings } from "@follow/shared/interface/settings"
import { atom, useAtomValue } from "jotai"
import { internal_feedColumnShowAtom } from "../sidebar"
import { jotaiStore } from "@follow/utils/jotai"
import { atom, useAtomValue, useSetAtom } from "jotai"
import { useEventCallback } from "usehooks-ts"
export const createDefaultSettings = (): UISettings => ({
// Sidebar
@ -42,6 +42,8 @@ export const createDefaultSettings = (): UISettings => ({
wideMode: false,
})
const zenModeAtom = atom(false)
export const {
useSettingKey: useUISettingKey,
useSettingSelector: useUISettingSelector,
@ -61,9 +63,28 @@ export const uiServerSyncWhiteListKeys: (keyof UISettings)[] = [
"customCSS",
]
const isZenModeAtom = atom((get) => {
const ui = get(__uiSettingAtom)
return ui.wideMode && !get(internal_feedColumnShowAtom)
})
export const useIsZenMode = () => useAtomValue(zenModeAtom)
export const getIsZenMode = () => jotaiStore.get(zenModeAtom)
export const useIsZenMode = () => useAtomValue(isZenModeAtom)
export const useSetZenMode = () => {
const setZenMode = useSetAtom(zenModeAtom)
return useEventCallback((checked: boolean) => {
setZenMode(checked)
})
}
export const useToggleZenMode = () => {
const setZenMode = useSetZenMode()
const isZenMode = useIsZenMode()
return useEventCallback(() => {
const newIsZenMode = !isZenMode
document.documentElement.dataset.zenMode = newIsZenMode.toString()
setZenMode(newIsZenMode)
})
}
export const useRealInWideMode = () => {
const wideMode = useUISettingKey("wideMode")
const isZenMode = useIsZenMode()
return wideMode || isZenMode
}

View File

@ -4,6 +4,8 @@ import { atom } from "jotai"
import { getRouteParams } from "~/hooks/biz/useRouteParams"
import { createAtomHooks } from "~/lib/jotai"
import { getIsZenMode, useIsZenMode } from "./settings/ui"
const defaultFeedView = FeedViewType.Articles
const viewAtom = atom<FeedViewType | -1>(-1)
const [
@ -48,14 +50,20 @@ viewAtom.onMount = () => {
setSidebarActiveView(view)
}
}
export const [
internal_feedColumnShowAtom,
,
useFeedColumnShow,
,
getFeedColumnShow,
setFeedColumnShow,
] = createAtomHooks(atom(true))
const [, , internal_useFeedColumnShow, , internal_getFeedColumnShow, setFeedColumnShow] =
createAtomHooks(atom(true))
export const useFeedColumnShow = () => {
const isZenMode = useIsZenMode()
return internal_useFeedColumnShow() && !isZenMode
}
export const getFeedColumnShow = () => {
const isZenMode = getIsZenMode()
return internal_getFeedColumnShow() && !isZenMode
}
export { setFeedColumnShow }
export const [, , useFeedColumnTempShow, , getFeedColumnTempShow, setFeedColumnTempShow] =
createAtomHooks(atom(false))

View File

@ -18,7 +18,7 @@ import {
} from "react"
import { useEventCallback } from "usehooks-ts"
import { useUISettingKey } from "~/atoms/settings/ui"
import { useRealInWideMode } from "~/atoms/settings/ui"
import {
useGetWrappedElementPosition,
useWrappedElementPosition,
@ -58,7 +58,7 @@ export const Toc: Component<TocProps> = ({ className, onItemClick }) => {
const renderContentElementPosition = useWrappedElementPosition()
const renderContentElementSize = useWrappedElementSize()
const entryContentInWideMode = useUISettingKey("wideMode")
const entryContentInWideMode = useRealInWideMode()
const shouldShowTitle = useViewport((v) => {
if (!entryContentInWideMode) return false
const { w } = v

View File

@ -32,6 +32,10 @@ export const shortcuts: Shortcuts = {
name: "keys.layout.toggleWideMode",
key: "Meta+[",
},
zenMode: {
name: "keys.layout.zenMode",
key: "Control+Shift+Z",
},
},
entries: {
refetch: {

View File

@ -20,8 +20,20 @@ initializeApp().finally(() => {
const $container = document.querySelector("#root") as HTMLElement
if (IN_ELECTRON && getOS() === "Windows") {
document.body.style.cssText += `--fo-window-padding-top: ${ElECTRON_CUSTOM_TITLEBAR_HEIGHT}px;`
if (IN_ELECTRON) {
const os = getOS()
switch (os) {
case "Windows": {
document.body.style.cssText += `--fo-window-padding-top: ${ElECTRON_CUSTOM_TITLEBAR_HEIGHT}px;`
break
}
case "macOS": {
document.body.style.cssText += `--fo-macos-traffic-light-width: 100px; --fo-macos-traffic-light-height: 30px;`
break
}
}
document.documentElement.dataset.os = getOS()
}
ReactDOM.createRoot($container).render(

View File

@ -1,3 +1,4 @@
import { MdiMeditation } from "@follow/components/icons/Meditation.jsx"
import { ActionButton } from "@follow/components/ui/button/index.js"
import { DividerVertical } from "@follow/components/ui/divider/index.js"
import { RotatingRefreshIcon } from "@follow/components/ui/loading/index.jsx"
@ -12,7 +13,13 @@ import * as React from "react"
import { useTranslation } from "react-i18next"
import { setGeneralSetting, useGeneralSettingKey } from "~/atoms/settings/general"
import { setUISetting, useUISettingKey } from "~/atoms/settings/ui"
import {
setUISetting,
useIsZenMode,
useRealInWideMode,
useSetZenMode,
useUISettingKey,
} from "~/atoms/settings/ui"
import { useWhoami } from "~/atoms/user"
import { ImpressionView } from "~/components/common/ImpressionTracker"
import { FEED_COLLECTION_LIST, ROUTE_ENTRY_PENDING, ROUTE_FEED_IN_LIST } from "~/constants"
@ -234,9 +241,11 @@ const SwitchToMasonryButton = () => {
}
const WideModeButton = () => {
const isWideMode = useUISettingKey("wideMode")
const isWideMode = useRealInWideMode()
const isZenMode = useIsZenMode()
const { t } = useTranslation()
const setIsZenMode = useSetZenMode()
return (
<ImpressionView
event="Switch to Wide Mode"
@ -247,23 +256,33 @@ const WideModeButton = () => {
<ActionButton
shortcut={shortcuts.layout.toggleWideMode.key}
onClick={() => {
setUISetting("wideMode", !isWideMode)
// TODO: Remove this after useMeasure can get bounds in time
window.dispatchEvent(new Event("resize"))
if (isZenMode) {
setIsZenMode(false)
} else {
setUISetting("wideMode", !isWideMode)
// TODO: Remove this after useMeasure can get bounds in time
window.dispatchEvent(new Event("resize"))
}
window.analytics?.capture("Switch to Wide Mode", {
wideMode: !isWideMode ? 1 : 0,
click: 1,
})
}}
tooltip={
!isWideMode
? t("entry_list_header.switch_to_widemode")
: t("entry_list_header.switch_to_normalmode")
isZenMode
? t("zen.exit")
: !isWideMode
? t("entry_list_header.switch_to_widemode")
: t("entry_list_header.switch_to_normalmode")
}
>
<i
className={cn(isWideMode ? "i-mgc-align-justify-cute-re" : "i-mgc-align-left-cute-re")}
/>
{isZenMode ? (
<MdiMeditation />
) : (
<i
className={cn(isWideMode ? "i-mgc-align-justify-cute-re" : "i-mgc-align-left-cute-re")}
/>
)}
</ActionButton>
</ImpressionView>
)

View File

@ -3,7 +3,7 @@ import { cn, isSafari } from "@follow/utils/utils"
import { useDebounceCallback } from "usehooks-ts"
import { AudioPlayer, useAudioPlayerAtomSelector } from "~/atoms/player"
import { useUISettingKeys } from "~/atoms/settings/ui"
import { useRealInWideMode, useUISettingKey } from "~/atoms/settings/ui"
import { RelativeTime } from "~/components/ui/datetime"
import { Media } from "~/components/ui/media"
import { FEED_COLLECTION_LIST } from "~/constants"
@ -62,7 +62,8 @@ export function ListItem({
{ leading: false },
)
const [settingWideMode, thumbnailRatio] = useUISettingKeys(["wideMode", "thumbnailRatio"])
const settingWideMode = useRealInWideMode()
const thumbnailRatio = useUISettingKey("thumbnailRatio")
const rid = `list-item-${entryId}`
// NOTE: prevent 0 height element, react virtuoso will not stop render any more

View File

@ -70,7 +70,7 @@ function EntryHeaderImpl({
{!hideRecentReader && (
<div
className={cn(
"absolute left-5 top-0 flex h-full items-center gap-2 text-[13px] leading-none text-zinc-500",
"absolute left-5 top-0 flex h-full items-center gap-2 text-[13px] leading-none text-zinc-500 zen-mode-macos:left-12",
"visible z-[11]",
views[view].wideMode && "static",
shouldShowMeta && "hidden",

View File

@ -1,4 +1,5 @@
import { Logo } from "@follow/components/icons/logo.jsx"
import { MdiMeditation } from "@follow/components/icons/Meditation.js"
import { ActionButton } from "@follow/components/ui/button/index.js"
import { Popover, PopoverContent, PopoverTrigger } from "@follow/components/ui/popover/index.jsx"
import { stopPropagation } from "@follow/utils/dom"
@ -13,6 +14,7 @@ import { toast } from "sonner"
import { setAppSearchOpen } from "~/atoms/app"
import { useGeneralSettingKey } from "~/atoms/settings/general"
import { useIsZenMode, useSetZenMode } from "~/atoms/settings/ui"
import { setFeedColumnShow, useFeedColumnShow, useSidebarActiveView } from "~/atoms/sidebar"
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
import { useI18n } from "~/hooks/common"
@ -81,7 +83,8 @@ const LayoutActionButton = () => {
const [animation, setAnimation] = useState({
width: !feedColumnShow ? "auto" : 0,
})
const isZenMode = useIsZenMode()
const setIsZenMode = useSetZenMode()
useEffect(() => {
setAnimation({
width: !feedColumnShow ? "auto" : 0,
@ -95,18 +98,28 @@ const LayoutActionButton = () => {
return (
<m.div initial={animation} animate={animation} className="overflow-hidden">
<ActionButton
tooltip={t("app.toggle_sidebar")}
tooltip={isZenMode ? t("zen.exit") : t("app.toggle_sidebar")}
icon={
<i
className={cn(
!feedColumnShow
? "i-mgc-layout-leftbar-open-cute-re "
: "i-mgc-layout-leftbar-close-cute-re",
"text-theme-vibrancyFg",
)}
/>
isZenMode ? (
<MdiMeditation />
) : (
<i
className={cn(
!feedColumnShow
? "i-mgc-layout-leftbar-open-cute-re "
: "i-mgc-layout-leftbar-close-cute-re",
"text-theme-vibrancyFg",
)}
/>
)
}
onClick={() => setFeedColumnShow(!feedColumnShow)}
onClick={() => {
if (isZenMode) {
setIsZenMode(false)
} else {
setFeedColumnShow(!feedColumnShow)
}
}}
/>
</m.div>
)

View File

@ -18,11 +18,12 @@ import { bundledThemesInfo } from "shiki/themes"
import {
getUISettings,
setUISetting,
useIsZenMode,
useToggleZenMode,
useUISettingKey,
useUISettingSelector,
useUISettingValue,
} from "~/atoms/settings/ui"
import { setFeedColumnShow, useFeedColumnShow } from "~/atoms/sidebar"
import { useCurrentModal, useModalStack } from "~/components/ui/modal/stacked/hooks"
import { isElectronBuild } from "~/constants"
import { useSetTheme } from "~/hooks/common"
@ -246,22 +247,14 @@ export const AppThemeSegment = () => {
const ZenMode = () => {
const { t } = useTranslation("settings")
const feedColumnShow = useFeedColumnShow()
const isWideMode = useUISettingKey("wideMode")
const isZenMode = useIsZenMode()
const toggleZenMode = useToggleZenMode()
return (
<SettingItemGroup>
<SettingSwitch
checked={!feedColumnShow && isWideMode}
checked={isZenMode}
className="mt-4"
onCheckedChange={(checked) => {
if (checked) {
setFeedColumnShow(false)
setUISetting("wideMode", true)
} else {
setFeedColumnShow(true)
setUISetting("wideMode", false)
}
}}
onCheckedChange={toggleZenMode}
label={t("appearance.zen_mode.label")}
/>
<SettingDescription>{t("appearance.zen_mode.description")}</SettingDescription>

View File

@ -7,7 +7,7 @@ import { useState } from "react"
import { useHotkeys } from "react-hotkeys-hook"
import { useParams } from "react-router-dom"
import { useUISettingKey } from "~/atoms/settings/ui"
import { useRealInWideMode } from "~/atoms/settings/ui"
import { useFeedColumnShow, useFeedColumnTempShow } from "~/atoms/sidebar"
import { m } from "~/components/common/Motion"
import { FixedModalCloseButton } from "~/components/ui/modal/components/close"
@ -24,7 +24,7 @@ export const Component = () => {
const view = useRouteView()
const navigate = useNavigateEntry()
const settingWideMode = useUISettingKey("wideMode")
const settingWideMode = useRealInWideMode()
const realEntryId = entryId === ROUTE_ENTRY_PENDING ? "" : entryId
const showEntryContent = !(views[view].wideMode || (settingWideMode && !realEntryId))
const wideMode = !!(settingWideMode && realEntryId)
@ -45,7 +45,6 @@ export const Component = () => {
)
const { feedId } = useRouteParams()
const enableEntryWideMode = useUISettingKey("wideMode")
if (!showEntryContent) {
return null
@ -56,7 +55,7 @@ export const Component = () => {
<EntryGridContainer showEntryContent={showEntryContent} wideMode={wideMode}>
{wideMode && (
<FixedModalCloseButton
className="no-drag-region absolute left-4 top-4 z-10"
className="no-drag-region absolute left-4 top-4 z-10 macos:translate-y-margin-macos-traffic-light-y"
onClick={() => navigate({ entryId: null })}
/>
)}
@ -71,7 +70,7 @@ export const Component = () => {
: "",
}}
/>
) : !enableEntryWideMode ? (
) : !settingWideMode ? (
<m.div
className="center size-full flex-col"
initial={{ opacity: 0.01, y: 300 }}

View File

@ -5,7 +5,12 @@ import { useMemo, useRef } from "react"
import { useResizable } from "react-resizable-layout"
import { Outlet } from "react-router-dom"
import { getUISettings, setUISetting, useUISettingKey } from "~/atoms/settings/ui"
import {
getUISettings,
setUISetting,
useRealInWideMode,
useUISettingKey,
} from "~/atoms/settings/ui"
import { useRouteParams } from "~/hooks/biz/useRouteParams"
import { EntryColumn } from "~/modules/entry-column"
import { AppLayoutGridContainerProvider } from "~/providers/app-grid-layout-container-provider"
@ -15,7 +20,7 @@ export function Component() {
// Memo this initial value to avoid re-render
const settingWideMode = useUISettingKey("wideMode")
const settingWideMode = useRealInWideMode()
const entryColWidth = useMemo(() => getUISettings().entryColWidth, [])
const { view } = useRouteParams()
const inWideMode = (view ? views[view].wideMode : false) || settingWideMode

View File

@ -16,7 +16,7 @@ import { useResizable } from "react-resizable-layout"
import { Outlet } from "react-router-dom"
import { setMainContainerElement } from "~/atoms/dom"
import { getUISettings, setUISetting, useUISettingKey } from "~/atoms/settings/ui"
import { getIsZenMode, getUISettings, setUISetting, useUISettingKey } from "~/atoms/settings/ui"
import {
getFeedColumnTempShow,
setFeedColumnShow,
@ -214,8 +214,8 @@ const FeedResponsiveResizerContainer = ({
const uiSettings = getUISettings()
const feedColumnTempShow = getFeedColumnTempShow()
const isInEntryContentWideMode = uiSettings.wideMode
if (mouseY < 100 && isInEntryContentWideMode) return
const isInEntryContentWideMode = uiSettings.wideMode || getIsZenMode()
if (mouseY < 200 && isInEntryContentWideMode) return
const threshold = feedColumnTempShow ? uiSettings.feedColWidth : 100
if (mouseX < threshold) {

View File

@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { getGeneralSettings } from "~/atoms/settings/general"
import { getUISettings } from "~/atoms/settings/ui"
import { getUISettings, useToggleZenMode } from "~/atoms/settings/ui"
import { useModalStack } from "~/components/ui/modal/stacked/hooks"
import { useDiscoverRSSHubRouteModal } from "~/hooks/biz/useDiscoverRSSHubRoute"
import { useFollow } from "~/hooks/biz/useFollow"
@ -64,5 +64,12 @@ export const ExtensionExposeProvider = () => {
},
})
}, [follow, present, presentDiscoverRSSHubRoute, presentUserProfile, t])
const toggleZenMode = useToggleZenMode()
useEffect(() => {
registerGlobalContext({
zenMode: toggleZenMode,
})
}, [toggleZenMode])
return null
}

View File

@ -6,7 +6,6 @@ import { getIconCollections, iconsPlugin } from "@egoist/tailwindcss-icons"
import { cleanupSVG, importDirectorySync, isEmptyColor, parseColors, runSVGO } from "@iconify/tools"
import { compareColors, stringToColor } from "@iconify/utils/lib/colors"
import type { Config } from "tailwindcss"
import plugin from "tailwindcss/plugin"
export const baseConfig = {
darkMode: ["class", '[data-theme="dark"]'],
@ -22,9 +21,6 @@ export const baseConfig = {
},
extend: {
spacing: {
"safe-inset-top": "var(--fo-window-padding-top, 0)",
},
fontFamily: {
theme: "var(--fo-font-family)",
default: "SN pro, sans-serif, system-ui",
@ -147,37 +143,6 @@ export const baseConfig = {
require("tailwindcss-motion"),
require(resolve(__dirname, "./tailwind-extend.css")),
plugin(({ addVariant }) => {
addVariant("f-motion-reduce", '[data-motion-reduce="true"] &')
addVariant("group-motion-reduce", ':merge(.group)[data-motion-reduce="true"] &')
addVariant("peer-motion-reduce", ':merge(.peer)[data-motion-reduce="true"] ~ &')
}),
plugin(({ addUtilities, matchUtilities, theme }) => {
addUtilities({
".safe-inset-top": {
top: "var(--fo-window-padding-top, 0)",
},
})
const safeInsetTopVariants = {}
for (let i = 1; i <= 16; i++) {
safeInsetTopVariants[`.safe-inset-top-${i}`] = {
top: `calc(var(--fo-window-padding-top, 0px) + ${theme(`spacing.${i}`)})`,
}
}
addUtilities(safeInsetTopVariants)
// Add arbitrary value support
matchUtilities(
{
"safe-inset-top": (value) => ({
top: `calc(var(--fo-window-padding-top, 0px) + ${value})`,
}),
},
{ values: theme("spacing") },
)
}),
],
} satisfies Config

View File

@ -373,5 +373,6 @@
"words.unread": "Unread",
"words.user": "User",
"words.which.all": "all",
"words.zero_items": "Zero items"
"words.zero_items": "Zero items",
"zen.exit": "Exit Zen Mode"
}

View File

@ -62,6 +62,7 @@
"menu.undo": "Undo",
"menu.view": "View",
"menu.window": "Window",
"menu.zenMode": "Zen Mode",
"menu.zoom": "Zoom",
"menu.zoomIn": "Zoom In",
"menu.zoomOut": "Zoom Out"

View File

@ -22,6 +22,7 @@
"keys.layout.showShortcuts": "Show/Hide Shortcuts",
"keys.layout.toggleSidebar": "Show/Hide Feed Sidebar",
"keys.layout.toggleWideMode": "Toggle Wide Mode",
"keys.layout.zenMode": "Zen Mode",
"keys.misc.quickSearch": "Quick Search",
"keys.type.audio": "audio",
"keys.type.entries": "entries",

View File

@ -0,0 +1,12 @@
import type { SVGProps } from "react"
export function MdiMeditation(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props}>
<path
fill="currentColor"
d="M12 4c1.11 0 2 .89 2 2s-.89 2-2 2s-2-.89-2-2s.9-2 2-2m9 12v-2c-2.24 0-4.16-.96-5.6-2.68l-1.34-1.6A1.98 1.98 0 0 0 12.53 9H11.5c-.61 0-1.17.26-1.55.72l-1.34 1.6C7.16 13.04 5.24 14 3 14v2c2.77 0 5.19-1.17 7-3.25V15l-3.88 1.55c-.67.27-1.12.95-1.12 1.66C5 19.2 5.8 20 6.79 20H9v-.5a2.5 2.5 0 0 1 2.5-2.5h3c.28 0 .5.22.5.5s-.22.5-.5.5h-3c-.83 0-1.5.67-1.5 1.5v.5h7.21c.99 0 1.79-.8 1.79-1.79c0-.71-.45-1.39-1.12-1.66L14 15v-2.25c1.81 2.08 4.23 3.25 7 3.25"
/>
</svg>
)
}

View File

@ -33,6 +33,9 @@ interface RenderGlobalContext {
// URL
getWebUrl: () => string
getApiUrl: () => string
// View
zenMode: () => void
}
export const registerGlobalContext = (context: Partial<RenderGlobalContext>) => {

View File

@ -1,8 +1,8 @@
import plugin from "tailwindcss/plugin"
import resolveConfig from "tailwindcss/resolveConfig"
import { baseConfig } from "./configs/tailwind.base.config"
/** @type {import('tailwindcss').Config} */
export default resolveConfig({
...baseConfig,
content: [
@ -13,4 +13,71 @@ export default resolveConfig({
"./apps/web/index.html",
"./packages/**/*.{ts,tsx}",
],
theme: {
...baseConfig.theme,
extend: {
...baseConfig.theme?.extend,
spacing: {
"safe-inset-top": "var(--fo-window-padding-top, 0)",
"margin-macos-traffic-light-x": "var(--fo-macos-traffic-light-width, 0)",
"margin-macos-traffic-light-y": "var(--fo-macos-traffic-light-height, 0)",
},
},
},
plugins: [
...baseConfig.plugins,
plugin(({ addVariant }) => {
addVariant("f-motion-reduce", '[data-motion-reduce="true"] &')
addVariant("group-motion-reduce", ':merge(.group)[data-motion-reduce="true"] &')
addVariant("peer-motion-reduce", ':merge(.peer)[data-motion-reduce="true"] ~ &')
addVariant("zen-mode-macos", ":where(html[data-zen-mode='true'][data-os='macOS']) &")
addVariant("zen-mode-windows", ":where(html[data-zen-mode='true'][data-os='Windows']) &")
addVariant("zen-mode", ":where(html[data-zen-mode='true']) &")
addVariant("macos", ":where(html[data-os='macOS']) &")
addVariant("windows", ":where(html[data-os='Windows']) &")
}),
plugin(({ addUtilities, matchUtilities, theme }) => {
addUtilities({
".safe-inset-top": {
top: "var(--fo-window-padding-top, 0)",
},
})
const safeInsetTopVariants = {}
for (let i = 1; i <= 16; i++) {
safeInsetTopVariants[`.safe-inset-top-${i}`] = {
top: `calc(var(--fo-window-padding-top, 0px) + ${theme(`spacing.${i}`)})`,
}
}
addUtilities(safeInsetTopVariants)
// left macos traffic light
const leftMacosTrafficLightVariants = {}
addUtilities({
".left-macos-traffic-light": {
left: "var(--fo-macos-traffic-light-width, 0)",
},
})
for (let i = 1; i <= 16; i++) {
leftMacosTrafficLightVariants[`.left-macos-traffic-light-${i}`] = {
left: `calc(var(--fo-macos-traffic-light-width, 0px) + ${theme(`spacing.${i}`)})`,
}
}
addUtilities(leftMacosTrafficLightVariants)
// Add arbitrary value support
matchUtilities(
{
"safe-inset-top": (value) => ({
top: `calc(var(--fo-window-padding-top, 0px) + ${value})`,
}),
},
{ values: theme("spacing") },
)
}),
],
})