From 51a4bd273eabe7e740cfca5b2544b541f0226ca4 Mon Sep 17 00:00:00 2001 From: Innei Date: Tue, 5 Nov 2024 21:17:28 +0800 Subject: [PATCH] refactor: zen modal implemention and fix macos traffic light overlay Signed-off-by: Innei --- apps/main/src/menu.ts | 13 ++++ apps/renderer/src/atoms/settings/ui.ts | 37 +++++++--- apps/renderer/src/atoms/sidebar.ts | 24 ++++--- .../components/ui/markdown/components/Toc.tsx | 4 +- apps/renderer/src/constants/shortcuts.ts | 4 ++ apps/renderer/src/main.tsx | 16 ++++- .../entry-column/layouts/EntryListHeader.tsx | 41 ++++++++--- .../templates/list-item-template.tsx | 5 +- .../src/modules/entry-content/header.tsx | 2 +- .../src/modules/feed-column/header.tsx | 35 +++++++--- .../src/modules/settings/tabs/apperance.tsx | 19 ++--- .../feeds/[feedId]/[entryId]/index.tsx | 9 ++- .../(main)/(layer)/feeds/[feedId]/layout.tsx | 9 ++- apps/renderer/src/pages/(main)/layout.tsx | 6 +- .../providers/extension-expose-provider.tsx | 9 ++- configs/tailwind.base.config.ts | 35 ---------- locales/app/en.json | 3 +- locales/native/en.json | 1 + locales/shortcuts/en.json | 1 + packages/components/src/icons/Meditation.tsx | 12 ++++ packages/shared/src/bridge.ts | 3 + tailwind.config.ts | 69 ++++++++++++++++++- 22 files changed, 251 insertions(+), 106 deletions(-) create mode 100644 packages/components/src/icons/Meditation.tsx diff --git a/apps/main/src/menu.ts b/apps/main/src/menu.ts index e985df608..7ecc66da5 100644 --- a/apps/main/src/menu.ts +++ b/apps/main/src/menu.ts @@ -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") }, ], }, diff --git a/apps/renderer/src/atoms/settings/ui.ts b/apps/renderer/src/atoms/settings/ui.ts index abd685b44..ed8b83b23 100644 --- a/apps/renderer/src/atoms/settings/ui.ts +++ b/apps/renderer/src/atoms/settings/ui.ts @@ -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 +} diff --git a/apps/renderer/src/atoms/sidebar.ts b/apps/renderer/src/atoms/sidebar.ts index f39b05478..910e70c22 100644 --- a/apps/renderer/src/atoms/sidebar.ts +++ b/apps/renderer/src/atoms/sidebar.ts @@ -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(-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)) diff --git a/apps/renderer/src/components/ui/markdown/components/Toc.tsx b/apps/renderer/src/components/ui/markdown/components/Toc.tsx index 248a3d0d5..80505cfaf 100644 --- a/apps/renderer/src/components/ui/markdown/components/Toc.tsx +++ b/apps/renderer/src/components/ui/markdown/components/Toc.tsx @@ -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 = ({ 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 diff --git a/apps/renderer/src/constants/shortcuts.ts b/apps/renderer/src/constants/shortcuts.ts index 153126649..a92ba8d00 100644 --- a/apps/renderer/src/constants/shortcuts.ts +++ b/apps/renderer/src/constants/shortcuts.ts @@ -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: { diff --git a/apps/renderer/src/main.tsx b/apps/renderer/src/main.tsx index 72080d1ff..227317e52 100644 --- a/apps/renderer/src/main.tsx +++ b/apps/renderer/src/main.tsx @@ -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( diff --git a/apps/renderer/src/modules/entry-column/layouts/EntryListHeader.tsx b/apps/renderer/src/modules/entry-column/layouts/EntryListHeader.tsx index bb219b42b..2db164dde 100644 --- a/apps/renderer/src/modules/entry-column/layouts/EntryListHeader.tsx +++ b/apps/renderer/src/modules/entry-column/layouts/EntryListHeader.tsx @@ -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 ( { { - 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") } > - + {isZenMode ? ( + + ) : ( + + )} ) diff --git a/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx b/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx index 1570786dd..c4f3f0a72 100644 --- a/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx +++ b/apps/renderer/src/modules/entry-column/templates/list-item-template.tsx @@ -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 diff --git a/apps/renderer/src/modules/entry-content/header.tsx b/apps/renderer/src/modules/entry-content/header.tsx index 57555e7ab..dc7f41c04 100644 --- a/apps/renderer/src/modules/entry-content/header.tsx +++ b/apps/renderer/src/modules/entry-content/header.tsx @@ -70,7 +70,7 @@ function EntryHeaderImpl({ {!hideRecentReader && (
{ 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 ( + isZenMode ? ( + + ) : ( + + ) } - onClick={() => setFeedColumnShow(!feedColumnShow)} + onClick={() => { + if (isZenMode) { + setIsZenMode(false) + } else { + setFeedColumnShow(!feedColumnShow) + } + }} /> ) diff --git a/apps/renderer/src/modules/settings/tabs/apperance.tsx b/apps/renderer/src/modules/settings/tabs/apperance.tsx index cfd923c8f..f145746d1 100644 --- a/apps/renderer/src/modules/settings/tabs/apperance.tsx +++ b/apps/renderer/src/modules/settings/tabs/apperance.tsx @@ -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 ( { - if (checked) { - setFeedColumnShow(false) - setUISetting("wideMode", true) - } else { - setFeedColumnShow(true) - setUISetting("wideMode", false) - } - }} + onCheckedChange={toggleZenMode} label={t("appearance.zen_mode.label")} /> {t("appearance.zen_mode.description")} diff --git a/apps/renderer/src/pages/(main)/(layer)/feeds/[feedId]/[entryId]/index.tsx b/apps/renderer/src/pages/(main)/(layer)/feeds/[feedId]/[entryId]/index.tsx index 5dcbf156c..f5fdddebd 100644 --- a/apps/renderer/src/pages/(main)/(layer)/feeds/[feedId]/[entryId]/index.tsx +++ b/apps/renderer/src/pages/(main)/(layer)/feeds/[feedId]/[entryId]/index.tsx @@ -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 = () => { {wideMode && ( navigate({ entryId: null })} /> )} @@ -71,7 +70,7 @@ export const Component = () => { : "", }} /> - ) : !enableEntryWideMode ? ( + ) : !settingWideMode ? ( getUISettings().entryColWidth, []) const { view } = useRouteParams() const inWideMode = (view ? views[view].wideMode : false) || settingWideMode diff --git a/apps/renderer/src/pages/(main)/layout.tsx b/apps/renderer/src/pages/(main)/layout.tsx index fd4a6f474..f5a5cf55e 100644 --- a/apps/renderer/src/pages/(main)/layout.tsx +++ b/apps/renderer/src/pages/(main)/layout.tsx @@ -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) { diff --git a/apps/renderer/src/providers/extension-expose-provider.tsx b/apps/renderer/src/providers/extension-expose-provider.tsx index b1de4110d..745e8b601 100644 --- a/apps/renderer/src/providers/extension-expose-provider.tsx +++ b/apps/renderer/src/providers/extension-expose-provider.tsx @@ -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 } diff --git a/configs/tailwind.base.config.ts b/configs/tailwind.base.config.ts index 3ba90b42e..58f961427 100644 --- a/configs/tailwind.base.config.ts +++ b/configs/tailwind.base.config.ts @@ -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 diff --git a/locales/app/en.json b/locales/app/en.json index d1045ff2b..0d02bb7f0 100644 --- a/locales/app/en.json +++ b/locales/app/en.json @@ -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" } diff --git a/locales/native/en.json b/locales/native/en.json index 683ae2ba4..d5e9e681c 100644 --- a/locales/native/en.json +++ b/locales/native/en.json @@ -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" diff --git a/locales/shortcuts/en.json b/locales/shortcuts/en.json index 00dc5adb2..8a89fc50d 100644 --- a/locales/shortcuts/en.json +++ b/locales/shortcuts/en.json @@ -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", diff --git a/packages/components/src/icons/Meditation.tsx b/packages/components/src/icons/Meditation.tsx new file mode 100644 index 000000000..bc837df20 --- /dev/null +++ b/packages/components/src/icons/Meditation.tsx @@ -0,0 +1,12 @@ +import type { SVGProps } from "react" + +export function MdiMeditation(props: SVGProps) { + return ( + + + + ) +} diff --git a/packages/shared/src/bridge.ts b/packages/shared/src/bridge.ts index 080650be2..b598840c6 100644 --- a/packages/shared/src/bridge.ts +++ b/packages/shared/src/bridge.ts @@ -33,6 +33,9 @@ interface RenderGlobalContext { // URL getWebUrl: () => string getApiUrl: () => string + + // View + zenMode: () => void } export const registerGlobalContext = (context: Partial) => { diff --git a/tailwind.config.ts b/tailwind.config.ts index 3076a591f..94cd3569a 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -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") }, + ) + }), + ], })