diff --git a/.gitignore b/.gitignore
index 5493e498a..7783a6550 100644
--- a/.gitignore
+++ b/.gitignore
@@ -174,6 +174,7 @@ node_modules
slides/webgl
# Generated distributions
+/public/assets
/public/js
/public/css
/public/zh/documents
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 000000000..6fed3d0f8
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,6 @@
+{
+ "tabWidth": 2,
+ "useTabs": false,
+ "semi": false,
+ "singleQuote": true
+}
diff --git a/build/plugin-svg-sprite.js b/build/plugin-svg-sprite.js
new file mode 100644
index 000000000..624256987
--- /dev/null
+++ b/build/plugin-svg-sprite.js
@@ -0,0 +1,87 @@
+const { RawSource } = require('webpack-sources')
+const path = require('path')
+const fs = require('fs')
+
+class SvgSpritePlugin {
+ constructor({ spriteFilename = 'sprite-doc.svg', svgPath } = {}) {
+ this.spriteFilename = spriteFilename
+ this.svgPath = svgPath
+ }
+
+ apply(compiler) {
+ compiler.hooks.compilation.tap('SvgSpritePlugin', (compilation) => {
+ const stage = compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS
+ compilation.hooks.processAssets.tap(
+ { name: 'SvgSpritePlugin', stage },
+ () => {
+ if (!this.svgPath || !fs.existsSync(this.svgPath)) return
+
+ const svgFiles = this._getSvgFiles(this.svgPath)
+ if (compilation.contextDependencies && compilation.fileDependencies) {
+ compilation.contextDependencies.add(this.svgPath)
+ svgFiles.forEach((file) => compilation.fileDependencies.add(file))
+ }
+
+ const symbols = svgFiles
+ .map((file) =>
+ this._processSvg(file, fs.readFileSync(file, 'utf8')),
+ )
+ .filter(Boolean)
+
+ // Assemble the final sprite content
+ const content = ``
+ // Emit the single sprite file to the build directory
+ compilation.emitAsset(this.spriteFilename, new RawSource(content))
+ },
+ )
+ })
+ }
+
+ // Recursively get all .svg files
+ _getSvgFiles(dir) {
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
+ const files = []
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name)
+ if (entry.isDirectory()) {
+ files.push(...this._getSvgFiles(fullPath))
+ } else if (entry.name.endsWith('.svg')) {
+ files.push(fullPath)
+ }
+ }
+ return files
+ }
+
+ _processSvg(filePath, rawSvg) {
+ const id = path
+ .basename(filePath, '.svg')
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]+/g, '-')
+
+ // Strip XML declaration, DOCTYPE and comments
+ const svg = rawSvg
+ .replace(/<\?xml[\s\S]*?\?>/g, '')
+ .replace(//gi, '')
+ .replace(//g, '')
+
+ // Extract viewBox (crucial for icon scaling)
+ const viewBoxMatch = svg.match(/viewBox="([^"]*)"/i)
+ if (!viewBoxMatch) {
+ console.warn(
+ `Warning: SVG file '${filePath}' is missing a viewBox attribute. Skipping.`,
+ )
+ return null
+ }
+
+ return svg
+ .replace(/