├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── LICENSE
├── README-zh.md
├── README.md
├── docs
├── demo-en.gif
└── demo-zh.gif
├── favicon.png
├── index.html
├── package.json
├── postcss.config.js
├── public
├── background
│ ├── blue.jpg
│ ├── dark-blue.jpg
│ ├── gold.jpg
│ ├── green.jpg
│ ├── marble.jpg
│ ├── soft-green.jpg
│ └── yellow.jpg
└── logo.svg
├── src
├── App.tsx
├── components
│ ├── icons.tsx
│ ├── lang-button.tsx
│ ├── layout-setting-menu.tsx
│ ├── markdown-editor.tsx
│ ├── navbar.tsx
│ ├── primitives.ts
│ ├── resizable-split-pane.tsx
│ ├── theme-switch.tsx
│ ├── toolbar
│ │ ├── copy-button-group.tsx
│ │ ├── download-button-group.tsx
│ │ ├── style-setting-popover-content.tsx
│ │ ├── style-setting-popover.tsx
│ │ └── toolbar.tsx
│ ├── typewriter-hero.tsx
│ └── typewriter.tsx
├── config
│ ├── post-styles.ts
│ └── site.ts
├── css
│ └── globals.css
├── data
│ ├── welcome-en.md
│ └── welcome-zh.md
├── hooks
│ └── use-theme.ts
├── layouts
│ └── default.tsx
├── lib
│ ├── copy-html.tsx
│ ├── i18n.ts
│ ├── image-store.tsx
│ ├── inline-styles.tsx
│ ├── is-safari.ts
│ ├── marked-mermaid-extension.ts
│ ├── use-tailwind-breakpoints.tsx
│ ├── use-window-size.tsx
│ └── utils.ts
├── locales
│ ├── en.json
│ └── zh.json
├── main.tsx
├── pages
│ ├── about.tsx
│ ├── blog.tsx
│ ├── docs.tsx
│ ├── index.tsx
│ └── pricing.tsx
├── provider.tsx
├── state
│ └── toolbarState.ts
├── styles
│ ├── github.css
│ ├── newspaper.css
│ ├── note.css
│ ├── poster.css
│ ├── slim.css
│ └── thoughtworks.css
├── types
│ └── index.ts
├── utils
│ └── styletransfer.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.json
├── tsconfig.node.json
├── vercel.json
├── vite.config.ts
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | .now/*
2 | *.css
3 | .changeset
4 | dist
5 | esm/*
6 | public/*
7 | tests/*
8 | scripts/*
9 | *.config.js
10 | .DS_Store
11 | node_modules
12 | coverage
13 | .next
14 | build
15 | !.commitlintrc.cjs
16 | !.lintstagedrc.cjs
17 | !jest.config.js
18 | !plopfile.js
19 | !react-shim.js
20 | !tsup.config.ts
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc.json",
3 | "env": {
4 | "browser": false,
5 | "es2021": true,
6 | "node": true
7 | },
8 | "extends": [
9 | "plugin:react/recommended",
10 | "plugin:prettier/recommended",
11 | "plugin:react-hooks/recommended",
12 | "plugin:jsx-a11y/recommended"
13 | ],
14 | "plugins": ["react", "unused-imports", "import", "@typescript-eslint", "jsx-a11y", "prettier"],
15 | "parser": "@typescript-eslint/parser",
16 | "parserOptions": {
17 | "ecmaFeatures": {
18 | "jsx": true
19 | },
20 | "ecmaVersion": 12,
21 | "sourceType": "module"
22 | },
23 | "settings": {
24 | "react": {
25 | "version": "detect"
26 | }
27 | },
28 | "rules": {
29 | "no-console": "warn",
30 | "react/prop-types": "off",
31 | "react/jsx-uses-react": "off",
32 | "react/react-in-jsx-scope": "off",
33 | "react-hooks/exhaustive-deps": "off",
34 | "jsx-a11y/click-events-have-key-events": "warn",
35 | "jsx-a11y/interactive-supports-focus": "warn",
36 | "prettier/prettier": "warn",
37 | "no-unused-vars": "off",
38 | "unused-imports/no-unused-vars": "off",
39 | "unused-imports/no-unused-imports": "off",
40 | "@typescript-eslint/no-unused-vars": [
41 | "off",
42 | {
43 | "args": "after-used",
44 | "ignoreRestSiblings": false,
45 | "argsIgnorePattern": "^_.*?$"
46 | }
47 | ],
48 | "import/order": [
49 | "warn",
50 | {
51 | "groups": [
52 | "type",
53 | "builtin",
54 | "object",
55 | "external",
56 | "internal",
57 | "parent",
58 | "sibling",
59 | "index"
60 | ],
61 | "pathGroups": [
62 | {
63 | "pattern": "~/**",
64 | "group": "external",
65 | "position": "after"
66 | }
67 | ],
68 | "newlines-between": "always"
69 | }
70 | ],
71 | "react/self-closing-comp": "warn",
72 | "react/jsx-sort-props": [
73 | "warn",
74 | {
75 | "callbacksLast": true,
76 | "shorthandFirst": true,
77 | "noSortAlphabetically": false,
78 | "reservedFirst": true
79 | }
80 | ],
81 | "padding-line-between-statements": [
82 | "warn",
83 | {"blankLine": "always", "prev": "*", "next": "return"},
84 | {"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"},
85 | {
86 | "blankLine": "any",
87 | "prev": ["const", "let", "var"],
88 | "next": ["const", "let", "var"]
89 | }
90 | ]
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Next UI
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README-zh.md:
--------------------------------------------------------------------------------
1 |
MarkdownPost
2 |
3 | 👋 欢迎来到`MarkdownPost`!很高兴见到你!
4 |
5 | 你只需要专注于内容创作,MarkdownPost 帮助你转换为`邮件`、`图片`、`PDF`等格式。
6 |
7 | 在线使用:[https://mdpost.vercel.app](https://mdpost.vercel.app)
8 |
9 | 
10 |
11 | ## 功能亮点
12 |
13 | - 💡 **简洁易用:** 实时预览效果,所见即所得。
14 | - 🏞️ **图片上传:** 粘贴图片,自动生成图片链接。
15 | - 🧮 **数学公式:** 支持 $\LaTeX$ 数学公式。
16 | - 🎨 **多种主题:** 不断更新多种主题以满足不同排版需求。
17 | - 📧 **快速分享:** 一键复制,即可发布在多种平台。
18 | - 📄 **自动适应:** 在邮件中可以自适应窗口宽度,更美观的展示内容。
19 | - 🔒 **数据安全:** 文本和图片完全在浏览器中处理,不会上传到服务器。
20 | - 🌟 **免费开源:** 完全免费使用,欢迎社区贡献。
21 |
22 | # 本地开发
23 |
24 | ```bash
25 | yarn install
26 |
27 | yarn run dev
28 | ```
29 |
30 | ## 贡献文章样式
31 |
32 | 在`src/styles/`下添加css文件,然后在`src/config/post-styles.ts`中添加样式:
33 |
34 | > Gmail会移除不支持的CSS,请参考
35 |
36 | ```ts
37 | import githubStyle from "@/styles/github.css?raw";
38 | import newspaperStyle from "@/styles/newspaper.css?raw";
39 | import posterStyle from "@/styles/poster.css?raw";
40 |
41 | export const markdownStyles = [
42 | { name: "github", css: githubStyle },
43 | { name: "newspaper", css: newspaperStyle },
44 | { name: "poster", css: posterStyle },
45 | ];
46 | ```
47 |
48 | ## 许可
49 |
50 | [MIT许可证](https://github.com/Cyronlee/markdown-post/blob/master/LICENSE)
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
MarkdownPost
2 |
3 | > English | [中文](https://github.com/Cyronlee/markdown-post/blob/master/README-zh.md)
4 |
5 | 👋 Welcome to `MarkdownPost`! Nice to meet you!
6 |
7 | Just focus on creating your content, **MarkdownPost** will handle the conversion to formats like `email`, `image`,
8 | `PDF`, and more.
9 |
10 | Website:[https://mdpost.vercel.app](https://mdpost.vercel.app)
11 |
12 | 
13 |
14 | ## Features
15 |
16 | - 💡 **Simple to Use:** Real-time preview, what you see is what you get.
17 | - 🏞️ **Image Upload:** Paste images, automatically generate image links.
18 | - 🧮 **Math Formula:** Support for $\LaTeX$ math formula.
19 | - 🎨 **Multiple Themes:** Continuously updated to meet different layout needs.
20 | - 📧 **Quick Sharing:** One-click copy, ready to publish on multiple platforms.
21 | - 📄 **Auto-Adapt:** Adapts to email window widths for a more attractive display.
22 | - 🔒 **Data Security:** Text and images are processed entirely in the browser, not uploaded to servers.
23 | - 🌟 **Free & Open Source:** Completely free to use, community contributions welcome.
24 |
25 | # Local Development
26 |
27 | ```bash
28 | yarn install
29 |
30 | yarn run dev
31 | ```
32 |
33 | ## Contributing Styles
34 |
35 | Add a css file in `src/styles/`, then add the style in `src/config/post-styles.ts`:
36 |
37 | > Unsupported CSS may be ignored by Gmail, please refer to
38 |
39 | ```ts
40 | import githubStyle from "@/styles/github.css?raw";
41 | import newspaperStyle from "@/styles/newspaper.css?raw";
42 | import posterStyle from "@/styles/poster.css?raw";
43 |
44 | export const markdownStyles = [
45 | { name: "github", css: githubStyle },
46 | { name: "newspaper", css: newspaperStyle },
47 | { name: "poster", css: posterStyle },
48 | ];
49 | ```
50 |
51 | ## License
52 |
53 | [MIT License](https://github.com/Cyronlee/markdown-post/blob/master/LICENSE)
54 |
--------------------------------------------------------------------------------
/docs/demo-en.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cyronlee/markdown-post/817861b9334e2a9cde11fc56065e8169c93662b6/docs/demo-en.gif
--------------------------------------------------------------------------------
/docs/demo-zh.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cyronlee/markdown-post/817861b9334e2a9cde11fc56065e8169c93662b6/docs/demo-zh.gif
--------------------------------------------------------------------------------
/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cyronlee/markdown-post/817861b9334e2a9cde11fc56065e8169c93662b6/favicon.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | MarkdownPost
8 |
9 |
13 |
17 |
22 |
23 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-template",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint -c .eslintrc.json ./src/**/**/*.{ts,tsx} --fix",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@codemirror/lang-markdown": "^6.3.0",
14 | "@codemirror/state": "^6.4.1",
15 | "@codemirror/view": "^6.34.1",
16 | "@nextui-org/button": "2.0.38",
17 | "@nextui-org/card": "^2.0.34",
18 | "@nextui-org/code": "2.0.33",
19 | "@nextui-org/dropdown": "^2.1.31",
20 | "@nextui-org/image": "^2.0.32",
21 | "@nextui-org/input": "2.2.5",
22 | "@nextui-org/kbd": "2.0.34",
23 | "@nextui-org/link": "2.0.35",
24 | "@nextui-org/navbar": "2.0.37",
25 | "@nextui-org/popover": "^2.1.29",
26 | "@nextui-org/select": "^2.2.7",
27 | "@nextui-org/slider": "^2.2.17",
28 | "@nextui-org/snippet": "2.0.43",
29 | "@nextui-org/switch": "2.0.34",
30 | "@nextui-org/system": "2.2.6",
31 | "@nextui-org/theme": "2.2.11",
32 | "@react-aria/visually-hidden": "3.8.12",
33 | "@vercel/analytics": "^1.3.2",
34 | "clsx": "2.1.1",
35 | "codemirror": "^6.0.1",
36 | "framer-motion": "~11.1.1",
37 | "highlight.js": "^11.10.0",
38 | "html-to-image": "^1.11.11",
39 | "i18next": "^23.16.4",
40 | "katex": "^0.16.11",
41 | "lucide-react": "^0.454.0",
42 | "marked": "^14.1.3",
43 | "marked-highlight": "^2.2.0",
44 | "marked-katex-extension": "^5.1.2",
45 | "mermaid": "^11.4.1",
46 | "react": "18.3.1",
47 | "react-color": "^2.19.3",
48 | "react-dom": "18.3.1",
49 | "react-i18next": "^15.1.0",
50 | "react-router-dom": "6.23.0",
51 | "sonner": "^1.5.0",
52 | "tailwind-variants": "0.1.20",
53 | "tailwindcss": "3.4.3",
54 | "unstated-next": "^1.1.0"
55 | },
56 | "devDependencies": {
57 | "@types/node": "20.5.7",
58 | "@types/react": "18.3.3",
59 | "@types/react-color": "^3.0.12",
60 | "@types/react-dom": "18.3.0",
61 | "@typescript-eslint/eslint-plugin": "8.11.0",
62 | "@typescript-eslint/parser": "8.11.0",
63 | "@vitejs/plugin-react": "^4.2.1",
64 | "autoprefixer": "10.4.19",
65 | "eslint": "^8.57.0",
66 | "eslint-config-prettier": "9.1.0",
67 | "eslint-plugin-import": "^2.26.0",
68 | "eslint-plugin-jsx-a11y": "^6.4.1",
69 | "eslint-plugin-node": "^11.1.0",
70 | "eslint-plugin-prettier": "5.2.1",
71 | "eslint-plugin-react": "^7.23.2",
72 | "eslint-plugin-react-hooks": "^4.6.0",
73 | "eslint-plugin-unused-imports": "4.1.4",
74 | "postcss": "8.4.38",
75 | "prettier": "3.3.3",
76 | "typescript": "5.6.3",
77 | "vite": "^5.2.0",
78 | "vite-tsconfig-paths": "^4.3.2"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
--------------------------------------------------------------------------------
/public/background/blue.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cyronlee/markdown-post/817861b9334e2a9cde11fc56065e8169c93662b6/public/background/blue.jpg
--------------------------------------------------------------------------------
/public/background/dark-blue.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cyronlee/markdown-post/817861b9334e2a9cde11fc56065e8169c93662b6/public/background/dark-blue.jpg
--------------------------------------------------------------------------------
/public/background/gold.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cyronlee/markdown-post/817861b9334e2a9cde11fc56065e8169c93662b6/public/background/gold.jpg
--------------------------------------------------------------------------------
/public/background/green.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cyronlee/markdown-post/817861b9334e2a9cde11fc56065e8169c93662b6/public/background/green.jpg
--------------------------------------------------------------------------------
/public/background/marble.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cyronlee/markdown-post/817861b9334e2a9cde11fc56065e8169c93662b6/public/background/marble.jpg
--------------------------------------------------------------------------------
/public/background/soft-green.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cyronlee/markdown-post/817861b9334e2a9cde11fc56065e8169c93662b6/public/background/soft-green.jpg
--------------------------------------------------------------------------------
/public/background/yellow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Cyronlee/markdown-post/817861b9334e2a9cde11fc56065e8169c93662b6/public/background/yellow.jpg
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { Route, Routes, useLocation, useNavigate } from "react-router-dom";
2 | import { useTranslation } from "react-i18next";
3 | import { useEffect } from "react";
4 |
5 | import IndexPage from "@/pages/index";
6 | import DocsPage from "@/pages/docs";
7 | import PricingPage from "@/pages/pricing";
8 | import BlogPage from "@/pages/blog";
9 | import AboutPage from "@/pages/about";
10 |
11 | function LanguageSwitcher() {
12 | const location = useLocation();
13 | const { i18n } = useTranslation();
14 | const navigate = useNavigate();
15 |
16 | useEffect(() => {
17 | const lang = location.pathname.split("/")[1];
18 | if (["en", "zh"].includes(lang)) {
19 | i18n.changeLanguage(lang);
20 | localStorage.setItem("APP_LANG", lang);
21 | } else {
22 | let appLang = localStorage.getItem("APP_LANG") || "";
23 |
24 | if (["en", "zh"].includes(appLang)) {
25 | navigate(`/${appLang}${location.pathname}`, { replace: true });
26 | } else {
27 | const userLang = navigator.language.startsWith("zh") ? "zh" : "en";
28 |
29 | navigate(`/${userLang}${location.pathname}`, { replace: true });
30 | }
31 | }
32 | }, [location, i18n]);
33 |
34 | return null;
35 | }
36 |
37 | function App() {
38 | return (
39 | <>
40 |
41 |
42 | } path="/:lang" />
43 | } path="/:lang/docs" />
44 | } path="/:lang/pricing" />
45 | } path="/:lang/blog" />
46 | } path="/:lang/about" />
47 |
48 | >
49 | );
50 | }
51 |
52 | export default App;
53 |
--------------------------------------------------------------------------------
/src/components/icons.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { IconSvgProps } from "@/types";
4 |
5 | export const Logo: React.FC = ({
6 | size = 36,
7 | height,
8 | ...props
9 | }) => (
10 |
26 | );
27 |
28 | export const DiscordIcon: React.FC = ({
29 | size = 24,
30 | width,
31 | height,
32 | ...props
33 | }) => {
34 | return (
35 |
46 | );
47 | };
48 |
49 | export const TwitterIcon: React.FC = ({
50 | size = 24,
51 | width,
52 | height,
53 | ...props
54 | }) => {
55 | return (
56 |
67 | );
68 | };
69 |
70 | export const GithubIcon: React.FC = ({
71 | size = 24,
72 | width,
73 | height,
74 | ...props
75 | }) => {
76 | return (
77 |
90 | );
91 | };
92 |
93 | export const LangIcon: React.FC = ({
94 | size = 24,
95 | width,
96 | height,
97 | ...props
98 | }) => {
99 | return (
100 |
113 | );
114 | };
115 |
116 | export const MoonFilledIcon = ({
117 | size = 24,
118 | width,
119 | height,
120 | ...props
121 | }: IconSvgProps) => (
122 |
136 | );
137 |
138 | export const SunFilledIcon = ({
139 | size = 24,
140 | width,
141 | height,
142 | ...props
143 | }: IconSvgProps) => (
144 |
158 | );
159 |
160 | export const HeartFilledIcon = ({
161 | size = 24,
162 | width,
163 | height,
164 | ...props
165 | }: IconSvgProps) => (
166 |
183 | );
184 |
185 | export const SearchIcon = (props: IconSvgProps) => (
186 |
211 | );
212 |
213 | export const NextUILogo: React.FC = (props) => {
214 | const { width, height = 40 } = props;
215 |
216 | return (
217 |
238 | );
239 | };
240 |
241 | export const ChevronDownIcon = () => (
242 |
254 | );
255 |
--------------------------------------------------------------------------------
/src/components/lang-button.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Dropdown,
4 | DropdownTrigger,
5 | DropdownMenu,
6 | DropdownSection,
7 | DropdownItem,
8 | } from "@nextui-org/dropdown";
9 | import { Button } from "@nextui-org/button";
10 | import { LangIcon } from "@/components/icons.tsx";
11 | import { useNavigate } from "react-router-dom";
12 |
13 | const LangButton = () => {
14 | const navigate = useNavigate();
15 |
16 | const handleLanguageSelected = (lang: any) => {
17 | const pathSegments = location.pathname.split("/");
18 |
19 | pathSegments[1] = lang;
20 | navigate(pathSegments.join("/"), { replace: true });
21 | };
22 |
23 | return (
24 |
25 |
26 |
29 |
30 |
36 | 中文
37 | English
38 |
39 |
40 | );
41 | };
42 |
43 | export default LangButton;
44 |
--------------------------------------------------------------------------------
/src/components/layout-setting-menu.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Popover, PopoverTrigger, PopoverContent } from "@nextui-org/popover";
3 | import { Button } from "@nextui-org/button";
4 | import { Input } from "@nextui-org/input";
5 | import { Copy } from "lucide-react";
6 | import { Slider } from "@nextui-org/slider";
7 |
8 | export interface LayoutSetting {
9 | containerEnabled: boolean;
10 | containerPadding: number;
11 | containerBgColor: string;
12 | articlePadding: number;
13 | articleBgColor: string;
14 | }
15 |
16 | export const defaultLayoutSetting: LayoutSetting = {
17 | containerEnabled: true,
18 | containerPadding: 24,
19 | containerBgColor: "#e5e5e5",
20 | articlePadding: 24,
21 | articleBgColor: "#333",
22 | };
23 |
24 | type LayoutSettingProps = {
25 | layoutSetting: LayoutSetting;
26 | setLayoutSetting: React.Dispatch>;
27 | };
28 |
29 | const ColorBox = ({ color }: { color: string }) => (
30 |
34 | );
35 |
36 | const LayoutSettingMenu = ({
37 | layoutSetting,
38 | setLayoutSetting,
39 | }: LayoutSettingProps) => {
40 | return (
41 |
42 |
43 |
47 |
48 |
49 |
50 | {(titleProps) => (
51 |
52 |
53 | Layout Customizer
54 |
55 |
56 | {
60 | setLayoutSetting((prevState) => ({
61 | ...prevState,
62 | containerBgColor: e.target.value as string,
63 | }));
64 | }}
65 | labelPlacement="outside"
66 | startContent={
67 |
68 | }
69 | />
70 | {
77 | setLayoutSetting((prevState) => ({
78 | ...prevState,
79 | containerPadding: value as number,
80 | }));
81 | }}
82 | getValue={(donuts) => `${donuts}px`}
83 | className="max-w-md"
84 | />
85 |
86 |
87 | )}
88 |
89 |
90 | );
91 | };
92 |
93 | export default LayoutSettingMenu;
94 |
--------------------------------------------------------------------------------
/src/components/markdown-editor.tsx:
--------------------------------------------------------------------------------
1 | import { markdown } from "@codemirror/lang-markdown";
2 | import { EditorView } from "@codemirror/view";
3 | import { EditorState } from "@codemirror/state";
4 | import { basicSetup } from "codemirror";
5 | import React, { useCallback, useEffect, useRef } from "react";
6 | import { toast } from "sonner";
7 |
8 | import { createMarkdownImage, saveImageBase64 } from "@/lib/image-store";
9 | import { useTranslation } from "react-i18next";
10 |
11 | interface CodeEditorProps {
12 | value: string;
13 | onChange: (value: string) => void;
14 | }
15 |
16 | const theme = EditorView.baseTheme({
17 | "&": {
18 | fontSize: "16px",
19 | },
20 | "&.cm-focused": {
21 | outline: "none",
22 | },
23 | ".cm-gutters": {
24 | fontWeight: "bold",
25 | border: "none",
26 | backgroundColor: "white",
27 | },
28 | });
29 |
30 | export const MarkdownEditor: React.FC = ({
31 | value,
32 | onChange,
33 | }) => {
34 | const { t } = useTranslation();
35 |
36 | const editorRef = useRef(null);
37 | const viewRef = useRef();
38 |
39 | // Add paste handler
40 | const handlePaste = useCallback(
41 | async (event: React.ClipboardEvent) => {
42 | const items = event.clipboardData?.items;
43 |
44 | if (!items) return;
45 |
46 | for (let i = 0; i < items.length; i++) {
47 | if (items[i].type.indexOf("image") !== -1) {
48 | event.preventDefault();
49 | const file = items[i].getAsFile();
50 |
51 | if (file) {
52 | try {
53 | const reader = new FileReader();
54 |
55 | reader.onload = async (e) => {
56 | const base64String = e.target?.result as string;
57 | const imageId = saveImageBase64(base64String);
58 | const markdownText = createMarkdownImage(imageId);
59 |
60 | // Insert markdown text at cursor position
61 | if (viewRef.current) {
62 | const pos = viewRef.current.state.selection.main.head;
63 |
64 | viewRef.current.dispatch({
65 | changes: {
66 | from: pos,
67 | insert: markdownText,
68 | },
69 | });
70 | }
71 | };
72 | reader.readAsDataURL(file);
73 | } catch (error) {
74 | console.error(error);
75 | toast.error(t("commonToast.error"));
76 | }
77 | }
78 | break;
79 | }
80 | }
81 | },
82 | [],
83 | );
84 |
85 | useEffect(() => {
86 | if (!editorRef.current) return;
87 |
88 | // Create editor state
89 | const state = EditorState.create({
90 | doc: value,
91 | extensions: [
92 | basicSetup,
93 | markdown(),
94 | EditorView.lineWrapping,
95 | EditorView.updateListener.of((update) => {
96 | if (update.docChanged) {
97 | onChange(update.state.doc.toString());
98 | }
99 | }),
100 | theme,
101 | ],
102 | });
103 |
104 | // Create and mount editor view
105 | const view = new EditorView({
106 | state,
107 | parent: editorRef.current,
108 | });
109 |
110 | viewRef.current = view;
111 |
112 | return () => {
113 | view.destroy();
114 | };
115 | }, []); // Only run once on mount
116 |
117 | // Update editor content when markdown prop changes
118 | useEffect(() => {
119 | if (viewRef.current && value !== viewRef.current.state.doc.toString()) {
120 | viewRef.current.dispatch({
121 | changes: {
122 | from: 0,
123 | to: viewRef.current.state.doc.length,
124 | insert: value,
125 | },
126 | });
127 | }
128 | }, [value]);
129 |
130 | return ;
131 | };
132 |
--------------------------------------------------------------------------------
/src/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@nextui-org/button";
2 | import { Kbd } from "@nextui-org/kbd";
3 | import { Link } from "@nextui-org/link";
4 | import { Input } from "@nextui-org/input";
5 | import {
6 | Navbar as NextUINavbar,
7 | NavbarBrand,
8 | NavbarContent,
9 | NavbarItem,
10 | NavbarMenuToggle,
11 | NavbarMenu,
12 | NavbarMenuItem,
13 | } from "@nextui-org/navbar";
14 | import { link as linkStyles } from "@nextui-org/theme";
15 | import clsx from "clsx";
16 |
17 | import { siteConfig } from "@/config/site";
18 | import { ThemeSwitch } from "@/components/theme-switch";
19 | import {
20 | TwitterIcon,
21 | GithubIcon,
22 | DiscordIcon,
23 | HeartFilledIcon,
24 | SearchIcon,
25 | } from "@/components/icons";
26 | import { Logo } from "@/components/icons";
27 | import LangButton from "@/components/lang-button.tsx";
28 |
29 | export const Navbar = () => {
30 | const searchInput = (
31 |
39 | K
40 |
41 | }
42 | labelPlacement="outside"
43 | placeholder="Search..."
44 | startContent={
45 |
46 | }
47 | type="search"
48 | />
49 | );
50 |
51 | return (
52 |
53 |
54 |
55 |
60 |
61 | MarkdownPost
62 |
63 |
64 | {/**/}
65 | {/* {siteConfig.navItems.map((item) => (*/}
66 | {/* */}
67 | {/* */}
75 | {/* {item.label}*/}
76 | {/* */}
77 | {/* */}
78 | {/* ))}*/}
79 | {/*
*/}
80 |
81 |
82 |
86 |
87 | {/**/}
88 | {/* */}
89 | {/**/}
90 | {/**/}
91 | {/* */}
92 | {/**/}
93 |
94 |
95 |
96 |
97 | {/**/}
98 |
99 | {/*{searchInput}*/}
100 | {/**/}
101 | {/* }*/}
107 | {/* variant="flat"*/}
108 | {/* >*/}
109 | {/* Sponsor*/}
110 | {/* */}
111 | {/**/}
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | {/**/}
120 | {/**/}
121 |
122 |
123 |
124 | {/*{searchInput}*/}
125 | {/**/}
126 | {/* {siteConfig.navMenuItems.map((item, index) => (*/}
127 | {/* */}
128 | {/* */}
139 | {/* {item.label}*/}
140 | {/* */}
141 | {/* */}
142 | {/* ))}*/}
143 | {/*
*/}
144 |
145 |
146 | );
147 | };
148 |
--------------------------------------------------------------------------------
/src/components/primitives.ts:
--------------------------------------------------------------------------------
1 | import { tv } from "tailwind-variants";
2 |
3 | export const title = tv({
4 | base: "tracking-tight inline font-semibold",
5 | variants: {
6 | color: {
7 | violet: "from-[#FF1CF7] to-[#b249f8]",
8 | yellow: "from-[#FF705B] to-[#FFB457]",
9 | blue: "from-[#5EA2EF] to-[#0072F5]",
10 | cyan: "from-[#00b7fa] to-[#01cfea]",
11 | green: "from-[#6FEE8D] to-[#17c964]",
12 | pink: "from-[#FF72E1] to-[#F54C7A]",
13 | foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]",
14 | },
15 | size: {
16 | sm: "text-3xl lg:text-4xl",
17 | md: "text-[2.3rem] lg:text-5xl leading-9",
18 | lg: "text-4xl lg:text-6xl",
19 | },
20 | fullWidth: {
21 | true: "w-full block",
22 | },
23 | },
24 | defaultVariants: {
25 | size: "md",
26 | },
27 | compoundVariants: [
28 | {
29 | color: [
30 | "violet",
31 | "yellow",
32 | "blue",
33 | "cyan",
34 | "green",
35 | "pink",
36 | "foreground",
37 | ],
38 | class: "bg-clip-text text-transparent bg-gradient-to-b",
39 | },
40 | ],
41 | });
42 |
43 | export const subtitle = tv({
44 | base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full",
45 | variants: {
46 | fullWidth: {
47 | true: "!w-full",
48 | },
49 | },
50 | defaultVariants: {
51 | fullWidth: true,
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/src/components/resizable-split-pane.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState, useCallback, useEffect, useRef } from "react";
4 |
5 | import useTailwindBreakpoints from "@/lib/use-tailwind-breakpoints";
6 |
7 | interface ResizableSplitPaneProps {
8 | leftPane: React.ReactNode;
9 | rightPane: React.ReactNode;
10 | initialLeftWidth?: number;
11 | minLeftWidth?: number;
12 | maxLeftWidth?: number;
13 | }
14 |
15 | export default function ResizableSplitPane({
16 | leftPane,
17 | rightPane,
18 | initialLeftWidth = 50,
19 | minLeftWidth = 20,
20 | maxLeftWidth = 80,
21 | }: ResizableSplitPaneProps) {
22 | const containerRef = useRef(null);
23 | const [leftWidth, setLeftWidth] = useState(initialLeftWidth);
24 | const [isDragging, setIsDragging] = useState(false);
25 |
26 | const tailwindBreakpoints = useTailwindBreakpoints();
27 |
28 | const handleMouseDown = useCallback((e: React.MouseEvent) => {
29 | e.preventDefault();
30 | setIsDragging(true);
31 | }, []);
32 |
33 | const handleMouseUp = useCallback(() => {
34 | setIsDragging(false);
35 | }, []);
36 |
37 | const handleMouseMove = useCallback(
38 | (e: MouseEvent) => {
39 | if (!isDragging || !containerRef.current) return;
40 |
41 | const containerRect = containerRef.current.getBoundingClientRect();
42 | const newWidth = e.clientX - containerRect.left;
43 | const containerWidth = containerRect.width;
44 |
45 | const newLeftWidthPercent = (newWidth / containerWidth) * 100;
46 | const clampedWidth = Math.min(
47 | Math.max(newLeftWidthPercent, minLeftWidth),
48 | maxLeftWidth,
49 | );
50 |
51 | setLeftWidth(clampedWidth);
52 | },
53 | [isDragging, minLeftWidth, maxLeftWidth],
54 | );
55 |
56 | const handleDoubleClick = useCallback(() => {
57 | setLeftWidth(initialLeftWidth);
58 | }, [initialLeftWidth]);
59 |
60 | useEffect(() => {
61 | document.addEventListener("mousemove", handleMouseMove);
62 | document.addEventListener("mouseup", handleMouseUp);
63 |
64 | return () => {
65 | document.removeEventListener("mousemove", handleMouseMove);
66 | document.removeEventListener("mouseup", handleMouseUp);
67 | };
68 | }, [handleMouseMove, handleMouseUp]);
69 |
70 | return (
71 |
72 | {tailwindBreakpoints.has("md") ? (
73 |
74 |
{rightPane}
75 |
76 |
{leftPane}
77 |
78 | ) : (
79 |
83 |
87 | {leftPane}
88 |
89 |
95 |
{rightPane}
96 |
97 | )}
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/src/components/theme-switch.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useState, useEffect } from "react";
2 | import { VisuallyHidden } from "@react-aria/visually-hidden";
3 | import { SwitchProps, useSwitch } from "@nextui-org/switch";
4 | import clsx from "clsx";
5 |
6 | import { useTheme } from "@/hooks/use-theme";
7 | import { SunFilledIcon, MoonFilledIcon } from "@/components/icons";
8 |
9 | export interface ThemeSwitchProps {
10 | className?: string;
11 | classNames?: SwitchProps["classNames"];
12 | }
13 |
14 | export const ThemeSwitch: FC = ({
15 | className,
16 | classNames,
17 | }) => {
18 | const [isMounted, setIsMounted] = useState(false);
19 |
20 | const { theme, toggleTheme } = useTheme();
21 |
22 | const onChange = toggleTheme;
23 |
24 | const {
25 | Component,
26 | slots,
27 | isSelected,
28 | getBaseProps,
29 | getInputProps,
30 | getWrapperProps,
31 | } = useSwitch({
32 | isSelected: theme === "light",
33 | onChange,
34 | });
35 |
36 | useEffect(() => {
37 | setIsMounted(true);
38 | }, [isMounted]);
39 |
40 | // Prevent Hydration Mismatch
41 | if (!isMounted) return ;
42 |
43 | return (
44 |
54 |
55 |
56 |
57 |
76 | {isSelected ? (
77 |
78 | ) : (
79 |
80 | )}
81 |
82 |
83 | );
84 | };
85 |
--------------------------------------------------------------------------------
/src/components/toolbar/copy-button-group.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, ButtonGroup } from "@nextui-org/button";
3 | import {
4 | Dropdown,
5 | DropdownItem,
6 | DropdownMenu,
7 | DropdownTrigger,
8 | } from "@nextui-org/dropdown";
9 | import { toast } from "sonner";
10 | import * as htmlToImage from "html-to-image";
11 | import { Copy } from "lucide-react";
12 | import { useTranslation } from "react-i18next";
13 |
14 | import { ChevronDownIcon } from "@/components/icons.tsx";
15 | import { copyHtmlWithStyle } from "@/lib/copy-html.tsx";
16 | import { isSafari } from "@/lib/is-safari.ts";
17 |
18 | export default function CopyButtonGroup() {
19 | const { t } = useTranslation();
20 | const [selectedOption, setSelectedOption] = React.useState(
21 | new Set(["email"]),
22 | );
23 |
24 | const descriptionsMap: any = {
25 | email: t("copyEmail.buttonDescription"),
26 | image: t("copyImage.buttonDescription"),
27 | };
28 |
29 | const labelsMap: any = {
30 | email: t("copyEmail.buttonName"),
31 | image: t("copyImage.buttonName"),
32 | };
33 |
34 | const selectedOptionValue: any = Array.from(selectedOption)[0];
35 |
36 | const loadBlobForSafari: any = async (element: HTMLElement) => {
37 | // workaround to fix image missing in Safari
38 | await htmlToImage.toBlob(element);
39 | await htmlToImage.toBlob(element);
40 |
41 | return await htmlToImage.toBlob(element);
42 | };
43 |
44 | const handleCopyButtonClick = () => {
45 | if (selectedOption.has("email")) {
46 | copyHtmlWithStyle("markdown-body");
47 | toast.success(t("copyEmail.successMessage"), {
48 | description: t("copyEmail.successDescription"),
49 | duration: 4000,
50 | position: "top-center",
51 | });
52 | } else if (selectedOption.has("image")) {
53 | const element = document.getElementById("markdown-body");
54 |
55 | if (!element) {
56 | return;
57 | }
58 |
59 | toast.success(t("commonToast.processing"), {
60 | duration: 4000,
61 | position: "top-center",
62 | });
63 |
64 | if (isSafari) {
65 | // workaround to fix permission issue in Safari
66 | const text = new ClipboardItem({
67 | "image/png": loadBlobForSafari(element).then((blob: any) => blob),
68 | });
69 |
70 | navigator.clipboard.write([text]);
71 |
72 | toast.success(t("copyImage.successMessage"), {
73 | duration: 4000,
74 | position: "top-center",
75 | });
76 | } else {
77 | htmlToImage
78 | .toBlob(element)
79 | .then(function (blob: any) {
80 | navigator.clipboard
81 | .write([new ClipboardItem({ "image/png": blob })])
82 | .then(() => {
83 | toast.success(t("copyImage.successMessage"), {
84 | duration: 4000,
85 | position: "top-center",
86 | });
87 | })
88 | .catch((err) => {
89 | toast.error(t("copyImage.failedMessage"));
90 | console.error("Failed to copy image to clipboard:", err);
91 | });
92 | })
93 | .catch(function (error) {
94 | toast.error(t("copyImage.failedMessage"));
95 | console.error("oops, something went wrong!", error);
96 | });
97 | }
98 | }
99 | };
100 |
101 | return (
102 |
103 |
107 |
108 |
109 |
112 |
113 |
121 |
122 | {labelsMap["email"]}
123 |
124 |
125 | {labelsMap["image"]}
126 |
127 |
128 |
129 |
130 | );
131 | }
132 |
--------------------------------------------------------------------------------
/src/components/toolbar/download-button-group.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, ButtonGroup } from "@nextui-org/button";
3 | import {
4 | Dropdown,
5 | DropdownItem,
6 | DropdownMenu,
7 | DropdownTrigger,
8 | } from "@nextui-org/dropdown";
9 | import { useTranslation } from "react-i18next";
10 | import { toast } from "sonner";
11 | import * as htmlToImage from "html-to-image";
12 | import { Download } from "lucide-react";
13 |
14 | import { ChevronDownIcon } from "@/components/icons.tsx";
15 | import { isSafari } from "@/lib/is-safari.ts";
16 |
17 | interface DownloadButtonGroupProps {
18 | markdown: string;
19 | }
20 |
21 | export default function DownloadButtonGroup({ markdown }: DownloadButtonGroupProps) {
22 | const { t } = useTranslation();
23 | const [selectedOption, setSelectedOption] = React.useState(
24 | new Set(["image"]),
25 | );
26 |
27 | const descriptionsMap: any = {
28 | image: t("downloadImage.buttonDescription"),
29 | pdf: t("downloadPDF.buttonDescription"),
30 | html: t("downloadHTML.buttonDescription"),
31 | markdown: t("downloadMarkdown.buttonDescription"),
32 | };
33 |
34 | const labelsMap: any = {
35 | image: t("downloadImage.buttonName"),
36 | pdf: t("downloadPDF.buttonName"),
37 | html: t("downloadHTML.buttonName"),
38 | markdown: t("downloadMarkdown.buttonName"),
39 | };
40 |
41 | const selectedOptionValue: any = Array.from(selectedOption)[0];
42 |
43 | const handleDownloadButtonClick = async () => {
44 | const element = document.getElementById("markdown-body");
45 |
46 | if (!element) {
47 | return;
48 | }
49 |
50 | if (selectedOption.has("pdf")) {
51 | toast.success(t("commonToast.developing"), {
52 | duration: 4000,
53 | position: "top-center",
54 | });
55 | } else if (selectedOption.has("image")) {
56 | toast.success(t("commonToast.processing"), {
57 | duration: 4000,
58 | position: "top-center",
59 | });
60 |
61 | try {
62 | if (isSafari) {
63 | // workaround to fix image missing in Safari
64 | await htmlToImage.toPng(element);
65 | await htmlToImage.toPng(element);
66 | }
67 |
68 | const dataUrl = await htmlToImage.toPng(element);
69 |
70 | const link = document.createElement("a");
71 |
72 | link.download = "markdown-post.png";
73 | link.href = dataUrl;
74 | link.click();
75 | toast.success(t("downloadImage.successMessage"), {
76 | description: t("downloadImage.successDescription"),
77 | duration: 4000,
78 | position: "top-center",
79 | });
80 | } catch (error) {
81 | console.error("oops, something went wrong!", error);
82 | toast.error(t("downloadImage.failedMessage"));
83 | }
84 | } else if (selectedOption.has("html")) {
85 | try {
86 | const htmlContent = element.outerHTML;
87 | const blob = new Blob([htmlContent], { type: "text/html" });
88 | const url = URL.createObjectURL(blob);
89 | const link = document.createElement("a");
90 | link.download = "markdown-post.html";
91 | link.href = url;
92 | link.click();
93 | URL.revokeObjectURL(url);
94 | toast.success(t("downloadHTML.successMessage"), {
95 | description: t("downloadHTML.successDescription"),
96 | duration: 4000,
97 | position: "top-center",
98 | });
99 | } catch (error) {
100 | console.error("Failed to download HTML:", error);
101 | toast.error(t("downloadHTML.failedMessage"));
102 | }
103 | } else if (selectedOption.has("markdown")) {
104 | try {
105 | const blob = new Blob([markdown], { type: "text/markdown" });
106 | const url = URL.createObjectURL(blob);
107 | const link = document.createElement("a");
108 | link.download = "markdown-post.md";
109 | link.href = url;
110 | link.click();
111 | URL.revokeObjectURL(url);
112 | toast.success(t("downloadMarkdown.successMessage"), {
113 | description: t("downloadMarkdown.successDescription"),
114 | duration: 4000,
115 | position: "top-center",
116 | });
117 | } catch (error) {
118 | console.error("Failed to download Markdown:", error);
119 | toast.error(t("downloadMarkdown.failedMessage"));
120 | }
121 | }
122 | };
123 |
124 | return (
125 |
126 |
130 |
131 |
132 |
135 |
136 |
144 |
145 | {labelsMap["image"]}
146 |
147 |
148 | {labelsMap["html"]}
149 |
150 |
151 | {labelsMap["markdown"]}
152 |
153 |
154 | {labelsMap["pdf"]}
155 |
156 |
157 |
158 |
159 | );
160 | }
161 |
--------------------------------------------------------------------------------
/src/components/toolbar/style-setting-popover-content.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Popover, PopoverContent, PopoverTrigger } from "@nextui-org/popover";
3 | import { ChromePicker } from "react-color";
4 | import { Slider } from "@nextui-org/slider";
5 | import { useTranslation } from "react-i18next";
6 | import { Input } from "@nextui-org/input";
7 |
8 | import { ToolbarState } from "@/state/toolbarState.ts";
9 | import {
10 | cssToRecord,
11 | extractContainerLayoutContent,
12 | objectToStyleString,
13 | } from "@/utils/styletransfer.ts";
14 |
15 | const ColorBox = ({
16 | newStyle,
17 | setNewStyle,
18 | }: {
19 | newStyle: Record;
20 | setNewStyle: (newStyle: Record) => void;
21 | }) => {
22 | return (
23 |
24 |
25 |
29 |
30 |
31 | {
35 | setNewStyle({
36 | ...newStyle,
37 | ["background-color"]: `${color.hex}`,
38 | ["background"]: `${color.hex}`,
39 | });
40 | }}
41 | />
42 |
43 |
44 | );
45 | };
46 |
47 | export const StyleSettingPopoverContent = () => {
48 | const { t } = useTranslation();
49 |
50 | const { articleStyle, setArticleStyle } = ToolbarState.useContainer();
51 |
52 | const [newStyle, setNewStyle] = useState>(
53 | cssToRecord(extractContainerLayoutContent(articleStyle)),
54 | );
55 |
56 | useEffect(() => {
57 | if (objectToStyleString(newStyle)) {
58 | setArticleStyle((prev) => {
59 | return prev.replace(
60 | extractContainerLayoutContent(prev),
61 | objectToStyleString(newStyle),
62 | );
63 | });
64 | }
65 | }, [newStyle]);
66 |
67 | return (
68 |
69 | {(titleProps) => (
70 |
71 |
75 | {t(`customize.layoutCustomizer`)}
76 |
77 |
82 |
83 |
84 | }
85 | value={newStyle["background-color"]}
86 | onChange={(e) => {
87 | setNewStyle({
88 | ...newStyle,
89 | ["background-color"]: `${e.target.value}`,
90 | ["background"]: `${e.target.value}`,
91 | });
92 | }}
93 | />
94 |
95 | `${donuts}px`}
99 | label={t(`customize.containerPadding`)}
100 | maxValue={64}
101 | minValue={16}
102 | step={4}
103 | onChange={(value) => {
104 | setNewStyle({
105 | ...newStyle,
106 | ["padding"]: `${value}px`,
107 | });
108 | }}
109 | />
110 |
111 |
112 | )}
113 |
114 | );
115 | };
116 |
--------------------------------------------------------------------------------
/src/components/toolbar/style-setting-popover.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, ButtonGroup } from "@nextui-org/button";
3 | import { Palette } from "lucide-react";
4 | import { Popover, PopoverTrigger } from "@nextui-org/popover";
5 | import { useTranslation } from "react-i18next";
6 |
7 | import { StyleSettingPopoverContent } from "@/components/toolbar/style-setting-popover-content.tsx";
8 |
9 | export interface LayoutSetting {
10 | containerEnabled: boolean;
11 | containerPadding: number;
12 | containerBgColor: string;
13 | articlePadding: number;
14 | articleBgColor: string;
15 | }
16 |
17 | const StyleSettingPopover = () => {
18 | const { t } = useTranslation();
19 |
20 | return (
21 |
22 |
28 |
29 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default StyleSettingPopover;
41 |
--------------------------------------------------------------------------------
/src/components/toolbar/toolbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Select, SelectItem } from "@nextui-org/select";
3 | import { useTranslation } from "react-i18next";
4 |
5 | import CopyButtonGroup from "./copy-button-group.tsx";
6 | import DownloadButtonGroup from "./download-button-group.tsx";
7 |
8 | import StyleSettingPopover from "@/components/toolbar/style-setting-popover.tsx";
9 | import { ToolbarState } from "@/state/toolbarState";
10 | import { loadCSS, markdownStyles } from "@/config/post-styles.ts";
11 |
12 | interface ToolbarProps {
13 | markdown: string;
14 | }
15 |
16 | const Toolbar: React.FC = ({ markdown }) => {
17 | const { t } = useTranslation();
18 | const { selectedStyle, setSelectedStyle, setArticleStyle } =
19 | ToolbarState.useContainer();
20 |
21 | useEffect(() => {
22 | setArticleStyle(loadCSS(selectedStyle) as string);
23 | }, [selectedStyle]);
24 |
25 | return (
26 |
27 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default Toolbar;
54 |
--------------------------------------------------------------------------------
/src/components/typewriter-hero.tsx:
--------------------------------------------------------------------------------
1 | import { TypewriterEffectSmooth } from "@/components/typewriter";
2 | import { useTranslation } from "react-i18next";
3 |
4 | export function TypewriterHero() {
5 | const enWords = [
6 | {
7 | text: "Write",
8 | },
9 | {
10 | text: "Markdown",
11 | className: "text-blue-500 dark:text-blue-500",
12 | },
13 | {
14 | text: "And",
15 | },
16 | {
17 | text: "Post",
18 | className: "text-blue-500 dark:text-blue-500",
19 | },
20 | {
21 | text: "Everywhere.",
22 | },
23 | ];
24 |
25 | const zhWords = [
26 | {
27 | text: "创作",
28 | },
29 | {
30 | text: "Markdown",
31 | className: "text-blue-500 dark:text-blue-500",
32 | },
33 | {
34 | text: ",随处",
35 | },
36 | {
37 | text: "分享",
38 | className: "text-blue-500 dark:text-blue-500",
39 | },
40 | {
41 | text: "。",
42 | },
43 | ];
44 |
45 | const { i18n } = useTranslation();
46 |
47 | return (
48 |
49 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/typewriter.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion, stagger, useAnimate, useInView } from "framer-motion";
4 | import { useEffect } from "react";
5 |
6 | import { cn } from "@/lib/utils";
7 |
8 | export const Typewriter = ({
9 | words,
10 | className,
11 | cursorClassName,
12 | }: {
13 | words: {
14 | text: string;
15 | className?: string;
16 | }[];
17 | className?: string;
18 | cursorClassName?: string;
19 | }) => {
20 | // split text inside of words into array of characters
21 | const wordsArray = words.map((word) => {
22 | return {
23 | ...word,
24 | text: word.text.split(""),
25 | };
26 | });
27 |
28 | const [scope, animate] = useAnimate();
29 | const isInView = useInView(scope);
30 |
31 | useEffect(() => {
32 | if (isInView) {
33 | animate(
34 | "span",
35 | {
36 | display: "inline-block",
37 | opacity: 1,
38 | width: "fit-content",
39 | },
40 | {
41 | duration: 0.3,
42 | delay: stagger(0.1),
43 | ease: "easeInOut",
44 | },
45 | );
46 | }
47 | }, [isInView]);
48 |
49 | const renderWords = () => {
50 | return (
51 |
52 | {wordsArray.map((word, idx) => {
53 | return (
54 |
55 | {word.text.map((char, index) => (
56 |
64 | {char}
65 |
66 | ))}
67 |
68 |
69 | );
70 | })}
71 |
72 | );
73 | };
74 |
75 | return (
76 |
82 | {renderWords()}
83 |
100 |
101 | );
102 | };
103 |
104 | export const TypewriterEffectSmooth = ({
105 | words,
106 | className,
107 | cursorClassName,
108 | }: {
109 | words: {
110 | text: string;
111 | className?: string;
112 | }[];
113 | className?: string;
114 | cursorClassName?: string;
115 | }) => {
116 | // split text inside of words into array of characters
117 | const wordsArray = words.map((word) => {
118 | return {
119 | ...word,
120 | text: word.text.split(""),
121 | };
122 | });
123 | const renderWords = () => {
124 | return (
125 |
126 | {wordsArray.map((word, idx) => {
127 | return (
128 |
129 | {word.text.map((char, index) => (
130 |
134 | {char}
135 |
136 | ))}
137 |
138 |
139 | );
140 | })}
141 |
142 | );
143 | };
144 |
145 | return (
146 |
147 |
161 |
167 | {renderWords()}{" "}
168 |
{" "}
169 |
170 |
188 |
189 | );
190 | };
191 |
--------------------------------------------------------------------------------
/src/config/post-styles.ts:
--------------------------------------------------------------------------------
1 | import githubStyle from "@/styles/github.css?raw";
2 | import newspaperStyle from "@/styles/newspaper.css?raw";
3 | import posterStyle from "@/styles/poster.css?raw";
4 | import slimStyle from "@/styles/slim.css?raw";
5 | import noteStyle from "@/styles/note.css?raw";
6 | import twStyle from "@/styles/thoughtworks.css?raw";
7 |
8 | export const markdownStyles = [
9 | { name: "github", css: githubStyle },
10 | { name: "newspaper", css: newspaperStyle },
11 | { name: "poster", css: posterStyle },
12 | { name: "slim", css: slimStyle },
13 | { name: "note", css: noteStyle },
14 | { name: "tw", css: twStyle },
15 | ];
16 |
17 | export const loadCSS: any = (name: string) =>
18 | markdownStyles.find((style) => style.name === name)?.css;
19 |
--------------------------------------------------------------------------------
/src/config/site.ts:
--------------------------------------------------------------------------------
1 | export type SiteConfig = typeof siteConfig;
2 |
3 | export const siteConfig = {
4 | name: "MarkdownPost",
5 | description: "Write Markdown And Post Everywhere.",
6 | navItems: [
7 | // {
8 | // label: "Home",
9 | // href: "/",
10 | // },
11 | // {
12 | // label: "Docs",
13 | // href: "/docs",
14 | // },
15 | // {
16 | // label: "Pricing",
17 | // href: "/pricing",
18 | // },
19 | // {
20 | // label: "Blog",
21 | // href: "/blog",
22 | // },
23 | // {
24 | // label: "About",
25 | // href: "/about",
26 | // },
27 | ],
28 | navMenuItems: [
29 | // {
30 | // label: "About",
31 | // href: "/about",
32 | // },
33 | // {
34 | // label: "Projects",
35 | // href: "/projects",
36 | // },
37 | // {
38 | // label: "Team",
39 | // href: "/team",
40 | // },
41 | // {
42 | // label: "Calendar",
43 | // href: "/calendar",
44 | // },
45 | // {
46 | // label: "Settings",
47 | // href: "/settings",
48 | // },
49 | // {
50 | // label: "Help & Feedback",
51 | // href: "/help-feedback",
52 | // },
53 | // {
54 | // label: "Logout",
55 | // href: "/logout",
56 | // },
57 | ],
58 | links: {
59 | github: "https://github.com/Cyronlee/markdown-post",
60 | twitter: "https://github.com/Cyronlee/markdown-post",
61 | docs: "https://github.com/Cyronlee/markdown-post",
62 | discord: "https://github.com/Cyronlee/markdown-post",
63 | sponsor: "https://github.com/Cyronlee/markdown-post",
64 | },
65 | };
66 |
--------------------------------------------------------------------------------
/src/css/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/data/welcome-en.md:
--------------------------------------------------------------------------------
1 | # MarkdownPost
2 |
3 | 👋 Welcome to **MarkdownPost**! Nice to meet you!
4 |
5 | Just focus on creating your content, **MarkdownPost** will convert it to `email`, `image`, `PDF` and more.
6 |
7 | ## Quick Start
8 |
9 | 1. Write content: Enter Markdown text📝, paste images🏞️.
10 | 2. Choose a theme: Pick a theme🎨 that suits your content.
11 | 3. Copy content: Copy the formatted content with one click.
12 | 4. Paste & Share: Paste it into emails, chat software, or anywhere else for quick sharing.
13 |
14 | 
15 |
16 | ## Use Cases
17 |
18 | **MarkdownPost**'s various output formats (currently under development) can meet different use cases:
19 |
20 | | Format | Description | Scenario |
21 | |-----------|----------------------------------------------------------|-----------------------------------|
22 | | 🖼️ Image | Generate shareable images from Markdown | Social media sharing |
23 | | 📧 Email | Create content that can be embedded directly into emails | Newsletters, article sharing |
24 | | 📄 PDF | PDF format for easy saving, sharing, and printing | Document archiving, file transfer |
25 |
26 | ## Features
27 |
28 | - 💡 **Simple to Use:** Real-time preview, what you see is what you get.
29 | - 🏞️ **Image Upload:** Paste images, automatically generate image links.
30 | - 🧮 **Math Formula:** Support for $\LaTeX$ math formula.
31 | - 📊 **Data Visualization:** Create beautiful flowcharts, sequence diagrams, and more with Mermaid syntax, making your data more vivid.
32 | - 🎨 **Multiple Themes:** Continuously updated to meet different layout needs.
33 | - 📧 **Quick Sharing:** One-click copy, ready to publish on multiple platforms.
34 | - 📄 **Auto-Adapt:** Adapts to email window widths for a more attractive display.
35 | - 🔒 **Data Security:** Text and images are processed entirely in the browser, not uploaded to servers.
36 | - 🌟 **Free & Open Source:** Completely free to use, community contributions welcome.
37 |
38 | ```mermaid
39 | flowchart LR
40 | A[Write Markdown] -->|Real-time| B[Preview]
41 | B --> C{Export Options}
42 | C -->|Social Media| D[Twitter/LinkedIn]
43 | C -->|Document| E[PDF]
44 | C -->|Web| F[HTML]
45 | C -->|Email| G[Newsletter]
46 |
47 | style A fill:#d8dee9,stroke:#3b4252,stroke-width:2px
48 | style B fill:#81a1c1,stroke:#3b4252,stroke-width:2px
49 | style C fill:#d8dee9,stroke:#3b4252,stroke-width:2px
50 | style D fill:#d8dee9,stroke:#3b4252,stroke-width:2px
51 | style E fill:#d8dee9,stroke:#3b4252,stroke-width:2px
52 | style F fill:#d8dee9,stroke:#3b4252,stroke-width:2px
53 | style G fill:#d8dee9,stroke:#3b4252,stroke-width:2px
54 | ```
55 |
56 | ## Feedback
57 |
58 | Feel free to share your ideas and suggestions on [Github Issue](https://github.com/Cyronlee/markdown-post/issues). Your
59 | feedback will make **MarkdownPost** better!
60 |
--------------------------------------------------------------------------------
/src/data/welcome-zh.md:
--------------------------------------------------------------------------------
1 | # MarkdownPost
2 |
3 | 👋 欢迎来到`MarkdownPost`!很高兴见到你!
4 |
5 | 你只需要专注于内容创作,**MarkdownPost** 帮助你转换为`邮件`、`图片`、`PDF`等格式。
6 |
7 | ## 快速上手
8 |
9 | 1. 编写内容:输入 Markdown 文本📝,粘贴图片🏞️
10 | 2. 选择主题:挑选一个适合您内容的主题🎨
11 | 3. 复制内容:一键复制排版后的内容
12 | 4. 粘贴分享:粘贴到电子邮件、聊天软件等任何地方,快速分享
13 |
14 | 
15 |
16 | ## 使用场景
17 |
18 | **MarkdownPost** 的多种输出格式(正在开发中),可以满足不同的使用场景:
19 |
20 | | 格式 | 描述 | 场景 |
21 | |--------|-----------------------|-----------|
22 | | 🖼️ 图片 | 根据 Markdown 生成便于分享的图片 | 社交媒体分享图 |
23 | | 📧 邮件 | 创建可直接嵌入电子邮件的内容 | 新闻简报、文章分享 |
24 | | 📄 PDF | PDF 格式便于保存、分享、打印 | 文章归档、文件传输 |
25 |
26 | ## 功能亮点
27 |
28 | - 💡 **简洁易用:** 实时预览效果,所见即所得。
29 | - 🏞️ **图片上传:** 粘贴图片,自动生成图片链接。
30 | - 🧮 **数学公式:** 支持 $\LaTeX$ 数学公式。
31 | - 📈 **数据可视化:** 使用 Mermaid 语法,轻松创建流程图、序列图等,让数据更加生动。
32 | - 🎨 **多种主题:** 不断更新多种主题以满足不同排版需求。
33 | - 📧 **快速分享:** 一键复制,即可发布在多种平台。
34 | - 📄 **自动适应:** 在邮件中可以自适应窗口宽度,更美观的展示内容。
35 | - 🔒 **数据安全:** 文本和图片完全在浏览器中处理,不会上传到服务器。
36 | - 🌟 **免费开源:** 完全免费使用,欢迎社区贡献。
37 |
38 | ```mermaid
39 | flowchart LR
40 | A[编写 Markdown] -->|实时| B[预览]
41 | B --> C{导出选项}
42 | C -->|社交媒体| D[推特/领英]
43 | C -->|文档| E[PDF]
44 | C -->|网页| F[HTML]
45 | C -->|邮件| G[简报]
46 |
47 | style A fill:#d8dee9,stroke:#3b4252,stroke-width:2px
48 | style B fill:#81a1c1,stroke:#3b4252,stroke-width:2px
49 | style C fill:#d8dee9,stroke:#3b4252,stroke-width:2px
50 | style D fill:#d8dee9,stroke:#3b4252,stroke-width:2px
51 | style E fill:#d8dee9,stroke:#3b4252,stroke-width:2px
52 | style F fill:#d8dee9,stroke:#3b4252,stroke-width:2px
53 | style G fill:#d8dee9,stroke:#3b4252,stroke-width:2px
54 | ```
55 |
56 | ## 欢迎反馈
57 |
58 | 欢迎在 [Github Issue](https://github.com/Cyronlee/markdown-post/issues) 中提出你的想法和建议,你的反馈会让 **MarkdownPost**
59 | 变得更好!
60 |
--------------------------------------------------------------------------------
/src/hooks/use-theme.ts:
--------------------------------------------------------------------------------
1 | // originally written by @imoaazahmed
2 |
3 | import { useEffect, useMemo, useState } from "react";
4 |
5 | const ThemeProps = {
6 | key: "theme",
7 | light: "light",
8 | dark: "dark",
9 | } as const;
10 |
11 | type Theme = typeof ThemeProps.light | typeof ThemeProps.dark;
12 |
13 | export const useTheme = (defaultTheme?: Theme) => {
14 | const [theme, setTheme] = useState(() => {
15 | const storedTheme = localStorage.getItem(ThemeProps.key) as Theme | null;
16 |
17 | return storedTheme || (defaultTheme ?? ThemeProps.light);
18 | });
19 |
20 | const isDark = useMemo(() => {
21 | return theme === ThemeProps.dark;
22 | }, [theme]);
23 |
24 | const isLight = useMemo(() => {
25 | return theme === ThemeProps.light;
26 | }, [theme]);
27 |
28 | const _setTheme = (theme: Theme) => {
29 | localStorage.setItem(ThemeProps.key, theme);
30 | document.documentElement.classList.remove(
31 | ThemeProps.light,
32 | ThemeProps.dark,
33 | );
34 | document.documentElement.classList.add(theme);
35 | setTheme(theme);
36 | };
37 |
38 | const setLightTheme = () => _setTheme(ThemeProps.light);
39 |
40 | const setDarkTheme = () => _setTheme(ThemeProps.dark);
41 |
42 | const toggleTheme = () =>
43 | theme === ThemeProps.dark ? setLightTheme() : setDarkTheme();
44 |
45 | useEffect(() => {
46 | _setTheme(theme);
47 | });
48 |
49 | return { theme, isDark, isLight, setLightTheme, setDarkTheme, toggleTheme };
50 | };
51 |
--------------------------------------------------------------------------------
/src/layouts/default.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@nextui-org/link";
2 |
3 | import { Navbar } from "@/components/navbar";
4 |
5 | export default function DefaultLayout({
6 | children,
7 | }: {
8 | children: React.ReactNode;
9 | }) {
10 | return (
11 |
12 |
13 |
14 | {children}
15 |
16 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/src/lib/copy-html.tsx:
--------------------------------------------------------------------------------
1 | export const copyHtmlWithStyle = async (elementId: string) => {
2 | const element = document.getElementById(elementId);
3 |
4 | if (!element) {
5 | console.error("Element not found");
6 |
7 | return;
8 | }
9 |
10 | // Clone the element and include all styles as inline styles
11 | const clone = element.cloneNode(true) as HTMLElement;
12 |
13 | const inlineStyles = (element: HTMLElement) => {
14 | const computedStyle = window.getComputedStyle(element);
15 | // Convert CSSStyleDeclaration to array of property names
16 | const properties = Array.from(computedStyle);
17 |
18 | for (const key of properties) {
19 | element.style[key as any] = computedStyle.getPropertyValue(key);
20 | }
21 | };
22 |
23 | // Apply inline styles recursively
24 | const applyStylesRecursively = (element: HTMLElement) => {
25 | inlineStyles(element);
26 | Array.from(element.children).forEach((child) =>
27 | applyStylesRecursively(child as HTMLElement),
28 | );
29 | };
30 |
31 | applyStylesRecursively(clone);
32 |
33 | // Get the HTML content as a string
34 | const htmlContent = clone.outerHTML;
35 |
36 | try {
37 | // Use Clipboard API to copy the HTML with inline styles
38 | await navigator.clipboard.write([
39 | new ClipboardItem({
40 | "text/html": new Blob([htmlContent], { type: "text/html" }),
41 | }),
42 | ]);
43 | } catch (error) {
44 | console.error("Failed to copy content with style:", error);
45 | }
46 | };
47 |
--------------------------------------------------------------------------------
/src/lib/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import { initReactI18next } from "react-i18next";
3 | import en from "../locales/en.json";
4 | import zh from "../locales/zh.json";
5 |
6 | i18n.use(initReactI18next).init({
7 | resources: {
8 | en: { translation: en },
9 | zh: { translation: zh },
10 | },
11 | lng: "zh", // default language
12 | fallbackLng: "en",
13 | interpolation: {
14 | escapeValue: false, // react already safes from xss
15 | },
16 | });
17 |
18 | export default i18n;
19 |
--------------------------------------------------------------------------------
/src/lib/image-store.tsx:
--------------------------------------------------------------------------------
1 | const imageStore: Record = {};
2 |
3 | export function saveImageBase64(base64String: string): string {
4 | const imageId = generateImageId();
5 |
6 | imageStore[imageId] = base64String;
7 |
8 | return imageId;
9 | }
10 |
11 | function loadImageBase64(imageId: string): string | undefined {
12 | return imageStore[imageId];
13 | }
14 |
15 | export function replaceImgSrc(html: string): string {
16 | // Return original html if no localStorage image references found
17 | if (!html.includes("browser://")) {
18 | return html;
19 | }
20 |
21 | // Regular expression to match img tags with ls:// protocol
22 | const imgRegex = /
]*src="browser:\/\/([^"]+)"[^>]*>/g;
23 |
24 | // Replace all matching img tags
25 | return html.replace(imgRegex, (match, imgKey) => {
26 | const base64Data = loadImageBase64(imgKey);
27 |
28 | if (base64Data) {
29 | // Replace only the src attribute value
30 | return match.replace(`browser://${imgKey}`, base64Data);
31 | }
32 |
33 | // If no base64 data found, return original img tag
34 | return match;
35 | });
36 | }
37 |
38 | export function createMarkdownImage(imageId: string): string {
39 | return ``;
40 | }
41 |
42 | export function generateImageId(): string {
43 | return "img_" + Math.random().toString(36).substr(2, 9);
44 | }
45 |
--------------------------------------------------------------------------------
/src/lib/inline-styles.tsx:
--------------------------------------------------------------------------------
1 | interface StyleRule {
2 | [property: string]: string;
3 | }
4 |
5 | interface StyleRules {
6 | [selector: string]: StyleRule;
7 | }
8 |
9 | export default function inlineStyles(html: string, css: string): string {
10 | if (!html || !css) {
11 | return html;
12 | }
13 |
14 | const tempDiv = document.createElement("div");
15 |
16 | tempDiv.innerHTML = html;
17 |
18 | const styleRules: StyleRules = {};
19 |
20 | // Parse CSS rules
21 | css.split("}").forEach((rule) => {
22 | const parts = rule.split("{");
23 |
24 | if (parts.length !== 2) return;
25 |
26 | const [selector, styles] = parts;
27 |
28 | if (!selector?.trim() || !styles?.trim()) return;
29 |
30 | const trimmedSelector = selector.trim();
31 |
32 | styleRules[trimmedSelector] = {};
33 |
34 | // Parse individual style properties
35 | styles.split(";").forEach((style) => {
36 | const [property, value] = style.split(":");
37 |
38 | if (property?.trim() && value?.trim()) {
39 | styleRules[trimmedSelector][property.trim()] = value.trim();
40 | }
41 | });
42 | });
43 |
44 | // Apply styles to matching elements
45 | try {
46 | Object.entries(styleRules).forEach(([selector, styles]) => {
47 | const elements = tempDiv.querySelectorAll(selector);
48 |
49 | elements.forEach((element) => {
50 | if (element instanceof HTMLElement) {
51 | Object.entries(styles).forEach(([property, value]) => {
52 | element.style.setProperty(property, value);
53 | });
54 | }
55 | });
56 | });
57 | } catch (error) {
58 | console.error("Error applying inline styles:", error);
59 | }
60 |
61 | return tempDiv.innerHTML;
62 | }
63 |
--------------------------------------------------------------------------------
/src/lib/is-safari.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Determines if the current browser is Safari.
3 | */
4 | export const isSafari: boolean = /^((?!chrome|android).)*safari/i.test(
5 | window.navigator.userAgent,
6 | );
7 |
--------------------------------------------------------------------------------
/src/lib/marked-mermaid-extension.ts:
--------------------------------------------------------------------------------
1 | import { MarkedExtension } from "marked";
2 | import mermaid from "mermaid";
3 |
4 | mermaid.initialize({
5 | startOnLoad: false,
6 | theme: "default",
7 | securityLevel: "loose",
8 | });
9 |
10 | const generateId = () =>
11 | `mermaid-${Math.random().toString(36).substring(2, 10)}`;
12 |
13 | export const markedMermaid = (): MarkedExtension => {
14 | return {
15 | renderer: {
16 | code(token) {
17 | if (token.lang === "mermaid") {
18 | const id = generateId();
19 |
20 | return ``;
23 | }
24 |
25 | return false;
26 | },
27 | },
28 | };
29 | };
30 |
31 | export const renderMermaidDiagrams = () => {
32 | setTimeout(() => {
33 | try {
34 | mermaid.run({
35 | querySelector: ".mermaid",
36 | });
37 | } catch (error) {
38 | console.error("Mermaid render error:", error);
39 | }
40 | }, 0);
41 | };
42 |
--------------------------------------------------------------------------------
/src/lib/use-tailwind-breakpoints.tsx:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useState } from "react";
2 | import { useWindowSize } from "@/lib/use-window-size.tsx";
3 |
4 | const breakpoints = {
5 | sm: 640,
6 | md: 768,
7 | lg: 1024,
8 | xl: 1280,
9 | "2xl": 1536,
10 | };
11 |
12 | const useTailwindBreakpoints = () => {
13 | const [activeBreakpoints, setActiveBreakpoints] = useState(new Set());
14 | const { width } = useWindowSize();
15 |
16 | useLayoutEffect(() => {
17 | if (width === null) return;
18 |
19 | const newSet = new Set();
20 |
21 | if (width < breakpoints["2xl"]) {
22 | newSet.add("2xl");
23 | }
24 | if (width < breakpoints.xl) {
25 | newSet.add("xl");
26 | }
27 | if (width < breakpoints.lg) {
28 | newSet.add("lg");
29 | }
30 | if (width < breakpoints.md) {
31 | newSet.add("md");
32 | }
33 | if (width < breakpoints.sm) {
34 | newSet.add("sm");
35 | }
36 |
37 | setActiveBreakpoints(newSet);
38 | }, [width]);
39 |
40 | return activeBreakpoints;
41 | };
42 |
43 | export default useTailwindBreakpoints;
44 |
--------------------------------------------------------------------------------
/src/lib/use-window-size.tsx:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useState } from "react";
2 |
3 | export function useWindowSize() {
4 | const [size, setSize] = useState<{
5 | width: number | null;
6 | height: number | null;
7 | }>({
8 | width: null,
9 | height: null,
10 | });
11 |
12 | useLayoutEffect(() => {
13 | const handleResize = () => {
14 | setSize({
15 | width: window.innerWidth,
16 | height: window.innerHeight,
17 | });
18 | };
19 |
20 | handleResize();
21 | window.addEventListener("resize", handleResize);
22 |
23 | return () => {
24 | window.removeEventListener("resize", handleResize);
25 | };
26 | }, []);
27 |
28 | return size;
29 | }
30 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/src/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "postStyle": {
3 | "github": "Github",
4 | "newspaper": "Newspaper",
5 | "poster": "Poster",
6 | "slim": "Slim",
7 | "note": "Note",
8 | "tw": "Thoughtworks"
9 | },
10 | "toolbar": {
11 | "selectStyleText": "Select style"
12 | },
13 | "commonToast": {
14 | "error": "oops, something went wrong!",
15 | "processing": "Processing, please wait...",
16 | "developing": "Feature is under development..."
17 | },
18 | "copyEmail": {
19 | "buttonName": "Copy as email",
20 | "buttonDescription": "Then you can paste it to email editor like Gmail.",
21 | "successMessage": "Content copied",
22 | "successDescription": "You can paste into your email",
23 | "failedMessage": "Failed to copy content"
24 | },
25 | "copyImage": {
26 | "buttonName": "Copy as image",
27 | "buttonDescription": "Then you can paste it to anywhere.",
28 | "successMessage": "Image copied to clipboard",
29 | "successDescription": "You can now paste it anywhere",
30 | "failedMessage": "Failed to copy image"
31 | },
32 | "downloadImage": {
33 | "buttonName": "Download image",
34 | "buttonDescription": "Download .png image file.",
35 | "successMessage": "Image saved",
36 | "successDescription": "Image has been successfully saved",
37 | "failedMessage": "Failed to download image"
38 | },
39 | "downloadPDF": {
40 | "buttonName": "Download PDF",
41 | "buttonDescription": "Download .pdf file.",
42 | "successMessage": "PDF saved",
43 | "successDescription": "PDF has been successfully saved",
44 | "failedMessage": "Failed to download PDF"
45 | },
46 | "downloadHTML": {
47 | "buttonName": "Download HTML",
48 | "buttonDescription": "Download .html file.",
49 | "successMessage": "HTML saved",
50 | "successDescription": "HTML has been successfully saved",
51 | "failedMessage": "Failed to download HTML"
52 | },
53 | "downloadMarkdown": {
54 | "buttonName": "Download Markdown",
55 | "buttonDescription": "Download .md file.",
56 | "successMessage": "Markdown saved",
57 | "successDescription": "Markdown has been successfully saved",
58 | "failedMessage": "Failed to download Markdown"
59 | },
60 | "customize": {
61 | "containerBackground": "Container Background",
62 | "buttonName": "Customize",
63 | "layoutCustomizer": "Layout Customize",
64 | "containerPadding": "Container Padding"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/locales/zh.json:
--------------------------------------------------------------------------------
1 | {
2 | "postStyle": {
3 | "github": "默认",
4 | "newspaper": "报纸",
5 | "poster": "海报",
6 | "slim": "细体",
7 | "note": "笔记",
8 | "tw": "Thoughtworks"
9 | },
10 | "toolbar": {
11 | "selectStyleText": "选择样式"
12 | },
13 | "commonToast": {
14 | "error": "发生了意想不到的错误!",
15 | "processing": "处理中,请稍等...",
16 | "developing": "功能正在开发中..."
17 | },
18 | "copyEmail": {
19 | "buttonName": "复制邮件",
20 | "buttonDescription": "复制邮件后你可以粘贴到邮件编辑器,如Gmail中。",
21 | "successMessage": "内容已复制",
22 | "successDescription": "你可以粘贴到你的邮件中",
23 | "failedMessage": "复制失败"
24 | },
25 | "copyImage": {
26 | "buttonName": "复制图片",
27 | "buttonDescription": "复制图片后你可以粘贴到任何地方。",
28 | "successMessage": "图片已复制到剪贴板",
29 | "successDescription": "你现在可以粘贴到任何地方",
30 | "failedMessage": "复制图片失败"
31 | },
32 | "downloadImage": {
33 | "buttonName": "下载图片",
34 | "buttonDescription": "下载为.png格式的图片文件。",
35 | "successMessage": "图片已保存",
36 | "successDescription": "图片已成功保存",
37 | "failedMessage": "下载图片失败"
38 | },
39 | "downloadPDF": {
40 | "buttonName": "下载PDF",
41 | "buttonDescription": "下载为.pdf格式的文件。",
42 | "successMessage": "PDF已保存",
43 | "successDescription": "PDF已成功保存",
44 | "failedMessage": "下载PDF失败"
45 | },
46 | "downloadHTML": {
47 | "buttonName": "下载HTML",
48 | "buttonDescription": "下载为.html格式的文件。",
49 | "successMessage": "HTML已保存",
50 | "successDescription": "HTML已成功保存",
51 | "failedMessage": "下载HTML失败"
52 | },
53 | "downloadMarkdown": {
54 | "buttonName": "下载Markdown",
55 | "buttonDescription": "下载为.md格式的文件。",
56 | "successMessage": "Markdown已保存",
57 | "successDescription": "Markdown已成功保存",
58 | "failedMessage": "下载Markdown失败"
59 | },
60 | "customize": {
61 | "containerBackground": "背景色",
62 | "buttonName": "自定义",
63 | "layoutCustomizer": "布局调整",
64 | "containerPadding": "边距调整"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { BrowserRouter } from "react-router-dom";
4 | import { Toaster } from "sonner";
5 | import { Analytics } from "@vercel/analytics/react";
6 |
7 | import { Provider } from "./provider.tsx";
8 | // eslint-disable-next-line import/order
9 | import App from "./App.tsx";
10 | import "@/css/globals.css";
11 |
12 | import "./lib/i18n.ts";
13 | import { ToolbarState } from "./state/toolbarState.ts";
14 |
15 | ReactDOM.createRoot(document.getElementById("root")!).render(
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ,
27 | );
28 |
--------------------------------------------------------------------------------
/src/pages/about.tsx:
--------------------------------------------------------------------------------
1 | import { title } from "@/components/primitives";
2 | import DefaultLayout from "@/layouts/default";
3 |
4 | export default function DocsPage() {
5 | return (
6 |
7 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/blog.tsx:
--------------------------------------------------------------------------------
1 | import { title } from "@/components/primitives";
2 | import DefaultLayout from "@/layouts/default";
3 |
4 | export default function DocsPage() {
5 | return (
6 |
7 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/docs.tsx:
--------------------------------------------------------------------------------
1 | import { title } from "@/components/primitives";
2 | import DefaultLayout from "@/layouts/default";
3 |
4 | export default function DocsPage() {
5 | return (
6 |
7 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from "react";
2 | import { Marked } from "marked";
3 | import { markedHighlight } from "marked-highlight";
4 | import hljs from "highlight.js";
5 | import { useTranslation } from "react-i18next";
6 | import markedKatex from "marked-katex-extension";
7 | import DefaultLayout from "@/layouts/default";
8 | import ResizableSplitPane from "@/components/resizable-split-pane";
9 | import inlineStyles from "@/lib/inline-styles";
10 | import { replaceImgSrc } from "@/lib/image-store";
11 | import { TypewriterHero } from "@/components/typewriter-hero";
12 | import { MarkdownEditor } from "@/components/markdown-editor.tsx";
13 | import welcomeMarkdownZh from "@/data/welcome-zh.md?raw";
14 | import welcomeMarkdownEn from "@/data/welcome-en.md?raw";
15 | import Toolbar from "@/components/toolbar/toolbar.tsx";
16 | import { ToolbarState } from "@/state/toolbarState";
17 | import { markedMermaid, renderMermaidDiagrams } from "@/lib/marked-mermaid-extension";
18 |
19 | // Move marked configuration to a separate constant
20 | const markedInstance = new Marked(
21 | markedHighlight({
22 | emptyLangClass: "hljs",
23 | langPrefix: "hljs language-",
24 | highlight(code, lang) {
25 | const language = hljs.getLanguage(lang) ? lang : "plaintext";
26 | return hljs.highlight(code, { language }).value;
27 | },
28 | }),
29 | markedKatex({
30 | throwOnError: false,
31 | }),
32 | markedMermaid(),
33 | {
34 | breaks: true,
35 | },
36 | );
37 |
38 | // Helper functions
39 | const wrapWithContainer = (htmlString: string) => {
40 | return ``;
43 | };
44 |
45 | export default function IndexPage() {
46 | const { i18n } = useTranslation();
47 | const { articleStyle } = ToolbarState.useContainer();
48 | const [markdown, setMarkdown] = useState(welcomeMarkdownZh);
49 | const [isModified, setIsModified] = useState(false);
50 | const [inlineStyledHTML, setInlineStyledHTML] = useState("");
51 | const [showRenderedHTML, setShowRenderedHTML] = useState(true);
52 | const contentRenderedRef = useRef(false);
53 |
54 | useEffect(() => {
55 | setMarkdown(i18n.language === "zh" ? welcomeMarkdownZh : welcomeMarkdownEn);
56 | }, [i18n.language]);
57 |
58 | // Parse markdown to HTML and apply inline styles
59 | useEffect(() => {
60 | const parseMarkdown = async () => {
61 | const parsedHTML = await markedInstance.parse(markdown);
62 | const wrappedHTML = wrapWithContainer(replaceImgSrc(parsedHTML));
63 | setInlineStyledHTML(inlineStyles(wrappedHTML, articleStyle));
64 | contentRenderedRef.current = true;
65 | };
66 | parseMarkdown();
67 | }, [markdown, articleStyle]);
68 |
69 | useEffect(() => {
70 | if (contentRenderedRef.current && showRenderedHTML) {
71 | renderMermaidDiagrams();
72 | contentRenderedRef.current = false;
73 | }
74 | }, [inlineStyledHTML, showRenderedHTML]);
75 |
76 | const handleMarkdownChange = (newMarkdown: string) => {
77 | setMarkdown(newMarkdown);
78 | setIsModified(true);
79 | };
80 |
81 | useEffect(() => {
82 | const handleBeforeUnload = (event: BeforeUnloadEvent) => {
83 | event.preventDefault();
84 | event.returnValue = "";
85 | };
86 | if (isModified) {
87 | window.addEventListener("beforeunload", handleBeforeUnload);
88 | }
89 | return () => {
90 | window.removeEventListener("beforeunload", handleBeforeUnload);
91 | };
92 | }, [isModified]);
93 |
94 | // UI Components
95 | const LeftContent = (
96 |
97 |
98 |
99 | );
100 |
101 | const RightContent = (
102 |
103 | {showRenderedHTML ? (
104 | <>
105 |
109 | >
110 | ) : (
111 | inlineStyledHTML
112 | )}
113 |
114 | );
115 |
116 | return (
117 |
118 |
119 |
120 |
127 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/src/pages/pricing.tsx:
--------------------------------------------------------------------------------
1 | import { title } from "@/components/primitives";
2 | import DefaultLayout from "@/layouts/default";
3 |
4 | export default function DocsPage() {
5 | return (
6 |
7 |
8 |
9 |
Pricing
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/provider.tsx:
--------------------------------------------------------------------------------
1 | import { NextUIProvider } from "@nextui-org/system";
2 | import { useNavigate } from "react-router-dom";
3 |
4 | export function Provider({ children }: { children: React.ReactNode }) {
5 | const navigate = useNavigate();
6 |
7 | return {children};
8 | }
9 |
--------------------------------------------------------------------------------
/src/state/toolbarState.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { createContainer } from "unstated-next";
3 |
4 | import { loadCSS, markdownStyles } from "@/config/post-styles.ts";
5 |
6 | const useToolbarState = () => {
7 | const [selectedStyle, setSelectedStyle] = useState(
8 | markdownStyles[0].name,
9 | );
10 |
11 | const [articleStyle, setArticleStyle] = useState(
12 | loadCSS(selectedStyle) as string,
13 | );
14 |
15 | return {
16 | selectedStyle,
17 | setSelectedStyle,
18 | articleStyle,
19 | setArticleStyle,
20 | };
21 | };
22 |
23 | export const ToolbarState = createContainer(useToolbarState);
24 |
--------------------------------------------------------------------------------
/src/styles/github.css:
--------------------------------------------------------------------------------
1 | .container-layout{
2 | background-color: #e5e5e5;
3 | padding: 32px;
4 | }
5 |
6 | /* light */
7 | .article {
8 | padding: 32px;
9 | border-radius: 8px;
10 | background: white;
11 | color-scheme: light;
12 | color: #1f2328;
13 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
14 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
15 | font-size: 16px;
16 | line-height: 1.5;
17 | word-wrap: break-word;
18 | }
19 |
20 | h1,
21 | h2,
22 | h3,
23 | h4,
24 | h5,
25 | h6 {
26 | margin-top: 1.5rem;
27 | margin-bottom: 1rem;
28 | font-weight: 600;
29 | line-height: 1.25;
30 | }
31 |
32 | h1 {
33 | padding-bottom: 0.3em;
34 | font-size: 2em;
35 | border-bottom: 1px solid #d1d9e0b3;
36 | }
37 |
38 | h2 {
39 | font-size: 1.5em;
40 | border-bottom: 1px solid #d1d9e0b3;
41 | }
42 |
43 | h3 {
44 | font-size: 1.25em;
45 | }
46 |
47 | h4 {
48 | font-size: 1em;
49 | }
50 |
51 | h5 {
52 | font-size: 0.875em;
53 | }
54 |
55 | h6 {
56 | font-size: 0.85em;
57 | color: #59636e;
58 | }
59 |
60 | p,
61 | blockquote,
62 | ul,
63 | ol,
64 | dl,
65 | table,
66 | pre,
67 | details {
68 | margin-top: 0;
69 | margin-bottom: 1rem;
70 | }
71 |
72 | a {
73 | color: #0969da;
74 | text-decoration: none;
75 | }
76 |
77 | a:hover {
78 | text-decoration: underline;
79 | }
80 |
81 | img {
82 | border-style: none;
83 | width: 100%;
84 | }
85 |
86 | table {
87 | border-spacing: 0;
88 | border-collapse: collapse;
89 | width: 100%;
90 | overflow: auto;
91 | }
92 |
93 | th,
94 | td {
95 | padding: 6px 13px;
96 | border: 1px solid #d1d9e0;
97 | }
98 |
99 | blockquote {
100 | margin: 0;
101 | padding: 0 1em;
102 | color: #59636e;
103 | border-left: 0.25em solid #d1d9e0;
104 | }
105 |
106 | ul {
107 | list-style-type: disc;
108 | padding-left: 2em;
109 | }
110 |
111 | code {
112 | margin: 0 2px;
113 | border: 1px solid #ddd;
114 | background-color: #f8f8f8;
115 | border-radius: 4px;
116 | padding: 3px 4px;
117 | }
118 |
119 | pre {
120 | background-color: #f8f8f8;
121 | border: 1px solid #ddd;
122 | font-size: 13px;
123 | line-height: 19px;
124 | overflow: auto;
125 | padding: 6px 10px;
126 | border-radius: 3px;
127 | }
128 |
129 | pre code {
130 | margin: 0;
131 | padding: 0;
132 | background-color: transparent;
133 | border: none;
134 | word-wrap: normal;
135 | max-width: initial;
136 | display: inline;
137 | overflow: initial;
138 | line-height: inherit;
139 | }
140 |
141 | ol {
142 | padding-left: 2em;
143 | list-style-type: auto;
144 | }
145 |
146 | hr {
147 | border-bottom: 1px solid #d1d9e0b3;
148 | margin: 1.5rem 0;
149 | }
150 |
151 | details {
152 | display: block;
153 | }
154 |
155 | summary {
156 | display: list-item;
157 | }
158 |
159 | [hidden] {
160 | display: none !important;
161 | }
162 |
163 | /* Highlight.js */
164 | pre code.hljs {
165 | display: block;
166 | overflow-x: auto;
167 | padding: 1em;
168 | }
169 |
170 | code.hljs {
171 | padding: 3px 5px;
172 | }
173 |
174 | .hljs {
175 | color: #383a42;
176 | background: #fafafa;
177 | }
178 |
179 | .hljs-comment,
180 | .hljs-quote {
181 | color: #a0a1a7;
182 | font-style: italic;
183 | }
184 |
185 | .hljs-doctag,
186 | .hljs-keyword,
187 | .hljs-formula {
188 | color: #a626a4;
189 | }
190 |
191 | .hljs-section,
192 | .hljs-name,
193 | .hljs-selector-tag,
194 | .hljs-deletion,
195 | .hljs-subst {
196 | color: #e45649;
197 | }
198 |
199 | .hljs-literal {
200 | color: #0184bb;
201 | }
202 |
203 | .hljs-string,
204 | .hljs-regexp,
205 | .hljs-addition,
206 | .hljs-attribute,
207 | .hljs-meta .hljs-string {
208 | color: #50a14f;
209 | }
210 |
211 | .hljs-attr,
212 | .hljs-variable,
213 | .hljs-template-variable,
214 | .hljs-type,
215 | .hljs-selector-class,
216 | .hljs-selector-attr,
217 | .hljs-selector-pseudo,
218 | .hljs-number {
219 | color: #986801;
220 | }
221 |
222 | .hljs-symbol,
223 | .hljs-bullet,
224 | .hljs-link,
225 | .hljs-meta,
226 | .hljs-selector-id,
227 | .hljs-title {
228 | color: #4078f2;
229 | }
230 |
231 | .hljs-built_in,
232 | .hljs-title.class_,
233 | .hljs-class .hljs-title {
234 | color: #c18401;
235 | }
236 |
237 | .hljs-emphasis {
238 | font-style: italic;
239 | }
240 |
241 | .hljs-strong {
242 | font-weight: bold;
243 | }
244 |
245 | .hljs-link {
246 | text-decoration: underline;
247 | }
248 |
--------------------------------------------------------------------------------
/src/styles/newspaper.css:
--------------------------------------------------------------------------------
1 | .container-layout{
2 | background-color: #c0c0c0;
3 | padding: 16px;
4 | }
5 |
6 | .article {
7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
8 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
9 | background-color: #f5f5f5;
10 | padding: 32px;
11 | border-radius: 8px;
12 | color: #333333;
13 | line-height: 1.6;
14 | }
15 |
16 | /* 标题样式 */
17 | h1 {
18 | font-size: 36px;
19 | text-align: center;
20 | font-weight: bold;
21 | color: #333333;
22 | border-bottom: 2px solid #333333;
23 | padding-bottom: 10px;
24 | margin-bottom: 20px;
25 | }
26 |
27 | h2 {
28 | font-size: 28px;
29 | font-weight: bold;
30 | color: #222222;
31 | margin-top: 30px;
32 | margin-bottom: 10px;
33 | padding-bottom: 5px;
34 | border-bottom: 1px solid #bbbbbb;
35 | }
36 |
37 | /* 段落样式 */
38 | p {
39 | font-size: 16px;
40 | margin-bottom: 20px;
41 | text-align: justify;
42 | }
43 |
44 | /* 引用样式 */
45 | blockquote {
46 | border-left: 4px solid #333333;
47 | margin: 20px 0;
48 | padding: 10px 20px;
49 | font-style: italic;
50 | color: #555555;
51 | background-color: #f0f0f0;
52 | }
53 |
54 | /* 列表样式 */
55 | ul {
56 | list-style-type: square;
57 | padding-left: 20px;
58 | margin-bottom: 20px;
59 | }
60 |
61 | li {
62 | margin: 10px 0;
63 | }
64 |
65 | /* 图片样式 */
66 | img {
67 | max-width: 100%;
68 | height: auto;
69 | display: block;
70 | margin: 20px auto;
71 | border: 2px solid #dddddd;
72 | }
73 |
74 | /* 代码块样式 */
75 | pre {
76 | background-color: #fafafa;
77 | padding: 15px;
78 | border: 1px solid #dddddd;
79 | overflow-x: auto;
80 | font-size: 14px;
81 | line-height: 1.4;
82 | }
83 |
84 | code {
85 | background-color: #fafafa;
86 | padding: 2px 4px;
87 | font-size: 14px;
88 | color: #d6336c;
89 | }
90 |
91 | /* 表格样式 */
92 | table {
93 | width: 100%;
94 | border-collapse: collapse;
95 | margin: 20px 0;
96 | }
97 |
98 | thead th {
99 | background-color: #333333;
100 | color: #ffffff;
101 | padding: 10px;
102 | text-align: left;
103 | }
104 |
105 | tbody tr:nth-child(odd) {
106 | background-color: #f9f9f9;
107 | }
108 |
109 | tbody td,
110 | thead th {
111 | padding: 10px;
112 | border: 1px solid #dddddd;
113 | }
114 |
115 | /* 链接样式 */
116 | a {
117 | color: #006699;
118 | text-decoration: none;
119 | }
120 |
121 | a:hover {
122 | text-decoration: underline;
123 | }
124 |
125 | /* 按钮与输入框样式 */
126 | input[type="checkbox"] {
127 | margin-right: 8px;
128 | }
129 |
130 | /* 分割线 */
131 | hr {
132 | border: none;
133 | border-top: 2px solid #cccccc;
134 | margin: 40px 0;
135 | }
136 |
137 | /* Highlight.js */
138 | pre code.hljs {
139 | display: block;
140 | overflow-x: auto;
141 | padding: 1em;
142 | }
143 |
144 | code.hljs {
145 | padding: 3px 5px;
146 | }
147 |
148 | .hljs {
149 | color: #383a42;
150 | background: #fafafa;
151 | }
152 |
153 | .hljs-comment,
154 | .hljs-quote {
155 | color: #a0a1a7;
156 | font-style: italic;
157 | }
158 |
159 | .hljs-doctag,
160 | .hljs-keyword,
161 | .hljs-formula {
162 | color: #a626a4;
163 | }
164 |
165 | .hljs-section,
166 | .hljs-name,
167 | .hljs-selector-tag,
168 | .hljs-deletion,
169 | .hljs-subst {
170 | color: #e45649;
171 | }
172 |
173 | .hljs-literal {
174 | color: #0184bb;
175 | }
176 |
177 | .hljs-string,
178 | .hljs-regexp,
179 | .hljs-addition,
180 | .hljs-attribute,
181 | .hljs-meta .hljs-string {
182 | color: #50a14f;
183 | }
184 |
185 | .hljs-attr,
186 | .hljs-variable,
187 | .hljs-template-variable,
188 | .hljs-type,
189 | .hljs-selector-class,
190 | .hljs-selector-attr,
191 | .hljs-selector-pseudo,
192 | .hljs-number {
193 | color: #986801;
194 | }
195 |
196 | .hljs-symbol,
197 | .hljs-bullet,
198 | .hljs-link,
199 | .hljs-meta,
200 | .hljs-selector-id,
201 | .hljs-title {
202 | color: #4078f2;
203 | }
204 |
205 | .hljs-built_in,
206 | .hljs-title.class_,
207 | .hljs-class .hljs-title {
208 | color: #c18401;
209 | }
210 |
211 | .hljs-emphasis {
212 | font-style: italic;
213 | }
214 |
215 | .hljs-strong {
216 | font-weight: bold;
217 | }
218 |
219 | .hljs-link {
220 | text-decoration: underline;
221 | }
222 |
--------------------------------------------------------------------------------
/src/styles/note.css:
--------------------------------------------------------------------------------
1 | .container-layout{
2 | background-color: #efe7db;
3 | padding: 36px;
4 | }
5 |
6 | /* light */
7 | .article {
8 | padding: 32px;
9 | border-radius: 8px;
10 | background: white;
11 | color-scheme: light;
12 | color: #222222;
13 | font-family: AvenirNext-Regular, -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
14 | font-size: 16px;
15 | line-height: 1.5;
16 | word-wrap: break-word;
17 | }
18 |
19 | /* title */
20 | h1,
21 | h2,
22 | h3,
23 | h4,
24 | h5,
25 | h6 {
26 | margin: 1.5rem 0 0.75em;
27 | font-family: AvenirNext-Medium, serif;
28 | font-weight: 600;
29 | line-height: 1.5em;
30 | color: #262626;
31 | }
32 |
33 | h1 {
34 | font-size: 1.5em;
35 | }
36 |
37 | h2 {
38 | font-size: 1.3em;
39 | }
40 |
41 | h3 {
42 | font-size: 1.1em;
43 | }
44 |
45 | h4 {
46 | font-size: 1em;
47 | }
48 |
49 | h5 {
50 | font-size: 1em;
51 | }
52 |
53 | h6 {
54 | font-size: 1em;
55 | }
56 |
57 | /* general styles */
58 | p, pre, ul, ol, dl, form, details, dl, blockquote, table, xmp, plaintext, listing, figure,
59 | pre[class*="language-"] {
60 | margin: 0.75em 0 0.45em;
61 | }
62 |
63 | /* 段落样式 */
64 | p {
65 | margin-left: 0;
66 | margin-right: 0;
67 | }
68 |
69 | /* 引用样式 */
70 | blockquote {
71 | background-color: #ffffff;
72 | padding-left: 0.8em;
73 | border-left: 0.2em solid #007acb;
74 | margin: 1rem 0;
75 | }
76 |
77 | blockquote > blockquote {
78 | margin: 0;
79 | border-left: 0.2em double #007acb;
80 | }
81 |
82 | li > blockquote {
83 | margin: 0.5rem;
84 | margin-left: -0.8rem;
85 | }
86 |
87 | /* 列表样式 */
88 | ul ul, ul ol, ol ul, ol ol {
89 | margin-bottom: 0px;
90 | margin-top: 0.25em;
91 | margin-left: 2em;
92 | }
93 | li ul, li ol {
94 | margin-top: 0.25em;
95 | margin-left: 2em;
96 | }
97 |
98 | li {
99 | word-wrap: break-all;
100 | }
101 |
102 | ul, ol {
103 | list-style-type: disc;
104 | }
105 |
106 | ul {
107 | margin-left: 1.3em;
108 | padding: 0;
109 | }
110 |
111 | ol {
112 | margin-left: 1.3em;
113 | padding: 0;
114 | counter-reset: ol_counter;
115 | list-style-type: auto;
116 | }
117 |
118 | ul > li {
119 | position: relative;
120 | }
121 |
122 | ol > li {
123 | position: relative;
124 | }
125 |
126 | li + li {
127 | margin-top: 0.25em;
128 | }
129 |
130 | /* 图片样式 */
131 | img {
132 | max-width: 100%;
133 | height: auto;
134 | display: block;
135 | margin: 20px auto;
136 | border: 2px solid #dddddd;
137 | }
138 |
139 | /* 代码块样式 */
140 | pre, pre[class*="language-"] {
141 | padding: 0;
142 | border: 0;
143 | }
144 | pre, xmp, plaintext, listing, code, kbd, tt, samp {
145 | font-family: Menlo-Regular, Menlo, Monaco, Consolas, 'Courier New', monospace;
146 | }
147 |
148 | code {
149 | display: inline;
150 | border: solid 2px #f3f5f7;
151 | padding: 0.2em 0.5em;
152 | font-size: 0.9em;
153 | color: #444444;
154 | background-color: #f3f5f7;
155 | }
156 |
157 | pre > code {
158 | display: block;
159 | border: none;
160 | padding: 0.7em 1em;
161 | font-size: 0.9em;
162 | overflow-x: auto
163 | }
164 |
165 | pre {
166 | background-color: #fafafa;
167 | padding: 15px;
168 | border: 1px solid #dddddd;
169 | overflow-x: auto;
170 | font-size: 14px;
171 | line-height: 1.4;
172 | }
173 |
174 | mark {
175 | color: inherit;
176 | display: inline;
177 | padding: 0.2em 0.5em;
178 | background-color: #fcffc0;
179 | }
180 |
181 | figcaption {
182 | text-align: center;
183 | }
184 |
185 | /* 表格样式 */
186 | table {
187 | width: 100%;
188 | border-collapse: collapse;
189 | margin-top: 20px;
190 | margin-bottom: 20px;
191 | margin-left: 0px;
192 | margin-right: 0px;
193 | color: #222222;
194 | background-color: white;
195 | font-size: 1em;
196 | border-spacing: 0;
197 | border: 1px solid #cecece;
198 | }
199 |
200 | th, td {
201 | padding: 0.7em 1em;
202 | font-size: 0.9em;
203 | border: 1px solid #cecece;
204 | border-top: none;
205 | border-bottom: none;
206 | }
207 |
208 | caption, th, td {
209 | text-align: left;
210 | font-weight: normal;
211 | vertical-align: middle
212 | }
213 |
214 | thead th {
215 | background-color: #f3f4f6;
216 | color: #434343;
217 | font-weight: bold;
218 | padding: 10px;
219 | text-align: left;
220 | }
221 |
222 | tbody tr:nth-child(even) {
223 | background-color: #f3f4f6;
224 | }
225 |
226 | /* 链接样式 */
227 | a {
228 | color: #007acb;
229 | text-decoration: underline;
230 | }
231 |
232 | /* divider */
233 |
234 | hr {
235 | height: 1px;
236 | border: 0;
237 | background-color: #bfbfbf;
238 | border-style: inset;
239 | border-width: 1px;
240 | }
241 |
242 | details {
243 | display: block;
244 | }
245 |
246 | summary {
247 | display: list-item;
248 | }
249 |
250 | [hidden] {
251 | display: none !important;
252 | }
253 |
254 | /* Highlight.js */
255 |
256 | pre code.hljs {
257 | display: block;
258 | overflow-x: auto;
259 | padding: 1em;
260 | }
261 |
262 | code.hljs {
263 | padding: 3px 5px;
264 | }
265 |
266 | .hljs {
267 | color: #444444;
268 | background: #fafafa;
269 | }
270 |
271 | .hljs-comment,
272 | .hljs-quote {
273 | color: #a0a1a7;
274 | }
275 |
276 | .hljs-doctag,
277 | .hljs-keyword,
278 | .hljs-formula {
279 | color: #007acb;
280 | }
281 |
282 | .hljs-section,
283 | .hljs-name,
284 | .hljs-selector-tag,
285 | .hljs-deletion,
286 | .hljs-subst {
287 | color: #e45649;
288 | }
289 |
290 | .hljs-literal {
291 | color: #0184bb;
292 | }
293 |
294 | .hljs-string,
295 | .hljs-regexp,
296 | .hljs-addition,
297 | .hljs-attribute,
298 | .hljs-meta .hljs-string {
299 | color: #c92626;
300 | }
301 |
302 | .hljs-attr,
303 | .hljs-variable,
304 | .hljs-template-variable,
305 | .hljs-type,
306 | .hljs-selector-class,
307 | .hljs-selector-attr,
308 | .hljs-selector-pseudo,
309 | .hljs-number {
310 | color: #005f87;
311 | }
312 |
313 | .hljs-symbol,
314 | .hljs-bullet,
315 | .hljs-link,
316 | .hljs-meta,
317 | .hljs-selector-id,
318 | .hljs-title {
319 | color: #4078f2;
320 | }
321 |
322 | .hljs-built_in,
323 | .hljs-title.class_,
324 | .hljs-class .hljs-title {
325 | color: #c18401;
326 | }
327 |
328 | .hljs-emphasis {
329 | font-style: italic;
330 | }
331 |
332 | .hljs-strong {
333 | font-weight: bold;
334 | }
335 |
336 | .hljs-link {
337 | text-decoration: underline;
338 | }
339 |
--------------------------------------------------------------------------------
/src/styles/poster.css:
--------------------------------------------------------------------------------
1 | .container-layout{
2 | background-color: #39236b;
3 | padding: 32px;
4 | }
5 |
6 | /* 通用设置 */
7 | .article {
8 | gap: 16px;
9 | padding: 32px;
10 | background: linear-gradient(
11 | to bottom right,
12 | #6b46c1,
13 | rgba(79, 70, 229, 0.9),
14 | #5a34b0
15 | );
16 | line-height: 1.5;
17 | color: #fff;
18 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
19 | Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
20 | user-select: none;
21 | }
22 |
23 | h1,
24 | h2,
25 | h3,
26 | h4,
27 | h5,
28 | h6 {
29 | text-align: center;
30 | }
31 |
32 | h1 {
33 | font-size: 36px;
34 | font-weight: bold;
35 | text-align: center;
36 | border-bottom: 2px solid #fff;
37 | padding-bottom: 8px;
38 | margin-bottom: 16px;
39 | }
40 |
41 | h2 {
42 | font-size: 28px;
43 | margin-top: 24px;
44 | padding-bottom: 6px;
45 | border-bottom: 1px solid #fff;
46 | margin-bottom: 12px;
47 | }
48 |
49 | ul,
50 | ol {
51 | list-style-type: auto;
52 | padding-left: 2em;
53 | }
54 |
55 | p {
56 | font-size: 16px;
57 | text-align: center;
58 | }
59 |
60 | a {
61 | color: #fff;
62 | text-decoration: underline;
63 | }
64 |
65 | blockquote {
66 | font-size: 18px;
67 | font-style: italic;
68 | padding: 12px;
69 | background-color: #f9f9f9;
70 | border-left: 4px solid #333333;
71 | color: #666666;
72 | margin: 12px 0;
73 | }
74 |
75 | img {
76 | width: 100%;
77 | height: auto;
78 | border-radius: 12px;
79 | box-shadow: 0 8px 12px rgba(0, 0, 0, 0.2);
80 | margin: 16px 0;
81 | }
82 |
83 | ol {
84 | list-style-type: decimal;
85 | padding-left: 24px;
86 | }
87 |
88 | li {
89 | margin: 8px 0;
90 | }
91 |
92 | strong {
93 | font-weight: bold;
94 | }
95 |
96 | code {
97 | background-color: #fff;
98 | padding: 2px 4px;
99 | border-radius: 4px;
100 | font-size: 14px;
101 | color: #ef4444;
102 | }
103 |
104 | table {
105 | border-spacing: 0;
106 | border-collapse: collapse;
107 | width: max-content;
108 | max-width: 100%;
109 | overflow: auto;
110 | }
111 |
112 | th,
113 | td {
114 | padding: 6px 13px;
115 | border: 1px solid #fff;
116 | }
117 |
118 | /* Highlight.js */
119 | pre code.hljs {
120 | display: block;
121 | overflow-x: auto;
122 | padding: 1em;
123 | }
124 |
125 | code.hljs {
126 | padding: 3px 5px;
127 | }
128 |
129 | .hljs {
130 | color: #abb2bf;
131 | background: #282c34;
132 | }
133 |
134 | .hljs-comment,
135 | .hljs-quote {
136 | color: #5c6370;
137 | font-style: italic;
138 | }
139 |
140 | .hljs-doctag,
141 | .hljs-keyword,
142 | .hljs-formula {
143 | color: #c678dd;
144 | }
145 |
146 | .hljs-section,
147 | .hljs-name,
148 | .hljs-selector-tag,
149 | .hljs-deletion,
150 | .hljs-subst {
151 | color: #e06c75;
152 | }
153 |
154 | .hljs-literal {
155 | color: #56b6c2;
156 | }
157 |
158 | .hljs-string,
159 | .hljs-regexp,
160 | .hljs-addition,
161 | .hljs-attribute,
162 | .hljs-meta .hljs-string {
163 | color: #98c379;
164 | }
165 |
166 | .hljs-attr,
167 | .hljs-variable,
168 | .hljs-template-variable,
169 | .hljs-type,
170 | .hljs-selector-class,
171 | .hljs-selector-attr,
172 | .hljs-selector-pseudo,
173 | .hljs-number {
174 | color: #d19a66;
175 | }
176 |
177 | .hljs-symbol,
178 | .hljs-bullet,
179 | .hljs-link,
180 | .hljs-meta,
181 | .hljs-selector-id,
182 | .hljs-title {
183 | color: #61aeee;
184 | }
185 |
186 | .hljs-built_in,
187 | .hljs-title.class_,
188 | .hljs-class .hljs-title {
189 | color: #e6c07b;
190 | }
191 |
192 | .hljs-emphasis {
193 | font-style: italic;
194 | }
195 |
196 | .hljs-strong {
197 | font-weight: bold;
198 | }
199 |
200 | .hljs-link {
201 | text-decoration: underline;
202 | }
203 |
--------------------------------------------------------------------------------
/src/styles/slim.css:
--------------------------------------------------------------------------------
1 | .container-layout{
2 | background-color: #000000;
3 | padding: 24px;
4 | }
5 |
6 | /* 文章内容设置 */
7 | .article {
8 | padding: 2em 60px;
9 | font-size: 16px;
10 | border-radius: 8px;
11 | color: #333;
12 | background: #fff;
13 | font-family:
14 | PingFang SC,
15 | Verdana,
16 | Helvetica Neue,
17 | Microsoft Yahei,
18 | Hiragino Sans GB,
19 | Microsoft Sans Serif,
20 | WenQuanYi Micro Hei,
21 | sans-serif;
22 | -webkit-text-size-adjust: 100%;
23 | -ms-text-size-adjust: 100%;
24 | text-rendering: optimizelegibility;
25 | -webkit-font-smoothing: antialiased;
26 | }
27 |
28 | /* 标题样式 */
29 | h1,
30 | h2,
31 | h3,
32 | h4,
33 | h5,
34 | h6 {
35 | font-weight: 300;
36 | color: #333;
37 | line-height: 1.35;
38 | margin-top: 1.2rem;
39 | margin-bottom: 1rem;
40 | }
41 |
42 | h1 {
43 | font-size: 2.4em;
44 | padding-bottom: 1em;
45 | border-bottom: 3px double #eee;
46 | }
47 | h2 {
48 | font-size: 1.8em;
49 | }
50 | h3 {
51 | font-size: 1.6em;
52 | }
53 | h4 {
54 | font-size: 1.4em;
55 | }
56 | h5,
57 | h6 {
58 | font-size: 1.2em;
59 | }
60 |
61 | /* 段落样式 */
62 | p {
63 | font-weight: 300;
64 | margin-bottom: 1.2em;
65 | }
66 |
67 | strong {
68 | font-weight: bolder;
69 | color: #000;
70 | }
71 |
72 | em {
73 | font-style: italic;
74 | }
75 |
76 | /* 列表样式 */
77 | ul,
78 | ol {
79 | font-weight: 300;
80 | margin-left: 1.3em;
81 | }
82 |
83 | ul {
84 | font-weight: 300;
85 | list-style: circle;
86 | }
87 |
88 | ol {
89 | font-weight: 300;
90 | list-style: decimal;
91 | }
92 |
93 | li ul {
94 | font-weight: 300;
95 | list-style: circle;
96 | }
97 |
98 | /* 引用块样式 */
99 | blockquote {
100 | color: #999;
101 | border-left: 1px solid #1abc9c;
102 | padding-left: 1em;
103 | margin: 1em 3em 1em 2em;
104 | }
105 |
106 | /* 链接样式 */
107 | a {
108 | color: #1abc9c;
109 | text-decoration: none;
110 | border-bottom: 1px solid #1abc9c;
111 | }
112 |
113 | a:hover {
114 | text-decoration: underline;
115 | color: #555;
116 | border-bottom-color: #555;
117 | }
118 |
119 | /* 水平分割线 */
120 | hr {
121 | border: none;
122 | border-bottom: 1px solid #cfcfcf;
123 | margin-bottom: 0.8em;
124 | height: 10px;
125 | }
126 |
127 | /* 代码块和内联代码 */
128 | pre,
129 | code {
130 | font-family: "Courier New", monospace;
131 | background: #ededeb;
132 | color: #eb5757;
133 | border-radius: 4px;
134 | padding: 0.2em 0.4em;
135 | }
136 |
137 | pre {
138 | margin: 1rem 0;
139 | border: 1px solid #ddd;
140 | padding: 1em 0.5em;
141 | overflow-x: auto;
142 | }
143 |
144 | code {
145 | /*font-size: 85%;*/
146 | }
147 |
148 | /* 表格样式 */
149 | table {
150 | font-weight: 300;
151 | border-collapse: collapse;
152 | width: 100%;
153 | }
154 |
155 | thead th {
156 | background: #f1f1f1;
157 | border: 1px solid #ddd;
158 | }
159 |
160 | tbody td {
161 | border: 1px solid #ddd;
162 | padding: 0.5em 1em;
163 | color: #666;
164 | }
165 |
166 | /* 图片样式 */
167 | img {
168 | max-width: 100%;
169 | }
170 |
171 | /* Highlight.js */
172 | pre code.hljs {
173 | display: block;
174 | overflow-x: auto;
175 | padding: 1em;
176 | }
177 |
178 | code.hljs {
179 | padding: 3px 5px;
180 | }
181 |
182 | .hljs {
183 | color: #383a42;
184 | /*background: #fafafa;*/
185 | }
186 |
187 | .hljs-comment,
188 | .hljs-quote {
189 | color: #a0a1a7;
190 | font-style: italic;
191 | }
192 |
193 | .hljs-doctag,
194 | .hljs-keyword,
195 | .hljs-formula {
196 | color: #a626a4;
197 | }
198 |
199 | .hljs-section,
200 | .hljs-name,
201 | .hljs-selector-tag,
202 | .hljs-deletion,
203 | .hljs-subst {
204 | color: #e45649;
205 | }
206 |
207 | .hljs-literal {
208 | color: #0184bb;
209 | }
210 |
211 | .hljs-string,
212 | .hljs-regexp,
213 | .hljs-addition,
214 | .hljs-attribute,
215 | .hljs-meta .hljs-string {
216 | color: #50a14f;
217 | }
218 |
219 | .hljs-attr,
220 | .hljs-variable,
221 | .hljs-template-variable,
222 | .hljs-type,
223 | .hljs-selector-class,
224 | .hljs-selector-attr,
225 | .hljs-selector-pseudo,
226 | .hljs-number {
227 | color: #986801;
228 | }
229 |
230 | .hljs-symbol,
231 | .hljs-bullet,
232 | .hljs-link,
233 | .hljs-meta,
234 | .hljs-selector-id,
235 | .hljs-title {
236 | color: #4078f2;
237 | }
238 |
239 | .hljs-built_in,
240 | .hljs-title.class_,
241 | .hljs-class .hljs-title {
242 | color: #c18401;
243 | }
244 |
245 | .hljs-emphasis {
246 | font-style: italic;
247 | }
248 |
249 | .hljs-strong {
250 | font-weight: bold;
251 | }
252 |
253 | .hljs-link {
254 | text-decoration: underline;
255 | }
256 |
--------------------------------------------------------------------------------
/src/styles/thoughtworks.css:
--------------------------------------------------------------------------------
1 | .container-layout{
2 | background-color: #163c4d;
3 | padding: 28px;
4 | }
5 |
6 | /* light */
7 | .article {
8 | padding: 32px;
9 | background: #ffffff;
10 | color-scheme: light;
11 | color: #1f2328;
12 | font-family: Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
13 | "Segoe UI Emoji";
14 | font-size: 16px;
15 | line-height: 1.5;
16 | word-wrap: break-word;
17 | }
18 |
19 | h1,
20 | h2,
21 | h3,
22 | h4,
23 | h5,
24 | h6 {
25 | margin-top: 1.5rem;
26 | margin-bottom: 1rem;
27 | font-weight: 600;
28 | line-height: 1.25;
29 | color: #e16a7c;
30 | }
31 |
32 | h1 {
33 | padding-bottom: 0.3em;
34 | font-size: 2em;
35 | border-bottom: 1px solid #d1d9e0b3;
36 | }
37 |
38 | h2 {
39 | font-size: 1.5em;
40 | border-bottom: 1px solid #d1d9e0b3;
41 | }
42 |
43 | h3 {
44 | font-size: 1.25em;
45 | }
46 |
47 | h4 {
48 | font-size: 1em;
49 | }
50 |
51 | h5 {
52 | font-size: 0.875em;
53 | }
54 |
55 | h6 {
56 | font-size: 0.85em;
57 | }
58 |
59 | p,
60 | blockquote,
61 | ul,
62 | ol,
63 | dl,
64 | table,
65 | pre,
66 | details {
67 | margin-top: 0;
68 | margin-bottom: 1rem;
69 | }
70 |
71 | a {
72 | color: #0969da;
73 | text-decoration: none;
74 | }
75 |
76 | a:hover {
77 | text-decoration: underline;
78 | }
79 |
80 | img {
81 | border-style: none;
82 | width: 100%;
83 | }
84 |
85 | table {
86 | border-spacing: 0;
87 | border-collapse: collapse;
88 | width: 100%;
89 | overflow: auto;
90 | }
91 |
92 | th,
93 | td {
94 | padding: 6px 13px;
95 | border: 1px solid #d1d9e0;
96 | }
97 |
98 | blockquote {
99 | margin: 0;
100 | padding: 0 1em;
101 | color: #59636e;
102 | border-left: 0.25em solid #d1d9e0;
103 | }
104 |
105 | ul {
106 | list-style-type: disc;
107 | padding-left: 2em;
108 | }
109 |
110 | code {
111 | margin: 0 2px;
112 | padding: 2px 3px;
113 | background-color: #eef1f3;
114 | border: 1px solid #ddd;
115 | /*border-radius: 4px;*/
116 | }
117 |
118 | pre {
119 | background-color: #eef1f3;
120 | border: 1px solid #ddd;
121 | font-size: 13px;
122 | line-height: 19px;
123 | overflow: auto;
124 | padding: 6px 10px;
125 | /*border-radius: 6px;*/
126 | }
127 |
128 | pre code {
129 | margin: 0;
130 | padding: 0;
131 | background-color: #eef1f3;
132 | border: none;
133 | word-wrap: normal;
134 | max-width: initial;
135 | display: inline;
136 | overflow: initial;
137 | line-height: inherit;
138 | }
139 |
140 | ol {
141 | padding-left: 2em;
142 | list-style-type: auto;
143 | }
144 |
145 | hr {
146 | border-bottom: 1px solid #d1d9e0b3;
147 | margin: 1.5rem 0;
148 | }
149 |
150 | details {
151 | display: block;
152 | }
153 |
154 | summary {
155 | display: list-item;
156 | }
157 |
158 | [hidden] {
159 | display: none !important;
160 | }
161 |
162 | /* Highlight.js */
163 | pre code.hljs {
164 | display: block;
165 | overflow-x: auto;
166 | padding: 1em;
167 | }
168 |
169 | code.hljs {
170 | padding: 3px 5px;
171 | }
172 |
173 | .hljs {
174 | color: #000000;
175 | background-color: #eef1f3;
176 | }
177 |
178 | .hljs-comment,
179 | .hljs-quote {
180 | color: #6a737d;
181 | font-style: italic;
182 | }
183 |
184 | .hljs-doctag,
185 | .hljs-keyword,
186 | .hljs-formula {
187 | color: #f2617a;
188 | }
189 |
190 | .hljs-section,
191 | .hljs-name,
192 | .hljs-selector-tag,
193 | .hljs-deletion,
194 | .hljs-subst {
195 | color: #634f7d;
196 | }
197 |
198 | .hljs-literal {
199 | color: #6b9e78;
200 | }
201 |
202 | .hljs-string,
203 | .hljs-regexp,
204 | .hljs-addition,
205 | .hljs-attribute,
206 | .hljs-meta .hljs-string {
207 | color: #47a1ad;
208 | }
209 |
210 | .hljs-attr,
211 | .hljs-variable,
212 | .hljs-template-variable,
213 | .hljs-type,
214 | .hljs-selector-class,
215 | .hljs-selector-attr,
216 | .hljs-selector-pseudo,
217 | .hljs-number {
218 | color: #cc850a;
219 | }
220 |
221 | .hljs-symbol,
222 | .hljs-bullet,
223 | .hljs-link,
224 | .hljs-meta,
225 | .hljs-selector-id,
226 | .hljs-title {
227 | color: #6b9e78;
228 | }
229 |
230 | .hljs-built_in,
231 | .hljs-title.class_,
232 | .hljs-class .hljs-title {
233 | color: #47a1ad;
234 | }
235 |
236 | .hljs-emphasis {
237 | font-style: italic;
238 | }
239 |
240 | .hljs-strong {
241 | font-weight: bold;
242 | }
243 |
244 | .hljs-link {
245 | text-decoration: underline;
246 | }
247 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import { SVGProps } from "react";
2 |
3 | export type IconSvgProps = SVGProps & {
4 | size?: number;
5 | };
6 |
--------------------------------------------------------------------------------
/src/utils/styletransfer.ts:
--------------------------------------------------------------------------------
1 | export const objectToStyleString = (styleObject: any) =>
2 | Object.entries(styleObject)
3 | .map(([key, value]) => `${key}: ${value};`)
4 | .join(" ");
5 |
6 | export const cssToRecord = (cssString: string): Record => {
7 | const styles: Record = {};
8 | const cssDeclarations = cssString.split(";").filter(Boolean);
9 |
10 | cssDeclarations.forEach((declaration) => {
11 | const [property, value] = declaration.split(":").map((part) => part.trim());
12 |
13 | if (property && value) {
14 | styles[property] = value;
15 | }
16 | });
17 |
18 | return styles;
19 | };
20 |
21 | export const extractContainerLayoutContent = (cssString: string): string => {
22 | const match = cssString.match(/\.container-layout\s*\{([^}]+)}/);
23 |
24 | return match ? match[1].trim() : "";
25 | };
26 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import {nextui} from '@nextui-org/theme'
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | content: [
6 | "./index.html",
7 | './src/layouts/**/*.{js,ts,jsx,tsx,mdx}',
8 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
9 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
10 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}',
11 | ],
12 | theme: {
13 | extend: {},
14 | screens: {
15 | 'sm': '640px',
16 | // => @media (min-width: 640px) { ... }
17 |
18 | 'md': '768px',
19 | // => @media (min-width: 768px) { ... }
20 |
21 | 'lg': '1024px',
22 | // => @media (min-width: 1024px) { ... }
23 |
24 | 'xl': '1280px',
25 | // => @media (min-width: 1280px) { ... }
26 |
27 | '2xl': '1536px',
28 | // => @media (min-width: 1536px) { ... }
29 | }
30 | },
31 | darkMode: "class",
32 | plugins: [nextui()],
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 | "paths": {
9 | "@/*": ["./src/*"]
10 | },
11 |
12 | /* Bundler mode */
13 | "moduleResolution": "bundler",
14 | "allowImportingTsExtensions": true,
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": true,
18 | "jsx": "react-jsx",
19 |
20 | /* Linting */
21 | "strict": true,
22 | "noUnusedLocals": false,
23 | "noUnusedParameters": false,
24 | "noFallthroughCasesInSwitch": true
25 | },
26 | "include": ["src"],
27 | "references": [{ "path": "./tsconfig.node.json" }]
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | { "source": "/(.*)", "destination": "/" }
4 | ]
5 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 | import tsconfigPaths from 'vite-tsconfig-paths'
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react(), tsconfigPaths()],
8 | })
9 |
--------------------------------------------------------------------------------