@qlover/create-app 0.7.8 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/templates/next-app/config/Identifier/page.home.ts +7 -0
  5. package/dist/templates/next-app/config/i18n/HomeI18n .ts +22 -0
  6. package/dist/templates/next-app/config/theme.ts +1 -0
  7. package/dist/templates/next-app/package.json +4 -4
  8. package/dist/templates/next-app/public/locales/en.json +2 -1
  9. package/dist/templates/next-app/public/locales/zh.json +2 -1
  10. package/dist/templates/next-app/src/app/[locale]/layout.tsx +1 -7
  11. package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +3 -11
  12. package/dist/templates/next-app/src/app/[locale]/login/page.tsx +5 -14
  13. package/dist/templates/next-app/src/app/[locale]/page.tsx +92 -100
  14. package/dist/templates/next-app/src/styles/css/antd-themes/_default.css +12 -0
  15. package/dist/templates/next-app/src/styles/css/antd-themes/dark.css +26 -0
  16. package/dist/templates/next-app/src/styles/css/antd-themes/pink.css +16 -0
  17. package/dist/templates/next-app/src/styles/css/page.css +4 -3
  18. package/dist/templates/next-app/src/styles/css/themes/_default.css +1 -0
  19. package/dist/templates/next-app/src/styles/css/themes/dark.css +1 -0
  20. package/dist/templates/next-app/src/styles/css/themes/pink.css +1 -0
  21. package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +6 -11
  22. package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +27 -0
  23. package/dist/templates/next-app/src/uikit/components/LanguageSwitcher.tsx +49 -21
  24. package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +92 -48
  25. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @qlover/create-app
2
2
 
