From a13208a5f7059dfba26c41cfe5d1de114d7c426c Mon Sep 17 00:00:00 2001 From: byodian Date: Sun, 24 Nov 2024 21:05:55 +0800 Subject: [PATCH] Feature/v0.4.0 (#10) * feat: tss-react for dynamic theme * feat: create dynamic theme * feat: separate mobile and desktop page code * feat: add dynamic theme --- next.config.mjs | 4 +- package.json | 2 + pnpm-lock.yaml | 336 +++++++++++++++--- postcss.config.mjs | 4 +- src/app/layout.tsx | 8 +- src/app/page.tsx | 144 ++++---- src/app/styles/apple-style-theme.css | 142 -------- src/app/styles/index.css | 83 ----- src/app/styles/red-post-theme.css | 187 ---------- src/app/styles/wechat-post-theme.css | 214 ----------- src/components/config/config.tsx | 84 +++++ src/components/header/export-dialog.tsx | 45 ++- src/components/header/header.tsx | 67 ++-- src/components/header/types.ts | 4 +- src/components/preview/card.tsx | 111 ++++++ src/components/preview/image-list.tsx | 24 +- src/components/preview/preview-item.tsx | 53 --- src/components/preview/preview.tsx | 27 +- .../preview/styles/base-template.ts | 177 +++++++++ src/components/preview/styles/index.ts | 2 + .../preview/styles/theme-color-style.ts | 56 +++ src/components/ui/card.tsx | 79 ++++ .../workspace/content-item-buttons.tsx | 2 +- src/components/workspace/content-list.tsx | 4 +- .../workspace/theme-form-dialog.tsx | 14 +- src/components/workspace/workspace.tsx | 2 +- src/contexts/custom-theme-context.ts | 7 + src/lib/constants.ts | 36 +- src/lib/index.ts | 1 + src/lib/template.ts | 91 +++++ src/lib/utils.ts | 32 +- src/store/use-editor-store.ts | 3 +- src/store/use-theme-store.ts | 23 ++ src/theme/index.ts | 47 +++ src/theme/templates/index.ts | 2 + src/theme/templates/simple-template/index.ts | 2 + .../simple-template/simple-colors.ts | 49 +++ .../simple-template/simple-template.ts | 87 +++++ src/theme/templates/tech-template/index.ts | 2 + .../templates/tech-template/tech-colors.ts | 51 +++ .../templates/tech-template/tech-template.ts | 104 ++++++ src/types/common.ts | 20 +- src/types/index.ts | 2 + src/types/template.ts | 51 +++ 44 files changed, 1507 insertions(+), 978 deletions(-) delete mode 100644 src/app/styles/apple-style-theme.css delete mode 100644 src/app/styles/index.css delete mode 100644 src/app/styles/red-post-theme.css delete mode 100644 src/app/styles/wechat-post-theme.css create mode 100644 src/components/config/config.tsx create mode 100644 src/components/preview/card.tsx delete mode 100644 src/components/preview/preview-item.tsx create mode 100644 src/components/preview/styles/base-template.ts create mode 100644 src/components/preview/styles/index.ts create mode 100644 src/components/preview/styles/theme-color-style.ts create mode 100644 src/components/ui/card.tsx create mode 100644 src/contexts/custom-theme-context.ts create mode 100644 src/lib/template.ts create mode 100644 src/store/use-theme-store.ts create mode 100644 src/theme/index.ts create mode 100644 src/theme/templates/index.ts create mode 100644 src/theme/templates/simple-template/index.ts create mode 100644 src/theme/templates/simple-template/simple-colors.ts create mode 100644 src/theme/templates/simple-template/simple-template.ts create mode 100644 src/theme/templates/tech-template/index.ts create mode 100644 src/theme/templates/tech-template/tech-colors.ts create mode 100644 src/theme/templates/tech-template/tech-template.ts create mode 100644 src/types/index.ts create mode 100644 src/types/template.ts diff --git a/next.config.mjs b/next.config.mjs index 4678774..1d61478 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,4 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = {} -export default nextConfig; +export default nextConfig diff --git a/package.json b/package.json index a564342..0d4bf43 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@dnd-kit/modifiers": "^7.0.0", "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", + "@emotion/react": "^11.13.3", "@hookform/resolvers": "^3.9.0", "@pdf-lib/upng": "^1.0.1", "@radix-ui/react-dialog": "^1.1.1", @@ -74,6 +75,7 @@ "react-hook-form": "^7.53.0", "tailwind-merge": "^2.5.2", "tailwindcss-animate": "^1.0.7", + "tss-react": "^4.9.13", "zod": "^3.23.8", "zustand": "5.0.0-rc.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45f698c..907a1a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@18.3.1) + '@emotion/react': + specifier: ^11.13.3 + version: 11.13.3(@types/react@18.3.3)(react@18.3.1) '@hookform/resolvers': specifier: ^3.9.0 version: 3.9.0(react-hook-form@7.53.0) @@ -185,6 +188,9 @@ dependencies: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.10) + tss-react: + specifier: ^4.9.13 + version: 4.9.13(@emotion/react@11.13.3)(react@18.3.1) zod: specifier: ^3.23.8 version: 3.23.8 @@ -233,27 +239,91 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - /@babel/code-frame@7.24.7: - resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + /@babel/code-frame@7.26.2: + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/highlight': 7.24.7 - picocolors: 1.0.1 + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 dev: false - /@babel/helper-validator-identifier@7.24.7: - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + /@babel/generator@7.26.2: + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} engines: {node: '>=6.9.0'} + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 dev: false - /@babel/highlight@7.24.7: - resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} + /@babel/helper-module-imports@7.25.9: + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.24.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.0.1 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/helper-string-parser@7.25.9: + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-validator-identifier@7.25.9: + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/parser@7.26.2: + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.26.0 + dev: false + + /@babel/runtime@7.26.0: + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + dev: false + + /@babel/template@7.25.9: + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + dev: false + + /@babel/traverse@7.25.9: + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.6 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@babel/types@7.26.0: + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 dev: false /@byodian/eslint-config-basic@0.1.6(@typescript-eslint/parser@5.62.0)(eslint@8.57.0): @@ -490,6 +560,99 @@ packages: '@edge-runtime/primitives': 4.1.0 dev: true + /@emotion/babel-plugin@11.12.0: + resolution: {integrity: sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==} + dependencies: + '@babel/helper-module-imports': 7.25.9 + '@babel/runtime': 7.26.0 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.2 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + dev: false + + /@emotion/cache@11.13.1: + resolution: {integrity: sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==} + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.1 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + dev: false + + /@emotion/hash@0.9.2: + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + dev: false + + /@emotion/memoize@0.9.0: + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + dev: false + + /@emotion/react@11.13.3(@types/react@18.3.3)(react@18.3.1): + resolution: {integrity: sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.26.0 + '@emotion/babel-plugin': 11.12.0 + '@emotion/cache': 11.13.1 + '@emotion/serialize': 1.3.2 + '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) + '@emotion/utils': 1.4.1 + '@emotion/weak-memoize': 0.4.0 + '@types/react': 18.3.3 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@emotion/serialize@1.3.2: + resolution: {integrity: sha512-grVnMvVPK9yUVE6rkKfAJlYZgo0cu3l9iMC77V7DW6E1DUIrU68pSEXRmFZFOFB1QFo57TncmOcvcbMDWsL4yA==} + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.1 + csstype: 3.1.3 + dev: false + + /@emotion/sheet@1.4.0: + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + dev: false + + /@emotion/unitless@0.10.0: + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + dev: false + + /@emotion/use-insertion-effect-with-fallbacks@1.1.0(react@18.3.1): + resolution: {integrity: sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.3.1 + dev: false + + /@emotion/utils@1.4.1: + resolution: {integrity: sha512-BymCXzCG3r72VKJxaYVwOXATqXIZ85cuvg0YOUDxMGNrKc1DJRZk8MgV5wyXRyEayIMd4FuXJIUgTBXvDNW5cA==} + dev: false + + /@emotion/weak-memoize@0.4.0: + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + dev: false + /@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19): resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} peerDependencies: @@ -2030,6 +2193,10 @@ packages: resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} dev: false + /@types/parse-json@4.0.2: + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + dev: false + /@types/prop-types@15.7.12: resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -2342,8 +2509,8 @@ packages: dependencies: '@mapbox/node-pre-gyp': 1.0.11 '@rollup/pluginutils': 4.2.1 - acorn: 8.12.1 - acorn-import-attributes: 1.9.5(acorn@8.12.1) + acorn: 8.14.0 + acorn-import-attributes: 1.9.5(acorn@8.14.0) async-sema: 3.1.1 bindings: 1.5.0 estree-walker: 2.0.2 @@ -2456,12 +2623,12 @@ packages: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: true - /acorn-import-attributes@1.9.5(acorn@8.12.1): + /acorn-import-attributes@1.9.5(acorn@8.14.0): resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: acorn: ^8 dependencies: - acorn: 8.12.1 + acorn: 8.14.0 dev: true /acorn-jsx@5.3.2(acorn@8.12.1): @@ -2483,6 +2650,12 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + /acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -2524,13 +2697,6 @@ packages: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - dependencies: - color-convert: 1.9.3 - dev: false - /ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -2728,6 +2894,15 @@ packages: deep-equal: 2.2.3 dev: true + /babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + dependencies: + '@babel/runtime': 7.26.0 + cosmiconfig: 7.1.0 + resolve: 1.22.8 + dev: false + /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2874,15 +3049,6 @@ packages: - supports-color dev: true - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - dev: false - /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -3005,22 +3171,12 @@ packages: resolution: {integrity: sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==} dev: true - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - dev: false - /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: false - /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -3073,6 +3229,10 @@ packages: engines: {node: '>=8'} dev: true + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: false + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} @@ -3082,6 +3242,17 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: false + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true @@ -4347,7 +4518,7 @@ packages: peerDependencies: eslint: '>=8.8.0' dependencies: - '@babel/helper-validator-identifier': 7.24.7 + '@babel/helper-validator-identifier': 7.25.9 ci-info: 3.9.0 clean-regexp: 1.0.0 eslint: 8.57.0 @@ -4618,6 +4789,10 @@ packages: dependencies: to-regex-range: 5.0.1 + /find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + dev: false + /find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -4856,6 +5031,11 @@ packages: once: 1.4.0 path-is-absolute: 1.0.1 + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + dev: false + /globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -4894,11 +5074,6 @@ packages: /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - dev: false - /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -4932,6 +5107,12 @@ packages: dependencies: function-bind: 1.1.2 + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: false @@ -5359,6 +5540,12 @@ packages: dependencies: argparse: 2.0.1 + /jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + dev: false + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -6179,7 +6366,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.24.7 + '@babel/code-frame': 7.26.2 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -6271,6 +6458,10 @@ packages: /picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + dev: false + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -6753,6 +6944,10 @@ packages: globalthis: 1.0.4 which-builtin-type: 1.1.4 + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + dev: false + /regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true @@ -7036,6 +7231,11 @@ packages: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + dev: false + /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -7268,6 +7468,10 @@ packages: react: 18.3.1 dev: false + /stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + dev: false + /sucrase@3.35.0: resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} @@ -7281,13 +7485,6 @@ packages: pirates: 4.0.6 ts-interface-checker: 0.1.13 - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - dependencies: - has-flag: 3.0.0 - dev: false - /supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -7468,7 +7665,7 @@ packages: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 16.18.11 - acorn: 8.12.1 + acorn: 8.14.0 acorn-walk: 8.3.3 arg: 4.1.3 create-require: 1.1.1 @@ -7498,6 +7695,26 @@ packages: /tslib@2.6.3: resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + /tss-react@4.9.13(@emotion/react@11.13.3)(react@18.3.1): + resolution: {integrity: sha512-Gu19qqPH8/SAyKVIgDE5qHygirEDnNIQcXhiEc+l4Q9T7C1sfvUnbVWs+yBpmN26/wyk4FTOupjYS2wq4vH0yA==} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/server': ^11.4.0 + '@mui/material': ^5.0.0 || ^6.0.0 + react: ^16.8.0 || ^17.0.2 || ^18.0.0 + peerDependenciesMeta: + '@emotion/server': + optional: true + '@mui/material': + optional: true + dependencies: + '@emotion/cache': 11.13.1 + '@emotion/react': 11.13.3(@types/react@18.3.3)(react@18.3.1) + '@emotion/serialize': 1.3.2 + '@emotion/utils': 1.4.1 + react: 18.3.1 + dev: false + /tsutils@3.21.0(typescript@5.5.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -7943,6 +8160,11 @@ packages: yaml: 2.5.0 dev: false + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + dev: false + /yaml@2.5.0: resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} engines: {node: '>= 14'} diff --git a/postcss.config.mjs b/postcss.config.mjs index 1a69fd2..0dc456a 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -3,6 +3,6 @@ const config = { plugins: { tailwindcss: {}, }, -}; +} -export default config; +export default config diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b28dd9b..6863a59 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,8 +1,8 @@ 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' import { Toaster } from '@/components/ui/toaster' @@ -67,8 +67,10 @@ export default function RootLayout({ return ( - {children} - + + {children} + + ) diff --git a/src/app/page.tsx b/src/app/page.tsx index 5f2b5f3..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,56 +157,53 @@ export default function Home() { } return ( -
-
-
- - - 编辑器 - 预览 - - -
- -
-
- -
- -
-
-
-
-
+ + +
+
+
+ + + 编辑器 + 预览 + + +
+ +
+
+ +
+ +
+
+
+
+
+
) } 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 256624d..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/config/config.tsx b/src/components/config/config.tsx new file mode 100644 index 0000000..4cc6a86 --- /dev/null +++ b/src/components/config/config.tsx @@ -0,0 +1,84 @@ +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from '@/components/ui/tabs' + +export function Config() { + return ( + + + + 模版 + + + 样式 + + + + + + Account + + Make changes to your account here. Click save when you are done. + + + +
+ + +
+
+ + +
+
+ + + +
+
+ + + + Password + + Change your password here. After saving, you will be logged out. + + + +
+ + +
+
+ + +
+
+ + + +
+
+
+ ) +} 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 7f5b0ac..e0d2a2c 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 new file mode 100644 index 0000000..7dc142f --- /dev/null +++ b/src/components/preview/card.tsx @@ -0,0 +1,111 @@ +import DOMPurify from 'dompurify' +import parse from 'html-react-parser' +import { forwardRef, useContext, useMemo } from 'react' +import { ImageList } from './image-list' +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, +} + +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, + name: file.name, + dataUrl: URL.createObjectURL(base64ToBlob(file.dataUrl, file.type!)), + type: file.type, + })) + }, [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 + + return ( +
+ {/* card header */} + {content.title && ( +
+ {parse(DOMPurify.sanitize(content.title))} +
+ )} + {/* card content */} + { + ((stripEmptyParagraphs(content.content)) || (imageFiles.length > 0) || children) && ( +
+ {content.content && <>{parse(DOMPurify.sanitize(content.content))}} + {imageFiles.length > 0 && ( + + )} + + {/* thirdary content */} + {childContentsMap.get(content.id) && ( + <> + {childContentsMap.get(content.id)!.map((item, index) => ( + + ))} + + )} +
+ ) + } +
+ ) +}) +Card.displayName = 'Card' + +export { Card } diff --git a/src/components/preview/image-list.tsx b/src/components/preview/image-list.tsx index 06783ec..babb138 100644 --- a/src/components/preview/image-list.tsx +++ b/src/components/preview/image-list.tsx @@ -1,19 +1,17 @@ import Image from 'next/image' -import type { ImageFile } from '@/types/common' -import { cn, getImageLayout } from '@/lib/utils' -export function ImageList({ images, gridLayout }: { images: ImageFile[], gridLayout: boolean }) { +import type { ImageFile } from '@/types' +export function ImageList({ images }: { images: ImageFile[] }) { return ( -
+
{images.length > 0 && images.map(image => ( -
- {image.name} -
+ {image.name} ))}
) diff --git a/src/components/preview/preview-item.tsx b/src/components/preview/preview-item.tsx deleted file mode 100644 index 69fc691..0000000 --- a/src/components/preview/preview-item.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import DOMPurify from 'dompurify' -import parse from 'html-react-parser' -import { forwardRef, useMemo } from 'react' -import { ImageList } from './image-list' -import type { Content, ImageFile } from '@/types/common' -import { base64ToBlob, cn, getThemeBaseClass, stripEmptyParagraphs } from '@/lib/utils' - -const PreviewItem = forwardRef(({ content, children, index, theme }, ref) => { - const uploadFiles = content.uploadFiles - - const imageFiles: ImageFile[] = useMemo(() => { - return uploadFiles?.map(file => ({ - uid: file.uid, - name: file.name, - dataUrl: URL.createObjectURL(base64ToBlob(file.dataUrl, file.type!)), - type: file.type, - })) - }, [uploadFiles]) || [] - - return ( -
  • - {content.type === 'theme_content' && ( -
    - )} - {content.title && ( -
    - {parse(DOMPurify.sanitize(content.title))} -
    - )} - { - ((stripEmptyParagraphs(content.content)) || (imageFiles.length > 0) || children) && ( -
    - {content.content && <>{parse(DOMPurify.sanitize(content.content))}} - {imageFiles.length > 0 && ( - - )} - {children} -
    ) - } -
  • - ) -}) -PreviewItem.displayName = 'PreviewItem' - -export { PreviewItem } diff --git a/src/components/preview/preview.tsx b/src/components/preview/preview.tsx index 621d7d2..ff72bad 100644 --- a/src/components/preview/preview.tsx +++ b/src/components/preview/preview.tsx @@ -1,11 +1,11 @@ import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react' -import { PreviewItem } from './preview-item' -import type { ContentWithId, PreviewRef } from '@/types/common' -import { cn } from '@/lib/utils' +import { Card } from './card' +import type { ContentWithId, PreviewRef } from '@/types' +import { cn } from '@/lib' -const Preview = forwardRef(({ contents, className, theme }, ref) => { +const Preview = forwardRef(({ contents, className }, ref) => { const containerRef = useRef(null) - const itemRefs = useRef<{ [key: string | number]: HTMLLIElement }>({}) + const itemRefs = useRef<{ [key: string | number]: HTMLDivElement }>({}) // 将 ref 暴露给父组件 useImperativeHandle(ref, () => ({ @@ -39,26 +39,19 @@ const Preview = forwardRef无内容可预览

    ) : ( -
      +
      {parentContents.map((content, parentIndex) => ( - { itemRefs.current[content.id] = el! }}> - {childContentsMap.get(content.id as number) && ( -
        - {childContentsMap.get(content.id)!.map((item, index) => ( - - ))} -
      - )} -
      + ))} -
    +
    )}
    ) diff --git a/src/components/preview/styles/base-template.ts b/src/components/preview/styles/base-template.ts new file mode 100644 index 0000000..2a1a3ef --- /dev/null +++ b/src/components/preview/styles/base-template.ts @@ -0,0 +1,177 @@ +import type { ArticleModuleTemplate, CustomCSSProperties } from '@/types/template' + +export const commonTypography: CustomCSSProperties = { + '& :where(p)': { + marginTop: '5px', + marginBottom: '5px', + lineHeight: 1.7, + }, + '& :where(code)': { + backgroundColor: 'var(--gray-2)', + borderRadius: '0.4rem', + color: 'var(--black)', + fontSize: '0.85rem', + padding: '0.25em 0.3em', + }, + '& :where(pre)': { + background: 'var(--black)', + borderRadius: '0.5rem', + color: 'var(--white)', + fontFamily: 'JetBrainsMono, monospace', + margin: '1.5rem 0', + padding: '0.75rem 1rem', + code: { + background: 'none', + color: 'inherit', + fontSize: '0.8rem', + padding: '0', + }, + }, + '& :where(hr)': { + border: 'none', + borderTop: '1px solid var(--gray-2)', + margin: '2rem 0', + }, + '& :where(blockquote)': { + borderLeft: '3px solid var(--gray-2)', + margin: '1.5rem 0', + paddingLeft: '1rem', + }, + '& :where(img)': { + display: 'block', + }, + '& :where(ul)': { + listStyleType: 'disc', + paddingLeft: '1.625rem', + }, + '& :where(ol)': { + listStyleType: 'decimal', + paddingLeft: '1.625rem', + }, + '& :where(ul>li, ol>li)': { + marginTop: '.75rem', + marginBottom: '.75rem', + }, + '& :where(ul ul, ol ul, ol ol, ul ol)': { + marginTop: '.75rem', + marginBottom: '.75rem', + }, + '& [data-class="oneimg-images"]': { + display: 'grid', + rowGap: '0.5rem', + gridTemplateColumns: 'repeat(1, minmax(0, 1fr))', + marginTop: '5px', + marginBottom: '5px', + }, +} + +export const baseTemplate: ArticleModuleTemplate = { + common: { + container: {}, + title: {}, + content: commonTypography, + }, + hero: { + container: { + fontFamily: 'unset', + fontKerning: 'none', + fontStyle: 'normal', + fontSize: '18px', + fontWeight: 400, + fontSynthesis: 'none', + color: '#333', + contain: 'style', + direction: 'ltr', + height: 'auto', + minHeight: '250px', + padding: 0, + letterSpacing: 0, + lineHeight: '1.5', + overflow: 'visible', + overflowWrap: 'break-word', + tabSize: 4, + textAlign: 'left', + textIndent: 0, + textSizeAdjust: 'none', + textTransform: 'none', + whiteSpace: 'normal', + wordBreak: 'normal', + }, + title: { + fontSize: '36px', + fontWeight: 700, + lineHeight: '1.2', + }, + content: { + fontSize: '20px', + }, + }, + main: { + container: { + 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.5', + overflow: 'visible', + overflowWrap: 'break-word', + tabSize: 4, + textAlign: 'left', + textIndent: 0, + textSizeAdjust: 'none', + textTransform: 'none', + whiteSpace: 'normal', + wordBreak: 'normal', + }, + title: { + fontSize: '30px', + fontWeight: 700, + lineHeight: '1.2', + }, + content: { + }, + }, + sub: { + container: { + fontFamily: 'unset', + fontKerning: 'none', + fontStyle: 'normal', + fontSize: '16px', + fontWeight: 400, + fontSynthesis: 'none', + color: '#333', + contain: 'style', + direction: 'ltr', + height: 'auto', + padding: 0, + marginTop: '15px', + marginBottom: '15px', + letterSpacing: 0, + lineHeight: '1.5', + overflow: 'visible', + overflowWrap: 'break-word', + tabSize: 4, + textAlign: 'left', + textIndent: 0, + textSizeAdjust: 'none', + textTransform: 'none', + whiteSpace: 'normal', + wordBreak: 'normal', + }, + title: { + fontSize: '20px', + fontWeight: 700, + lineHeight: '1.2', + }, + content: { + }, + }, +} diff --git a/src/components/preview/styles/index.ts b/src/components/preview/styles/index.ts new file mode 100644 index 0000000..984fe07 --- /dev/null +++ b/src/components/preview/styles/index.ts @@ -0,0 +1,2 @@ +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/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..c5d18d4 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

    +)) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +CardFooter.displayName = 'CardFooter' + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 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/components/workspace/workspace.tsx b/src/components/workspace/workspace.tsx index 8ccd966..9952059 100644 --- a/src/components/workspace/workspace.tsx +++ b/src/components/workspace/workspace.tsx @@ -22,7 +22,7 @@ export function Workspace(props: WorkspaceProps) { const [open, setOpen] = useState(false) return ( -
    +
    {contents.length > 0 && ( <> { + 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 9c13695..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 type { ImageBase } from '@/types/common' +import type { ImageBase } from '@/types' export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) @@ -148,32 +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]' -} 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/theme/templates/tech-template/tech-template.ts b/src/theme/templates/tech-template/tech-template.ts new file mode 100644 index 0000000..99220f7 --- /dev/null +++ b/src/theme/templates/tech-template/tech-template.ts @@ -0,0 +1,104 @@ +import type { ArticleModuleTemplate } from '@/types' + +export const techTemplate: ArticleModuleTemplate = { + common: { + container: {}, + title: {}, + content: {}, + }, + hero: { + container: { + position: 'relative', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + paddingLeft: '12px', + paddingTop: '13px', + paddingRight: '12px', + paddingBottom: '13px', + textAlign: 'center', + backgroundSize: 'cover', + backgroundRepeat: 'no-repeat', + backgroundColor: '#ccedff', + backgroundImage: 'url(/images/them-bg-tech-blue.png)', + }, + title: { + position: 'relative', + zIndex: 1, + marginTop: '45px', + marginBottom: '5px', + fontWeight: 'bold', + fontSize: '30px', + color: '#333', + textAlign: 'center', + }, + content: { + position: 'relative', + zIndex: 2, + fontSize: '18px', + }, + + }, + main: { + container: { + backgroundColor: '#ccedff', + paddingLeft: '12px', + paddingRight: '12px', + paddingTop: '13px', + paddingBottom: '13px', + }, + title: { + 'position': 'relative', + 'marginBottom': '13px', + 'padding': '10px 19px', + 'fontSize': '19px', + 'fontWeight': 'bold', + 'lineHeight': '1.0', + 'color': '#fff', + 'backgroundImage': 'linear-gradient(90deg, #3CA0FF 0%, #1D6DFF 100%)', + 'borderRadius': '10px', + '&::after': { + content: '"NO." attr(data-index)', + position: 'absolute', + right: '19px', + top: '50%', + transform: 'translateY(-50%)', + display: 'inline-block', + marginLeft: '5px', + color: 'rgb(255 255 255 / 0.2)', + }, + 'p': { + marginRight: '45px', + }, + }, + content: { + position: 'relative', + padding: '12px 18px', + borderRadius: '12px', + fontSize: '15px', + backgroundColor: 'rgb(255 255 255 / 0.7)', + color: '#333', + }, + }, + sub: { + container: {}, + title: { + display: 'flex', + alignItems: 'center', + width: 'fit-content', + marginBottom: '10px', + lineHeight: '1.2', + borderRadius: '9999px 9999px 9999px 2px', + fontWeight: 'bold', + padding: '8px 20px', + fontSize: '17px', + backgroundImage: 'linear-gradient(90deg, #3CA0FF 0%, #1D6DFF 100%)', + color: '#fff', + p: { + lineHeight: 'inherit', + margin: 0, + }, + }, + content: {}, + }, +} diff --git a/src/types/common.ts b/src/types/common.ts index aa8baf4..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' @@ -98,7 +98,7 @@ export type ActionType = 'SET_TITLE' | 'SET_CONTENT' // Preview export interface PreviewRef { containerRef: React.RefObject, - itemRefs: React.RefObject<{ [key: string]: HTMLLIElement }>, + itemRefs: React.RefObject<{ [key: string]: HTMLDivElement }>, } export interface PreviewItem { @@ -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/index.ts b/src/types/index.ts new file mode 100644 index 0000000..1da4955 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,2 @@ +export * from './common' +export * from './template' diff --git a/src/types/template.ts b/src/types/template.ts new file mode 100644 index 0000000..edf1afb --- /dev/null +++ b/src/types/template.ts @@ -0,0 +1,51 @@ +export type CustomCSSProperties = React.CSSProperties & Record + +export interface ModuleSection { + container?: CustomCSSProperties; + title?: CustomCSSProperties; + content?: CustomCSSProperties; +} + +export interface ArticleModuleTemplate { + id?: string + name?: string + // layout: 'default' | 'compact' | 'card' | 'timeline' + common?: ModuleSection; + hero?: ModuleSection; + main?: ModuleSection; + sub?: ModuleSection +} + +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; +}