diff --git a/src/app/globals.css b/src/app/globals.css index 1b1ce4d..27b25b4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,7 +2,6 @@ @tailwind components; @tailwind utilities; - @layer base { :root { --background: hsl(0 0% 98%); @@ -228,8 +227,6 @@ /* } */ } -@layer reset, base, tokens, recipes, utilities; - :root { --scrollbar-thumb-idle: #c1c1c1; --scrollbar-thumb-hover: #848484; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9433013..6863a59 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,6 @@ import type { Metadata, Viewport } from 'next' import { Noto_Sans_SC } from 'next/font/google' import '@/app/globals.css' -import '@/app/styles/index.css' import { headers } from 'next/headers' import { NextAppDirEmotionCacheProvider } from 'tss-react/next/appDir' import { cn } from '@/lib/utils' diff --git a/src/app/page.tsx b/src/app/page.tsx index 7d43a8d..21501dd 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,16 @@ 'use client' import { useEffect, useRef, useState } from 'react' -import { addContent, deleteContent, getContents, updateContent } from '@/lib/indexed-db' +import { GlobalStyles } from 'tss-react' +import { + CACHE_KEY_TEMPLATE, + CACHE_KEY_THEME, + addContent, + cn, + deleteContent, + generateThemeVariables, + getContents, + updateContent, +} from '@/lib' import { useToast } from '@/components/ui/use-toast' import { ToastAction } from '@/components/ui/toast' import { Workspace } from '@/components/workspace/workspace' @@ -8,26 +18,28 @@ import { Header } from '@/components/header/header' import { Preview } from '@/components/preview/preview' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import type { Content, ContentWithId, PreviewRef, Theme, ThemeContent } from '@/types/common' -import { cn, getPreviewWidthClass, getThemeBaseClass } from '@/lib/utils' -import { defaultTheme, defaultThemeColor } from '@/lib' +import type { Content, ContentWithId, PreviewRef, ThemeContent } from '@/types' +import { DEFAULT_TEMPLATE, DEFAULT_THEME } from '@/theme' +import { CustomThemeContext } from '@/contexts/custom-theme-context' +import { useThemeStore } from '@/store/use-theme-store' export default function Home() { const [contents, setContents] = useState([]) - const [theme, setTheme] = useState(defaultTheme) - const [themeColor, setThemeColor] = useState(defaultThemeColor.label) + const { templateName, setTemplateName, theme, setTheme, templateMap, themeMap } = useThemeStore() + const [cssVariables, setCssVariables] = useState({}) const [tabValue, setTabValue] = useState('workspace') const { toast } = useToast() const previewRef = useRef(null) useEffect(() => { if (typeof window !== 'undefined') { - const currentTheme = (localStorage.getItem('currentTheme') || defaultTheme) as Theme + const currentTemplate = (localStorage.getItem(CACHE_KEY_TEMPLATE) || DEFAULT_TEMPLATE) + setTemplateName(currentTemplate) + + const currentTheme = localStorage.getItem(CACHE_KEY_THEME) || DEFAULT_THEME.label setTheme(currentTheme) - const currentThemeColor = localStorage.getItem('currentThemeColor') || defaultThemeColor.label - setThemeColor(currentThemeColor) } - }, []) + }, [setTemplateName, setTheme]) useEffect(() => { const fetchContents = async () => { @@ -46,6 +58,15 @@ export default function Home() { fetchContents() }, [toast]) + useEffect(() => { + const templateConfig = themeMap[templateName].find(item => item.label === theme) + + if (templateConfig) { + const cssVariables = generateThemeVariables(templateConfig.theme!) + setCssVariables(cssVariables) + } + }, [theme, templateName, themeMap]) + async function handleThemeContentSubmit(themeContent: ThemeContent) { try { if ('id' in themeContent) { @@ -62,9 +83,9 @@ export default function Home() { } as Content const id = await addContent(newContent) setContents(prevContents => [...prevContents, { ...newContent, id }]) + setTemplateName(themeContent.template) setTheme(themeContent.theme) - setThemeColor(themeContent.themeColor) - window.localStorage.setItem('currentTheme', themeContent.theme) + window.localStorage.setItem('currentTemplate', themeContent.template) } } catch (error) { toast({ @@ -136,82 +157,53 @@ export default function Home() { } return ( -
-
-
- {/* desktop */} -
-
- -
-
- -
-
- - {/* mobile phone */} - - - 编辑器 - 预览 - - -
- -
-
- -
- -
-
-
-
-
+ + +
+
+
+ + + 编辑器 + 预览 + + +
+ +
+
+ +
+ +
+
+
+
+
+
) } diff --git a/src/app/styles/apple-style-theme.css b/src/app/styles/apple-style-theme.css deleted file mode 100644 index a57fad5..0000000 --- a/src/app/styles/apple-style-theme.css +++ /dev/null @@ -1,142 +0,0 @@ -.apple-style { - background-color: var(--template-theme-background); - - --template-theme-heading1-font-size: 34px; - --template-theme-heading1-desc-font-size: 19px; - --template-theme-heading2-font-size: 30px; - --template-theme-heading2-padding-y: 10px; - --template-theme-heading2-padding-x: 19px; - --template-theme-heading2-line-height: 1.2; - --template-theme-primary-content-font-size: 15px; - --template-theme-heading3-font-size: 20px; - --template-theme-heading3-padding-y: 8px; - --template-theme-heading3-padding-x: 20px; - --template-theme-heading3-line-height: 1.2; - - &.snow_white { - --template-theme-background: #fcfcfc; - --template-theme-even-background: #f4f4f4; - --template-theme-odd-background: #fcfcfc; - --template-theme-primary-foreground: #161616; - --template-theme-secondary-foreground: #666; - --template-theme-secondary-background: #e8e8e8; - --template-theme-thirdary-foreground: #333; - } - - &.midnight_black { - --template-theme-background: #000; - --template-theme-even-background: #111; - --template-theme-odd-background: #000; - --template-theme-primary-foreground: #fff; - --template-theme-secondary-foreground: #989898; - --template-theme-secondary-background: #282828; - --template-theme-thirdary-foreground: #fff; - } - - .one-theme { - display: flex; - flex-direction: column; - justify-content: center; - min-height: 256px; - padding: 45px 36px; - text-align: center; - background-color: var(--template-theme-background); - line-height: 1.2; - } - - .one-theme__title { - margin-bottom: 14px; - font-weight: bold; - font-size: var(--template-theme-heading1-font-size); - color: var(--template-theme-primary-foreground); - } - - .one-theme__content { - font-size: var(--template-theme-heading1-desc-font-size); - color: var(--template-theme-secondary-foreground); - - :where(p) { - margin-top: 18px; - margin-bottom: 18px; - } - - :where(:first-child) { - margin-top: 0; - } - - :where(:last-child) { - margin-bottom: 0; - } - } - - .one-item { - padding: 45px 36px; - - &:where(:nth-of-type(even)) { - background-color: var(--template-theme-even-background); - } - - &:where(:nth-of-type(odd)) { - background-color: var(--template-theme-odd-background); - } - } - - .one-item__title { - font-size: var(--template-theme-heading2-font-size); - font-weight: bold; - line-height: var(--template-theme-heading2-line-height); - color: var(--template-theme-primary-foreground); - } - - .one-item__content { - font-size: var(--template-theme-primary-content-font-size); - color: var(--template-theme-secondary-foreground); - - :where(p) { - margin-top: 8px; - margin-bottom: 8px; - } - - :where(img) { - margin-top: 8px; - margin-bottom: 8px; - } - - :where(hr) { - border-top-color: var(--template-theme-secondary-foreground); - } - - :where(blockquote) { - border-left-color: var(--template-theme-secondary-foreground); - } - - :where(code):not(pre code) { - background-color: var(--template-theme-secondary-background); - color: var(--template-theme-thirdary-foreground); - } - } - - .one-item__children { - margin-top: 20px; - } - - .one-child-item { - margin-top: 40px; - } - - .one-item__images { - margin-top: 14px; - } - - .one-child-item__title { - line-height: var(--template-theme-heading3-line-height); - font-weight: bold; - font-size: var(--template-theme-heading3-font-size); - color: var(--template-theme-primary-foreground); - - p { - line-height: inherit; - margin: 0; - } - } -} diff --git a/src/app/styles/index.css b/src/app/styles/index.css deleted file mode 100644 index 3a44e7e..0000000 --- a/src/app/styles/index.css +++ /dev/null @@ -1,83 +0,0 @@ -/* @import url('./wechat-post-theme.css'); */ -/* @import url('./red-post-theme.css'); */ -/* @import url('./apple-style-theme.css'); */ - -.one-item__content { - /* :where(:first-child) { */ - /* margin-top: 0 !important; */ - /* } */ - /**/ - /* :where(:last-child) { */ - /* margin-bottom: 0 !important; */ - /* } */ - - :where(ul):not(:where([class~=one-item__children])) { - list-style-type: disc; - padding-left: 1.625rem; - } - - :where(p) { - margin-top: 5px; - margin-bottom: 5px; - line-height: 1.7; - } - - :where(ul>li, ol>li):not(:where([class~=one-child-item])) { - margin-top: .75rem; - margin-bottom: .75rem; - } - - :where(ol):not(:where([class~=one-item__children])) { - list-style-type: decimal; - padding-left: 1.625rem; - } - - :where(ul ul, ol ul, ol ol, ul ol):not(:where([class~=one-item__children])) { - margin-top: .75rem; - margin-bottom: .75rem; - } - - :where(img) { - display: block; - margin-top: .75rem; - margin-bottom: .75rem; - } - - :where(blockquote) { - border-left: 3px solid var(--gray-2); - margin: 1.5rem 0; - padding-left: 1rem; - } - - :where(hr) { - border: none; - border-top: 1px solid var(--gray-2); - margin: 2rem 0; - } - - /* Code and preformatted text styles */ - :where(code) { - background-color: var(--gray-2); - border-radius: 0.4rem; - color: var(--black); - font-size: 0.85rem; - padding: 0.25em 0.3em; - } - - :where(pre) { - background: var(--black); - border-radius: 0.5rem; - color: var(--white); - font-family: 'JetBrainsMono', monospace; - margin: 1.5rem 0; - padding: 0.75rem 1rem; - - code { - background: none; - color: inherit; - font-size: 0.8rem; - padding: 0; - } - - } -} diff --git a/src/app/styles/red-post-theme.css b/src/app/styles/red-post-theme.css deleted file mode 100644 index 623732a..0000000 --- a/src/app/styles/red-post-theme.css +++ /dev/null @@ -1,187 +0,0 @@ -.red-post { - background-color: var(--template-theme-background); - - --template-theme-heading1-font-size: 36px; - --template-theme-heading1-desc-font-size: 14px; - --template-theme-heading2-font-size: 18px; - --template-theme-heading2-padding-y: 10px; - --template-theme-heading2-padding-x: 19px; - --template-theme-heading2-line-height: 1.2; - --template-theme-primary-content-font-size: 14px; - --template-theme-heading3-font-size: 16px; - --template-theme-heading3-padding-y: 8px; - --template-theme-heading3-padding-x: 20px; - --template-theme-heading3-line-height: 1.2; - - &.tech_blue { - --template-theme-background: #ccedff; - --template-theme-heading1-foreground: #333; - --template-theme-heading1-desc-foreground: #333; - --template-theme-heading2-foreground: #333; - --template-theme-heading2-background: linear-gradient(90deg, #3CA0FF 0%, #1D6DFF 100%); - --template-theme-heading2-after-foreground: rgb(255 255 255 / 0.2); - --template-theme-primary-content-foreground: #333; - --template-theme-primary-content-background: rgb(255 255 255 / 0.7); - --template-theme-heading3-background: linear-gradient(90deg, #3CA0FF 0%, #1D6DFF 100%); - --template-theme-heading3-foreground: #333; - --template-theme-background-image: url('/images/them-bg-tech-blue.png') - } - - &.vibrant_orange { - --template-theme-background: #fff6ef; - --template-theme-heading1-foreground: #333; - --template-theme-heading1-desc-foreground: #333; - --template-theme-heading2-foreground: #333; - --template-theme-heading2-background: linear-gradient(90deg, #FF611D 0%, #FF8E3C 100%); - --template-theme-heading2-after-foreground: rgb(255 255 255 / 0.2); - --template-theme-primary-content-foreground: #333; - --template-theme-primary-content-background: rgb(255 255 255 / 0.7); - --template-theme-heading3-background: linear-gradient(90deg, #FF611D 0%, #FF8E3C 100%); - --template-theme-heading3-foreground: #333; - --template-theme-background-image: url('/images/them-bg-vibrant-orange.png') - } - - &.rose_red { - --template-theme-background: #f4f4f4; - --template-theme-heading1-foreground: #333; - --template-theme-heading1-desc-foreground: #333; - --template-theme-heading2-foreground: #333; - --template-theme-heading2-background: linear-gradient(90deg, #F14040 0%, #FF7676 100%); - --template-theme-heading2-after-foreground: rgb(255 255 255 / 0.2); - --template-theme-primary-content-foreground: #333; - --template-theme-primary-content-background: rgb(255 255 255 / 0.7); - --template-theme-heading3-background: linear-gradient(90deg, #F14040 0%, #FF7676 100%); - --template-theme-heading3-foreground: #333; - --template-theme-background-image: url('/images/them-bg-rose-red.png') - } - - /* common */ - .one-theme { - display: flex; - flex-direction: column; - padding: 12px 13px; - background-color: var(--template-theme-background); - height: 553px; - overflow: hidden; - outline: 0.1px solid gray; - } - - .one-theme__title { - font-weight: bold; - font-size: var(--template-theme-heading1-font-size); - color: var(--template-theme-heading1-foreground); - } - - .one-theme__content { - font-size: var(--template-theme-heading1-desc-font-size); - color: var(--template-theme-heading1-desc-foreground); - } - - .one-theme__image { - - img { - width: auto; - height: 100%; - } - } - - .one-item { - height: 553px; - background-color: var(--template-theme-background); - padding-left: 12px; - padding-right: 12px; - padding-top: 13px; - padding-bottom: 13px; - overflow: hidden; - outline: 0.1px solid gray; - } - - .one-item__title { - position: relative; - margin-bottom: 13px; - /* padding: var(--template-theme-heading2-padding-y) var(--template-theme-heading2-padding-x); */ - font-size: var(--template-theme-heading2-font-size); - font-weight: bold; - line-height: var(--template-theme-heading2-line-height); - color: var(--template-theme-heading2-foreground); - /* background: var(--template-theme-heading2-background); */ - } - - .one-item__content { - position: relative; - border-radius: 12px; - font-size: var(--template-theme-primary-content-font-size); - color: var(--template-theme-primary-content-foreground); - text-align: justify; - } - - .one-item__images { - margin-bottom: 10px; - } - - .one-item__children { - margin-bottom: 10px; - } - - .one-child-item { - margin-top: 20px; - margin-bottom: 20px; - - :where(:first-child) { - margin-top: 0; - } - - :where(:last-child) { - margin-bottom: 0; - } - } - - .one-child-item__title { - display: flex; - align-items: center; - width: fit-content; - margin-bottom: 10px !important; - line-height: var(--template-theme-heading3-line-height); - font-weight: bold; - font-size: var(--template-theme-heading3-font-size); - color: var(--template-theme-heading3-foreground); - - p { - line-height: inherit; - } - } -} - -.wechat-post-1 { - .one-theme__bg { - background-image: var(--template-theme-background-image); - } - - .one-item__title { - border-radius: 10px; - - &::after { - content: 'NO.' attr(data-index); - position: absolute; - right: 19px; - top: 50%; - transform: translateY(-50%); - display: inline-block; - margin-left: 5px; - color: var(--template-theme-heading2-after-foreground); - } - - p { - margin-right: 45px; - } - } -} - -.wechat-post-2 { - .one-item__title { - display: flex; - align-items: center; - width: fit-content; - border-radius: 9999px 9999px 9999px 2px; - } -} diff --git a/src/app/styles/wechat-post-theme.css b/src/app/styles/wechat-post-theme.css deleted file mode 100644 index 808419a..0000000 --- a/src/app/styles/wechat-post-theme.css +++ /dev/null @@ -1,214 +0,0 @@ -.wechat-post { - background-color: var(--template-theme-background); - - --template-theme-heading1-font-size: 30px; - --template-theme-heading1-desc-font-size: 18px; - --template-theme-heading2-font-size: 19px; - --template-theme-heading2-padding-y: 10px; - --template-theme-heading2-padding-x: 19px; - --template-theme-heading2-line-height: 1.2; - --template-theme-primary-content-font-size: 15px; - --template-theme-heading3-font-size: 17px; - --template-theme-heading3-padding-y: 8px; - --template-theme-heading3-padding-x: 20px; - --template-theme-heading3-line-height: 1.2; - - &.tech_blue { - --template-theme-background: #ccedff; - --template-theme-heading1-foreground: #333; - --template-theme-heading1-desc-foreground: #333; - --template-theme-heading2-foreground: #fff; - --template-theme-heading2-background: linear-gradient(90deg, #3CA0FF 0%, #1D6DFF 100%); - --template-theme-heading2-after-foreground: rgb(255 255 255 / 0.2); - --template-theme-primary-content-foreground: #333; - --template-theme-primary-content-background: rgb(255 255 255 / 0.7); - --template-theme-heading3-background: linear-gradient(90deg, #3CA0FF 0%, #1D6DFF 100%); - --template-theme-heading3-foreground: #fff; - --template-theme-background-image: url('/images/them-bg-tech-blue.png') - } - - &.wechat-post.vibrant_orange { - --template-theme-background: #fff6ef; - --template-theme-heading1-foreground: #333; - --template-theme-heading1-desc-foreground: #333; - --template-theme-heading2-foreground: #fff; - --template-theme-heading2-background: linear-gradient(90deg, #FF611D 0%, #FF8E3C 100%); - --template-theme-heading2-after-foreground: rgb(255 255 255 / 0.2); - --template-theme-primary-content-foreground: #333; - --template-theme-primary-content-background: rgb(255 255 255 / 0.7); - --template-theme-heading3-background: linear-gradient(90deg, #FF611D 0%, #FF8E3C 100%); - --template-theme-heading3-foreground: #fff; - --template-theme-background-image: url('/images/them-bg-vibrant-orange.png') - } - - &.wechat-post.rose_red { - --template-theme-background: #f4f4f4; - --template-theme-heading1-foreground: #333; - --template-theme-heading1-desc-foreground: #333; - --template-theme-heading2-foreground: #fff; - --template-theme-heading2-background: linear-gradient(90deg, #F14040 0%, #FF7676 100%); - --template-theme-heading2-after-foreground: rgb(255 255 255 / 0.2); - --template-theme-primary-content-foreground: #333; - --template-theme-primary-content-background: rgb(255 255 255 / 0.7); - --template-theme-heading3-background: linear-gradient(90deg, #F14040 0%, #FF7676 100%); - --template-theme-heading3-foreground: #fff; - --template-theme-background-image: url('/images/them-bg-rose-red.png') - } - - /* common */ - .one-theme { - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - min-height: 200px; - padding-left: 12px; - padding-right: 12px; - padding-top: 13px; - padding-bottom: 13px; - text-align: center; - } - - .one-theme__bg { - position: absolute; - left: 0; - top: 0; - width: 100%; - min-height: 150%; - background-size: cover; - background-repeat: no-repeat; - background-color: var(--template-theme-background); - z-index: 0; - } - - .one-theme__title { - position: relative; - z-index: 1; - margin-top: 45px; - margin-bottom: 5px; - font-weight: bold; - font-size: var(--template-theme-heading1-font-size); - color: var(--template-theme-heading1-foreground); - } - - .one-theme__content { - position: relative; - z-index: 2; - font-size: var(--template-theme-heading1-desc-font-size); - } - - .one-theme__image { - position: relative; - z-index: 2; - margin-top: 5px; - margin-bottom: 5px; - - img { - width: auto; - height: 100%; - } - } - - .one-item { - background-color: var(--template-theme-background); - padding-left: 12px; - padding-right: 12px; - padding-top: 13px; - padding-bottom: 13px; - } - - .one-item__title { - position: relative; - margin-bottom: 13px; - padding: var(--template-theme-heading2-padding-y) var(--template-theme-heading2-padding-x); - font-size: var(--template-theme-heading2-font-size); - font-weight: bold; - line-height: var(--template-theme-heading2-line-height); - color: var(--template-theme-heading2-foreground); - background: var(--template-theme-heading2-background); - border-radius: 9999px 9999px 9999px 2px; - } - - .one-item__content { - position: relative; - padding: 12px 18px; - border-radius: 12px; - font-size: var(--template-theme-primary-content-font-size); - background-color: var(--template-theme-primary-content-background); - color: var(--template-theme-primary-content-foreground); - } - - .one-item__images { - margin-top: 10px; - margin-bottom: 10px; - } - - .one-item__children { - margin-bottom: 10px; - } - - .one-child-item { - margin-top: 20px; - margin-bottom: 20px; - - :where(:first-child) { - margin-top: 0; - } - - :where(:last-child) { - margin-bottom: 0; - } - } - - .one-child-item__title { - display: flex; - align-items: center; - width: fit-content; - margin-bottom: 10px !important; - line-height: var(--template-theme-heading3-line-height); - border-radius: 9999px 9999px 9999px 2px; - font-weight: bold; - padding: var(--template-theme-heading3-padding-y) var(--template-theme-heading3-padding-x); - font-size: var(--template-theme-heading3-font-size); - background: var(--template-theme-heading3-background); - color: var(--template-theme-heading3-foreground); - - p { - line-height: inherit; - } - } -} - -.wechat-post-1 { - .one-theme__bg { - background-image: var(--template-theme-background-image); - } - - .one-item__title { - border-radius: 10px; - - &::after { - content: 'NO.' attr(data-index); - position: absolute; - right: 19px; - top: 50%; - transform: translateY(-50%); - display: inline-block; - margin-left: 5px; - color: var(--template-theme-heading2-after-foreground); - } - - p { - margin-right: 40px; - } - } -} - -.wechat-post-2 { - .one-item__title { - display: flex; - align-items: center; - width: fit-content; - border-radius: 9999px 9999px 9999px 2px; - } -} diff --git a/src/components/header/export-dialog.tsx b/src/components/header/export-dialog.tsx index d1a1006..bec35a2 100644 --- a/src/components/header/export-dialog.tsx +++ b/src/components/header/export-dialog.tsx @@ -78,19 +78,32 @@ export function ExportImageDialog({ // 导出整个 Preview let index = 1 - images.push(await exportImage(previewRef.current.containerRef.current!, `${index}_full_preview.png`, exportOption)) - index = index + 1 - - // 导出每个顶层 PreviewItem - const itemRefs = previewRef.current.itemRefs.current! - for (const [id, ref] of Object.entries(itemRefs)) { - if (ref) { - images.push(await exportImage(ref, `${index}_${removeHtmlTags(dataMap.get(Number(id)))}.png`, exportOption)) - index++ + if (previewRef.current.containerRef.current) { + const fullPreviewBlobObject = await exportImage(previewRef.current.containerRef.current!, `${index}_full_preview.png`, exportOption) + + if (fullPreviewBlobObject && fullPreviewBlobObject.data) { + images.push(fullPreviewBlobObject) } - } + index = index + 1 + + // 导出每个顶层 PreviewItem + const itemRefs = previewRef.current.itemRefs.current + if (itemRefs) { + for (const [id, ref] of Object.entries(itemRefs)) { + if (ref) { + const cardPreviewBlobObject = await exportImage(ref, `${index}_${removeHtmlTags(dataMap.get(Number(id)))}.png`, exportOption) + + if (cardPreviewBlobObject && cardPreviewBlobObject.data) { + images.push(cardPreviewBlobObject) + } + + index++ + } + } - setPreviewImages(images) + setPreviewImages(images) + } + } } catch (error) { console.log(error) } finally { @@ -99,13 +112,9 @@ export function ExportImageDialog({ } } - // 等待DOM节点更新,延迟生成图片 - const timer = setTimeout(() => { - if (isExporting) { - generateImages() - } - }, 1000) - return () => clearTimeout(timer) + if (previewRef.current && previewRef.current.itemRefs.current && previewRef.current.containerRef.current) { + generateImages() + } }, [previewRef, scale, setIsExporting, isExporting]) const exportImages = useCallback(async () => { diff --git a/src/components/header/header.tsx b/src/components/header/header.tsx index fe9166a..3db5164 100644 --- a/src/components/header/header.tsx +++ b/src/components/header/header.tsx @@ -18,9 +18,15 @@ import { } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Logo } from '@/components/logo' -import type { Content, ContentWithId, PreviewRef, Theme } from '@/types/common' +import type { Content, ContentWithId, PreviewRef } from '@/types' import type { ExportContent, ExportJSON } from '@/components/header/types' -import { addAllContents, removeAllContents } from '@/lib/indexed-db' +import { + CACHE_KEY_TEMPLATE, CACHE_KEY_THEME, + addAllContents, + cn, + removeAllContents, + removeHtmlTags, +} from '@/lib' import { Menubar, @@ -42,17 +48,22 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { cn, defaultTheme, defaultThemeColor, removeHtmlTags, themeColorMap, themeTemplates } from '@/lib' +import { + DEFAULT_TEMPLATE, + DEFAULT_TEMPLATES, + DEFAULT_THEME, + DEFAULT_THEME_COLOR_MAP, +} from '@/theme' import { usePlatform } from '@/hooks/use-platform' interface HeaderProps { contents: Content[]; setContents: (contents: ContentWithId[]) => void; previewRef: React.RefObject; - theme: Theme; - themeColor: string; - setTheme: (theme: Theme) => void; - setThemeColor: (color: string) => void + templateName: string; + theme: string; + setTemplateName: (template: string) => void; + setTheme: (color: string) => void setTableValue?: (tab: string) => void } @@ -65,7 +76,7 @@ export function Header(props: HeaderProps) { const [isExporting, setIsExporting] = useState(true) const [scale, setScale] = useState('1') const platform = usePlatform() - const { contents, setContents, previewRef, theme, themeColor, setTheme, setThemeColor, setTableValue } = props + const { contents, setContents, previewRef, templateName, theme, setTemplateName, setTheme, setTableValue } = props const { toast } = useToast() const fileRef = useRef(null) @@ -153,12 +164,12 @@ export function Header(props: HeaderProps) { await addAllContents(contents) setContents(contents) - const theme = (importData.theme ?? defaultTheme) as Theme - const themeColor = importData.themeColor ?? defaultThemeColor.label + const templateName = (importData.theme ?? DEFAULT_TEMPLATE) + const theme = importData.themeColor ?? DEFAULT_THEME.label + setTemplateName(templateName) setTheme(theme) - setThemeColor(themeColor) - localStorage.setItem('currentTheme', theme) - localStorage.setItem('currentThemeColor', themeColor) + localStorage.setItem(CACHE_KEY_TEMPLATE, templateName) + localStorage.setItem(CACHE_KEY_THEME, theme) // 允许前后两次选择相同文件 event.target.value = '' @@ -193,8 +204,8 @@ export function Header(props: HeaderProps) { type: 'oneimg', version: 1, source: 'https://oneimgai.com', - theme: theme ?? defaultTheme, - themeColor: themeColor ?? defaultThemeColor.label, + theme: templateName ?? DEFAULT_TEMPLATE, + themeColor: theme ?? DEFAULT_THEME.label, data: exportContents, } @@ -241,8 +252,8 @@ export function Header(props: HeaderProps) { localStorage.clear() setContents([]) setIsOpenFile(false) - setTheme(defaultTheme) - setThemeColor(defaultThemeColor.label) + setTemplateName(DEFAULT_TEMPLATE) + setTheme(DEFAULT_THEME.label) } // open the dialog of saving as image @@ -347,12 +358,12 @@ export function Header(props: HeaderProps) {
模板
- { + const themeColor = DEFAULT_THEME_COLOR_MAP[value][0].label + setTemplateName(value) + setTheme(themeColor) + localStorage.setItem(CACHE_KEY_TEMPLATE, value) + localStorage.setItem(CACHE_KEY_THEME, themeColor) }}> @@ -360,7 +371,7 @@ export function Header(props: HeaderProps) { { - themeTemplates.map(template => ( + DEFAULT_TEMPLATES.map(template => ( {template.label} @@ -373,16 +384,16 @@ export function Header(props: HeaderProps) {
模版色
- {themeColorMap[theme].map(color => ( + {DEFAULT_THEME_COLOR_MAP[templateName].map(color => ( ))} diff --git a/src/components/header/types.ts b/src/components/header/types.ts index bec4a8d..dbd998c 100644 --- a/src/components/header/types.ts +++ b/src/components/header/types.ts @@ -1,4 +1,4 @@ -import type { ImageFile, Theme } from '@/types/common' +import type { ImageFile } from '@/types' export interface ExportOption { scale: number; @@ -24,7 +24,7 @@ export interface ExportJSON { type: 'oneimg'; version: number; source: string; - theme: Theme; + theme: string; themeColor: string; data: ExportContent[]; } diff --git a/src/components/preview/card.tsx b/src/components/preview/card.tsx index dfc3426..7dc142f 100644 --- a/src/components/preview/card.tsx +++ b/src/components/preview/card.tsx @@ -1,22 +1,22 @@ import DOMPurify from 'dompurify' import parse from 'html-react-parser' -import { forwardRef, useMemo } from 'react' +import { forwardRef, useContext, useMemo } from 'react' import { ImageList } from './image-list' -import type { ContentWithId, ImageFile, ModuleClassNameMap } from '@/types' -import { base64ToBlob, cn, stripEmptyParagraphs } from '@/lib/utils' +import { baseTemplate, themeColorStyles } from './styles' +import type { ContentWithId, ImageFile } from '@/types' +import { base64ToBlob, cn, createStyleClassMap, stripEmptyParagraphs } from '@/lib' +import { CustomThemeContext } from '@/contexts/custom-theme-context' interface PreviewItemProps { content: ContentWithId, children?: React.ReactNode, index: number, childContentsMap: Map, - templateClassNameMap: ModuleClassNameMap, - themeClassNameMap: ModuleClassNameMap, } -const Card = forwardRef(({ content, children, index, templateClassNameMap, themeClassNameMap, childContentsMap }, ref) => { +const Card = forwardRef(({ content, children, index, childContentsMap }, ref) => { + const theme = useContext(CustomThemeContext) const uploadFiles = content.uploadFiles - const imageFiles: ImageFile[] = useMemo(() => { return uploadFiles?.map(file => ({ uid: file.uid, @@ -26,10 +26,15 @@ const Card = forwardRef(({ content, children, })) }, [uploadFiles]) || [] + const templateClassNameMap = createStyleClassMap(theme.template, 'template', baseTemplate) + const themeClassNameMap = createStyleClassMap(themeColorStyles, 'theme') + + // template const heroTemplate = templateClassNameMap.hero const mainTemplate = templateClassNameMap.main const subTemplate = templateClassNameMap.sub + // theme color const heroTheme = themeClassNameMap.hero const mainTheme = themeClassNameMap.main const subTheme = themeClassNameMap.sub @@ -90,8 +95,6 @@ const Card = forwardRef(({ content, children, content={item} key={item.id} index={index} - templateClassNameMap={templateClassNameMap} - themeClassNameMap={themeClassNameMap} childContentsMap={childContentsMap} /> ))} diff --git a/src/components/preview/image-list.tsx b/src/components/preview/image-list.tsx index e4bea2b..babb138 100644 --- a/src/components/preview/image-list.tsx +++ b/src/components/preview/image-list.tsx @@ -1,5 +1,5 @@ import Image from 'next/image' -import type { ImageFile } from '@/types/common' +import type { ImageFile } from '@/types' export function ImageList({ images }: { images: ImageFile[] }) { return (
diff --git a/src/components/preview/preview.tsx b/src/components/preview/preview.tsx index 3973ea9..ff72bad 100644 --- a/src/components/preview/preview.tsx +++ b/src/components/preview/preview.tsx @@ -1,8 +1,7 @@ import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react' import { Card } from './card' -import { baseTemplateStyle, createTheme, techTemplateStyle } from './styles' -import type { ContentWithId, PreviewRef } from '@/types/common' -import { cn, createStyleClassMap } from '@/lib/utils' +import type { ContentWithId, PreviewRef } from '@/types' +import { cn } from '@/lib' const Preview = forwardRef(({ contents, className }, ref) => { const containerRef = useRef(null) @@ -33,9 +32,6 @@ const Preview = forwardRef {contents.length === 0 ? ( @@ -49,8 +45,6 @@ const Preview = forwardRef { itemRefs.current[content.id] = el! diff --git a/src/components/preview/styles/default-styles.ts b/src/components/preview/styles/base-template.ts similarity index 50% rename from src/components/preview/styles/default-styles.ts rename to src/components/preview/styles/base-template.ts index 0f23ca8..2a1a3ef 100644 --- a/src/components/preview/styles/default-styles.ts +++ b/src/components/preview/styles/base-template.ts @@ -63,15 +63,9 @@ export const commonTypography: CustomCSSProperties = { marginTop: '5px', marginBottom: '5px', }, - '& :where(:first-child)': { - marginTop: 0, - }, - '& :where(:last-child)': { - marginBottom: 0, - }, } -export const baseTemplateStyle: ArticleModuleTemplate = { +export const baseTemplate: ArticleModuleTemplate = { common: { container: {}, title: {}, @@ -82,7 +76,7 @@ export const baseTemplateStyle: ArticleModuleTemplate = { fontFamily: 'unset', fontKerning: 'none', fontStyle: 'normal', - fontSize: '22px', + fontSize: '18px', fontWeight: 400, fontSynthesis: 'none', color: '#333', @@ -90,9 +84,9 @@ export const baseTemplateStyle: ArticleModuleTemplate = { direction: 'ltr', height: 'auto', minHeight: '250px', - padding: '16px', + padding: 0, letterSpacing: 0, - lineHeight: '1.2', + lineHeight: '1.5', overflow: 'visible', overflowWrap: 'break-word', tabSize: 4, @@ -104,53 +98,12 @@ export const baseTemplateStyle: ArticleModuleTemplate = { wordBreak: 'normal', }, title: { - fontFamily: 'unset', - fontKerning: 'none', - fontStyle: 'normal', - fontSize: '22px', - fontWeight: 400, - fontSynthesis: 'none', - color: '#333', - contain: 'style', - direction: 'ltr', - height: 'auto', - padding: '16px', - letterSpacing: 0, + fontSize: '36px', + fontWeight: 700, lineHeight: '1.2', - overflow: 'visible', - overflowWrap: 'break-word', - tabSize: 4, - textAlign: 'left', - textIndent: 0, - textSizeAdjust: 'none', - textTransform: 'none', - whiteSpace: 'normal', - wordBreak: 'normal', }, content: { - fontFamily: 'unset', - fontKerning: 'none', - fontStyle: 'normal', - fontSize: '22px', - fontWeight: 400, - fontSynthesis: 'none', - color: '#333', - contain: 'style', - direction: 'ltr', - height: 'auto', - padding: '16px', - letterSpacing: 0, - lineHeight: '1.2', - overflow: 'visible', - overflowWrap: 'break-word', - tabSize: 4, - textAlign: 'left', - textIndent: 0, - textSizeAdjust: 'none', - textTransform: 'none', - whiteSpace: 'normal', - wordBreak: 'normal', - + fontSize: '20px', }, }, main: { @@ -158,7 +111,7 @@ export const baseTemplateStyle: ArticleModuleTemplate = { fontFamily: 'unset', fontKerning: 'none', fontStyle: 'normal', - fontSize: '18px', + fontSize: '16px', fontWeight: 400, fontSynthesis: 'none', color: '#333', @@ -167,7 +120,7 @@ export const baseTemplateStyle: ArticleModuleTemplate = { height: 'auto', padding: 0, letterSpacing: 0, - lineHeight: '1.2', + lineHeight: '1.5', overflow: 'visible', overflowWrap: 'break-word', tabSize: 4, @@ -179,52 +132,11 @@ export const baseTemplateStyle: ArticleModuleTemplate = { wordBreak: 'normal', }, title: { - fontFamily: 'unset', - fontKerning: 'none', - fontStyle: 'normal', - fontSize: '18px', - fontWeight: 400, - fontSynthesis: 'none', - color: '#333', - contain: 'style', - direction: 'ltr', - height: 'auto', - padding: 0, - letterSpacing: 0, + fontSize: '30px', + fontWeight: 700, lineHeight: '1.2', - overflow: 'visible', - overflowWrap: 'break-word', - tabSize: 4, - textAlign: 'left', - textIndent: 0, - textSizeAdjust: 'none', - textTransform: 'none', - whiteSpace: 'normal', - wordBreak: 'normal', }, content: { - fontFamily: 'unset', - fontKerning: 'none', - fontStyle: 'normal', - fontSize: '18px', - fontWeight: 400, - fontSynthesis: 'none', - color: '#333', - contain: 'style', - direction: 'ltr', - height: 'auto', - padding: 0, - letterSpacing: 0, - lineHeight: '1.2', - overflow: 'visible', - overflowWrap: 'break-word', - tabSize: 4, - textAlign: 'left', - textIndent: 0, - textSizeAdjust: 'none', - textTransform: 'none', - whiteSpace: 'normal', - wordBreak: 'normal', }, }, sub: { @@ -243,7 +155,7 @@ export const baseTemplateStyle: ArticleModuleTemplate = { marginTop: '15px', marginBottom: '15px', letterSpacing: 0, - lineHeight: '1.2', + lineHeight: '1.5', overflow: 'visible', overflowWrap: 'break-word', tabSize: 4, @@ -255,52 +167,11 @@ export const baseTemplateStyle: ArticleModuleTemplate = { wordBreak: 'normal', }, title: { - fontFamily: 'unset', - fontKerning: 'none', - fontStyle: 'normal', - fontSize: '16px', - fontWeight: 400, - fontSynthesis: 'none', - color: '#333', - contain: 'style', - direction: 'ltr', - height: 'auto', - padding: 0, - letterSpacing: 0, + fontSize: '20px', + fontWeight: 700, lineHeight: '1.2', - overflow: 'visible', - overflowWrap: 'break-word', - tabSize: 4, - textAlign: 'left', - textIndent: 0, - textSizeAdjust: 'none', - textTransform: 'none', - whiteSpace: 'normal', - wordBreak: 'normal', }, content: { - fontFamily: 'unset', - fontKerning: 'none', - fontStyle: 'normal', - fontSize: '16px', - fontWeight: 400, - fontSynthesis: 'none', - color: '#333', - contain: 'style', - direction: 'ltr', - height: 'auto', - padding: 0, - letterSpacing: 0, - lineHeight: '1.2', - overflow: 'visible', - overflowWrap: 'break-word', - tabSize: 4, - textAlign: 'left', - textIndent: 0, - textSizeAdjust: 'none', - textTransform: 'none', - whiteSpace: 'normal', - wordBreak: 'normal', }, }, } diff --git a/src/components/preview/styles/create-theme.ts b/src/components/preview/styles/create-theme.ts deleted file mode 100644 index c20a129..0000000 --- a/src/components/preview/styles/create-theme.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { ArticleModuleTemplate } from '@/types/template' - -export function createTheme(): ArticleModuleTemplate { - return { - hero: { - container: { - backgroundColor: 'var(--hero-container-background)', - backgroundImage: 'var(--hero-container-background-image)', - color: 'var(--hero-container-foreground)', - }, - title: { - color: 'var(--hero-title-foreground)', - backgroundColor: 'var(--hero-title-background)', - backgroundImage: 'var(--hero-title-background-image)', - }, - content: { - color: 'var(--hero-content-foreground)', - backgroundColor: 'var(--hero-content-background)', - backgroundImage: 'var(--hero-content-background-image)', - }, - }, - main: { - container: { - backgroundColor: 'var(--main-container-background)', - backgroundImage: 'var(--main-container-background-image)', - color: 'var(--main-container-foreground)', - }, - title: { - color: 'var(--main-title-foreground)', - backgroundColor: 'var(--main-title-background)', - backgroundImage: 'var(--main-title-background-image)', - }, - content: { - color: 'var(--main-content-foreground)', - backgroundColor: 'var(--main-content-background)', - backgroundImage: 'var(--main-content-background-image)', - }, - }, - sub: { - container: { - backgroundColor: 'var(--sub-container-background)', - backgroundImage: 'var(--sub-container-background-image)', - color: 'var(--sub-container-foreground)', - }, - title: { - color: 'var(--sub-title-foreground)', - backgroundColor: 'var(--sub-title-background)', - backgroundImage: 'var(--sub-title-background-image)', - }, - content: { - color: 'var(--sub-content-foreground)', - backgroundColor: 'var(--sub-content-background)', - backgroundImage: 'var(--sub-content-background-image)', - }, - }, - } -} diff --git a/src/components/preview/styles/index.ts b/src/components/preview/styles/index.ts index 2c2553b..984fe07 100644 --- a/src/components/preview/styles/index.ts +++ b/src/components/preview/styles/index.ts @@ -1,4 +1,2 @@ -export * from './default-styles' -export * from './tech-template' -export * from './create-theme' - +export * from './base-template' +export * from './theme-color-style' diff --git a/src/components/preview/styles/theme-color-style.ts b/src/components/preview/styles/theme-color-style.ts new file mode 100644 index 0000000..a1c9971 --- /dev/null +++ b/src/components/preview/styles/theme-color-style.ts @@ -0,0 +1,56 @@ +import type { ArticleModuleTemplate } from '@/types/template' + +export const themeColorStyles: ArticleModuleTemplate = { + hero: { + container: { + backgroundColor: 'var(--hero-container-background)', + backgroundImage: 'var(--hero-container-background-image)', + color: 'var(--hero-container-foreground)', + }, + title: { + color: 'var(--hero-title-foreground)', + backgroundColor: 'var(--hero-title-background)', + backgroundImage: 'var(--hero-title-background-image)', + }, + content: { + color: 'var(--hero-content-foreground)', + backgroundColor: 'var(--hero-content-background)', + backgroundImage: 'var(--hero-content-background-image)', + }, + }, + main: { + container: { + backgroundColor: 'var(--main-container-background)', + backgroundImage: 'var(--main-container-background-image)', + color: 'var(--main-container-foreground)', + }, + title: { + color: 'var(--main-title-foreground)', + backgroundColor: 'var(--main-title-background)', + backgroundImage: 'var(--main-title-background-image)', + }, + content: { + color: 'var(--main-content-foreground)', + backgroundColor: 'var(--main-content-background)', + backgroundImage: 'var(--main-content-background-image)', + }, + }, + sub: { + container: { + backgroundColor: 'var(--sub-container-background)', + backgroundImage: 'var(--sub-container-background-image)', + color: 'var(--sub-container-foreground)', + }, + title: { + color: 'var(--sub-title-foreground)', + backgroundColor: 'var(--sub-title-background)', + backgroundImage: 'var(--sub-title-background-image)', + }, + content: { + color: 'var(--sub-content-foreground)', + backgroundColor: 'var(--sub-content-background)', + backgroundImage: 'var(--sub-content-background-image)', + }, + }, + +} diff --git a/src/components/workspace/content-item-buttons.tsx b/src/components/workspace/content-item-buttons.tsx index 29ba6be..602a55a 100644 --- a/src/components/workspace/content-item-buttons.tsx +++ b/src/components/workspace/content-item-buttons.tsx @@ -2,7 +2,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@radix-ui/react-tooltip import { Pencil, Plus, Trash2 } from 'lucide-react' import React from 'react' import { TooltipProvider } from '../ui/tooltip' -import type { ContentWithId } from '@/types/common' +import type { ContentWithId } from '@/types' import { cn } from '@/lib' export interface ContentItemButtonsProps { diff --git a/src/components/workspace/content-list.tsx b/src/components/workspace/content-list.tsx index 90bf7d6..1f7692c 100644 --- a/src/components/workspace/content-list.tsx +++ b/src/components/workspace/content-list.tsx @@ -23,8 +23,8 @@ import { import { ContentItem } from './content-item' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader } from '@/components/ui/dialog' -import type { ContentListProps, ContentWithId } from '@/types/common' -import { cn } from '@/lib/utils' +import type { ContentListProps, ContentWithId } from '@/types' +import { cn } from '@/lib' export default function ContentList(props: ContentListProps) { const { contents, setContents, onSubmit, onContentDelete } = props diff --git a/src/components/workspace/theme-form-dialog.tsx b/src/components/workspace/theme-form-dialog.tsx index 1875674..219f288 100644 --- a/src/components/workspace/theme-form-dialog.tsx +++ b/src/components/workspace/theme-form-dialog.tsx @@ -5,14 +5,14 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa import { Input } from '@/components/ui/input' import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form' import { Button } from '@/components/ui/button' -import type { Theme, ThemeContent } from '@/types/common' +import type { ThemeContent } from '@/types' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { themeColorMap, themeTemplates } from '@/lib/constants' +import { DEFAULT_TEMPLATES, DEFAULT_THEME_COLOR_MAP } from '@/theme' const formSchema = z.object({ title: z.string(), content: z.string(), - theme: z.string(), + template: z.string(), }) interface ThemeFormProps { @@ -35,9 +35,9 @@ export function ThemeFormDialog({ onSubmit, onOpenChange, open }: ThemeFormProps const content = { title: values.title, content: values.content, - theme: values.theme, + template: values.template, parentId: null, - themeColor: themeColorMap[(values.theme as Theme)][0].label, + theme: DEFAULT_THEME_COLOR_MAP[(values.template)][0].label, } as ThemeContent await onSubmit(content) // reset dialog state @@ -96,7 +96,7 @@ export function ThemeFormDialog({ onSubmit, onOpenChange, open }: ThemeFormProps /> ( {/* 模版 */} @@ -108,7 +108,7 @@ export function ThemeFormDialog({ onSubmit, onOpenChange, open }: ThemeFormProps - {themeTemplates.map(template => ( + {DEFAULT_TEMPLATES.map(template => ( {template.label} diff --git a/src/contexts/custom-theme-context.ts b/src/contexts/custom-theme-context.ts new file mode 100644 index 0000000..15f97d0 --- /dev/null +++ b/src/contexts/custom-theme-context.ts @@ -0,0 +1,7 @@ +import { createContext } from 'react' +import { techTemplate } from '@/theme/templates' + +export const CustomThemeContext = createContext({ + theme: 'tech_blue', + template: techTemplate, +}) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 34c20b6..e6f0dad 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,34 +1,2 @@ -import type { ThemeColorMap } from '@/types/common' - -export const themeTemplates = [ - { label: '简约科技风格', value: 'wechat-post-1', disabled: false }, - { label: '黑白苹果风格', value: 'apple-style', disabled: false }, - { label: '可爱卡通风格', value: 'cartoon-style', disabled: false }, - { label: '更多模版尽情期待', value: 'post-more', disabled: true }, -] as const - -export const themeColorMap: ThemeColorMap = { - 'wechat-post-1': [ - { value: '#4383ec', label: 'tech_blue' }, - { value: '#ff611d', label: 'vibrant_orange' }, - { value: '#f14040', label: 'rose_red' }, - ], - 'apple-style': [ - { value: '#ddd', label: 'snow_white' }, - { value: '#000', label: 'midnight_black' }, - ], - 'cartoon-style': [ - { value: '#000', label: 'snow_white' }, - ], - 'default': [ - { value: '#4383ec', label: 'tech_blue' }, - { value: '#ff611d', label: 'vibrant_orange' }, - { value: '#f14040', label: 'rose_red' }, - ], -} - -export const defaultTheme = 'apple-style' -export const defaultThemeColor = { - label: 'snow_white', - value: '#ddd', -} +export const CACHE_KEY_TEMPLATE = 'currentTemplate' +export const CACHE_KEY_THEME = 'currentTheme' diff --git a/src/lib/index.ts b/src/lib/index.ts index da651eb..09f07a3 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,3 +1,4 @@ export * from './utils' export * from './constants' export * from './indexed-db' +export * from './template' diff --git a/src/lib/template.ts b/src/lib/template.ts new file mode 100644 index 0000000..225d1bc --- /dev/null +++ b/src/lib/template.ts @@ -0,0 +1,91 @@ +import { tss } from 'tss-react' +import type { ArticleModuleTemplate, ModuleClassNameMap, ModuleSection, ThemeConfig } from '@/types' + +export const createStyle = (classNamePrefix: string) => { + return tss + .withParams<{ defaultStyles?: ModuleSection, templateStyles?: ModuleSection }>() + .withName(classNamePrefix) + .create(({ defaultStyles = {}, templateStyles = {} }) => ({ + container: { + ...defaultStyles.container, + ...templateStyles.container, + }, + title: { + ...defaultStyles.title, + ...templateStyles.title, + }, + content: { + ...defaultStyles.content, + ...templateStyles.content, + }, + })) +} + +export const createStyleClassMap = ( + templateStyles: ArticleModuleTemplate, prefix: string, baseStyles = {} as ArticleModuleTemplate, +) => { + const { classes: heroClasses } = createStyle(`${prefix}-hero`)({ + defaultStyles: baseStyles.hero ?? {}, + templateStyles: templateStyles.hero, + }) + + const { classes: mainClasses } = createStyle(`${prefix}-main`)({ + defaultStyles: baseStyles.main ?? {}, + templateStyles: templateStyles.main, + }) + + const { classes: subClasses } = createStyle(`${prefix}-sub`)({ + defaultStyles: baseStyles.sub ?? {}, + templateStyles: templateStyles.sub, + }) + + const { classes: defaultClasses } = createStyle(`${prefix}-common`)({ + defaultStyles: baseStyles.common ?? {}, + templateStyles: templateStyles.common, + }) + + const templateClassNameMap: ModuleClassNameMap = { + common: defaultClasses, + hero: heroClasses, + main: mainClasses, + sub: subClasses, + } + return templateClassNameMap +} + +// export function generateThemeVariables(theme: ThemeConfig): string { +// const cssVariables = flattenThemeConfig(theme, '', {}) +// return ` +// :root { +// ${Object.entries(cssVariables) +// .map(([key, value]) => `${key}: ${value};`) +// .join('\n')} +// } +// ` +// } + +export function generateThemeVariables(theme: ThemeConfig): Record { + return flattenThemeConfig(theme, '', {}) +} + +export function flattenThemeConfig( + obj: Record, + parentKey = '', + result: Record, +) { + for (const key in obj) { + const kebabKey = camelToKebab(key) + const newKey = parentKey ? `${parentKey}-${kebabKey}` : `--${kebabKey}` + if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + flattenThemeConfig(obj[key], newKey, result) + } else { + result[newKey] = obj[key] + } + } + + return result +} + +export function camelToKebab(str: string) { + return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e336919..05f4e43 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,8 +1,7 @@ import { type ClassValue, clsx } from 'clsx' import { twMerge } from 'tailwind-merge' import UPNG from '@pdf-lib/upng' -import { tss } from 'tss-react' -import type { ArticleModuleTemplate, ImageBase, ModuleClassNameMap, ModuleSection } from '@/types' +import type { ImageBase } from '@/types' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -148,84 +147,3 @@ export function removeHtmlTags(html?: string) { } return html.replace(/<[^>]*>/g, '').trim() } - -/** - * 获取主题 CSS 类基础名 - * @param theme - * @returns - */ -export function getThemeBaseClass(theme: string) { - if (theme.startsWith('wechat-post')) { - return 'wechat-post' - } - - if (theme.startsWith('red-post')) { - return 'red-post' - } - - return theme -} - -export function getPreviewWidthClass(theme: string) { - if (theme.startsWith('wechat-post')) { - return 'w-[375px] ' - } - - if (theme.startsWith('red-post')) { - return 'w-[414px]' - } - - return 'w-[375px]' -} - -export const createStyle = (classNamePrefix: string) => { - return tss - .withParams<{ defaultStyles?: ModuleSection, templateStyles?: ModuleSection }>() - .withName(classNamePrefix) - .create(({ defaultStyles = {}, templateStyles = {} }) => ({ - container: { - ...defaultStyles.container, - ...templateStyles.container, - }, - title: { - ...defaultStyles.title, - ...templateStyles.title, - }, - content: { - ...defaultStyles.content, - ...templateStyles.content, - }, - })) -} - -export const createStyleClassMap = ( - templateStyles: ArticleModuleTemplate, prefix: string, baseStyles = {} as ArticleModuleTemplate, -) => { - const { classes: heroClasses } = createStyle(`${prefix}-hero`)({ - defaultStyles: baseStyles.hero ?? {}, - templateStyles: templateStyles.hero, - }) - - const { classes: mainClasses } = createStyle(`${prefix}-main`)({ - defaultStyles: baseStyles.main ?? {}, - templateStyles: templateStyles.main, - }) - - const { classes: subClasses } = createStyle(`${prefix}-sub`)({ - defaultStyles: baseStyles.sub ?? {}, - templateStyles: templateStyles.sub, - }) - - const { classes: defaultClasses } = createStyle(`${prefix}-common`)({ - defaultStyles: baseStyles.common ?? {}, - templateStyles: templateStyles.common, - }) - - const templateClassNameMap: ModuleClassNameMap = { - common: defaultClasses, - hero: heroClasses, - main: mainClasses, - sub: subClasses, - } - return templateClassNameMap -} diff --git a/src/store/use-editor-store.ts b/src/store/use-editor-store.ts index 7f65e4a..c42fe26 100644 --- a/src/store/use-editor-store.ts +++ b/src/store/use-editor-store.ts @@ -1,5 +1,6 @@ import { create } from 'zustand' -import type { EditorType } from '@/types/common' +import type { EditorType } from '@/types' + interface EditorStore { editorType: EditorType; editingContentId: number | null; diff --git a/src/store/use-theme-store.ts b/src/store/use-theme-store.ts new file mode 100644 index 0000000..3ba1446 --- /dev/null +++ b/src/store/use-theme-store.ts @@ -0,0 +1,23 @@ +import { create } from 'zustand' +import type { ArticleModuleTemplate, ThemeColorItem } from '@/types' +import { DEFAULT_TEMPLATE, DEFAULT_TEMPLATE_MAP, DEFAULT_THEME, DEFAULT_THEME_COLOR_MAP } from '@/theme' + +interface TemplateStore { + templateMap: Record; + templateName: string; + theme: string; + themeMap: Record; + setTemplateName: (templateName: string) => void; + setTheme: (theme: string) => void; + setTemplateMap: (templateMap: Record) => void; +} + +export const useThemeStore = create(set => ({ + templateName: DEFAULT_TEMPLATE, + theme: DEFAULT_THEME.label, + templateMap: DEFAULT_TEMPLATE_MAP, + themeMap: DEFAULT_THEME_COLOR_MAP, + setTemplateName: templateName => set({ templateName }), + setTemplateMap: templateMap => set({ templateMap }), + setTheme: theme => set({ theme }), +})) diff --git a/src/theme/index.ts b/src/theme/index.ts new file mode 100644 index 0000000..f78b5dd --- /dev/null +++ b/src/theme/index.ts @@ -0,0 +1,47 @@ +import { + simpleSnowBlack, + simpleSnowWhite, + simpleTemplate, + techBlue, + techRoseRed, + techTemplate, + techVibrantOrange, +} from './templates' +import type { ArticleModuleTemplate, ThemeColorItem } from '@/types' + +export const DEFAULT_TEMPLATES = [ + { label: '简约科技风格', value: 'wechat-post-1', disabled: false, template: techTemplate }, + { label: '黑白苹果风格', value: 'apple-style', disabled: false, template: simpleTemplate }, + { label: '更多模版尽情期待', value: 'post-more', disabled: true, template: null }, +] as const + +export const DEFAULT_TEMPLATE_MAP = DEFAULT_TEMPLATES + .filter(item => !item.disabled) + .reduce((acc, cur) => { + const { value, template } = cur + acc[value] = template + return acc + }, {} as Record) + +export const DEFAULT_THEME_COLOR_MAP: Record = { + 'wechat-post-1': [ + { value: '#4383ec', label: 'tech_blue', theme: techBlue }, + { value: '#ff611d', label: 'vibrant_orange', theme: techVibrantOrange }, + { value: '#f14040', label: 'rose_red', theme: techRoseRed }, + ], + 'apple-style': [ + { value: '#ddd', label: 'snow_white', theme: simpleSnowWhite }, + { value: '#000', label: 'midnight_black', theme: simpleSnowBlack }, + ], + 'default': [ + { value: '#4383ec', label: 'tech_blue', theme: techBlue }, + { value: '#ff611d', label: 'vibrant_orange', theme: techBlue }, + { value: '#f14040', label: 'rose_red', theme: techBlue }, + ], +} + +export const DEFAULT_TEMPLATE = 'apple-style' +export const DEFAULT_THEME = { + label: 'snow_white', + value: '#ddd', +} diff --git a/src/theme/templates/index.ts b/src/theme/templates/index.ts new file mode 100644 index 0000000..8948c5e --- /dev/null +++ b/src/theme/templates/index.ts @@ -0,0 +1,2 @@ +export * from './tech-template' +export * from './simple-template' diff --git a/src/theme/templates/simple-template/index.ts b/src/theme/templates/simple-template/index.ts new file mode 100644 index 0000000..3e69a03 --- /dev/null +++ b/src/theme/templates/simple-template/index.ts @@ -0,0 +1,2 @@ +export * from './simple-template' +export * from './simple-colors' diff --git a/src/theme/templates/simple-template/simple-colors.ts b/src/theme/templates/simple-template/simple-colors.ts new file mode 100644 index 0000000..25f7d35 --- /dev/null +++ b/src/theme/templates/simple-template/simple-colors.ts @@ -0,0 +1,49 @@ +import type { ThemeConfig } from '@/types' + +export const simpleSnowWhite: ThemeConfig = createSimpleThemeColor('#fcfcfc', '#161616', '#666', '#f4f4f4', '#e8e8e8') + +export const simpleSnowBlack: ThemeConfig = createSimpleThemeColor('#000', '#fff', '#989898', '#111', '#282828') + +function createSimpleThemeColor(containerBgColor: string, titlefgColor: string, contentColor: string, mainContainerEvenBgColor: string, mainContentSecondaryBgColor: string) { + return { + hero: { + container: { + background: containerBgColor, + foreground: '#333', + }, + title: { + foreground: titlefgColor, + background: 'transparent', + }, + content: { + foreground: contentColor, + background: 'transparent', + }, + }, + main: { + container: { + background: containerBgColor, + backgroundEven: mainContainerEvenBgColor, + backgroundOdd: containerBgColor, + }, + title: { + foreground: titlefgColor, + }, + content: { + foreground: contentColor, + background: 'tranparent', + secondaryBackground: mainContentSecondaryBgColor, + }, + }, + sub: { + container: { + }, + title: { + foreground: titlefgColor, + }, + content: { + foreground: contentColor, + }, + }, + } +} diff --git a/src/theme/templates/simple-template/simple-template.ts b/src/theme/templates/simple-template/simple-template.ts new file mode 100644 index 0000000..aefa3fd --- /dev/null +++ b/src/theme/templates/simple-template/simple-template.ts @@ -0,0 +1,87 @@ +import type { ArticleModuleTemplate } from '@/types' + +export const simpleTemplate: ArticleModuleTemplate = { + common: { + container: {}, + title: {}, + content: {}, + }, + hero: { + container: { + position: 'relative', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + minHeight: '256px', + padding: '45px 36px', + textAlign: 'center', + lineHeight: 1.2, + }, + title: { + marginBottom: '14px', + fontWeight: 700, + fontSize: '34px', + }, + content: { + 'fontSize': '19px', + ':where(p)': { + marginTop: '18px', + marginBottom: '18px', + }, + ':where(:first-child)': { + marginTop: 0, + }, + ':where(:last-child)': { + marginBottom: 0, + }, + }, + }, + main: { + container: { + 'padding': '45px 36px', + + '&:nth-of-type(2n)': { + backgroundColor: 'var(--main-container-background-even)', + }, + + '&:nth-of-type(2n + 1)': { + backgroundColor: 'var(--main-container-background-odd)', + }, + }, + title: { + fontSize: '30px', + fontWeight: 700, + lineHeight: 1.2, + }, + content: { + 'fontSize': '15px', + '& :where(p)': { + marginTop: '8px', + marginBottom: '8px', + }, + '& :where(img)': { + marginTop: '8px', + marginBottom: '8px', + }, + '& :where(hr)': { + borderTopColor: 'var(--main-content-foreground)', + }, + '& :where(blockquote)': { + borderLeftColor: 'var(--main-content-foreground)', + }, + '& :where(code):not(pre code)': { + backgroundColor: 'var(--main-content-secondary-background)', + color: 'var(--sub-container-foreground)', + }, + }, + }, + sub: { + container: {}, + title: { + lineHeight: 1.2, + fontWeight: 700, + fontSize: '20px', + }, + content: {}, + }, +} diff --git a/src/theme/templates/tech-template/index.ts b/src/theme/templates/tech-template/index.ts new file mode 100644 index 0000000..072203b --- /dev/null +++ b/src/theme/templates/tech-template/index.ts @@ -0,0 +1,2 @@ +export * from './tech-template' +export * from './tech-colors' diff --git a/src/theme/templates/tech-template/tech-colors.ts b/src/theme/templates/tech-template/tech-colors.ts new file mode 100644 index 0000000..3dfda91 --- /dev/null +++ b/src/theme/templates/tech-template/tech-colors.ts @@ -0,0 +1,51 @@ +import type { ThemeConfig } from '@/types' + +export const techBlue: ThemeConfig = createTechThemeColor('#ccedff', 'tech-blue', '90deg, #3ca0ff 0%, #1d6dff 100%') + +export const techVibrantOrange: ThemeConfig = createTechThemeColor('#fff6ef', 'vibrant-orange', '90deg, #ff611d 0%, #ff8e3c 100%') + +export const techRoseRed: ThemeConfig = createTechThemeColor('#f4f4f4', 'rose-red', '90deg, #f14040 0%, #ff7676 100%') + +export function createTechThemeColor(containerBgColor: string, containerBgImage: string, titleBgImage: string) { + return { + hero: { + container: { + background: containerBgColor, + backgroundImage: `url(/images/them-bg-${containerBgImage}.png)`, + foreground: '#333', + }, + title: { + foreground: '#333', + background: 'transparent', + }, + content: { + foreground: '#333', + background: 'transparent', + }, + }, + main: { + container: { + background: containerBgColor, + }, + title: { + foreground: '#fff', + backgroundImage: `linear-gradient(${titleBgImage})`, + }, + content: { + foreground: '#333', + background: 'rgba(255, 255, 255, 0.7)', + }, + }, + sub: { + container: { + }, + title: { + foreground: '#fff', + backgroundImage: `linear-gradient(${titleBgImage})`, + }, + content: { + foreground: '#333', + }, + }, + } +} diff --git a/src/components/preview/styles/tech-template.ts b/src/theme/templates/tech-template/tech-template.ts similarity index 95% rename from src/components/preview/styles/tech-template.ts rename to src/theme/templates/tech-template/tech-template.ts index 293903b..99220f7 100644 --- a/src/components/preview/styles/tech-template.ts +++ b/src/theme/templates/tech-template/tech-template.ts @@ -1,6 +1,6 @@ -import type { ArticleModuleTemplate } from '@/types/template' +import type { ArticleModuleTemplate } from '@/types' -export const techTemplateStyle: ArticleModuleTemplate = { +export const techTemplate: ArticleModuleTemplate = { common: { container: {}, title: {}, @@ -13,8 +13,8 @@ export const techTemplateStyle: ArticleModuleTemplate = { flexDirection: 'column', justifyContent: 'center', paddingLeft: '12px', - paddingRight: '12px', paddingTop: '13px', + paddingRight: '12px', paddingBottom: '13px', textAlign: 'center', backgroundSize: 'cover', diff --git a/src/types/common.ts b/src/types/common.ts index 88f97a1..04c4fe5 100644 --- a/src/types/common.ts +++ b/src/types/common.ts @@ -18,8 +18,8 @@ export interface ThemeContent { id?: number; title: string; content?: string; - theme: Theme; - themeColor: string; + template: string; + theme: string; } export type EditorType = 'add' | 'add_sub' | 'close' @@ -119,17 +119,3 @@ export interface ContainerProps { onSubmit: (content: Content) => Promise; handleDialogOpen: (content: ContentWithId) => void; } - -export type ThemeColorItem = { - value: string; - label: string; -} - -export type ThemeColorMap = { - 'wechat-post-1': readonly ThemeColorItem[]; - 'apple-style': readonly ThemeColorItem[]; - 'cartoon-style': readonly ThemeColorItem[]; - 'default': readonly ThemeColorItem[] -} - -export type Theme = keyof ThemeColorMap diff --git a/src/types/template.ts b/src/types/template.ts index f408fd8..edf1afb 100644 --- a/src/types/template.ts +++ b/src/types/template.ts @@ -19,3 +19,33 @@ export interface ArticleModuleTemplate { export type ModuleClassName = Record<'container' | 'title' | 'content', string> export type ModuleClassNameMap = Record<'common' | 'hero' | 'main' | 'sub', ModuleClassName> + +export interface ThemeConfigProperty { + background?: string; + foreground?: string; + backgroundImage?: string; +} + +export interface ThemeConfig { + hero?: { + container?: ThemeConfigProperty; + title?: ThemeConfigProperty; + content?: ThemeConfigProperty; + }; + main?: { + container?: ThemeConfigProperty; + title?: ThemeConfigProperty; + content?: ThemeConfigProperty; + }; + sub?: { + container?: ThemeConfigProperty; + title?: ThemeConfigProperty; + content?: ThemeConfigProperty; + }; +} + +export type ThemeColorItem = { + value: string; + label: string; + theme?: ThemeConfig; +}