feat: render mark as read (#111)
* feat: render mark as read Signed-off-by: Innei <i@innei.in> * fix: mark read when active Signed-off-by: Innei <i@innei.in> * fix: default Signed-off-by: Innei <i@innei.in> --------- Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
296c285a5d
commit
246efa2265
|
|
@ -34,5 +34,6 @@
|
|||
],
|
||||
"cSpell.words": [
|
||||
"rsshub"
|
||||
]
|
||||
],
|
||||
"editor.foldingImportsByDefault": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@ import { createSettingAtom } from "./helper"
|
|||
const createDefaultSettings = () => ({
|
||||
dataPersist: true,
|
||||
|
||||
// view
|
||||
unreadOnly: false,
|
||||
// mark unread
|
||||
scrollMarkUnread: true,
|
||||
hoverMarkUnread: true,
|
||||
hoverMarkUnread: false,
|
||||
renderMarkUnread: true,
|
||||
})
|
||||
export const {
|
||||
useSettingKey: useGeneralSettingKey,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import { entryActions } from "@renderer/store/entry"
|
||||
import { entryActions, getEntry } from "@renderer/store/entry"
|
||||
import { create, keyResolver, windowScheduler } from "@yornaath/batshit"
|
||||
|
||||
type EntryId = string
|
||||
|
|
@ -7,13 +7,17 @@ type FeedId = string
|
|||
const unread = create({
|
||||
fetcher: async (ids: [FeedId, EntryId][]) => {
|
||||
await apiClient.reads.$post({ json: { entryIds: ids.map((i) => i[1]) } })
|
||||
for (const [feedId, entryId] of ids) {
|
||||
entryActions.markRead(feedId, entryId, true)
|
||||
}
|
||||
|
||||
return []
|
||||
},
|
||||
resolver: keyResolver("id"),
|
||||
scheduler: windowScheduler(1000),
|
||||
})
|
||||
|
||||
export const batchMarkUnread = unread.fetch
|
||||
export const batchMarkUnread = (...args: Parameters<typeof unread.fetch>) => {
|
||||
const [, entryId] = args[0]
|
||||
const currentIsRead = getEntry(entryId)?.read
|
||||
if (currentIsRead) return
|
||||
entryActions.markRead(args[0][0], args[0][1], true)
|
||||
return unread.fetch.apply(null, args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
import { useGeneralSettingKey } from "@renderer/atoms/settings/general"
|
||||
import {
|
||||
useRouteParamsSelector,
|
||||
useRouteParms,
|
||||
} from "@renderer/hooks/biz/useRouteParams"
|
||||
import { levels, ROUTE_FEED_PENDING, views } from "@renderer/lib/constants"
|
||||
import { shortcuts } from "@renderer/lib/shortcuts"
|
||||
import { useEntries } from "@renderer/queries/entries"
|
||||
import { entryActions, useEntryIdsByFeedIdOrView } from "@renderer/store/entry"
|
||||
import { useFolderFeedsByFeedId } from "@renderer/store/subscription"
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import type { ListRange } from "react-virtuoso"
|
||||
import { useDebounceCallback } from "usehooks-ts"
|
||||
|
||||
import { batchMarkUnread } from "./helper"
|
||||
|
||||
export const useEntryMarkReadHandler = (entriesIds: string[]) => {
|
||||
const renderAsRead = useGeneralSettingKey("renderMarkUnread")
|
||||
const scrollMarkUnread = useGeneralSettingKey("scrollMarkUnread")
|
||||
const feedView = useRouteParamsSelector((params) => params.view)
|
||||
|
||||
const handleMarkReadInRange = useDebounceCallback(
|
||||
async ({ startIndex }: ListRange) => {
|
||||
const idSlice = entriesIds?.slice(0, startIndex)
|
||||
|
||||
if (!idSlice) return
|
||||
batchMarkRead(idSlice)
|
||||
},
|
||||
1000,
|
||||
{ leading: false },
|
||||
)
|
||||
|
||||
const handleRenderAsRead = useCallback(
|
||||
async ({ startIndex, endIndex }: ListRange) => {
|
||||
const idSlice = entriesIds?.slice(startIndex, endIndex)
|
||||
|
||||
if (!idSlice) return
|
||||
batchMarkRead(idSlice)
|
||||
},
|
||||
[entriesIds],
|
||||
)
|
||||
|
||||
return useMemo(() => {
|
||||
if (views[feedView].wideMode && renderAsRead) {
|
||||
return handleRenderAsRead
|
||||
}
|
||||
|
||||
if (scrollMarkUnread) {
|
||||
return handleMarkReadInRange
|
||||
}
|
||||
return
|
||||
}, [
|
||||
feedView,
|
||||
handleMarkReadInRange,
|
||||
handleRenderAsRead,
|
||||
renderAsRead,
|
||||
scrollMarkUnread,
|
||||
])
|
||||
}
|
||||
export const useEntriesByView = () => {
|
||||
const routeParams = useRouteParms()
|
||||
const unreadOnly = useGeneralSettingKey("unreadOnly")
|
||||
|
||||
const { level, feedId, view } = routeParams
|
||||
|
||||
const folderIds = useFolderFeedsByFeedId(feedId)
|
||||
|
||||
const query = useEntries({
|
||||
level,
|
||||
id: level === levels.folder ? folderIds?.join(",") : feedId,
|
||||
view,
|
||||
...(unreadOnly === true && { read: false }),
|
||||
})
|
||||
const entries = useEntryIdsByFeedIdOrView(
|
||||
feedId === ROUTE_FEED_PENDING ? view : feedId!,
|
||||
{
|
||||
unread: unreadOnly,
|
||||
view,
|
||||
},
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
shortcuts.entries.refetch.key,
|
||||
() => {
|
||||
query.refetch()
|
||||
},
|
||||
{ scopes: ["home"] },
|
||||
)
|
||||
|
||||
// in unread only entries only can grow the data, but not shrink
|
||||
// so we memo this previous data to avoid the flicker
|
||||
const prevEntries = useRef(entries)
|
||||
|
||||
useEffect(() => {
|
||||
prevEntries.current = []
|
||||
}, [routeParams.feedId, routeParams.view])
|
||||
const localEntries = useMemo(() => {
|
||||
if (!unreadOnly) {
|
||||
prevEntries.current = []
|
||||
return entries
|
||||
}
|
||||
if (!prevEntries.current) {
|
||||
prevEntries.current = entries
|
||||
return entries
|
||||
}
|
||||
if (entries.length > prevEntries.current.length) {
|
||||
prevEntries.current = entries
|
||||
return entries
|
||||
}
|
||||
// merge the new entries with the old entries, and unique them
|
||||
const nextIds = [...new Set([...prevEntries.current, ...entries])]
|
||||
prevEntries.current = nextIds
|
||||
return nextIds
|
||||
}, [entries, prevEntries, unreadOnly])
|
||||
|
||||
const remoteEntryIds = useMemo(
|
||||
() =>
|
||||
query.data ?
|
||||
query.data.pages.reduce((acc, page) => {
|
||||
if (!page.data) return acc
|
||||
acc.push(...page.data.map((entry) => entry.entries.id))
|
||||
return acc
|
||||
}, [] as string[]) :
|
||||
null,
|
||||
[query.data],
|
||||
)
|
||||
|
||||
return {
|
||||
...query,
|
||||
|
||||
// If remote data is not available, we use the local data, get the local data length
|
||||
totalCount: query.data?.pages?.[0]?.total ?? localEntries.length,
|
||||
entriesIds:
|
||||
// NOTE: if we use the remote data, priority will be given to the remote data, local data maybe had sort issue
|
||||
remoteEntryIds ?? localEntries,
|
||||
}
|
||||
}
|
||||
|
||||
function batchMarkRead(ids: string[]) {
|
||||
const batchLikeIds = [] as [string, string][]
|
||||
const entriesId2Map = entryActions.getFlattenMapEntries()
|
||||
for (const id of ids) {
|
||||
const entry = entriesId2Map[id]
|
||||
|
||||
if (!entry) continue
|
||||
const isRead = entry.read
|
||||
if (!isRead) {
|
||||
batchLikeIds.push([entry.feeds.id, id])
|
||||
}
|
||||
}
|
||||
|
||||
if (batchLikeIds.length > 0) {
|
||||
for (const [feedId, id] of batchLikeIds) {
|
||||
batchMarkUnread([feedId, id])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import { useMainContainerElement } from "@renderer/atoms/dom"
|
||||
import { useGeneralSettingKey } from "@renderer/atoms/settings/general"
|
||||
import {
|
||||
setGeneralSetting,
|
||||
useGeneralSettingKey,
|
||||
} from "@renderer/atoms/settings/general"
|
||||
import { useUser } from "@renderer/atoms/user"
|
||||
import { m } from "@renderer/components/common/Motion"
|
||||
import { EmptyIcon } from "@renderer/components/icons/empty"
|
||||
|
|
@ -13,7 +16,6 @@ import {
|
|||
PopoverTrigger,
|
||||
} from "@renderer/components/ui/popover"
|
||||
import { EllipsisHorizontalTextWithTooltip } from "@renderer/components/ui/typography"
|
||||
import { useRead } from "@renderer/hooks/biz/useEntryActions"
|
||||
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
|
||||
import {
|
||||
useRouteEntryId,
|
||||
|
|
@ -29,26 +31,17 @@ import {
|
|||
ROUTE_FEED_PENDING,
|
||||
views,
|
||||
} from "@renderer/lib/constants"
|
||||
import { getStorageNS } from "@renderer/lib/ns"
|
||||
import { shortcuts } from "@renderer/lib/shortcuts"
|
||||
import { cn, getEntriesParams, getOS, isBizId } from "@renderer/lib/utils"
|
||||
import { EntryHeader } from "@renderer/modules/entry-content/header"
|
||||
import { useEntries } from "@renderer/queries/entries"
|
||||
import { useRefreshFeedMutation } from "@renderer/queries/feed"
|
||||
import { entryActions } from "@renderer/store/entry"
|
||||
import {
|
||||
useEntry,
|
||||
useEntryIdsByFeedIdOrView,
|
||||
} from "@renderer/store/entry/hooks"
|
||||
import { entryActions, useEntry } from "@renderer/store/entry"
|
||||
import { useFeedById, useFeedHeaderTitle } from "@renderer/store/feed"
|
||||
import {
|
||||
subscriptionActions,
|
||||
useFolderFeedsByFeedId,
|
||||
} from "@renderer/store/subscription"
|
||||
import type { HTMLMotionProps } from "framer-motion"
|
||||
import { useAtom, useAtomValue } from "jotai"
|
||||
import { atomWithStorage } from "jotai/utils"
|
||||
import { debounce } from "lodash-es"
|
||||
import type { FC } from "react"
|
||||
import {
|
||||
forwardRef,
|
||||
|
|
@ -60,65 +53,27 @@ import {
|
|||
useState,
|
||||
} from "react"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import type { ListRange, VirtuosoHandle, VirtuosoProps } from "react-virtuoso"
|
||||
import type { VirtuosoHandle, VirtuosoProps } from "react-virtuoso"
|
||||
import { Virtuoso, VirtuosoGrid } from "react-virtuoso"
|
||||
import { useEventCallback } from "usehooks-ts"
|
||||
|
||||
import { batchMarkUnread } from "./helper"
|
||||
import { useEntriesByView, useEntryMarkReadHandler } from "./hooks"
|
||||
import { EntryItem } from "./item"
|
||||
|
||||
const unreadOnlyAtom = atomWithStorage<boolean>(
|
||||
getStorageNS("entry-unreadonly"),
|
||||
true,
|
||||
undefined,
|
||||
{
|
||||
getOnInit: true,
|
||||
},
|
||||
)
|
||||
|
||||
export function EntryColumn() {
|
||||
const entries = useEntriesByView()
|
||||
const { entriesIds, isFetchingNextPage } = entries
|
||||
|
||||
const { entryId: activeEntryId, view, feedId } = useRouteParms()
|
||||
const activeEntry = useEntry(activeEntryId)
|
||||
const markReadMutation = useRead()
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeEntry || activeEntry.read) {
|
||||
return
|
||||
}
|
||||
markReadMutation.mutate(activeEntry)
|
||||
}, [activeEntry?.entries?.id, activeEntry?.read])
|
||||
if (!feedId || !activeEntryId) return
|
||||
|
||||
const handleMarkreadInRange = useEventCallback(
|
||||
debounce(
|
||||
async ({ startIndex }: ListRange) => {
|
||||
const idSlice = entriesIds?.slice(0, startIndex)
|
||||
batchMarkUnread([feedId, activeEntryId])
|
||||
}, [activeEntry, activeEntryId, feedId])
|
||||
|
||||
if (!idSlice) return
|
||||
|
||||
const batchLikeIds = [] as [string, string][]
|
||||
const entriesId2Map = entryActions.getFlattenMapEntries()
|
||||
for (const id of idSlice) {
|
||||
const entry = entriesId2Map[id]
|
||||
|
||||
if (!entry) continue
|
||||
const isRead = entry.read
|
||||
if (!isRead) {
|
||||
batchLikeIds.push([entry.feeds.id, id])
|
||||
}
|
||||
}
|
||||
|
||||
if (batchLikeIds.length > 0) {
|
||||
for (const [feedId, id] of batchLikeIds) {
|
||||
batchMarkUnread([feedId, id])
|
||||
}
|
||||
}
|
||||
},
|
||||
1000,
|
||||
{ leading: false },
|
||||
),
|
||||
)
|
||||
const scrollMarkUnread = useGeneralSettingKey("scrollMarkUnread")
|
||||
const handleMarkReadInRange = useEntryMarkReadHandler(entriesIds)
|
||||
|
||||
const virtuosoOptions = {
|
||||
components: {
|
||||
|
|
@ -133,7 +88,7 @@ export function EntryColumn() {
|
|||
)
|
||||
}, [isFetchingNextPage]),
|
||||
},
|
||||
rangeChanged: scrollMarkUnread ? handleMarkreadInRange : undefined,
|
||||
rangeChanged: handleMarkReadInRange,
|
||||
totalCount: entries.totalCount,
|
||||
endReached: () => entries.hasNextPage && entries.fetchNextPage(),
|
||||
data: entriesIds,
|
||||
|
|
@ -182,90 +137,12 @@ export function EntryColumn() {
|
|||
)
|
||||
}
|
||||
|
||||
const useEntriesByView = () => {
|
||||
const routeParams = useRouteParms()
|
||||
const unreadOnly = useAtomValue(unreadOnlyAtom)
|
||||
|
||||
const { level, feedId, view } = routeParams
|
||||
|
||||
const folderIds = useFolderFeedsByFeedId(feedId)
|
||||
|
||||
const query = useEntries({
|
||||
level,
|
||||
id: level === levels.folder ? folderIds?.join(",") : feedId,
|
||||
view,
|
||||
...(unreadOnly === true && { read: false }),
|
||||
})
|
||||
const entries = useEntryIdsByFeedIdOrView(
|
||||
feedId === ROUTE_FEED_PENDING ? view : feedId!,
|
||||
{
|
||||
unread: unreadOnly,
|
||||
view,
|
||||
},
|
||||
)
|
||||
|
||||
useHotkeys(
|
||||
shortcuts.entries.refetch.key,
|
||||
() => {
|
||||
query.refetch()
|
||||
},
|
||||
{ scopes: ["home"] },
|
||||
)
|
||||
|
||||
// in unread only entries only can grow the data, but not shrink
|
||||
// so we memo this previous data to avoid the flicker
|
||||
const prevEntries = useRef(entries)
|
||||
|
||||
useEffect(() => {
|
||||
prevEntries.current = []
|
||||
}, [routeParams.feedId, routeParams.view])
|
||||
const localEntries = useMemo(() => {
|
||||
if (!unreadOnly) {
|
||||
prevEntries.current = []
|
||||
return entries
|
||||
}
|
||||
if (!prevEntries.current) {
|
||||
prevEntries.current = entries
|
||||
return entries
|
||||
}
|
||||
if (entries.length > prevEntries.current.length) {
|
||||
prevEntries.current = entries
|
||||
return entries
|
||||
}
|
||||
// merge the new entries with the old entries, and unique them
|
||||
const nextIds = [...new Set([...prevEntries.current, ...entries])]
|
||||
prevEntries.current = nextIds
|
||||
return nextIds
|
||||
}, [entries, prevEntries, unreadOnly])
|
||||
|
||||
const remoteEntryIds = useMemo(
|
||||
() =>
|
||||
query.data ?
|
||||
query.data.pages.reduce((acc, page) => {
|
||||
if (!page.data) return acc
|
||||
acc.push(...page.data.map((entry) => entry.entries.id))
|
||||
return acc
|
||||
}, [] as string[]) :
|
||||
null,
|
||||
[query.data],
|
||||
)
|
||||
|
||||
return {
|
||||
...query,
|
||||
|
||||
// If remote data is not available, we use the local data, get the local data length
|
||||
totalCount: query.data?.pages?.[0]?.total ?? localEntries.length,
|
||||
entriesIds:
|
||||
// NOTE: if we use the remote data, priority will be given to the remote data, local data maybe had sort issue
|
||||
remoteEntryIds ?? localEntries,
|
||||
}
|
||||
}
|
||||
|
||||
const ListHeader: FC<{
|
||||
totalCount: number
|
||||
}> = ({ totalCount }) => {
|
||||
const routerParams = useRouteParms()
|
||||
const [unreadOnly, setUnreadOnly] = useAtom(unreadOnlyAtom)
|
||||
|
||||
const unreadOnly = useGeneralSettingKey("unreadOnly")
|
||||
|
||||
const { feedId, entryId, view } = routerParams
|
||||
const folderIds = useFolderFeedsByFeedId(feedId)
|
||||
|
|
@ -377,7 +254,7 @@ const ListHeader: FC<{
|
|||
<ActionButton
|
||||
tooltip={unreadOnly ? "Unread Only" : "All"}
|
||||
shortcut={shortcuts.entries.toggleUnreadOnly.key}
|
||||
onClick={() => setUnreadOnly(!unreadOnly)}
|
||||
onClick={() => setGeneralSetting("unreadOnly", !unreadOnly)}
|
||||
>
|
||||
{unreadOnly ? (
|
||||
<i className="i-mgc-round-cute-fi" />
|
||||
|
|
@ -421,8 +298,7 @@ const ListContent = forwardRef<HTMLDivElement>((props, ref) => (
|
|||
|
||||
const EmptyList = forwardRef<HTMLDivElement, HTMLMotionProps<"div">>(
|
||||
(props, ref) => {
|
||||
const unreadOnly = useAtomValue(unreadOnlyAtom)
|
||||
|
||||
const unreadOnly = useGeneralSettingKey("unreadOnly")
|
||||
return (
|
||||
<m.div
|
||||
className="-mt-6 flex h-full flex-col items-center justify-center gap-2 text-zinc-400"
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Queries } from "@renderer/queries"
|
|||
import { useEntry } from "@renderer/store/entry/hooks"
|
||||
import type { FC } from "react"
|
||||
import { memo, useCallback } from "react"
|
||||
import { useDebounceCallback } from "usehooks-ts"
|
||||
|
||||
import { ReactVirtuosoItemPlaceholder } from "../../components/ui/placeholder"
|
||||
import { ArticleItem } from "./article-item"
|
||||
|
|
@ -60,12 +61,18 @@ function EntryItemImpl({
|
|||
const asRead = useAsRead(entry)
|
||||
const hoverMarkUnread = useGeneralSettingKey("hoverMarkUnread")
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (!hoverMarkUnread) return
|
||||
if (asRead) return
|
||||
const handleMouseEnter = useDebounceCallback(
|
||||
() => {
|
||||
if (!hoverMarkUnread) return
|
||||
if (asRead) return
|
||||
|
||||
batchMarkUnread([entry.feeds.id, entry.entries.id])
|
||||
}, [asRead, entry.entries.id, entry.feeds.id, hoverMarkUnread])
|
||||
batchMarkUnread([entry.feeds.id, entry.entries.id])
|
||||
},
|
||||
233,
|
||||
{
|
||||
leading: false,
|
||||
},
|
||||
)
|
||||
let Item: FC<UniversalItemProps>
|
||||
|
||||
switch (view) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
Mark as read has three logic now.
|
||||
|
||||
- Rendering entry as read.
|
||||
- Hover entry as read
|
||||
- Scrolling entry as read
|
||||
|
|
@ -36,6 +36,16 @@ export const SettingGeneral = () => {
|
|||
/>
|
||||
)}
|
||||
|
||||
<SettingSectionTitle title="view" />
|
||||
<SettingSwitch
|
||||
checked={settings.unreadOnly}
|
||||
onCheckedChange={(checked) =>
|
||||
setGeneralSetting("unreadOnly", checked)}
|
||||
label="Show unread content initially"
|
||||
/>
|
||||
<SettingDescription>
|
||||
Only show unread content initially when you open the app
|
||||
</SettingDescription>
|
||||
<SettingSectionTitle title="Mark read" />
|
||||
|
||||
<SettingSwitch
|
||||
|
|
@ -45,7 +55,7 @@ export const SettingGeneral = () => {
|
|||
label="Mark as read when scrolling"
|
||||
/>
|
||||
<SettingDescription>
|
||||
Automatic marking of feed entry as read when the item is scrolled up
|
||||
Automatic marking of feed entries as read when the item is scrolled up
|
||||
out of the viewport.
|
||||
</SettingDescription>
|
||||
|
||||
|
|
@ -57,7 +67,19 @@ export const SettingGeneral = () => {
|
|||
label="Mark as read when hovering"
|
||||
/>
|
||||
<SettingDescription>
|
||||
Automatic marking of feed entry as read when the item is hovered.
|
||||
Automatic marking of feed entries as read when the item is hovered.
|
||||
</SettingDescription>
|
||||
|
||||
<SettingSwitch
|
||||
className="mt-6"
|
||||
checked={settings.renderMarkUnread}
|
||||
onCheckedChange={(checked) =>
|
||||
setGeneralSetting("renderMarkUnread", checked)}
|
||||
label="Mark as read when in the viewport"
|
||||
/>
|
||||
<SettingDescription>
|
||||
Automatically mark feed entries with only one level of content(e.g. Social Media, Picture, Video views) as read when
|
||||
the item is in the viewport.
|
||||
</SettingDescription>
|
||||
|
||||
<SettingSectionTitle title="Data control" />
|
||||
|
|
|
|||
|
|
@ -231,3 +231,5 @@ export const useEntryStore = createZustandStore<EntryState & EntryActions>(
|
|||
}))
|
||||
|
||||
export const entryActions = getStoreActions(useEntryStore)
|
||||
|
||||
export const getEntry = (entryId: string) => useEntryStore.getState().flatMapEntries[entryId]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
.mask-both {
|
||||
mask-image: linear-gradient(
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgb(255, 255, 255) 20px,
|
||||
rgb(255, 255, 255) calc(100% - 20px),
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
.mask-both-lg {
|
||||
mask-image: linear-gradient(
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgb(255, 255, 255) 50px,
|
||||
rgb(255, 255, 255) calc(100% - 50px),
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.mask-b {
|
||||
mask-image: linear-gradient(
|
||||
rgb(255, 255, 255) calc(100% - 20px),
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.mask-b-lg {
|
||||
mask-image: linear-gradient(
|
||||
rgb(255, 255, 255) calc(100% - 50px),
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.mask-t {
|
||||
mask-image: linear-gradient(
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgb(255, 255, 255) 20px
|
||||
);
|
||||
}
|
||||
|
||||
.mask-t-lg {
|
||||
mask-image: linear-gradient(
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgb(255, 255, 255) 50px
|
||||
);
|
||||
}
|
||||
|
|
@ -2,3 +2,4 @@
|
|||
@import "./font.css";
|
||||
@import "./base.css";
|
||||
@import "./colors.css";
|
||||
@import "./additional.css";
|
||||
|
|
|
|||
|
|
@ -229,50 +229,3 @@
|
|||
rgba(0, 0, 0, 0.067) 0px 2px 5px, rgba(0, 0, 0, 0.067) 0px 1px 1px;
|
||||
}
|
||||
}
|
||||
/* Container Mask */
|
||||
@layer components {
|
||||
.mask-both {
|
||||
mask-image: linear-gradient(
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgb(255, 255, 255) 20px,
|
||||
rgb(255, 255, 255) calc(100% - 20px),
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
.mask-both-lg {
|
||||
mask-image: linear-gradient(
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgb(255, 255, 255) 50px,
|
||||
rgb(255, 255, 255) calc(100% - 50px),
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.mask-b {
|
||||
mask-image: linear-gradient(
|
||||
rgb(255, 255, 255) calc(100% - 20px),
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.mask-b-lg {
|
||||
mask-image: linear-gradient(
|
||||
rgb(255, 255, 255) calc(100% - 50px),
|
||||
rgba(255, 255, 255, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.mask-t {
|
||||
mask-image: linear-gradient(
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgb(255, 255, 255) 20px
|
||||
);
|
||||
}
|
||||
|
||||
.mask-t-lg {
|
||||
mask-image: linear-gradient(
|
||||
rgba(255, 255, 255, 0) 0%,
|
||||
rgb(255, 255, 255) 50px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue