feat: Implement a rich text editor using Lexical
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 14s

- Added RichTextEditor component with basic formatting features.
- Integrated toolbar with undo/redo, text formatting, and color pickers.
- Created EditorTheme for styling the editor components.
- Added styles for editor and toolbar.
- Introduced DropdownColorPicker for color selection.
- Updated package.json to include Lexical dependencies.
- Created EditorDemo page to showcase the rich text editor.
- Added README documentation for the editor's features and usage.
This commit is contained in:
2025-10-22 10:20:47 +08:00
parent 6dbb6ff7fb
commit f29f53dec6
13 changed files with 1306 additions and 0 deletions

440
package-lock.json generated
View File

@@ -8,8 +8,15 @@
"name": "www.cialloo.com", "name": "www.cialloo.com",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@lexical/code": "^0.37.0",
"@lexical/list": "^0.37.0",
"@lexical/react": "^0.37.0",
"@lexical/rich-text": "^0.37.0",
"@lexical/selection": "^0.37.0",
"@lexical/utils": "^0.37.0",
"i18next": "^25.5.3", "i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"lexical": "^0.37.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-i18next": "^16.0.0", "react-i18next": "^16.0.0",
@@ -60,6 +67,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3", "@babel/generator": "^7.28.3",
@@ -508,6 +516,59 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/react": {
"version": "0.27.16",
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
"integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
"license": "MIT",
"dependencies": {
"@floating-ui/react-dom": "^2.1.6",
"@floating-ui/utils": "^0.2.10",
"tabbable": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.0.0",
"react-dom": ">=17.0.0"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
"integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
"license": "MIT",
"dependencies": {
"@floating-ui/dom": "^1.7.4"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -610,6 +671,281 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@lexical/clipboard": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.37.0.tgz",
"integrity": "sha512-hRwASFX/ilaI5r8YOcZuQgONFshRgCPfdxfofNL7uruSFYAO6LkUhsjzZwUgf0DbmCJmbBADFw15FSthgCUhGA==",
"license": "MIT",
"dependencies": {
"@lexical/html": "0.37.0",
"@lexical/list": "0.37.0",
"@lexical/selection": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/code": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.37.0.tgz",
"integrity": "sha512-ZXA4j/S8yLrxjrTnEp39VeDMp4Rd8bLYUlT4Buy1MQlS1WafxOiMhNQJG7k0BP/pO96YPkAebpA81ATKJL0IgA==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.37.0",
"lexical": "0.37.0",
"prismjs": "^1.30.0"
}
},
"node_modules/@lexical/devtools-core": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.37.0.tgz",
"integrity": "sha512-iOR+aKLJR92nKYcEOW3K/bgjTN7dJIRC/OM4OvzigU0Xygxped0lXV6UmkYBp0eoqOOwckB8+rZWZszj9lKA8Q==",
"license": "MIT",
"dependencies": {
"@lexical/html": "0.37.0",
"@lexical/link": "0.37.0",
"@lexical/mark": "0.37.0",
"@lexical/table": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
},
"peerDependencies": {
"react": ">=17.x",
"react-dom": ">=17.x"
}
},
"node_modules/@lexical/dragon": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.37.0.tgz",
"integrity": "sha512-iC4OKivEPtt7cGVSwZylLfz5T7Oqr9q9EOosS6E/byMyoqwkYWGjXn/qFiwIv1Xo3+G19vhfChi/+ZcYLXpHPw==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/extension": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/extension/-/extension-0.37.0.tgz",
"integrity": "sha512-Z58f2tIdz9bn8gltUu5cVg37qROGha38dUZv20gI2GeNugXAkoPzJYEcxlI1D/26tkevJ/7VaFUr9PTk+iKmaA==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.37.0",
"@preact/signals-core": "^1.11.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/hashtag": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.37.0.tgz",
"integrity": "sha512-DHoDpiokJRBu+GnC0qQH529hamn9YNjL7vzzkTAeEMKsT9+4O848Cq6F2GJn8QjQToySlkVZW3mkh76uf/XLfg==",
"license": "MIT",
"dependencies": {
"@lexical/text": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/history": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.37.0.tgz",
"integrity": "sha512-QKkrWCw4bsn/ZeLIkMVIpbtWKPhMYeax1nE7erHqTEwE52QR6pmZsZBgGSQDO73Ae29vahOmqlN7+ZJFvTKMVA==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/html": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.37.0.tgz",
"integrity": "sha512-oTsBc45eL8/lmF7fqGR+UCjrJYP04gumzf5nk4TczrxWL2pM4GIMLLKG1mpQI2H1MDiRLzq3T/xdI7Gh74z7Zw==",
"license": "MIT",
"dependencies": {
"@lexical/selection": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/link": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.37.0.tgz",
"integrity": "sha512-gglkjE99tKYnGAxQbrUq9TcaVKBQhidXhgPPbVw3x1Fba9biMafkbSJhE/7/pzQTPoQBAIl0w7DOUWmBOv+JbQ==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/list": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.37.0.tgz",
"integrity": "sha512-AOC6yAA3mfNvJKbwo+kvAbPJI+13yF2ISA65vbA578CugvJ08zIVgM+pSzxquGhD0ioJY3cXVW7+gdkCP1qu5g==",
"license": "MIT",
"dependencies": {
"@lexical/extension": "0.37.0",
"@lexical/selection": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/mark": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.37.0.tgz",
"integrity": "sha512-ncjaL6kNHVioekx6vI5oJRDExFDJLbnXT7AdMnUv2LE3sxn/ea+JsZO/MDI4Ygmxq+lGtgZvbBDER8Yh/+5jdA==",
"license": "MIT",
"dependencies": {
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/markdown": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.37.0.tgz",
"integrity": "sha512-pcLMpxWkSxU2QaN2GLA3hNy4lV2A8sJOvb5YEkcsFEcVvFFbAz7lxgyKVYtDboRCW1eZFks1UGGuJEogLeEFdg==",
"license": "MIT",
"dependencies": {
"@lexical/code": "0.37.0",
"@lexical/link": "0.37.0",
"@lexical/list": "0.37.0",
"@lexical/rich-text": "0.37.0",
"@lexical/text": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/offset": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.37.0.tgz",
"integrity": "sha512-q9Ckftfhb+VepJQeaClOYzpuV+WqWWGkSUuoexV4zjAm/HVjOie9lrNF4NkhQe5crnIBXI5zOofhuEfiCQWsbQ==",
"license": "MIT",
"dependencies": {
"lexical": "0.37.0"
}
},
"node_modules/@lexical/overflow": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.37.0.tgz",
"integrity": "sha512-GC5qoQJQzaofCq1eMMvv9wIGMAbpFbFwny5BKA1C2Nmn+/2bi6v+7qlHwiBlbSVqfLVPvT4nYdrmNdnKoE0jZg==",
"license": "MIT",
"dependencies": {
"lexical": "0.37.0"
}
},
"node_modules/@lexical/plain-text": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.37.0.tgz",
"integrity": "sha512-4IxG9Tr0NnQ+clN1eoXfe2W8JTgw0xtPMzqvHP2IaO7RILUE6H8VFSOdhAOI0dHrjlXRMUS3I2Fhqr2ZRq8kdQ==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.37.0",
"@lexical/dragon": "0.37.0",
"@lexical/selection": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/react": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.37.0.tgz",
"integrity": "sha512-PGIGmI5xDSAguqpAStd+89TfWsi6hs/R4a3hQAyNwXXDEt4anUFJic4Qet4YftybLGajP3vMvouLE5hrkmBihg==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.27.16",
"@lexical/devtools-core": "0.37.0",
"@lexical/dragon": "0.37.0",
"@lexical/extension": "0.37.0",
"@lexical/hashtag": "0.37.0",
"@lexical/history": "0.37.0",
"@lexical/link": "0.37.0",
"@lexical/list": "0.37.0",
"@lexical/mark": "0.37.0",
"@lexical/markdown": "0.37.0",
"@lexical/overflow": "0.37.0",
"@lexical/plain-text": "0.37.0",
"@lexical/rich-text": "0.37.0",
"@lexical/table": "0.37.0",
"@lexical/text": "0.37.0",
"@lexical/utils": "0.37.0",
"@lexical/yjs": "0.37.0",
"lexical": "0.37.0",
"react-error-boundary": "^6.0.0"
},
"peerDependencies": {
"react": ">=17.x",
"react-dom": ">=17.x"
}
},
"node_modules/@lexical/rich-text": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.37.0.tgz",
"integrity": "sha512-A9i5Es/RrZv71tB6dDSyd4TYdbkn/+oUrUdTwnWa+B8EZW26q0h+wgxCGwPtTU7ho4JNP9HOot+EIhe2DbyaYg==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.37.0",
"@lexical/dragon": "0.37.0",
"@lexical/selection": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/selection": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.37.0.tgz",
"integrity": "sha512-Lix1s2r71jHfsTEs4q/YqK2s3uXKOnyA3fd1VDMWysO+bZzRwEO5+qyDvENZ0WrXSDCnlibNFV1HttWX9/zqyw==",
"license": "MIT",
"dependencies": {
"lexical": "0.37.0"
}
},
"node_modules/@lexical/table": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.37.0.tgz",
"integrity": "sha512-g7S8ml8kIujEDLWlzYKETgPCQ2U9oeWqdytRuHjHGi/rjAAGHSej5IRqTPIMxNP3VVQHnBoQ+Y9hBtjiuddhgQ==",
"license": "MIT",
"dependencies": {
"@lexical/clipboard": "0.37.0",
"@lexical/extension": "0.37.0",
"@lexical/utils": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/text": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.37.0.tgz",
"integrity": "sha512-qByNjHp88mlUWHxfYutH4vhSs3nzfCGHKsf/MqUMOC8K7Kmp0V1NK6cOW1sgsHpzkovfpgcNOGDzZxTNCFgHtg==",
"license": "MIT",
"dependencies": {
"lexical": "0.37.0"
}
},
"node_modules/@lexical/utils": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.37.0.tgz",
"integrity": "sha512-CFp4diY/kR5RqhzQSl/7SwsMod1sgLpI1FBifcOuJ6L/S6YywGpEB4B7aV5zqW21A/jU2T+2NZtxSUn6S+9gMg==",
"license": "MIT",
"dependencies": {
"@lexical/list": "0.37.0",
"@lexical/selection": "0.37.0",
"@lexical/table": "0.37.0",
"lexical": "0.37.0"
}
},
"node_modules/@lexical/yjs": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.37.0.tgz",
"integrity": "sha512-7UjHvXDd+Is/qTdNkpQ/K04Zduh2uh7UTlSWbMiqwbQh8VRJNXXgcH8iK0TXLwc7M3VgVk+FlnNApNvcReKB6g==",
"license": "MIT",
"dependencies": {
"@lexical/offset": "0.37.0",
"@lexical/selection": "0.37.0",
"lexical": "0.37.0"
},
"peerDependencies": {
"yjs": ">=13.5.22"
}
},
"node_modules/@napi-rs/wasm-runtime": { "node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.5.tgz", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.5.tgz",
@@ -681,6 +1017,16 @@
"url": "https://github.com/sponsors/Boshen" "url": "https://github.com/sponsors/Boshen"
} }
}, },
"node_modules/@preact/signals-core": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.12.1.tgz",
"integrity": "sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-beta.39", "version": "1.0.0-beta.39",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.39.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.39.tgz",
@@ -1002,6 +1348,7 @@
"integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==", "integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -1062,6 +1409,7 @@
"integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.45.0", "@typescript-eslint/scope-manager": "8.45.0",
"@typescript-eslint/types": "8.45.0", "@typescript-eslint/types": "8.45.0",
@@ -1314,6 +1662,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -1442,6 +1791,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.3", "baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741", "caniuse-lite": "^1.0.30001741",
@@ -1640,6 +1990,7 @@
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -2033,6 +2384,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.6" "@babel/runtime": "^7.27.6"
}, },
@@ -2131,6 +2483,16 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/isomorphic.js": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -2222,6 +2584,33 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/lexical": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/lexical/-/lexical-0.37.0.tgz",
"integrity": "sha512-r5VJR2TioQPAsZATfktnJFrGIiy6gjQN8b/+0a2u1d7/QTH7lhbB7byhGSvcq1iaa1TV/xcf/pFV55a5V5hTDQ==",
"license": "MIT"
},
"node_modules/lib0": {
"version": "0.2.114",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz",
"integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==",
"license": "MIT",
"dependencies": {
"isomorphic.js": "^0.2.4"
},
"bin": {
"0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
"0gentesthtml": "bin/gentesthtml.js",
"0serve": "bin/0serve.js"
},
"engines": {
"node": ">=16"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/lightningcss": { "node_modules/lightningcss": {
"version": "1.30.2", "version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
@@ -2735,6 +3124,15 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -2771,6 +3169,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -2780,6 +3179,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.26.0" "scheduler": "^0.26.0"
}, },
@@ -2787,6 +3187,18 @@
"react": "^19.1.1" "react": "^19.1.1"
} }
}, },
"node_modules/react-error-boundary": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-6.0.0.tgz",
"integrity": "sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-i18next": { "node_modules/react-i18next": {
"version": "16.0.0", "version": "16.0.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.0.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.0.tgz",
@@ -3028,6 +3440,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/tabbable": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -3069,6 +3487,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3129,6 +3548,7 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -3209,6 +3629,7 @@
"integrity": "sha512-JREtUS+Lpa3s5Ha3ajf2F4LMS4BFxlVjpGz0k0ZR8rV3ZO3tzk5hukqyi9yRBcrvnTUg/BEForyCDahALFYAZA==", "integrity": "sha512-JREtUS+Lpa3s5Ha3ajf2F4LMS4BFxlVjpGz0k0ZR8rV3ZO3tzk5hukqyi9yRBcrvnTUg/BEForyCDahALFYAZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@oxc-project/runtime": "0.90.0", "@oxc-project/runtime": "0.90.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -3303,6 +3724,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3352,6 +3774,24 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/yjs": {
"version": "13.6.27",
"resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
"integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==",
"license": "MIT",
"peer": true,
"dependencies": {
"lib0": "^0.2.99"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -10,8 +10,15 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@lexical/code": "^0.37.0",
"@lexical/list": "^0.37.0",
"@lexical/react": "^0.37.0",
"@lexical/rich-text": "^0.37.0",
"@lexical/selection": "^0.37.0",
"@lexical/utils": "^0.37.0",
"i18next": "^25.5.3", "i18next": "^25.5.3",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"lexical": "^0.37.0",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-i18next": "^16.0.0", "react-i18next": "^16.0.0",

95
src/editor/README.md Normal file
View File

@@ -0,0 +1,95 @@
# Rich Text Editor
A basic rich text editor built with Lexical, ported from the lexical-playground project.
## Features
### Toolbar Controls
1. **Undo/Redo** - Navigate through editing history
2. **Text Size** - Select from multiple font sizes (10px - 36px)
3. **Bold** - Make text bold
4. **Italic** - Italicize text
5. **Underline** - Underline text
6. **Strikethrough** - Strike through text
7. **Code** - Format text as inline code
8. **Text Color** - Choose from 30 predefined colors
9. **Background Color** - Choose background color for text
### Additional Features
- Quote blocks
- Code blocks (without syntax highlighting)
- Lists (bullet and numbered)
- Headings (H1, H2, H3)
## Usage
### Basic Usage
```tsx
import RichTextEditor from './editor/RichTextEditor';
function MyComponent() {
return <RichTextEditor />;
}
```
### Accessing the Demo
Navigate to `/editor` to see the editor in action.
## Files Structure
```
src/editor/
├── RichTextEditor.tsx # Main editor component
├── index.ts # Export file
├── plugins/
│ └── ToolbarPlugin.tsx # Toolbar with formatting controls
├── ui/
│ ├── DropdownColorPicker.tsx # Color picker component
│ └── DropdownColorPicker.css # Color picker styles
├── themes/
│ └── EditorTheme.ts # Editor theme configuration
└── styles/
├── editor.css # Editor styles
└── toolbar.css # Toolbar styles
```
## Dependencies
- `lexical` - Core editor framework
- `@lexical/react` - React bindings
- `@lexical/code` - Code block support
- `@lexical/list` - List support
- `@lexical/rich-text` - Rich text features (headings, quotes)
- `@lexical/selection` - Selection utilities
- `@lexical/utils` - Utility functions
## Customization
### Theme
Edit `src/editor/themes/EditorTheme.ts` to customize the editor's appearance.
### Toolbar
Modify `src/editor/plugins/ToolbarPlugin.tsx` to add or remove toolbar buttons.
### Styles
- `src/editor/styles/editor.css` - Editor content styles
- `src/editor/styles/toolbar.css` - Toolbar styles
## Future Enhancements
Possible additions (not currently implemented):
- Syntax highlighting for code blocks
- Links
- Images
- Tables
- Alignment controls
- More list types (checklists)
- Markdown shortcuts

View File

@@ -0,0 +1,51 @@
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { CodeNode } from '@lexical/code';
import { ListItemNode, ListNode } from '@lexical/list';
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
import ToolbarPlugin from './plugins/ToolbarPlugin';
import editorTheme from './themes/EditorTheme';
import './styles/editor.css';
const editorConfig = {
namespace: 'CiallooEditor',
theme: editorTheme,
onError(error: Error) {
console.error(error);
},
nodes: [
HeadingNode,
QuoteNode,
CodeNode,
ListNode,
ListItemNode,
],
};
export default function RichTextEditor() {
return (
<LexicalComposer initialConfig={editorConfig}>
<div className="editor-container">
<ToolbarPlugin />
<div className="editor-inner">
<RichTextPlugin
contentEditable={
<ContentEditable className="editor-input" />
}
placeholder={
<div className="editor-placeholder">Enter some text...</div>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<ListPlugin />
</div>
</div>
</LexicalComposer>
);
}

1
src/editor/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default } from './RichTextEditor';

View File

@@ -0,0 +1,230 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
CAN_REDO_COMMAND,
CAN_UNDO_COMMAND,
REDO_COMMAND,
UNDO_COMMAND,
SELECTION_CHANGE_COMMAND,
FORMAT_TEXT_COMMAND,
COMMAND_PRIORITY_CRITICAL,
$getSelection,
$isRangeSelection,
} from 'lexical';
import type { TextFormatType } from 'lexical';
import { $getSelectionStyleValueForProperty, $patchStyleText } from '@lexical/selection';
import DropdownColorPicker from '../ui/DropdownColorPicker';
import '../styles/toolbar.css';
export default function ToolbarPlugin() {
const [editor] = useLexicalComposerContext();
const toolbarRef = useRef(null);
const [canUndo, setCanUndo] = useState(false);
const [canRedo, setCanRedo] = useState(false);
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const [isUnderline, setIsUnderline] = useState(false);
const [isStrikethrough, setIsStrikethrough] = useState(false);
const [isCode, setIsCode] = useState(false);
const [fontColor, setFontColor] = useState('#000');
const [bgColor, setBgColor] = useState('#fff');
const [fontSize, setFontSize] = useState('15px');
const updateToolbar = useCallback(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
// Update text format
setIsBold(selection.hasFormat('bold'));
setIsItalic(selection.hasFormat('italic'));
setIsUnderline(selection.hasFormat('underline'));
setIsStrikethrough(selection.hasFormat('strikethrough'));
setIsCode(selection.hasFormat('code'));
// Update color
setFontColor(
$getSelectionStyleValueForProperty(selection, 'color', '#000')
);
setBgColor(
$getSelectionStyleValueForProperty(selection, 'background-color', '#fff')
);
setFontSize(
$getSelectionStyleValueForProperty(selection, 'font-size', '15px')
);
}
}, [editor]);
useEffect(() => {
return editor.registerCommand(
SELECTION_CHANGE_COMMAND,
() => {
updateToolbar();
return false;
},
COMMAND_PRIORITY_CRITICAL
);
}, [editor, updateToolbar]);
useEffect(() => {
return editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
updateToolbar();
});
});
}, [editor, updateToolbar]);
useEffect(() => {
return editor.registerCommand(
CAN_UNDO_COMMAND,
(payload) => {
setCanUndo(payload);
return false;
},
COMMAND_PRIORITY_CRITICAL
);
}, [editor]);
useEffect(() => {
return editor.registerCommand(
CAN_REDO_COMMAND,
(payload) => {
setCanRedo(payload);
return false;
},
COMMAND_PRIORITY_CRITICAL
);
}, [editor]);
const formatText = (format: TextFormatType) => {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
};
const onFontColorSelect = useCallback(
(value: string) => {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$patchStyleText(selection, { color: value });
}
});
},
[editor]
);
const onBgColorSelect = useCallback(
(value: string) => {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$patchStyleText(selection, { 'background-color': value });
}
});
},
[editor]
);
const onFontSizeChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
$patchStyleText(selection, { 'font-size': value });
}
});
},
[editor]
);
return (
<div className="toolbar" ref={toolbarRef}>
<button
disabled={!canUndo}
onClick={() => {
editor.dispatchCommand(UNDO_COMMAND, undefined);
}}
className="toolbar-item spaced"
aria-label="Undo">
<i className="format undo" />
</button>
<button
disabled={!canRedo}
onClick={() => {
editor.dispatchCommand(REDO_COMMAND, undefined);
}}
className="toolbar-item"
aria-label="Redo">
<i className="format redo" />
</button>
<div className="divider" />
<select
className="toolbar-item block-controls"
value={fontSize}
onChange={onFontSizeChange}>
<option value="10px">10px</option>
<option value="11px">11px</option>
<option value="12px">12px</option>
<option value="13px">13px</option>
<option value="14px">14px</option>
<option value="15px">15px</option>
<option value="16px">16px</option>
<option value="18px">18px</option>
<option value="20px">20px</option>
<option value="24px">24px</option>
<option value="30px">30px</option>
<option value="36px">36px</option>
</select>
<div className="divider" />
<button
onClick={() => formatText('bold')}
className={'toolbar-item spaced ' + (isBold ? 'active' : '')}
aria-label="Format Bold">
<i className="format bold" />
</button>
<button
onClick={() => formatText('italic')}
className={'toolbar-item spaced ' + (isItalic ? 'active' : '')}
aria-label="Format Italics">
<i className="format italic" />
</button>
<button
onClick={() => formatText('underline')}
className={'toolbar-item spaced ' + (isUnderline ? 'active' : '')}
aria-label="Format Underline">
<i className="format underline" />
</button>
<button
onClick={() => formatText('strikethrough')}
className={'toolbar-item spaced ' + (isStrikethrough ? 'active' : '')}
aria-label="Format Strikethrough">
<i className="format strikethrough" />
</button>
<button
onClick={() => formatText('code')}
className={'toolbar-item spaced ' + (isCode ? 'active' : '')}
aria-label="Insert Code">
<i className="format code" />
</button>
<div className="divider" />
<DropdownColorPicker
buttonClassName="toolbar-item color-picker"
buttonAriaLabel="Formatting text color"
buttonIconClassName="icon font-color"
color={fontColor}
onChange={onFontColorSelect}
title="text color"
/>
<DropdownColorPicker
buttonClassName="toolbar-item color-picker"
buttonAriaLabel="Formatting background color"
buttonIconClassName="icon bg-color"
color={bgColor}
onChange={onBgColorSelect}
title="bg color"
/>
</div>
);
}