3
+ ## 0.7.9
4
+
5
+ ### Patch Changes
6
+
7
+ #### ✨ Features
8
+
9
+ - **next-app:** enhance home page internationalization and SEO support ([54d7714](https://github.com/qlover/fe-base/commit/54d77146f26ce132ddd20edace52a1a10eb03149)) ([#508](https://github.com/qlover/fe-base/pull/508))
10
+ - Added PAGE_HOME_KEYWORDS constant to improve SEO with relevant keywords for the home page.
11
+ - Created HomeI18nInterface to manage internationalization for the home page, incorporating keywords and welcome messages.
12
+ - Updated English and Chinese localization files to include new keywords for the home page, enhancing multilingual support.
13
+ - Refactored Home component to utilize the new i18n structure for better localization handling.
14
+
15
+ These changes aim to improve the user experience by providing comprehensive localization and SEO enhancements for the home page.
16
+
17
+ - **next-app:** enhance login components and improve styling consistency ([59b8b85](https://github.com/qlover/fe-base/commit/59b8b85179e78f29f1eca5b8e7e375b6b8c660eb)) ([#508](https://github.com/qlover/fe-base/pull/508))
18
+ - Updated LoginForm component to use new border color variable for improved styling consistency.
19
+ - Simplified LocaleLink components in LoginForm for cleaner code and better readability.
20
+ - Refactored LoginPage to remove unnecessary elements, streamlining the layout.
21
+ - Enhanced CSS variables in page.css for better color management across the application.
22
+
23
+ These changes aim to improve the user interface and maintain a consistent design across the login components.
24
+
25
+ - **next-app:** update scripts, enhance theme support, and improve styling ([aebcdfa](https://github.com/qlover/fe-base/commit/aebcdfaeb5bae706ac96dc410056f3064eb1e8e9)) ([#508](https://github.com/qlover/fe-base/pull/508))
26
+ - Updated package.json scripts to specify ports for development and production environments.
27
+ - Added new CSS variables for hover states and improved theme management in various CSS files.
28
+ - Enhanced the LanguageSwitcher and ThemeSwitcher components to utilize dropdowns for better user experience.
29
+ - Refactored BaseLayout to include a background color for the main content area.
30
+
31
+ These changes aim to improve the application's usability and maintainability by enhancing the theme management and user interface components.
32
+
3
33
  ## 0.7.8
4
34
 
5
35
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -8,4 +8,4 @@ ${t}`,dn=Object.getOwnPropertyDescriptor(Function.prototype,"toString"),_n=Objec
8
8
  `))this.#e+=Math.max(1,Math.ceil(Ye(D,{countAnsiEscapeCodes:!0})/t))}get isEnabled(){return this.#a&&!this.#F}set isEnabled(t){if(typeof t!="boolean")throw new TypeError("The `isEnabled` option must be a boolean");this.#a=t}get isSilent(){return this.#F}set isSilent(t){if(typeof t!="boolean")throw new TypeError("The `isSilent` option must be a boolean");this.#F=t}frame(){let t=Date.now();(this.#i===-1||t-this.#c>=this.interval)&&(this.#i=++this.#i%this.#D.frames.length,this.#c=t);let{frames:r}=this.#D,u=r[this.#i];this.color&&(u=d[this.color](u));let i=typeof this.#s=="string"&&this.#s!==""?this.#s+" ":"",D=typeof this.text=="string"?" "+this.text:"",o=typeof this.#o=="string"&&this.#o!==""?" "+this.#o:"";return i+u+D+o}clear(){if(!this.#a||!this.#u.isTTY)return this;this.#u.cursorTo(0);for(let t=0;t<this.#n;t++)t>0&&this.#u.moveCursor(0,-1),this.#u.clearLine(1);return(this.#l||this.lastIndent!==this.#l)&&this.#u.cursorTo(this.#l),this.lastIndent=this.#l,this.#n=0,this}render(){return this.#F?this:(this.clear(),this.#u.write(this.frame()),this.#n=this.#e,this)}start(t){return t&&(this.text=t),this.#F?this:this.#a?this.isSpinning?this:(this.#t.hideCursor&&$e.hide(this.#u),this.#t.discardStdin&&V.default.stdin.isTTY&&(this.#r=!0,Je.start()),this.render(),this.#f=setInterval(this.render.bind(this),this.interval),this):(this.text&&this.#u.write(`- ${this.text}
9
9
  `),this)}stop(){return this.#a?(clearInterval(this.#f),this.#f=void 0,this.#i=0,this.clear(),this.#t.hideCursor&&$e.show(this.#u),this.#t.discardStdin&&V.default.stdin.isTTY&&this.#r&&(Je.stop(),this.#r=!1),this):this}succeed(t){return this.stopAndPersist({symbol:H.success,text:t})}fail(t){return this.stopAndPersist({symbol:H.error,text:t})}warn(t){return this.stopAndPersist({symbol:H.warning,text:t})}info(t){return this.stopAndPersist({symbol:H.info,text:t})}stopAndPersist(t={}){if(this.#F)return this;let r=t.prefixText??this.#s,u=this.#E(r," "),i=t.symbol??" ",D=t.text??this.text,s=typeof D=="string"?(i?" ":"")+D:"",a=t.suffixText??this.#o,f=this.#g(a," "),p=u+i+s+f+`
10
10
  `;return this.stop(),this.#u.write(p),this}};function tr(e){return new Ze(e)}async function rr(e,t){let r=typeof e=="function",u=typeof e.then=="function";if(!r&&!u)throw new TypeError("Parameter `action` must be a Function or a Promise");let{successText:i,failText:D}=typeof t=="object"?t:{successText:void 0,failText:void 0},o=tr(t).start();try{let a=await(r?e(o):e);return o.succeed(i===void 0?void 0:typeof i=="string"?i:i(a)),a}catch(s){throw o.fail(D===void 0?void 0:typeof D=="string"?D:D(s)),s}}var HD=require("fs");var P=require("path"),me=require("fs"),cr=h(lr(),1),Dt=require("fs");var de=require("fs"),X=class{static ensureDir(t){(0,de.existsSync)(t)||(0,de.mkdirSync)(t,{recursive:!0})}};var{copyFile:rs,stat:us}=Dt.promises,_e=class e{constructor(t,r=e.IGNORE_FILE){this.ignoreTargetPath=t;this.ignoreFile=r}static IGNORE_FILE=".gitignore.template";getIg(t=this.ignoreTargetPath){let r=(0,P.join)(t,this.ignoreFile);if(!(0,me.existsSync)(r))return;let D=(0,me.readFileSync)(r,"utf8").split(`
11
- `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,cr.default)().add(D)}async copyFiles(t,r,u,i){let D=await Dt.promises.readdir(t);await Promise.all(D.map(async o=>{let s=(0,P.join)(t,o),a=(0,P.join)(r,o);if(u&&u.ignores(o))return;if(X.ensureDir((0,P.dirname)(a)),(await us(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await rs(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){X.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var L=require("fs"),kD=h(LD(),1),xe=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return(0,L.readFileSync)(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){(0,L.writeFileSync)(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,kD.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return(0,L.existsSync)(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var $D=["pack-app"],ye=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!(0,HD.existsSync)(r))throw new Error("template path not exit");this.ora=rr,this.context=new UD.FeScriptContext(t),this.subPackages=["node-lib","react-app"],this.copyer=new _e((0,v.join)(this.context.options.configsRootPath,"_common")),this.compose=new xe}get logger(){return this.context.logger}async steps(t){try{return await WD.default.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return $D.includes(t)}async getGeneratorContext(){let t=Ot(this.subPackages,$D),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=wt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=(0,v.join)(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:(0,v.join)(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:(0,v.join)(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=(0,v.join)(D,o),a=(0,v.join)(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var bt={name:"@qlover/create-app",version:"0.7.8",description:"Create a new app with a single command",private:!1,type:"module",files:["dist","package.json","README.md","CHANGELOG.md"],bin:{"create-app":"dist/index.js"},scripts:{build:"tsup","create:app":"node ./dist/index.js"},repository:{type:"git",url:"git+https://github.com/qlover/fe-base.git",directory:"packages/create-app"},homepage:"https://github.com/qlover/fe-base#readme",keywords:["create-app","fe-scripts","scripts"],author:"qlover",license:"ISC",publishConfig:{access:"public"},devDependencies:{"@qlover/logger":"workspace:*",ignore:"^7.0.3",lodash:"^4.17.21",ora:"^8.1.1"},dependencies:{"@qlover/scripts-context":"workspace:*",commander:"^13.1.0",inquirer:"^12.3.2"}};function Ul(){let e=new YD.Command;return e.version(bt.version,"-v, --version","Show version").description(bt.description).option("-d, --dry-run","Do not touch or write anything, but show the commands").option("-V, --verbose","Show more information").option("--config","Copy config files (default: true)",!0).option("--no-config","Do not copy config files"),e.parse(),e.opts()}async function Wl(){let{dryRun:e,verbose:t,...r}=Ul(),u=(0,vt.resolve)("./templates"),i=(0,vt.resolve)("./configs");(0,xt.existsSync)(u)||(console.error("Template is empty!"),process.exit(1)),(0,xt.existsSync)(i)||(console.error("Configs is empty!"),process.exit(1)),await new ye({dryRun:e,verbose:t,options:{...r,templateRootPath:u,configsRootPath:i}}).generate()}Wl().catch(e=>{console.error(e),process.exit(1)});
11
+ `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,cr.default)().add(D)}async copyFiles(t,r,u,i){let D=await Dt.promises.readdir(t);await Promise.all(D.map(async o=>{let s=(0,P.join)(t,o),a=(0,P.join)(r,o);if(u&&u.ignores(o))return;if(X.ensureDir((0,P.dirname)(a)),(await us(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await rs(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){X.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var L=require("fs"),kD=h(LD(),1),xe=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return(0,L.readFileSync)(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){(0,L.writeFileSync)(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,kD.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return(0,L.existsSync)(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var $D=["pack-app"],ye=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!(0,HD.existsSync)(r))throw new Error("template path not exit");this.ora=rr,this.context=new UD.FeScriptContext(t),this.subPackages=["node-lib","react-app"],this.copyer=new _e((0,v.join)(this.context.options.configsRootPath,"_common")),this.compose=new xe}get logger(){return this.context.logger}async steps(t){try{return await WD.default.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return $D.includes(t)}async getGeneratorContext(){let t=Ot(this.subPackages,$D),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=wt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=(0,v.join)(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:(0,v.join)(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:(0,v.join)(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=(0,v.join)(D,o),a=(0,v.join)(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var bt={name:"@qlover/create-app",version:"0.7.9",description:"Create a new app with a single command",private:!1,type:"module",files:["dist","package.json","README.md","CHANGELOG.md"],bin:{"create-app":"dist/index.js"},scripts:{build:"tsup","create:app":"node ./dist/index.js"},repository:{type:"git",url:"git+https://github.com/qlover/fe-base.git",directory:"packages/create-app"},homepage:"https://github.com/qlover/fe-base#readme",keywords:["create-app","fe-scripts","scripts"],author:"qlover",license:"ISC",publishConfig:{access:"public"},devDependencies:{"@qlover/logger":"workspace:*",ignore:"^7.0.3",lodash:"^4.17.21",ora:"^8.1.1"},dependencies:{"@qlover/scripts-context":"workspace:*",commander:"^13.1.0",inquirer:"^12.3.2"}};function Ul(){let e=new YD.Command;return e.version(bt.version,"-v, --version","Show version").description(bt.description).option("-d, --dry-run","Do not touch or write anything, but show the commands").option("-V, --verbose","Show more information").option("--config","Copy config files (default: true)",!0).option("--no-config","Do not copy config files"),e.parse(),e.opts()}async function Wl(){let{dryRun:e,verbose:t,...r}=Ul(),u=(0,vt.resolve)("./templates"),i=(0,vt.resolve)("./configs");(0,xt.existsSync)(u)||(console.error("Template is empty!"),process.exit(1)),(0,xt.existsSync)(i)||(console.error("Configs is empty!"),process.exit(1)),await new ye({dryRun:e,verbose:t,options:{...r,templateRootPath:u,configsRootPath:i}}).generate()}Wl().catch(e=>{console.error(e),process.exit(1)});
package/dist/index.js CHANGED
@@ -8,4 +8,4 @@ ${t}`,cn=Object.getOwnPropertyDescriptor(Function.prototype,"toString"),fn=Objec
8
8
  `))this.#e+=Math.max(1,Math.ceil(Le(D,{countAnsiEscapeCodes:!0})/t))}get isEnabled(){return this.#a&&!this.#F}set isEnabled(t){if(typeof t!="boolean")throw new TypeError("The `isEnabled` option must be a boolean");this.#a=t}get isSilent(){return this.#F}set isSilent(t){if(typeof t!="boolean")throw new TypeError("The `isSilent` option must be a boolean");this.#F=t}frame(){let t=Date.now();(this.#i===-1||t-this.#c>=this.interval)&&(this.#i=++this.#i%this.#D.frames.length,this.#c=t);let{frames:r}=this.#D,u=r[this.#i];this.color&&(u=E[this.color](u));let i=typeof this.#s=="string"&&this.#s!==""?this.#s+" ":"",D=typeof this.text=="string"?" "+this.text:"",o=typeof this.#o=="string"&&this.#o!==""?" "+this.#o:"";return i+u+D+o}clear(){if(!this.#a||!this.#u.isTTY)return this;this.#u.cursorTo(0);for(let t=0;t<this.#n;t++)t>0&&this.#u.moveCursor(0,-1),this.#u.clearLine(1);return(this.#l||this.lastIndent!==this.#l)&&this.#u.cursorTo(this.#l),this.lastIndent=this.#l,this.#n=0,this}render(){return this.#F?this:(this.clear(),this.#u.write(this.frame()),this.#n=this.#e,this)}start(t){return t&&(this.text=t),this.#F?this:this.#a?this.isSpinning?this:(this.#t.hideCursor&&je.hide(this.#u),this.#t.discardStdin&&ce.stdin.isTTY&&(this.#r=!0,We.start()),this.render(),this.#f=setInterval(this.render.bind(this),this.interval),this):(this.text&&this.#u.write(`- ${this.text}
9
9
  `),this)}stop(){return this.#a?(clearInterval(this.#f),this.#f=void 0,this.#i=0,this.clear(),this.#t.hideCursor&&je.show(this.#u),this.#t.discardStdin&&ce.stdin.isTTY&&this.#r&&(We.stop(),this.#r=!1),this):this}succeed(t){return this.stopAndPersist({symbol:k.success,text:t})}fail(t){return this.stopAndPersist({symbol:k.error,text:t})}warn(t){return this.stopAndPersist({symbol:k.warning,text:t})}info(t){return this.stopAndPersist({symbol:k.info,text:t})}stopAndPersist(t={}){if(this.#F)return this;let r=t.prefixText??this.#s,u=this.#E(r," "),i=t.symbol??" ",D=t.text??this.text,s=typeof D=="string"?(i?" ":"")+D:"",a=t.suffixText??this.#o,f=this.#g(a," "),p=u+i+s+f+`
10
10
  `;return this.stop(),this.#u.write(p),this}};function zt(e){return new He(e)}async function Kt(e,t){let r=typeof e=="function",u=typeof e.then=="function";if(!r&&!u)throw new TypeError("Parameter `action` must be a Function or a Promise");let{successText:i,failText:D}=typeof t=="object"?t:{successText:void 0,failText:void 0},o=zt(t).start();try{let a=await(r?e(o):e);return o.succeed(i===void 0?void 0:typeof i=="string"?i:i(a)),a}catch(s){throw o.fail(D===void 0?void 0:typeof D=="string"?D:D(s)),s}}import{existsSync as Hl}from"fs";var Dr=ue(ir(),1);import{dirname as Jn,join as Ze}from"path";import{existsSync as Zn,readFileSync as Qn}from"fs";import{promises as nr}from"fs";import{existsSync as Kn,mkdirSync as Xn}from"fs";var H=class{static ensureDir(t){Kn(t)||Xn(t,{recursive:!0})}};var{copyFile:es,stat:ts}=nr,Ce=class e{constructor(t,r=e.IGNORE_FILE){this.ignoreTargetPath=t;this.ignoreFile=r}static IGNORE_FILE=".gitignore.template";getIg(t=this.ignoreTargetPath){let r=Ze(t,this.ignoreFile);if(!Zn(r))return;let D=Qn(r,"utf8").split(`
11
- `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,Dr.default)().add(D)}async copyFiles(t,r,u,i){let D=await nr.readdir(t);await Promise.all(D.map(async o=>{let s=Ze(t,o),a=Ze(r,o);if(u&&u.ignores(o))return;if(H.ensureDir(Jn(a)),(await ts(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await es(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){H.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var ID=ue(qD(),1);import{readFileSync as Ll,writeFileSync as kl,existsSync as $l}from"fs";var me=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return Ll(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){kl(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,ID.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return $l(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var jD=["pack-app"],Be=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!Hl(r))throw new Error("template path not exit");this.ora=Kt,this.context=new Ul(t),this.subPackages=["node-lib","react-app"],this.copyer=new Ce(N(this.context.options.configsRootPath,"_common")),this.compose=new me}get logger(){return this.context.logger}async steps(t){try{return await Wl.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return jD.includes(t)}async getGeneratorContext(){let t=_t(this.subPackages,jD),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=mt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=N(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:N(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:N(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=N(D,o),a=N(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var Et={name:"@qlover/create-app",version:"0.7.8",description:"Create a new app with a single command",private:!1,type:"module",files:["dist","package.json","README.md","CHANGELOG.md"],bin:{"create-app":"dist/index.js"},scripts:{build:"tsup","create:app":"node ./dist/index.js"},repository:{type:"git",url:"git+https://github.com/qlover/fe-base.git",directory:"packages/create-app"},homepage:"https://github.com/qlover/fe-base#readme",keywords:["create-app","fe-scripts","scripts"],author:"qlover",license:"ISC",publishConfig:{access:"public"},devDependencies:{"@qlover/logger":"workspace:*",ignore:"^7.0.3",lodash:"^4.17.21",ora:"^8.1.1"},dependencies:{"@qlover/scripts-context":"workspace:*",commander:"^13.1.0",inquirer:"^12.3.2"}};function zl(){let e=new Vl;return e.version(Et.version,"-v, --version","Show version").description(Et.description).option("-d, --dry-run","Do not touch or write anything, but show the commands").option("-V, --verbose","Show more information").option("--config","Copy config files (default: true)",!0).option("--no-config","Do not copy config files"),e.parse(),e.opts()}async function Kl(){let{dryRun:e,verbose:t,...r}=zl(),u=ND("./templates"),i=ND("./configs");GD(u)||(console.error("Template is empty!"),process.exit(1)),GD(i)||(console.error("Configs is empty!"),process.exit(1)),await new Be({dryRun:e,verbose:t,options:{...r,templateRootPath:u,configsRootPath:i}}).generate()}Kl().catch(e=>{console.error(e),process.exit(1)});
11
+ `).map(o=>o.trim()).filter(o=>o&&!o.startsWith("#"));return(0,Dr.default)().add(D)}async copyFiles(t,r,u,i){let D=await nr.readdir(t);await Promise.all(D.map(async o=>{let s=Ze(t,o),a=Ze(r,o);if(u&&u.ignores(o))return;if(H.ensureDir(Jn(a)),(await ts(s)).isDirectory())await this.copyFiles(s,a,u,i);else{if(i&&await i(s,a))return;await es(s,a)}}))}copyPaths({sourcePath:t,targetPath:r,copyCallback:u}){H.ensureDir(r);let i=this.getIg();return this.copyFiles(t,r,i,u)}};var ID=ue(qD(),1);import{readFileSync as Ll,writeFileSync as kl,existsSync as $l}from"fs";var me=class{constructor(){}isJSONFilePath(t){return t.endsWith(".json")||t.endsWith(".json.template")}isTemplateFilePath(t){return t.endsWith(".template")}getRealTemplateFilePath(t){return t.replace(".template","")}readFile(t){return Ll(t,"utf-8")}readJSONFile(t){return JSON.parse(this.readFile(t))}writeFile(t,r){kl(this.getRealTemplateFilePath(t),r,{encoding:"utf-8"})}replaceFile(t,r){let u=this.readFile(t);return Object.keys(r).forEach(i=>{let D=r[i];u=u.replace(new RegExp(`\\[TPL:${i}\\]`,"g"),typeof D=="string"?D:JSON.stringify(D))}),u}mergeJSONFile(t,r){let u=this.readJSONFile(t),i=(0,ID.default)(r,u);this.writeFile(t,JSON.stringify(i,null,2))}composeConfigFile(t,r,u){if(this.isTemplateFilePath(r)){let i=this.replaceFile(r,t);if(this.isJSONFilePath(r)&&this.isJSONFilePath(u)){let D=this.getRealTemplateFilePath(u);return $l(D)?(this.mergeJSONFile(D,JSON.parse(i)),!0):(this.writeFile(D,i),!0)}return this.writeFile(u,i),!0}return!1}};var jD=["pack-app"],Be=class{ora;context;subPackages;copyer;compose;constructor(t){let r=t.options?.templateRootPath;if(!r)throw new Error("template path not exit");if(!Hl(r))throw new Error("template path not exit");this.ora=Kt,this.context=new Ul(t),this.subPackages=["node-lib","react-app"],this.copyer=new Ce(N(this.context.options.configsRootPath,"_common")),this.compose=new me}get logger(){return this.context.logger}async steps(t){try{return await Wl.prompt(t)}catch(r){throw r.isTtyError,this.logger.error(r),r}}async action({label:t,task:r}){let u=r();u instanceof Promise||(u=Promise.resolve(u));let i=t;return this.ora(u,i),u}isPackageTemplate(t){return jD.includes(t)}async getGeneratorContext(){let t=_t(this.subPackages,jD),r=await this.steps(t);if(this.isPackageTemplate(r.template)){let u=mt(this.subPackages),i=await this.steps(u);Object.assign(r,i)}return r.targetPath=N(process.cwd(),r.projectName),r.releasePath=r.releasePath||"src",r}async generate(){let t=await this.getGeneratorContext();if(this.logger.debug("context is:",t,this.context.options.templateRootPath),t.subPackages){await this.action({label:"Generate Directories(subPackages)",task:async()=>{await this.generateTemplateDir(t),await this.generateSubPackages(t),await this.generateConfigs(t,t.targetPath,"_common")}});return}await this.action({label:"Generate Directory",task:async()=>{await this.generateTemplateDir(t),await this.generateConfigs(t,t.targetPath,"_common"),await this.generateConfigs(t,t.targetPath,t.template)}})}async generateConfigs(t,r,u){let i=(s,a)=>(this.logger.debug("copyCallback",s,a),this.compose.composeConfigFile(t,s,a)),{configsRootPath:D,config:o}=this.context.options;if(!o){this.logger.debug("no copy config files");return}await this.copyer.copyPaths({sourcePath:N(D,u),targetPath:r,copyCallback:i})}generateTemplateDir(t){return this.copyer.copyPaths({sourcePath:N(this.context.options.templateRootPath,t.template),targetPath:t.targetPath})}async generateSubPackages(t){let{packagesNames:r="packages",subPackages:u=[],targetPath:i=""}=t,{templateRootPath:D}=this.context.options;for(let o of u){let s=N(D,o),a=N(i,r,o);this.logger.debug("copy sub package",s,a),await this.copyer.copyPaths({sourcePath:s,targetPath:a})}}};var Et={name:"@qlover/create-app",version:"0.7.9",description:"Create a new app with a single command",private:!1,type:"module",files:["dist","package.json","README.md","CHANGELOG.md"],bin:{"create-app":"dist/index.js"},scripts:{build:"tsup","create:app":"node ./dist/index.js"},repository:{type:"git",url:"git+https://github.com/qlover/fe-base.git",directory:"packages/create-app"},homepage:"https://github.com/qlover/fe-base#readme",keywords:["create-app","fe-scripts","scripts"],author:"qlover",license:"ISC",publishConfig:{access:"public"},devDependencies:{"@qlover/logger":"workspace:*",ignore:"^7.0.3",lodash:"^4.17.21",ora:"^8.1.1"},dependencies:{"@qlover/scripts-context":"workspace:*",commander:"^13.1.0",inquirer:"^12.3.2"}};function zl(){let e=new Vl;return e.version(Et.version,"-v, --version","Show version").description(Et.description).option("-d, --dry-run","Do not touch or write anything, but show the commands").option("-V, --verbose","Show more information").option("--config","Copy config files (default: true)",!0).option("--no-config","Do not copy config files"),e.parse(),e.opts()}async function Kl(){let{dryRun:e,verbose:t,...r}=zl(),u=ND("./templates"),i=ND("./configs");GD(u)||(console.error("Template is empty!"),process.exit(1)),GD(i)||(console.error("Configs is empty!"),process.exit(1)),await new Be({dryRun:e,verbose:t,options:{...r,templateRootPath:u,configsRootPath:i}}).generate()}Kl().catch(e=>{console.error(e),process.exit(1)});
@@ -12,6 +12,13 @@ export const PAGE_HOME_TITLE = 'page__home__title';
12
12
  */
13
13
  export const PAGE_HOME_DESCRIPTION = 'page__home__description';
14
14
 
15
+ /**
16
+ * @description Home page keywords
17
+ * @localZh 现代前端实用库, 实用工具, 组件
18
+ * @localEn Modern frontend utility library, practical tools, components
19
+ */
20
+ export const PAGE_HOME_KEYWORDS = 'page__home__keywords';
21
+
15
22
  /**
16
23
  * @description Home page welcome message
17
24
  * @localZh 欢迎来到主页
@@ -0,0 +1,22 @@
1
+ import * as i18nKeys from '../Identifier/page.home';
2
+
3
+ /**
4
+ * Home page i18n interface
5
+ *
6
+ * @description
7
+ * - welcome: welcome message
8
+ */
9
+ export type HomeI18nInterface = typeof homeI18n;
10
+
11
+ export const homeI18n = Object.freeze({
12
+ // basic meta properties
13
+ title: i18nKeys.PAGE_HOME_TITLE,
14
+ description: i18nKeys.PAGE_HOME_DESCRIPTION,
15
+ content: i18nKeys.PAGE_HOME_DESCRIPTION,
16
+ keywords: i18nKeys.PAGE_HOME_KEYWORDS,
17
+
18
+ welcome: i18nKeys.HOME_WELCOME,
19
+ getStartedTitle: i18nKeys.HOME_GET_STARTED_TITLE,
20
+ getStartedDescription: i18nKeys.HOME_GET_STARTED_DESCRIPTION,
21
+ getStartedButton: i18nKeys.HOME_GET_STARTED_BUTTON
22
+ });
@@ -20,4 +20,5 @@ export const themeConfig = {
20
20
  } as ThemeConfig
21
21
  } as const;
22
22
 
23
+ export type SupportedTheme = (typeof themeConfig.supportedThemes)[number];
23
24
  export type CommonThemeConfig = typeof themeConfig;
@@ -3,13 +3,13 @@
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "scripts": {
6
- "dev": "cross-env APP_ENV=localhost next dev --turbopack",
7
- "dev:staging": "cross-env APP_ENV=staging next dev --turbopack",
8
- "dev:prod": "cross-env APP_ENV=production next dev --turbopack",
6
+ "dev": "cross-env APP_ENV=localhost next dev --turbopack --port 3100",
7
+ "dev:staging": "cross-env APP_ENV=staging next dev --turbopack --port 3100",
8
+ "dev:prod": "cross-env APP_ENV=production next dev --turbopack --port 3100",
9
9
  "build": "cross-env APP_ENV=localhost next build --turbopack",
10
10
  "build:staging": "cross-env APP_ENV=staging next build --turbopack",
11
11
  "build:prod": "cross-env APP_ENV=production next build --turbopack",
12
- "start": "next start",
12
+ "start": "next start --port 3101",
13
13
  "lint": "eslint .",
14
14
  "lint:fix": "eslint . --ext .ts,.tsx --fix",
15
15
  "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
@@ -180,5 +180,6 @@
180
180
  "page__request__stop_api_catch": "Stop API Catch Result",
181
181
  "page__login__content": "Login Page Content",
182
182
  "page__login__keywords": "Login Page Keywords",
183
- "err__server__auth__error": "Server auth error"
183
+ "err__server__auth__error": "Server auth error",
184
+ "page__home__keywords": "Modern frontend utility library, practical tools, components"
184
185
  }
@@ -180,5 +180,6 @@
180
180
  "page__request__stop_api_catch": "停止 API 捕获结果",
181
181
  "page__login__content": "登录页面内容",
182
182
  "page__login__keywords": "登录页面关键词",
183
- "err__server__auth__error": "服务器认证错误"
183
+ "err__server__auth__error": "服务器认证错误",
184
+ "page__home__keywords": "现代前端实用库, 实用工具, 组件"
184
185
  }
@@ -2,7 +2,6 @@ import { NextIntlClientProvider } from 'next-intl';
2
2
  import { themeConfig } from '@config/theme';
3
3
  import { PageParams } from '@/base/cases/PageParams';
4
4
  import type { PageLayoutProps } from '@/base/types/PageProps';
5
- import { BaseHeader } from '@/uikit/components/BaseHeader';
6
5
  import { ComboProvider } from '@/uikit/components/ComboProvider';
7
6
  import '@/styles/css/index.css';
8
7
 
@@ -19,12 +18,7 @@ export default async function RootLayout({
19
18
  <html data-testid="RootLayout" lang={locale} suppressHydrationWarning>
20
19
  <body>
21
20
  <NextIntlClientProvider locale={locale} messages={messages}>
22
- <ComboProvider themeConfig={themeConfig}>
23
- <div className="flex flex-col min-h-screen">
24
- <BaseHeader showLogoutButton />
25
- <div className="flex flex-col">{children}</div>
26
- </div>
27
- </ComboProvider>
21
+ <ComboProvider themeConfig={themeConfig}>{children}</ComboProvider>
28
22
  </NextIntlClientProvider>
29
23
  </body>
30
24
  </html>
@@ -47,7 +47,7 @@ export function LoginForm(props: { tt: LoginI18nInterface }) {
47
47
  prefix={<UserOutlined className="text-text-tertiary" />}
48
48
  placeholder={tt.email}
49
49
  title={tt.emailTitle}
50
- className="h-12 text-base bg-secondary border-border"
50
+ className="h-12 text-base bg-secondary border-c-border"
51
51
  autoComplete="off"
52
52
  />
53
53
  </Form.Item>
@@ -66,11 +66,7 @@ export function LoginForm(props: { tt: LoginI18nInterface }) {
66
66
  </Form.Item>
67
67
 
68
68
  <div className="flex justify-end">
69
- <LocaleLink
70
- href="#"
71
- className="text-brand hover:text-brand-hover"
72
- title={tt.forgotPasswordTitle}
73
- >
69
+ <LocaleLink href="#" title={tt.forgotPasswordTitle}>
74
70
  {tt.forgotPassword}
75
71
  </LocaleLink>
76
72
  </div>
@@ -101,11 +97,7 @@ export function LoginForm(props: { tt: LoginI18nInterface }) {
101
97
 
102
98
  <div className="text-center mt-6">
103
99
  <span className="text-text-tertiary">{tt.noAccount} </span>
104
- <LocaleLink
105
- href="/register"
106
- className="text-brand hover:text-brand-hover"
107
- title={tt.createAccountTitle}
108
- >
100
+ <LocaleLink href="/register" title={tt.createAccountTitle}>
109
101
  {tt.createAccount}
110
102
  </LocaleLink>
111
103
  </div>
@@ -5,6 +5,7 @@ import { ServerAuth } from '@/base/cases/ServerAuth';
5
5
  import type { PageParamsProps } from '@/base/types/PageProps';
6
6
  import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
7
7
  import { redirect } from '@/i18n/routing';
8
+ import { BaseLayout } from '@/uikit/components/BaseLayout';
8
9
  import { FeatureItem } from './FeatureItem';
9
10
  import { LoginForm } from './LoginForm';
10
11
  import type { Metadata } from 'next';
@@ -29,9 +30,7 @@ export async function generateMetadata({
29
30
  }): Promise<Metadata> {
30
31
  const pageParams = new PageParams(await params);
31
32
 
32
- const tt = await pageParams.getI18nInterface(loginI18n);
33
-
34
- return tt;
33
+ return await pageParams.getI18nInterface(loginI18n);
35
34
  }
36
35
 
37
36
  export default async function LoginPage(props: PageParamsProps) {
@@ -51,18 +50,11 @@ export default async function LoginPage(props: PageParamsProps) {
51
50
  const tt = await pageParams.getI18nInterface(loginI18n);
52
51
 
53
52
  return (
54
- <div
53
+ <BaseLayout
55
54
  data-testid="LoginPage"
56
- className="flex text-xs1 bg-primary min-h-screen"
55
+ className="text-xs1 bg-primary flex flex-col min-h-screen"
57
56
  >
58
- {/* Left side - Brand section */}
59
57
  <div className="hidden lg:flex bg-secondary lg:w-1/2 p-12 flex-col">
60
- <div className="flex items-center gap-3 mb-12">
61
- <div className="w-10 h-10 bg-brand rounded-lg"></div>
62
- <span className="text-2xl font-semibold text-text">
63
- {'AppConfig.appName'}
64
- </span>
65
- </div>
66
58
  <h1 className="text-4xl font-bold text-text mb-4">{tt.welcome}</h1>
67
59
  <p className="text-text-secondary text-lg mb-8">{tt.subtitle}</p>
68
60
  <div className="space-y-4">
@@ -72,7 +64,6 @@ export default async function LoginPage(props: PageParamsProps) {
72
64
  </div>
73
65
  </div>
74
66
 
75
- {/* Right side - Login form */}
76
67
  <div className="w-full lg:w-1/2 p-8 sm:p-12 flex items-center justify-center">
77
68
  <div className="w-full max-w-[420px]">
78
69
  <h2 className="text-2xl font-semibold mb-2 text-text">{tt.title}</h2>
@@ -81,6 +72,6 @@ export default async function LoginPage(props: PageParamsProps) {
81
72
  <LoginForm tt={tt} />
82
73
  </div>
83
74
  </div>
84
- </div>
75
+ </BaseLayout>
85
76
  );
86
77
  }
@@ -1,119 +1,111 @@
1
- import Image from 'next/image';
2
- import { PageParams } from '@/base/cases/PageParams';
1
+ import { Button } from 'antd';
2
+ import { i18nConfig } from '@config/i18n';
3
+ import { homeI18n } from '@config/i18n/HomeI18n ';
4
+ import { PageParams, type PageParamsType } from '@/base/cases/PageParams';
3
5
  import { ServerAuth } from '@/base/cases/ServerAuth';
4
6
  import type { PageParamsProps } from '@/base/types/PageProps';
5
7
  import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
6
8
  import { redirect } from '@/i18n/routing';
9
+ import { BaseLayout } from '@/uikit/components/BaseLayout';
10
+ import type { Metadata } from 'next';
11
+
12
+ // const navigationItems = [
13
+ // {
14
+ // href: '/identifier',
15
+ // titleKey: 'HOME_IDENTIFIER',
16
+ // descriptionKey: 'HOME_IDENTIFIER_DESCRIPTION'
17
+ // }
18
+ // ];
19
+
20
+ // Generate static params for all supported locales (used for SSG)
21
+ export async function generateStaticParams() {
22
+ // Return one entry for each supported locale
23
+ return i18nConfig.supportedLngs.map((locale) => ({ locale }));
24
+ }
25
+
26
+ // Allow Next.js to statically generate this page if possible (default behavior)
27
+ export const dynamic = 'auto'; // Enable static generation when possible, fallback to dynamic if needed
28
+
29
+ // Optional: Use revalidate if you want ISR (Incremental Static Regeneration)
30
+ // export const revalidate = 3600; // Rebuild every hour (optional)
31
+
32
+ // Generate localized SEO metadata per locale (Next.js 15+ best practice)
33
+ export async function generateMetadata({
34
+ params
35
+ }: {
36
+ params: Promise<PageParamsType>;
37
+ }): Promise<Metadata> {
38
+ const pageParams = new PageParams(await params);
39
+ return await pageParams.getI18nInterface(homeI18n);
40
+ }
7
41
 
8
42
  export default async function Home({ params }: PageParamsProps) {
9
43
  const server = new BootstrapServer();
10
44
  const pageParams = new PageParams(await params!);
11
45
  const locale = pageParams.getLocale();
46
+ const tt = await pageParams.getI18nInterface(homeI18n);
12
47
 
13
48
  if (!(await new ServerAuth(server).hasAuth())) {
14
49
  return redirect({ href: '/login', locale });
15
50
  }
16
51
 
17
52
  return (
18
- <div
19
- data-testid="Home"
20
- className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
21
- >
22
- <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
23
- <Image
24
- className="dark:invert"
25
- src="/next.svg"
26
- alt="Next.js logo"
27
- width={180}
28
- height={38}
29
- priority
30
- />
31
- <ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
32
- <li className="mb-2 tracking-[-.01em]">
33
- Get started by editing{' '}
34
- <code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
35
- src/app/page.tsx
36
- </code>
37
- .
38
- </li>
39
- <li className="tracking-[-.01em]">
40
- Save and see your changes instantly.
41
- </li>
42
- </ol>
53
+ <BaseLayout data-testid="HomePage">
54
+ {/* Hero Section */}
55
+ <section className="py-16 px-4">
56
+ <div className="max-w-4xl mx-auto text-center">
57
+ <h1 className="text-4xl md:text-5xl font-bold mb-6 text-text">
58
+ {tt.welcome}
59
+ </h1>
60
+ <p className="text-xl text-text-secondary mb-8">{tt.description}</p>
61
+ </div>
62
+ </section>
63
+
64
+ {/* Navigation Grid */}
65
+ <section className="max-w-6xl mx-auto px-4 py-12">
66
+ <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
67
+ {/* {navigationItems.map((item) => (
68
+ <LocaleLink
69
+ data-testid={`HomePage-navigation-${item.href}`}
70
+ key={item.href}
71
+ title={item.titleKey}
72
+ className={clsx(
73
+ href={item.href}
74
+ 'block rounded-lg p-6',
75
+ 'bg-secondary',
76
+ 'border border-border',
77
+ 'hover:bg-elevated',
78
+ 'transition-colors duration-200'
79
+ )}
80
+ >
81
+ <h3 className={`text-xl font-semibold mb-3 text-text`}>
82
+ {t(item.titleKey)}
83
+ </h3>
84
+ <p className="text-text-secondary mb-4">
85
+ {t(item.descriptionKey)}
86
+ </p>
87
+ <Button type="primary" className="w-full">
88
+ {t(i18nKeys.HOME_EXPLORE)}
89
+ </Button>
90
+ </LocaleLink>
91
+ ))} */}
92
+ </div>
93
+ </section>
43
94
 
44
- <div className="flex gap-4 items-center flex-col sm:flex-row">
45
- <a
46
- className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
47
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
48
- target="_blank"
49
- rel="noopener noreferrer"
50
- >
51
- <Image
52
- className="dark:invert"
53
- src="/vercel.svg"
54
- alt="Vercel logomark"
55
- width={20}
56
- height={20}
57
- />
58
- Deploy now
59
- </a>
60
- <a
61
- className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
62
- href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
63
- target="_blank"
64
- rel="noopener noreferrer"
65
- >
66
- Read our docs
67
- </a>
95
+ {/* Call to Action Section */}
96
+ <section className="py-16 px-4 bg-elevated">
97
+ <div className="max-w-4xl mx-auto text-center">
98
+ <h2 className="text-3xl font-bold mb-4 text-text">
99
+ {tt.getStartedTitle}
100
+ </h2>
101
+ <p className="text-lg text-text-secondary mb-8">
102
+ {tt.getStartedDescription}
103
+ </p>
104
+ <Button type="primary" size="large" className="px-8">
105
+ {tt.getStartedButton}
106
+ </Button>
68
107
  </div>
69
- </main>
70
- <footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
71
- <a
72
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
73
- href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
74
- target="_blank"
75
- rel="noopener noreferrer"
76
- >
77
- <Image
78
- aria-hidden
79
- src="/file.svg"
80
- alt="File icon"
81
- width={16}
82
- height={16}
83
- />
84
- Learn
85
- </a>
86
- <a
87
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
88
- href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
89
- target="_blank"
90
- rel="noopener noreferrer"
91
- >
92
- <Image
93
- aria-hidden
94
- src="/window.svg"
95
- alt="Window icon"
96
- width={16}
97
- height={16}
98
- />
99
- Examples
100
- </a>
101
- <a
102
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
103
- href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
104
- target="_blank"
105
- rel="noopener noreferrer"
106
- >
107
- <Image
108
- aria-hidden
109
- src="/globe.svg"
110
- alt="Globe icon"
111
- width={16}
112
- height={16}
113
- />
114
- Go to nextjs.org →
115
- </a>
116
- </footer>
117
- </div>
108
+ </section>
109
+ </BaseLayout>
118
110
  );
119
111
  }
@@ -236,4 +236,16 @@ html,
236
236
  --fe-message-info-color: var(--fe-color-primary);
237
237
  --fe-message-loading-color: var(--fe-color-primary);
238
238
  }
239
+
240
+ .ant-dropdown-css-var {
241
+ /* Control Variables - Default Theme */
242
+ --fe-control-outline-width: 2px;
243
+ --fe-control-interactive-size: 16px;
244
+ --fe-control-item-bg-hover: rgba(0, 0, 0, 0.04);
245
+ --fe-control-item-bg-active: #e6f4ff;
246
+ --fe-control-item-bg-active-hover: #bae0ff;
247
+ --fe-control-item-bg-active-disabled: rgba(0, 0, 0, 0.15);
248
+ --fe-control-tmp-outline: rgba(0, 0, 0, 0.02);
249
+ --fe-control-outline: rgba(96, 165, 250, 0.1); /* blue-400 with 0.1 opacity */
250
+ }
239
251
  }
@@ -175,4 +175,30 @@
175
175
  0.85
176
176
  ); /* 确保文字在深色背景上清晰可见 */
177
177
  }
178
+ .ant-dropdown-css-var {
179
+ /* Control Variables - Dark Theme */
180
+ --fe-control-outline-width: 2px;
181
+ --fe-control-interactive-size: 16px;
182
+ --fe-control-item-bg-hover: rgba(255, 255, 255, 0.08);
183
+ --fe-control-item-bg-active: rgba(
184
+ 96,
185
+ 165,
186
+ 250,
187
+ 0.2
188
+ ); /* blue-400 with 0.2 opacity */
189
+ --fe-control-item-bg-active-hover: rgba(
190
+ 96,
191
+ 165,
192
+ 250,
193
+ 0.3
194
+ ); /* blue-400 with 0.3 opacity */
195
+ --fe-control-item-bg-active-disabled: rgba(255, 255, 255, 0.15);
196
+ --fe-control-tmp-outline: rgba(255, 255, 255, 0.02);
197
+ --fe-control-outline: rgba(
198
+ 96,
199
+ 165,
200
+ 250,
201
+ 0.1
202
+ ); /* blue-400 with 0.1 opacity */
203
+ }
178
204
  }
@@ -201,4 +201,20 @@
201
201
  0 9px 28px 8px rgba(244, 114, 182, 0.05);
202
202
  --fe-modal-mask-bg: rgba(244, 114, 182, 0.45);
203
203
  }
204
+ .ant-dropdown-css-var {
205
+ /* Control Variables - Pink Theme */
206
+ --fe-control-outline-width: 2px;
207
+ --fe-control-interactive-size: 16px;
208
+ --fe-control-item-bg-hover: rgba(0, 0, 0, 0.04);
209
+ --fe-control-item-bg-active: #fce7f3; /* pink-100 */
210
+ --fe-control-item-bg-active-hover: #fbcfe8; /* pink-200 */
211
+ --fe-control-item-bg-active-disabled: rgba(0, 0, 0, 0.15);
212
+ --fe-control-tmp-outline: rgba(0, 0, 0, 0.02);
213
+ --fe-control-outline: rgba(
214
+ 244,
215
+ 114,
216
+ 182,
217
+ 0.1
218
+ ); /* pink-400 with 0.1 opacity */
219
+ }
204
220
  }
@@ -11,9 +11,10 @@
11
11
  --color-secondary: rgba(var(--color-bg-secondary));
12
12
  --color-elevated: rgba(var(--color-bg-elevated));
13
13
  --color-text: rgba(var(--text-primary));
14
+ --color-text-hover: rgba(var(--text-primary-hover));
14
15
  --color-text-secondary: rgba(var(--text-secondary));
15
16
  --color-text-tertiary: rgba(var(--text-tertiary));
16
- --color-border: rgba(var(--color-border));
17
- --color-brand: rgba(var(--color-brand));
18
- --color-brand-hover: rgba(var(--color-brand-hover));
17
+ --color-c-border: rgba(var(--color-border));
18
+ --color-c-brand: rgba(var(--color-brand));
19
+ --color-c-brand-hover: rgba(var(--color-brand-hover));
19
20
  }
@@ -8,6 +8,7 @@
8
8
 
9
9
  /* 文字颜色 */
10
10
  --text-primary: 15 23 42; /* slate-900 */
11
+ --text-primary-hover: 100 116 139; /* slate-500 */
11
12
  --text-secondary: 71 85 105; /* slate-600 */
12
13
  --text-tertiary: 100 116 139; /* slate-500 */
13
14
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  /* 文字颜色 */
10
10
  --text-primary: 255 255 255;
11
+ --text-primary-hover: 148 163 184; /* slate-400 */
11
12
  --text-secondary: 148 163 184; /* slate-400 */
12
13
  --text-tertiary: 100 116 139; /* slate-500 */
13
14
 
@@ -8,6 +8,7 @@
8
8
 
9
9
  /* 文字颜色 */
10
10
  --text-primary: 190 18 60; /* rose-700 */
11
+ --text-primary-hover: 244 63 94; /* rose-500 */
11
12
  --text-secondary: 225 29 72; /* rose-600 */
12
13
  --text-tertiary: 244 63 94; /* rose-500 */
13
14
 
@@ -10,10 +10,12 @@ import { ThemeSwitcher } from './ThemeSwitcher';
10
10
  export function BaseHeader(props: { showLogoutButton?: boolean }) {
11
11
  const { showLogoutButton } = props;
12
12
  const appConfig = useIOC(IOCIdentifier.AppConfig);
13
+ const i18nService = useIOC(IOCIdentifier.I18nServiceInterface);
14
+
13
15
  return (
14
16
  <header
15
- data-testid="base-header"
16
- className="h-14 bg-secondary border-b border-border sticky top-0 z-50"
17
+ data-testid="BaseHeader"
18
+ className="h-14 bg-secondary border-b border-c-border sticky top-0 z-50"
17
19
  >
18
20
  <div className="flex items-center justify-between h-full px-4 mx-auto max-w-7xl">
19
21
  <div className="flex items-center">
@@ -21,12 +23,6 @@ export function BaseHeader(props: { showLogoutButton?: boolean }) {
21
23
  href="/"
22
24
  className="flex items-center hover:opacity-80 transition-opacity"
23
25
  >
24
- {/* <img
25
- data-testid="base-header-logo"
26
- src={IOC(PublicAssetsPath).getPath('/logo.svg')}
27
- alt="logo"
28
- className="h-8 w-auto"
29
- /> */}
30
26
  <span
31
27
  data-testid="base-header-app-name"
32
28
  className="ml-2 text-lg font-semibold text-text"
@@ -35,10 +31,9 @@ export function BaseHeader(props: { showLogoutButton?: boolean }) {
35
31
  </span>
36
32
  </Link>
37
33
  </div>
38
- <div className="flex items-center gap-4">
39
- <LanguageSwitcher />
34
+ <div className="flex items-center gap-2">
35
+ <LanguageSwitcher i18nService={i18nService} />
40
36
  <ThemeSwitcher />
41
-
42
37
  {showLogoutButton && <LogoutButton />}
43
38
  </div>
44
39
  </div>
@@ -0,0 +1,27 @@
1
+ import { BaseHeader } from './BaseHeader';
2
+ import type { HTMLAttributes } from 'react';
3
+
4
+ export interface BaseLayoutProps extends HTMLAttributes<HTMLDivElement> {
5
+ showLogoutButton?: boolean;
6
+ mainProps?: HTMLAttributes<HTMLElement>;
7
+ }
8
+
9
+ export function BaseLayout({
10
+ children,
11
+ showLogoutButton,
12
+ mainProps,
13
+ ...props
14
+ }: BaseLayoutProps) {
15
+ return (
16
+ <div
17
+ data-testid="BaseLayout"
18
+ className="flex flex-col min-h-screen"
19
+ {...props}
20
+ >
21
+ <BaseHeader showLogoutButton={showLogoutButton} />
22
+ <main className="flex flex-1 flex-col bg-primary" {...mainProps}>
23
+ {children}
24
+ </main>
25
+ </div>
26
+ );
27
+ }
@@ -1,24 +1,38 @@
1
1
  'use client';
2
2
 
3
- import { Select } from 'antd';
3
+ import { TranslationOutlined } from '@ant-design/icons';
4
+ import { Dropdown } from 'antd';
4
5
  import { useLocale } from 'next-intl';
5
- import { useCallback } from 'react';
6
+ import { useCallback, useMemo } from 'react';
6
7
  import { i18nConfig } from '@config/i18n';
7
- import { IOCIdentifier } from '@config/IOCIdentifier';
8
- import type { I18nServiceLocale } from '@/base/port/I18nServiceInterface';
8
+ import type {
9
+ I18nServiceInterface,
10
+ I18nServiceLocale
11
+ } from '@/base/port/I18nServiceInterface';
9
12
  import { usePathname, useRouter } from '@/i18n/routing';
10
- import { useIOC } from '../hook/useIOC';
11
- import { useStore } from '../hook/useStore';
12
13
  import type { LocaleType } from '@config/i18n';
14
+ import type { ItemType } from 'antd/es/menu/interface';
13
15
 
14
- export function LanguageSwitcher() {
15
- const i18nService = useIOC(IOCIdentifier.I18nServiceInterface);
16
- const { loading } = useStore(i18nService);
16
+ export function LanguageSwitcher(props: { i18nService: I18nServiceInterface }) {
17
+ const { i18nService } = props;
17
18
  const pathname = usePathname(); // current pathname, aware of i18n
18
19
 
19
20
  const router = useRouter(); // i18n-aware router instance
20
21
  const currentLocale = useLocale() as LocaleType; // currently active locale
21
22
 
23
+ const options: ItemType[] = useMemo(() => {
24
+ return i18nConfig.supportedLngs.map(
25
+ (lang) =>
26
+ ({
27
+ type: 'item',
28
+ key: lang,
29
+ value: lang,
30
+ label:
31
+ i18nConfig.localeNames[lang as keyof typeof i18nConfig.localeNames]
32
+ }) as ItemType
33
+ );
34
+ }, []);
35
+
22
36
  const handleLanguageChange = useCallback(
23
37
  async (value: string) => {
24
38
  // Set a persistent cookie with the user's preferred locale (valid for 1 year)
@@ -35,18 +49,32 @@ export function LanguageSwitcher() {
35
49
  [i18nService, pathname, router]
36
50
  );
37
51
 
52
+ const nextLocale = useMemo(() => {
53
+ const targetIndex = i18nConfig.supportedLngs.indexOf(currentLocale) + 1;
54
+ return i18nConfig.supportedLngs[
55
+ targetIndex % i18nConfig.supportedLngs.length
56
+ ];
57
+ }, [currentLocale]);
58
+
38
59
  return (
39
- <Select
40
- data-testid="LanguageSwitcher"
41
- loading={loading}
42
- value={currentLocale}
43
- onChange={handleLanguageChange}
44
- options={i18nConfig.supportedLngs.map((lang) => ({
45
- value: lang,
46
- label:
47
- i18nConfig.localeNames[lang as keyof typeof i18nConfig.localeNames]
48
- }))}
49
- className="w-24"
50
- />
60
+ <Dropdown
61
+ data-testid="LanguageSwitcherDropdown"
62
+ trigger={['hover']}
63
+ menu={{
64
+ selectedKeys: [currentLocale],
65
+ items: options,
66
+ onClick: ({ key }) => {
67
+ handleLanguageChange(key);
68
+ }
69
+ }}
70
+ >
71
+ <span
72
+ data-testid="LanguageSwitcher"
73
+ className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
74
+ onClick={() => handleLanguageChange(nextLocale)}
75
+ >
76
+ <TranslationOutlined />
77
+ </span>
78
+ </Dropdown>
51
79
  );
52
80
  }
@@ -1,73 +1,84 @@
1
1
  'use client';
2
2
 
3
3
  import {
4
- BulbOutlined,
5
- BulbFilled,
6
4
  HeartFilled,
7
- HeartOutlined,
8
- DesktopOutlined
5
+ SettingOutlined,
6
+ SunOutlined,
7
+ MoonOutlined,
8
+ SettingFilled,
9
+ SunFilled,
10
+ MoonFilled,
11
+ HeartOutlined
9
12
  } from '@ant-design/icons';
10
- import { Select } from 'antd';
13
+ import { Dropdown } from 'antd';
11
14
  import { clsx } from 'clsx';
12
15
  import { useTheme } from 'next-themes';
13
- import { themeConfig } from '@config/theme';
16
+ import { useMemo } from 'react';
17
+ import { type SupportedTheme, themeConfig } from '@config/theme';
14
18
  import { useMountedClient } from '../hook/useMountedClient';
19
+ import type { ItemType } from 'antd/es/menu/interface';
15
20
 
16
21
  const { supportedThemes } = themeConfig;
17
22
 
23
+ const defaultTheme = supportedThemes[0] || 'system';
24
+ const themesList = ['system', ...supportedThemes];
25
+
18
26
  const colorMap: Record<
19
27
  string,
20
- { i18nkey: string; colors: string[]; icons: React.ElementType[] }
28
+ {
29
+ i18nkey: string;
30
+ selectedColor: string;
31
+ normalColor: string;
32
+ Icon: React.ElementType;
33
+ SelectedIcon: React.ElementType;
34
+ TriggerIcon: React.ElementType;
35
+ }
21
36
  > = {
22
37
  system: {
23
38
  i18nkey: 'System',
24
- colors: ['text-text', 'text-text-secondary'],
25
- icons: [DesktopOutlined, DesktopOutlined]
39
+ selectedColor: 'text-text',
40
+ normalColor: 'text-text-secondary',
41
+ Icon: SettingOutlined,
42
+ SelectedIcon: SettingFilled,
43
+ TriggerIcon: SettingOutlined
26
44
  },
27
45
  light: {
28
46
  i18nkey: 'Light',
29
- colors: ['text-text', 'text-text-secondary'],
30
- icons: [BulbFilled, BulbOutlined]
47
+ selectedColor: 'text-text',
48
+ normalColor: 'text-text-secondary',
49
+ Icon: SunOutlined,
50
+ SelectedIcon: SunFilled,
51
+ TriggerIcon: SunOutlined
31
52
  },
32
53
  dark: {
33
54
  i18nkey: 'Dark',
34
- colors: ['text-[#9333ea]', 'text-[#a855f7]'],
35
- icons: [BulbFilled, BulbOutlined]
55
+ selectedColor: 'text-[#9333ea]',
56
+ normalColor: 'text-[#a855f7]',
57
+ Icon: MoonOutlined,
58
+ SelectedIcon: MoonFilled,
59
+ TriggerIcon: MoonOutlined
36
60
  },
37
61
  pink: {
38
62
  i18nkey: 'Pink',
39
- colors: ['text-[#f472b6]', 'text-[#ec4899]'],
40
- icons: [HeartFilled, HeartOutlined]
63
+ selectedColor: 'text-[#f472b6]',
64
+ normalColor: 'text-[#ec4899]',
65
+ Icon: HeartOutlined,
66
+ SelectedIcon: HeartFilled,
67
+ TriggerIcon: HeartOutlined
41
68
  }
42
69
  };
43
70
 
44
71
  export function ThemeSwitcher() {
45
- const { theme, resolvedTheme, setTheme } = useTheme();
72
+ const { theme: currentTheme, resolvedTheme, setTheme } = useTheme();
46
73
  const mounted = useMountedClient();
47
74
 
48
- // 如果组件未挂载,返回空的 Select 以避免闪烁
49
- if (!mounted) {
50
- return (
51
- <Select
52
- data-testid="ThemeSwitcher"
53
- loading
54
- value="system"
55
- options={[]}
56
- style={{ width: 120 }}
57
- className="min-w-40 max-w-full"
58
- disabled
59
- />
60
- );
61
- }
62
-
63
- const themeOptions = ['system', ...supportedThemes!].map((themeName) => {
64
- const { i18nkey, colors, icons } = colorMap[themeName] || colorMap.light;
65
- const [currentColor, normalColor] = colors;
66
- const [CurrentIcon, NormalIcon] = icons;
75
+ const themeOptions = themesList.map((themeName) => {
76
+ const { i18nkey, selectedColor, normalColor, Icon, SelectedIcon } =
77
+ colorMap[themeName] || colorMap.light;
67
78
 
68
79
  const isCurrentTheme =
69
- theme === themeName ||
70
- (themeName === resolvedTheme && theme === 'system');
80
+ currentTheme === themeName ||
81
+ (themeName === resolvedTheme && currentTheme === 'system');
71
82
 
72
83
  return {
73
84
  key: themeName,
@@ -76,24 +87,57 @@ export function ThemeSwitcher() {
76
87
  <div
77
88
  className={clsx(
78
89
  'flex items-center gap-2',
79
- isCurrentTheme ? currentColor : normalColor
90
+ isCurrentTheme ? selectedColor : normalColor
80
91
  )}
81
92
  >
82
- {isCurrentTheme ? <CurrentIcon /> : <NormalIcon />}
93
+ {isCurrentTheme ? <SelectedIcon /> : <Icon />}
83
94
  <span>{i18nkey}</span>
84
95
  </div>
85
96
  )
86
- };
97
+ } as ItemType;
87
98
  });
88
99
 
100
+ const nextTheme = useMemo(() => {
101
+ if (!currentTheme) {
102
+ return defaultTheme;
103
+ }
104
+ const targetIndex =
105
+ supportedThemes.indexOf(currentTheme as SupportedTheme) + 1;
106
+ return supportedThemes[targetIndex % supportedThemes.length];
107
+ }, [currentTheme]);
108
+
109
+ const TriggerIcon = colorMap[currentTheme || defaultTheme].TriggerIcon;
110
+
111
+ if (!mounted) {
112
+ return (
113
+ <span
114
+ data-testid="ThemeSwitcher"
115
+ className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
116
+ >
117
+ <SettingOutlined />
118
+ </span>
119
+ );
120
+ }
121
+
89
122
  return (
90
- <Select
91
- data-testid="ThemeSwitcher"
92
- value={theme}
93
- onChange={setTheme}
94
- options={themeOptions}
95
- style={{ width: 120 }}
96
- className="min-w-40 max-w-full"
97
- />
123
+ <Dropdown
124
+ data-testid="ThemeSwitcherDropdown"
125
+ trigger={['hover']}
126
+ menu={{
127
+ items: themeOptions,
128
+ selectedKeys: [currentTheme!],
129
+ onClick: ({ key }) => {
130
+ setTheme(key);
131
+ }
132
+ }}
133
+ >
134
+ <span
135
+ data-testid="ThemeSwitcher"
136
+ className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
137
+ onClick={() => setTheme(nextTheme)}
138
+ >
139
+ <TriggerIcon />
140
+ </span>
141
+ </Dropdown>
98
142
  );
99
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlover/create-app",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "Create a new app with a single command",
5
5
  "private": false,
6
6
  "type": "module",