View File

@@ -0,0 +1,148 @@
.editor-container {
margin: 20px auto;
border-radius: 8px;
max-width: 1100px;
color: #000;
position: relative;
line-height: 1.7;
font-weight: 400;
text-align: left;
border: 1px solid #e0e0e0;
background: #fff;
}
.editor-inner {
background: #fff;
position: relative;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.editor-input {
min-height: 300px;
resize: vertical;
font-size: 15px;
caret-color: rgb(5, 5, 5);
position: relative;
tab-size: 1;
outline: 0;
padding: 15px 20px;
caret-color: #444;
}
.editor-placeholder {
color: #999;
overflow: hidden;
position: absolute;
text-overflow: ellipsis;
top: 15px;
left: 20px;
font-size: 15px;
user-select: none;
display: inline-block;
pointer-events: none;
}
.editor-paragraph {
margin: 0;
margin-bottom: 8px;
position: relative;
}
.editor-paragraph:last-child {
margin-bottom: 0;
}
.editor-heading-h1 {
font-size: 2em;
font-weight: 700;
margin: 0;
margin-bottom: 12px;
padding: 0;
}
.editor-heading-h2 {
font-size: 1.5em;
font-weight: 700;
margin: 0;
margin-bottom: 10px;
padding: 0;
}
.editor-heading-h3 {
font-size: 1.25em;
font-weight: 700;
margin: 0;
margin-bottom: 8px;
padding: 0;
}
.editor-quote {
margin: 0;
margin-left: 20px;
margin-bottom: 10px;
font-size: 15px;
color: #666;
border-left: 4px solid #ccc;
padding-left: 16px;
}
.editor-code {
background-color: #f4f4f4;
font-family: Menlo, Consolas, Monaco, monospace;
display: block;
padding: 8px 12px;
line-height: 1.53;
font-size: 13px;
margin: 0;
margin-top: 8px;
margin-bottom: 8px;
overflow-x: auto;
position: relative;
tab-size: 2;
border-radius: 4px;
}
.editor-text-bold {
font-weight: bold;
}
.editor-text-italic {
font-style: italic;
}
.editor-text-underline {
text-decoration: underline;
}
.editor-text-strikethrough {
text-decoration: line-through;
}
.editor-text-code {
background-color: #f4f4f4;
padding: 1px 4px;
font-family: Menlo, Consolas, Monaco, monospace;
font-size: 90%;
border-radius: 3px;
}
.editor-list-ol {
padding: 0;
margin: 0;
margin-left: 20px;
}
.editor-list-ul {
padding: 0;
margin: 0;
margin-left: 20px;
}
.editor-listitem {
margin: 4px 0;
}
.editor-nested-listitem {
list-style-type: none;
}

View File

@@ -0,0 +1,130 @@
.toolbar {
display: flex;
background: #fff;
padding: 8px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
vertical-align: middle;
border-bottom: 1px solid #e0e0e0;
gap: 4px;
flex-wrap: wrap;
align-items: center;
}
.toolbar button {
border: 0;
display: flex;
background: none;
border-radius: 4px;
padding: 6px;
cursor: pointer;
vertical-align: middle;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.toolbar button:hover:not([disabled]) {
background-color: #f0f0f0;
}
.toolbar button:disabled {
cursor: not-allowed;
opacity: 0.3;
}
.toolbar button.active {
background-color: rgba(24, 144, 255, 0.1);
}
.toolbar-item {
border: 0;
display: flex;
background: none;
border-radius: 4px;
padding: 6px;
cursor: pointer;
vertical-align: middle;
flex-shrink: 0;
align-items: center;
justify-content: space-between;
}
.toolbar-item.spaced {
margin-right: 2px;
}
.toolbar-item.block-controls {
background: none;
border: 1px solid #d0d0d0;
border-radius: 4px;
padding: 4px 8px;
font-size: 14px;
cursor: pointer;
min-width: 80px;
}
.toolbar-item i.format {
background-size: contain;
display: inline-block;
height: 18px;
width: 18px;
vertical-align: -0.25em;
opacity: 0.7;
}
.toolbar-item.active i.format,
.toolbar-item:hover:not([disabled]) i.format {
opacity: 1;
}
.divider {
width: 1px;
background-color: #e0e0e0;
margin: 0 4px;
height: 24px;
}
i.format.undo {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7v6h6"/><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/></svg>');
}
i.format.redo {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 7v6h-6"/><path d="M3 17a9 9 0 019-9 9 9 0 016 2.3l3 2.7"/></svg>');
}
i.format.bold {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/><path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"/></svg>');
}
i.format.italic {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>');
}
i.format.underline {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 3v7a6 6 0 0 0 6 6 6 6 0 0 0 6-6V3"/><line x1="4" y1="21" x2="20" y2="21"/></svg>');
}
i.format.strikethrough {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.3 5.3c-.94-.94-2.2-1.3-3.3-1.3H10a4 4 0 0 0 0 8"/><path d="M14 12a4 4 0 0 1 0 8h-4"/><line x1="4" y1="12" x2="20" y2="12"/></svg>');
}
i.format.code {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>');
}
i.icon.font-color {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20h16"/><path d="m6 16 6-12 6 12"/><path d="M8 12h8"/></svg>');
display: inline-block;
height: 18px;
width: 18px;
background-size: contain;
}
i.icon.bg-color {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 14c0-2.5-2-4-4-4H8c-2 0-4 1.5-4 4v6h16v-6Z"/><path d="M4 14V6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8"/></svg>');
display: inline-block;
height: 18px;
width: 18px;
background-size: contain;
}

View File

@@ -0,0 +1,33 @@
import type { EditorThemeClasses } from 'lexical';
const theme: EditorThemeClasses = {
paragraph: 'editor-paragraph',
quote: 'editor-quote',
heading: {
h1: 'editor-heading-h1',
h2: 'editor-heading-h2',
h3: 'editor-heading-h3',
h4: 'editor-heading-h4',
h5: 'editor-heading-h5',
h6: 'editor-heading-h6',
},
list: {
nested: {
listitem: 'editor-nested-listitem',
},
ol: 'editor-list-ol',
ul: 'editor-list-ul',
listitem: 'editor-listitem',
},
text: {
bold: 'editor-text-bold',
italic: 'editor-text-italic',
underline: 'editor-text-underline',
strikethrough: 'editor-text-strikethrough',
code: 'editor-text-code',
},
code: 'editor-code',
codeHighlight: {},
};
export default theme;

View File

@@ -0,0 +1,50 @@
.color-picker-wrapper {
position: relative;
}
.color-preview {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 2px;
border: 1px solid #ccc;
margin-left: 4px;
vertical-align: middle;
}
.color-picker-dropdown {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
padding: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
}
.color-picker-basic-color {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 4px;
}
.color-picker-basic-color button {
width: 24px;
height: 24px;
border: 1px solid #ccc;
border-radius: 2px;
cursor: pointer;
padding: 0;
}
.color-picker-basic-color button:hover {
border-color: #333;
}
.color-picker-basic-color button.active {
border: 2px solid #1890ff;
border-radius: 4px;
}

View File

@@ -0,0 +1,87 @@
import { useEffect, useRef, useState } from 'react';
import './DropdownColorPicker.css';
const basicColors = [
'#000000', '#ffffff', '#888888', '#ff0000', '#00ff00', '#0000ff',
'#ffff00', '#00ffff', '#ff00ff', '#c0c0c0', '#808080', '#800000',
'#808000', '#008000', '#800080', '#008080', '#000080', '#ffa500',
'#a52a2a', '#dc143c', '#ff1493', '#ff69b4', '#ffd700', '#adff2f',
'#00fa9a', '#00ced1', '#1e90ff', '#9370db', '#ff6347', '#40e0d0',
];
type Props = {
buttonClassName: string;
buttonAriaLabel?: string;
buttonIconClassName?: string;
color: string;
onChange?: (color: string) => void;
title?: string;
};
export default function DropdownColorPicker({
buttonClassName,
buttonAriaLabel,
buttonIconClassName,
color,
onChange,
title,
}: Props) {
const [showPicker, setShowPicker] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
buttonRef.current &&
!buttonRef.current.contains(event.target as Node)
) {
setShowPicker(false);
}
};
if (showPicker) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [showPicker]);
return (
<div className="color-picker-wrapper">
<button
ref={buttonRef}
type="button"
className={buttonClassName}
onClick={() => setShowPicker(!showPicker)}
aria-label={buttonAriaLabel || title}
style={{ position: 'relative' }}>
{buttonIconClassName && <i className={buttonIconClassName} />}
<span
className="color-preview"
style={{ backgroundColor: color }}
/>
</button>
{showPicker && (
<div ref={dropdownRef} className="color-picker-dropdown">
<div className="color-picker-basic-color">
{basicColors.map((basicColor) => (
<button
key={basicColor}
className={basicColor === color ? ' active' : ''}
style={{ backgroundColor: basicColor }}
onClick={() => {
onChange?.(basicColor);
setShowPicker(false);
}}
/>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -14,6 +14,7 @@ import Blog from './pages/Blog.tsx'
import Servers from './pages/Servers.tsx' import Servers from './pages/Servers.tsx'
import Forum from './pages/Forum.tsx' import Forum from './pages/Forum.tsx'
import AuthCallback from './pages/AuthCallback.tsx' import AuthCallback from './pages/AuthCallback.tsx'
import EditorDemo from './pages/EditorDemo.tsx'
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
@@ -30,6 +31,7 @@ createRoot(document.getElementById('root')!).render(
<Route path="/servers" element={<Servers />} /> <Route path="/servers" element={<Servers />} />
<Route path="/forum" element={<Forum />} /> <Route path="/forum" element={<Forum />} />
<Route path="/auth/callback" element={<AuthCallback />} /> <Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/editor" element={<EditorDemo />} />
</Routes> </Routes>
</Router> </Router>
</ServerProvider> </ServerProvider>

32
src/pages/EditorDemo.tsx Normal file
View File

@@ -0,0 +1,32 @@
import RichTextEditor from '../editor/RichTextEditor';
import '../editor/styles/editor.css';
import '../editor/styles/toolbar.css';
export default function EditorDemo() {
return (
<div style={{
padding: '40px 20px',
maxWidth: '1200px',
margin: '0 auto',
minHeight: '100vh'
}}>
<h1 style={{
marginBottom: '20px',
fontSize: '2em',
fontWeight: 'bold'
}}>
Rich Text Editor Demo
</h1>
<p style={{
marginBottom: '30px',
color: '#666',
fontSize: '1.1em'
}}>
A basic rich text editor with formatting toolbar featuring:
undo/redo, text size, bold, italic, underline, strikethrough,
code blocks, text color, and background color.
</p>
<RichTextEditor />
</div>
);
}