diff --git a/package-lock.json b/package-lock.json index e7c7d2f3c..d849a508b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,9 +15,15 @@ "@angular/platform-browser": "^21", "@angular/platform-browser-dynamic": "^21", "@angular/router": "^21", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/state": "^6.5.4", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.39.13", "@primeuix/themes": "^2.0.0", "@tailwindcss/postcss": "^4.1.11", "chart.js": "4.4.2", + "codemirror": "^6.0.2", + "js-yaml": "^4.1.1", "primeclt": "^0.1.5", "primeicons": "^7.0.0", "primeng": "^21.0.2", @@ -31,6 +37,7 @@ "@angular/cli": "^21", "@angular/compiler-cli": "^21", "@types/jasmine": "~5.1.0", + "@types/js-yaml": "^4.0.9", "autoprefixer": "^10.4.20", "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", @@ -709,7 +716,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.6.tgz", "integrity": "sha512-Yd8PF0dR37FAzqEcBHAyVCiSGMJOezSJe6rV/4BC6AVLfaZ7oZLl8CNVxKsod2UHd6rKxt1hzx05QdVcVvYNeA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -726,7 +732,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.0.6.tgz", "integrity": "sha512-rBMzG7WnQMouFfDST+daNSAOVYdtw560645PhlxyVeIeHMlCm0j1jjBgVPGTBNpVgKRdT/sqbi6W6JYkY9mERA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -740,7 +745,6 @@ "integrity": "sha512-UcIUx+fbn0VLlCBCIYxntAzWG3zPRUo0K7wvuK0MC6ZFCWawgewx9SdLLZTqcaWe1g5FRQlQeVQcFgHAO5R2Mw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.4", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -773,7 +777,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.6.tgz", "integrity": "sha512-SvWbOkkrsqprYJSBmzQEWkWjfZB/jkRYyFp2ClMJBPqOLxP1a+i3Om2rolcNQjZPz87bs9FszwgRlXUy7sw5cQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -799,7 +802,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.0.6.tgz", "integrity": "sha512-aAkAAKuUrP8U7R4aH/HbmG/CXP90GlML77ECBI5b4qCSb+bvaTEYsaf85mCyTpr9jvGkia2LTe42hPcOuyzdsQ==", "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "tslib": "^2.3.0" @@ -819,7 +821,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.0.6.tgz", "integrity": "sha512-tPk8rlUEBPXIUPRYq6Xu7QhJgKtnVr0dOHHuhyi70biKTupr5VikpZC5X9dy2Q3H3zYbK6MHC6384YMuwfU2kg==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -860,7 +861,6 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.0.6.tgz", "integrity": "sha512-HOfomKq7jRSgxt/uUvpdbB8RNaYuGB/FJQ3BfQCFfGw1O9L3B72b7Hilk6AcjCruul6cfv/kmT4EB6Vqi3dQtA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -905,7 +905,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -2517,6 +2516,114 @@ "node": ">=6.9.0" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", + "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", + "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz", + "integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/lr": "^1.0.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", + "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.3.tgz", + "integrity": "sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.39.13", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.13.tgz", + "integrity": "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -3464,7 +3571,6 @@ "integrity": "sha512-X7/+dG9SLpSzRkwgG5/xiIzW0oMrV3C0HOa7YHG1WnrLK+vCQHfte4k/T80059YBdei29RBC3s+pSMvPJDU9/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.0", "@inquirer/confirm": "^5.1.19", @@ -3814,6 +3920,41 @@ "dev": true, "license": "MIT" }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", + "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", @@ -3929,6 +4070,12 @@ "win32" ] }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.24.0.tgz", @@ -6353,6 +6500,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -6733,7 +6887,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6821,7 +6974,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6987,7 +7139,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { @@ -7450,7 +7601,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7913,6 +8063,21 @@ "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", "license": "MIT" }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -8270,6 +8435,12 @@ } } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -8762,7 +8933,6 @@ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "iconv-lite": "^0.6.2" } @@ -9197,7 +9367,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9591,7 +9760,6 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -11714,8 +11882,7 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.4.0.tgz", "integrity": "sha512-T4fio3W++llLd7LGSGsioriDHgWyhoL6YTu4k37uwJLF7DzOzspz7mNxRoM3cQdLWtL/ebazQpIf/yZGJx/gzg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jest-worker": { "version": "27.5.1", @@ -11778,7 +11945,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -11877,7 +12043,6 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -12367,7 +12532,6 @@ "integrity": "sha512-j1n1IuTX1VQjIy3tT7cyGbX7nvQOsFLoIqobZv4ttI5axP923gA44zUj6miiA6R5Aoms4sEGVIIcucXUbRI14g==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -14633,7 +14797,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -14782,7 +14945,6 @@ "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -15372,17 +15534,6 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, - "node_modules/primeclt/node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "optional": true, - "peer": true, - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/primeicons": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", @@ -16043,7 +16194,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -16135,7 +16285,6 @@ "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -17176,6 +17325,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -17205,8 +17360,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-primeui": { "version": "0.6.1", @@ -17263,7 +17417,6 @@ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", @@ -17465,8 +17618,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.21.0", @@ -18090,7 +18242,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18928,6 +19079,12 @@ "node": ">=0.10.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", @@ -18990,7 +19147,6 @@ "integrity": "sha512-5DeICTX8BVgNp6afSPYXAFjskIgWGlygQH58bcozPOXgo2r/6xx39Y1+cULZ3gTxUYQP88jmwLj2anu4Xaq84g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -19865,7 +20021,6 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -20005,7 +20160,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 9e030faf9..50944531a 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,15 @@ "@angular/platform-browser": "^21", "@angular/platform-browser-dynamic": "^21", "@angular/router": "^21", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/state": "^6.5.4", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.39.13", "@primeuix/themes": "^2.0.0", "@tailwindcss/postcss": "^4.1.11", "chart.js": "4.4.2", + "codemirror": "^6.0.2", + "js-yaml": "^4.1.1", "primeclt": "^0.1.5", "primeicons": "^7.0.0", "primeng": "^21.0.2", @@ -34,6 +40,7 @@ "@angular/cli": "^21", "@angular/compiler-cli": "^21", "@types/jasmine": "~5.1.0", + "@types/js-yaml": "^4.0.9", "autoprefixer": "^10.4.20", "eslint": "^9.14.0", "eslint-config-prettier": "^9.1.0", diff --git a/src/app.component.ts b/src/app.component.ts index e124abf3e..d023fb3a9 100644 --- a/src/app.component.ts +++ b/src/app.component.ts @@ -1,5 +1,6 @@ -import { Component } from '@angular/core'; +import { Component, inject, OnInit } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { LayoutService } from './app/layout/service/layout.service'; @Component({ selector: 'app-root', @@ -7,4 +8,13 @@ import { RouterModule } from '@angular/router'; imports: [RouterModule], template: `` }) -export class AppComponent {} +export class AppComponent implements OnInit { + private layoutService = inject(LayoutService); + + ngOnInit() { + // Force initialization of LayoutService to load theme from storage + // This ensures the theme is applied before any components render + const config = this.layoutService.layoutConfig(); + console.log('App initialized with theme config:', config); + } +} diff --git a/src/app.routes.ts b/src/app.routes.ts index 96e5ba31e..d1ab85883 100644 --- a/src/app.routes.ts +++ b/src/app.routes.ts @@ -3,7 +3,9 @@ import { AppLayout } from './app/layout/component/app.layout'; import { Dashboard } from './app/pages/dashboard/dashboard'; import { Documentation } from './app/pages/documentation/documentation'; import { Landing } from './app/pages/landing/landing'; +import { Layout } from './app/pages/layout/layout'; import { Notfound } from './app/pages/notfound/notfound'; +import { LayoutBuilder } from './app/pages/builder/builder'; export const appRoutes: Routes = [ { @@ -17,6 +19,8 @@ export const appRoutes: Routes = [ ] }, { path: 'landing', component: Landing }, + { path: 'layout', component: Layout }, + { path: 'builder', component: LayoutBuilder }, { path: 'notfound', component: Notfound }, { path: 'auth', loadChildren: () => import('./app/pages/auth/auth.routes') }, { path: '**', redirectTo: '/notfound' } diff --git a/src/app/layout/component/app.configurator.ts b/src/app/layout/component/app.configurator.ts index b8c7ff2d2..db83373e4 100644 --- a/src/app/layout/component/app.configurator.ts +++ b/src/app/layout/component/app.configurator.ts @@ -9,6 +9,7 @@ import Nora from '@primeuix/themes/nora'; import { PrimeNG } from 'primeng/config'; import { SelectButtonModule } from 'primeng/selectbutton'; import { LayoutService } from '@/app/layout/service/layout.service'; +import { ConfigService } from '@/app/pages/service/config.service'; const presets = { Aura, @@ -102,6 +103,8 @@ export class AppConfigurator { layoutService: LayoutService = inject(LayoutService); + configService = inject(ConfigService); + platformId = inject(PLATFORM_ID); primeng = inject(PrimeNG); @@ -417,8 +420,10 @@ export class AppConfigurator { updateColors(event: any, type: string, color: any) { if (type === 'primary') { this.layoutService.layoutConfig.update((state) => ({ ...state, primary: color.name })); + this.configService.updateThemeConfig({ primaryColor: color.name }); } else if (type === 'surface') { this.layoutService.layoutConfig.update((state) => ({ ...state, surface: color.name })); + this.configService.updateThemeConfig({ surfaceColor: color.name }); } this.applyTheme(type, color); @@ -435,6 +440,7 @@ export class AppConfigurator { onPresetChange(event: any) { this.layoutService.layoutConfig.update((state) => ({ ...state, preset: event })); + this.configService.updateThemeConfig({ preset: event }); const preset = presets[event as KeyOfType]; const surfacePalette = this.surfaces.find((s) => s.name === this.selectedSurfaceColor())?.palette; $t().preset(preset).preset(this.getPresetExt()).surfacePalette(surfacePalette).use({ useDefaultOptions: true }); diff --git a/src/app/layout/component/app.menu.ts b/src/app/layout/component/app.menu.ts index fe501586f..deb9d6e3c 100644 --- a/src/app/layout/component/app.menu.ts +++ b/src/app/layout/component/app.menu.ts @@ -93,6 +93,21 @@ export class AppMenu { label: 'Empty', icon: 'pi pi-fw pi-circle-off', routerLink: ['/pages/empty'] + }, + { + label: 'Layout', + icon: 'pi pi-fw pi-objects-column', + routerLink: ['/layout'] + }, + { + label: 'Config', + icon: 'pi pi-fw pi-cog', + routerLink: ['/pages/config'] + }, + { + label: 'Layout Builder', + icon: 'pi pi-fw pi-code', + routerLink: ['/builder'] } ] }, diff --git a/src/app/layout/component/app.topbar.ts b/src/app/layout/component/app.topbar.ts index 9baa4619c..d47ecaf33 100644 --- a/src/app/layout/component/app.topbar.ts +++ b/src/app/layout/component/app.topbar.ts @@ -3,13 +3,13 @@ import { MenuItem } from 'primeng/api'; import { RouterModule } from '@angular/router'; import { CommonModule } from '@angular/common'; import { StyleClassModule } from 'primeng/styleclass'; -import { AppConfigurator } from './app.configurator'; import { LayoutService } from '@/app/layout/service/layout.service'; +import { ConfigService } from '@/app/pages/service/config.service'; @Component({ selector: 'app-topbar', standalone: true, - imports: [RouterModule, CommonModule, StyleClassModule, AppConfigurator], + imports: [RouterModule, CommonModule, StyleClassModule], template: `
-
- - -
+ } +
+ + +
+

Surface Color

+
+ @for (surface of surfaces; track surface.name) { + + } +
+
+ +
+

Preset

+ +
+ + + + + +
+ +
+ +

Notification Settings

+

+ Enable/disable notifications, configure notification sounds, and manage notification preferences. + This feature will be implemented in a future update. +

+
+
+
+ + + +
+ +
+ +

Dashboard Widgets

+

+ Customize dashboard widgets, set refresh intervals, and configure widget layouts. + This feature will be implemented in a future update. +

+
+
+
+ + + +
+ +
+ +

Advanced Settings

+

+ Configure advanced application settings, API endpoints, performance tuning, and more. + This feature will be implemented in a future update. +

+
+
+
+ + + + + + `, +}) +export class Config { + configCode = signal(''); + editMode = signal<'yaml' | 'json'>('yaml'); + errorMessage = signal(''); + isYamlMode = true; + themeConfig = signal({ + darkMode: false, + primaryColor: 'emerald', + surfaceColor: 'slate', + preset: 'Aura' + }); + + layoutService = inject(LayoutService); + platformId = inject(PLATFORM_ID); + primeng = inject(PrimeNG); + + presets = Object.keys(presets); + + surfaces: SurfacesType[] = [ + { name: 'slate', palette: { 0: '#ffffff', 50: '#f8fafc', 100: '#f1f5f9', 200: '#e2e8f0', 300: '#cbd5e1', 400: '#94a3b8', 500: '#64748b', 600: '#475569', 700: '#334155', 800: '#1e293b', 900: '#0f172a', 950: '#020617' } }, + { name: 'gray', palette: { 0: '#ffffff', 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db', 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151', 800: '#1f2937', 900: '#111827', 950: '#030712' } }, + { name: 'zinc', palette: { 0: '#ffffff', 50: '#fafafa', 100: '#f4f4f5', 200: '#e4e4e7', 300: '#d4d4d8', 400: '#a1a1aa', 500: '#71717a', 600: '#52525b', 700: '#3f3f46', 800: '#27272a', 900: '#18181b', 950: '#09090b' } }, + { name: 'neutral', palette: { 0: '#ffffff', 50: '#fafafa', 100: '#f5f5f5', 200: '#e5e5e5', 300: '#d4d4d4', 400: '#a3a3a3', 500: '#737373', 600: '#525252', 700: '#404040', 800: '#262626', 900: '#171717', 950: '#0a0a0a' } }, + { name: 'stone', palette: { 0: '#ffffff', 50: '#fafaf9', 100: '#f5f5f4', 200: '#e7e5e4', 300: '#d6d3d1', 400: '#a8a29e', 500: '#78716c', 600: '#57534e', 700: '#44403c', 800: '#292524', 900: '#1c1917', 950: '#0c0a09' } }, + { name: 'soho', palette: { 0: '#ffffff', 50: '#ececec', 100: '#dedfdf', 200: '#c4c4c6', 300: '#adaeb0', 400: '#97979b', 500: '#7f8084', 600: '#6a6b70', 700: '#55565b', 800: '#3f4046', 900: '#2c2c34', 950: '#16161d' } }, + { name: 'viva', palette: { 0: '#ffffff', 50: '#f3f3f3', 100: '#e7e7e8', 200: '#cfd0d0', 300: '#b7b8b9', 400: '#9fa1a1', 500: '#87898a', 600: '#6e7173', 700: '#565a5b', 800: '#3e4244', 900: '#262b2c', 950: '#0e1315' } }, + { name: 'ocean', palette: { 0: '#ffffff', 50: '#fbfcfc', 100: '#F7F9F8', 200: '#EFF3F2', 300: '#DADEDD', 400: '#B1B7B6', 500: '#828787', 600: '#5F7274', 700: '#415B61', 800: '#29444E', 900: '#183240', 950: '#0c1920' } } + ]; + + selectedPrimaryColor = computed(() => this.layoutService.layoutConfig().primary); + selectedSurfaceColor = computed(() => this.layoutService.layoutConfig().surface); + selectedPreset = computed(() => this.layoutService.layoutConfig().preset); + + primaryColors = computed(() => { + const presetPalette = presets[this.layoutService.layoutConfig().preset as KeyOfType].primitive; + const colors = ['emerald', 'green', 'lime', 'orange', 'amber', 'yellow', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose']; + const palettes: SurfacesType[] = [{ name: 'noir', palette: {} }]; + colors.forEach((color) => { + palettes.push({ + name: color, + palette: presetPalette?.[color as KeyOfType] as SurfacesType['palette'] + }); + }); + return palettes; + }); + + constructor( + private configService: ConfigService, + private messageService: MessageService + ) { + this.reloadConfigCode(); + this.loadThemeConfig(); + console.log('Config component initialized, code length:', this.configCode().length); + + if (isPlatformBrowser(this.platformId)) { + // Load and apply saved theme on initialization + const savedTheme = this.configService.getThemeConfig(); + this.layoutService.layoutConfig.update(state => ({ + ...state, + darkTheme: savedTheme.darkMode, + primary: savedTheme.primaryColor, + surface: savedTheme.surfaceColor, + preset: savedTheme.preset + })); + this.onPresetChange(savedTheme.preset); + } + } + + ngOnInit() { + } + + loadThemeConfig() { + const theme = this.configService.getThemeConfig(); + this.themeConfig.set(theme); + } + + updateDarkMode(darkMode: boolean) { + this.configService.updateThemeConfig({ darkMode }); + this.themeConfig.update(config => ({ ...config, darkMode })); + + // Update LayoutService + this.layoutService.layoutConfig.update(state => ({ + ...state, + darkTheme: darkMode + })); + + this.messageService.add({ + severity: 'success', + summary: 'Theme Updated', + detail: `Switched to ${darkMode ? 'dark' : 'light'} mode` + }); + } + + toggleEditMode() { + this.editMode.set(this.isYamlMode ? 'yaml' : 'json'); + this.reloadConfigCode(); + } + + reloadConfigCode() { + this.errorMessage.set(''); + if (this.editMode() === 'yaml') { + this.configCode.set(this.configService.exportConfigAsYaml()); + } else { + this.configCode.set(this.configService.exportConfigAsJson()); + } + } + + applyConfigCode() { + try { + this.errorMessage.set(''); + console.log('Applying config, mode:', this.editMode(), 'code length:', this.configCode().length); + if (this.editMode() === 'yaml') { + this.configService.importConfigFromYaml(this.configCode()); + } else { + this.configService.importConfigFromJson(this.configCode()); + } + this.messageService.add({ + severity: 'success', + summary: 'Success', + detail: 'Configuration applied successfully' + }); + console.log('Config applied successfully'); + } catch (e) { + console.error('Failed to apply config:', e); + this.errorMessage.set((e as Error).message); + this.messageService.add({ + severity: 'error', + summary: 'Error', + detail: (e as Error).message + }); + } + } + + exportYaml() { + const yaml = this.configService.exportConfigAsYaml(); + this.downloadFile(yaml, 'config.yaml', 'text/yaml'); + this.messageService.add({ + severity: 'success', + summary: 'Exported', + detail: 'Configuration exported as YAML' + }); + } + + exportJson() { + const json = this.configService.exportConfigAsJson(); + this.downloadFile(json, 'config.json', 'application/json'); + this.messageService.add({ + severity: 'success', + summary: 'Exported', + detail: 'Configuration exported as JSON' + }); + } + + resetConfig() { + this.configService.resetConfig(); + this.reloadConfigCode(); + this.messageService.add({ + severity: 'success', + summary: 'Reset', + detail: 'Configuration reset to default' + }); + } + + onFileSelect(event: any) { + const file = event.files[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e: any) => { + try { + const content = e.target.result; + const isJson = file.name.endsWith('.json'); + + if (isJson) { + this.configService.importConfigFromJson(content); + } else { + this.configService.importConfigFromYaml(content); + } + + this.reloadConfigCode(); + this.messageService.add({ + severity: 'success', + summary: 'Imported', + detail: `Configuration imported from ${file.name}` + }); + } catch (e) { + this.errorMessage.set((e as Error).message); + this.messageService.add({ + severity: 'error', + summary: 'Import Failed', + detail: (e as Error).message + }); + } + }; + reader.readAsText(file); + } + + getPresetExt() { + const color: SurfacesType = this.primaryColors().find((c) => c.name === this.selectedPrimaryColor()) || {}; + const preset = this.layoutService.layoutConfig().preset; + + if (color.name === 'noir') { + return { + semantic: { + primary: { 50: '{surface.50}', 100: '{surface.100}', 200: '{surface.200}', 300: '{surface.300}', 400: '{surface.400}', 500: '{surface.500}', 600: '{surface.600}', 700: '{surface.700}', 800: '{surface.800}', 900: '{surface.900}', 950: '{surface.950}' }, + colorScheme: { + light: { primary: { color: '{primary.950}', contrastColor: '#ffffff', hoverColor: '{primary.800}', activeColor: '{primary.700}' }, highlight: { background: '{primary.950}', focusBackground: '{primary.700}', color: '#ffffff', focusColor: '#ffffff' } }, + dark: { primary: { color: '{primary.50}', contrastColor: '{primary.950}', hoverColor: '{primary.200}', activeColor: '{primary.300}' }, highlight: { background: '{primary.50}', focusBackground: '{primary.300}', color: '{primary.950}', focusColor: '{primary.950}' } } + } + } + }; + } else { + if (preset === 'Nora') { + return { + semantic: { + primary: color.palette, + colorScheme: { + light: { primary: { color: '{primary.600}', contrastColor: '#ffffff', hoverColor: '{primary.700}', activeColor: '{primary.800}' }, highlight: { background: '{primary.600}', focusBackground: '{primary.700}', color: '#ffffff', focusColor: '#ffffff' } }, + dark: { primary: { color: '{primary.500}', contrastColor: '{surface.900}', hoverColor: '{primary.400}', activeColor: '{primary.300}' }, highlight: { background: '{primary.500}', focusBackground: '{primary.400}', color: '{surface.900}', focusColor: '{surface.900}' } } + } + } + }; + } else { + return { + semantic: { + primary: color.palette, + colorScheme: { + light: { primary: { color: '{primary.500}', contrastColor: '#ffffff', hoverColor: '{primary.600}', activeColor: '{primary.700}' }, highlight: { background: '{primary.50}', focusBackground: '{primary.100}', color: '{primary.700}', focusColor: '{primary.800}' } }, + dark: { primary: { color: '{primary.400}', contrastColor: '{surface.900}', hoverColor: '{primary.300}', activeColor: '{primary.200}' }, highlight: { background: 'color-mix(in srgb, {primary.400}, transparent 84%)', focusBackground: 'color-mix(in srgb, {primary.400}, transparent 76%)', color: 'rgba(255,255,255,.87)', focusColor: 'rgba(255,255,255,.87)' } } + } + } + }; + } + } + } + + updateColors(event: any, type: string, color: any) { + if (type === 'primary') { + this.layoutService.layoutConfig.update((state) => ({ ...state, primary: color.name })); + this.configService.updateThemeConfig({ primaryColor: color.name }); + } else if (type === 'surface') { + this.layoutService.layoutConfig.update((state) => ({ ...state, surface: color.name })); + this.configService.updateThemeConfig({ surfaceColor: color.name }); + } + this.applyTheme(type, color); + event.stopPropagation(); + } + + applyTheme(type: string, color: any) { + if (type === 'primary') { + updatePreset(this.getPresetExt()); + } else if (type === 'surface') { + updateSurfacePalette(color.palette); + } + } + + onPresetChange(event: any) { + this.layoutService.layoutConfig.update((state) => ({ ...state, preset: event })); + this.configService.updateThemeConfig({ preset: event }); + const preset = presets[event as KeyOfType]; + const surfacePalette = this.surfaces.find((s) => s.name === this.selectedSurfaceColor())?.palette; + $t().preset(preset).preset(this.getPresetExt()).surfacePalette(surfacePalette).use({ useDefaultOptions: true }); + } + + private downloadFile(content: string, filename: string, mimeType: string) { + const blob = new Blob([content], { type: mimeType }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + link.click(); + window.URL.revokeObjectURL(url); + } +} diff --git a/src/app/pages/layout/layout.ts b/src/app/pages/layout/layout.ts new file mode 100644 index 000000000..5112553ed --- /dev/null +++ b/src/app/pages/layout/layout.ts @@ -0,0 +1,495 @@ +import { Component, signal, computed, effect } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { DrawerModule } from 'primeng/drawer'; +import { ToolbarModule } from 'primeng/toolbar'; +import { ButtonModule } from 'primeng/button'; +import { PanelMenuModule } from 'primeng/panelmenu'; +import { AvatarModule } from 'primeng/avatar'; +import { BadgeModule } from 'primeng/badge'; +import { InputTextModule } from 'primeng/inputtext'; +import { TextareaModule } from 'primeng/textarea'; +import { MenuItem } from 'primeng/api'; +import { ConfigService } from '../service/config.service'; + +interface EditableMenuItem { + label: string; + icon: string; + routerLink?: string; + items?: EditableMenuItem[]; +} + +const DEFAULT_MENU: EditableMenuItem[] = [ + { + label: 'Home', + icon: 'pi pi-home', + items: [ + { label: 'Dashboard', icon: 'pi pi-chart-bar', routerLink: '/dashboard' }, + { label: 'Analytics', icon: 'pi pi-chart-line', routerLink: '/analytics' } + ] + }, + { + label: 'Users', + icon: 'pi pi-users', + items: [ + { label: 'List', icon: 'pi pi-list', routerLink: '/users' }, + { label: 'Create', icon: 'pi pi-user-plus', routerLink: '/users/create' }, + { label: 'Roles', icon: 'pi pi-shield', routerLink: '/users/roles' } + ] + }, + { + label: 'Content', + icon: 'pi pi-file', + items: [ + { label: 'Articles', icon: 'pi pi-file-edit', routerLink: '/content/articles' }, + { label: 'Media', icon: 'pi pi-images', routerLink: '/content/media' }, + { label: 'Categories', icon: 'pi pi-tags', routerLink: '/content/categories' } + ] + }, + { + label: 'Settings', + icon: 'pi pi-cog', + items: [ + { label: 'General', icon: 'pi pi-sliders-h', routerLink: '/settings/general' }, + { label: 'Security', icon: 'pi pi-lock', routerLink: '/settings/security' }, + { label: 'Integrations', icon: 'pi pi-link', routerLink: '/settings/integrations' } + ] + } +]; + +@Component({ + selector: 'app-layout-showcase', + standalone: true, + imports: [ + CommonModule, FormsModule, DrawerModule, ToolbarModule, ButtonModule, + PanelMenuModule, AvatarModule, BadgeModule, + InputTextModule, TextareaModule + ], + template: ` +
+ + + + + Layout Showcase + + + + + + + + +
+ + @if (leftVisible()) { +
+ @if (mode() === 'demo') { + + } @else { + +
+ @for (group of menuItems(); track group.label; let gi = $index) { +
+ @if (isEditing(gi, -1)) { +
+ + +
+ + +
+
+ } @else { +
+
+ + {{ group.label }} +
+ +
+ } + + @if (group.items) { + @for (item of group.items; track item.label; let ii = $index) { + @if (isEditing(gi, ii)) { +
+ + + +
+ + +
+
+ } @else { +
+
+ + {{ item.label }} +
+ +
+ } + } +
+ +
+ } +
+ } + +
+ } +
+ } + + +
+ @if (mode() === 'demo') { + +
+ @for (stat of stats; track stat.label) { +
+
+
+
+ {{ stat.label }} +
{{ stat.value }}
+
+
+ +
+
+ {{ stat.change }} + {{ stat.period }} +
+
+ } +
+ +
+
Main Content Area
+

+ This is a full-page layout showcase with its own topbar, left navigation sidenav, right settings drawer, main content area, and footer. + Switch to Config mode using the toggle in the topbar to customize the navigation menu items via code or visual editing. +

+
+ } @else { + +
+
Live Preview
+

+ Edit menu items by clicking them in the left sidenav, use the visual form in the right panel, or edit the JSON code directly. +

+
+
Current Menu Structure
+
{{ menuItemsPreview() }}
+
+
+ } +
+ + + + @if (mode() === 'demo') { +
+
+
Notifications
+
+ @for (notification of notifications; track notification.id) { +
+ +
+
{{ notification.title }}
+
{{ notification.time }}
+
+
+ } +
+
+ +
+
Quick Actions
+
+ + + +
+
+ +
+
Activity
+
+
+ + System updated successfully +
+
+ + New user registered +
+
+ + Server maintenance scheduled +
+
+
+
+ } @else { +
+ +
+
Add Menu Group
+
+ + + +
+
+ +
+
Add Menu Item
+
+ + + + + +
+
+ + +
+
Reorder Groups
+
+ @for (group of menuItems(); track group.label; let i = $index) { +
+
+ + {{ group.label }} +
+
+ + +
+
+ } +
+
+ + +
+
+
Code Editor (JSON)
+ +
+ @if (jsonError()) { +
{{ jsonError() }}
+ } + +
+
+ } +
+
+ + +
+ © 2026 Layout Showcase. All rights reserved. + +
+
+ ` +}) +export class Layout { + mode = signal<'demo' | 'config'>('demo'); + leftVisible = signal(true); + rightVisible = signal(false); + menuItems = signal([]); + menuJson = signal(''); + jsonError = signal(null); + + editingGroup = signal(-1); + editingItem = signal(-1); + + // Form fields for adding items + newGroupLabel = ''; + newGroupIcon = 'pi pi-folder'; + newItemParentIndex = -1; + newItemLabel = ''; + newItemIcon = 'pi pi-circle'; + newItemRoute = ''; + + stats = [ + { label: 'Orders', value: '152', icon: 'pi pi-shopping-cart', iconClass: 'text-blue-500', bgClass: 'bg-blue-100 dark:bg-blue-400/10', change: '24 new', period: 'since last visit' }, + { label: 'Revenue', value: '$2,100', icon: 'pi pi-dollar', iconClass: 'text-orange-500', bgClass: 'bg-orange-100 dark:bg-orange-400/10', change: '%52+', period: 'since last week' }, + { label: 'Customers', value: '28,441', icon: 'pi pi-users', iconClass: 'text-cyan-500', bgClass: 'bg-cyan-100 dark:bg-cyan-400/10', change: '520', period: 'newly registered' }, + { label: 'Comments', value: '152 Unread', icon: 'pi pi-comment', iconClass: 'text-purple-500', bgClass: 'bg-purple-100 dark:bg-purple-400/10', change: '85', period: 'responded' } + ]; + + notifications = [ + { id: 1, icon: 'pi pi-envelope', color: '#3B82F6', title: 'New message from admin', time: '2 min ago' }, + { id: 2, icon: 'pi pi-shopping-cart', color: '#F59E0B', title: 'Order #1234 completed', time: '15 min ago' }, + { id: 3, icon: 'pi pi-exclamation-triangle', color: '#EF4444', title: 'Server alert detected', time: '1 hour ago' }, + { id: 4, icon: 'pi pi-user', color: '#10B981', title: 'New user registered', time: '3 hours ago' } + ]; + + panelMenuItems = computed(() => { + return this.menuItems().map(group => ({ + label: group.label, + icon: group.icon, + items: group.items?.map(item => ({ + label: item.label, + icon: item.icon, + routerLink: item.routerLink ? [item.routerLink] : undefined + })) + })) as MenuItem[]; + }); + + menuItemsPreview = computed(() => JSON.stringify(this.menuItems(), null, 2)); + + constructor(private configService: ConfigService) { + // Load menu from config service + const configMenu = this.configService.getShowcaseMenuConfig(); + this.menuItems.set(structuredClone(configMenu as any)); + this.menuJson.set(JSON.stringify(configMenu, null, 2)); + + // Watch for changes in config service and update menu + effect(() => { + const menu = this.configService.showcaseMenuConfig(); + this.menuItems.set(structuredClone(menu as any)); + this.menuJson.set(JSON.stringify(menu, null, 2)); + }); + + } + + // Inline editing + isEditing(groupIndex: number, itemIndex: number): boolean { + return this.editingGroup() === groupIndex && this.editingItem() === itemIndex; + } + + startEdit(groupIndex: number, itemIndex: number) { + this.editingGroup.set(groupIndex); + this.editingItem.set(itemIndex); + } + + finishEdit() { + this.editingGroup.set(-1); + this.editingItem.set(-1); + this.syncJsonFromItems(); + } + + removeItem(groupIndex: number) { + this.menuItems.update(items => { + const updated = [...items]; + updated.splice(groupIndex, 1); + return updated; + }); + this.finishEdit(); + } + + removeSubItem(groupIndex: number, itemIndex: number) { + this.menuItems.update(items => { + const updated = structuredClone(items); + updated[groupIndex].items?.splice(itemIndex, 1); + return updated; + }); + this.finishEdit(); + } + + addSubItem(groupIndex: number) { + this.menuItems.update(items => { + const updated = structuredClone(items); + if (!updated[groupIndex].items) { + updated[groupIndex].items = []; + } + updated[groupIndex].items!.push({ label: 'New Item', icon: 'pi pi-circle', routerLink: '/' }); + return updated; + }); + const group = this.menuItems()[groupIndex]; + this.startEdit(groupIndex, group.items!.length - 1); + } + + addGroup() { + this.menuItems.update(items => [...items, { label: 'New Group', icon: 'pi pi-folder', items: [] }]); + this.startEdit(this.menuItems().length - 1, -1); + } + + // Form-based adding + addGroupFromForm() { + if (!this.newGroupLabel) return; + this.menuItems.update(items => [...items, { label: this.newGroupLabel, icon: this.newGroupIcon, items: [] }]); + this.newGroupLabel = ''; + this.newGroupIcon = 'pi pi-folder'; + this.syncJsonFromItems(); + } + + addItemFromForm() { + if (!this.newItemLabel || this.newItemParentIndex === -1) return; + this.menuItems.update(items => { + const updated = structuredClone(items); + if (!updated[this.newItemParentIndex].items) { + updated[this.newItemParentIndex].items = []; + } + updated[this.newItemParentIndex].items!.push({ + label: this.newItemLabel, + icon: this.newItemIcon, + routerLink: this.newItemRoute || undefined + }); + return updated; + }); + this.newItemLabel = ''; + this.newItemIcon = 'pi pi-circle'; + this.newItemRoute = ''; + this.syncJsonFromItems(); + } + + // Reorder + moveGroup(index: number, direction: number) { + this.menuItems.update(items => { + const updated = [...items]; + const target = index + direction; + [updated[index], updated[target]] = [updated[target], updated[index]]; + return updated; + }); + this.syncJsonFromItems(); + } + + // JSON sync + syncJsonFromItems() { + this.menuJson.set(JSON.stringify(this.menuItems(), null, 2)); + this.jsonError.set(null); + // Save to config service + this.configService.updateShowcaseMenuConfig(this.menuItems() as any); + } + + onJsonInput() { + this.jsonError.set(null); + } + + applyJson() { + try { + const parsed = JSON.parse(this.menuJson()); + if (!Array.isArray(parsed)) { + this.jsonError.set('JSON must be an array of menu groups'); + return; + } + this.menuItems.set(parsed); + this.jsonError.set(null); + // Save to config service + this.configService.updateShowcaseMenuConfig(parsed); + } catch (e: any) { + this.jsonError.set('Invalid JSON: ' + e.message); + } + } +} diff --git a/src/app/pages/pages.routes.ts b/src/app/pages/pages.routes.ts index 6561edc49..dfce46ecf 100644 --- a/src/app/pages/pages.routes.ts +++ b/src/app/pages/pages.routes.ts @@ -2,10 +2,11 @@ import { Routes } from '@angular/router'; import { Documentation } from './documentation/documentation'; import { Crud } from './crud/crud'; import { Empty } from './empty/empty'; - +import { Config } from './config/config'; export default [ { path: 'documentation', component: Documentation }, { path: 'crud', component: Crud }, { path: 'empty', component: Empty }, + { path: 'config', component: Config }, { path: '**', redirectTo: '/notfound' } ] as Routes; diff --git a/src/app/pages/service/config.service.ts b/src/app/pages/service/config.service.ts new file mode 100644 index 000000000..ab9f3b504 --- /dev/null +++ b/src/app/pages/service/config.service.ts @@ -0,0 +1,353 @@ +import { Injectable, signal } from '@angular/core'; +import { MenuItem } from 'primeng/api'; +import * as yaml from 'js-yaml'; + +export interface ConfigState { + showcaseMenu: MenuItem[]; // Menu for showcase demos, not the main app menu + theme: { + darkMode: boolean; + primaryColor: string; + surfaceColor: string; + preset: string; + }; + notifications?: { + enabled?: boolean; + sound?: boolean; + }; + dashboard?: { + widgets?: string[]; + refreshInterval?: number; + }; +} + +// Default menu configuration for showcase demos (not the main app menu) +const DEFAULT_SHOWCASE_MENU_CONFIG: MenuItem[] = [ + { + label: 'Home', + items: [{ label: 'Dashboard', icon: 'pi pi-fw pi-home', routerLink: ['/'] }] + }, + { + label: 'UI Components', + items: [ + { label: 'Form Layout', icon: 'pi pi-fw pi-id-card', routerLink: ['/uikit/formlayout'] }, + { label: 'Input', icon: 'pi pi-fw pi-check-square', routerLink: ['/uikit/input'] }, + { label: 'Button', icon: 'pi pi-fw pi-mobile', class: 'rotated-icon', routerLink: ['/uikit/button'] }, + { label: 'Table', icon: 'pi pi-fw pi-table', routerLink: ['/uikit/table'] }, + { label: 'List', icon: 'pi pi-fw pi-list', routerLink: ['/uikit/list'] }, + { label: 'Tree', icon: 'pi pi-fw pi-share-alt', routerLink: ['/uikit/tree'] }, + { label: 'Panel', icon: 'pi pi-fw pi-tablet', routerLink: ['/uikit/panel'] }, + { label: 'Overlay', icon: 'pi pi-fw pi-clone', routerLink: ['/uikit/overlay'] }, + { label: 'Media', icon: 'pi pi-fw pi-image', routerLink: ['/uikit/media'] }, + { label: 'Menu', icon: 'pi pi-fw pi-bars', routerLink: ['/uikit/menu'] }, + { label: 'Message', icon: 'pi pi-fw pi-comment', routerLink: ['/uikit/message'] }, + { label: 'File', icon: 'pi pi-fw pi-file', routerLink: ['/uikit/file'] }, + { label: 'Chart', icon: 'pi pi-fw pi-chart-bar', routerLink: ['/uikit/charts'] }, + { label: 'Timeline', icon: 'pi pi-fw pi-calendar', routerLink: ['/uikit/timeline'] }, + { label: 'Misc', icon: 'pi pi-fw pi-circle', routerLink: ['/uikit/misc'] } + ] + }, + { + label: 'Pages', + icon: 'pi pi-fw pi-briefcase', + items: [ + { + label: 'Landing', + icon: 'pi pi-fw pi-globe', + routerLink: ['/landing'] + }, + { + label: 'Auth', + icon: 'pi pi-fw pi-user', + items: [ + { + label: 'Login', + icon: 'pi pi-fw pi-sign-in', + routerLink: ['/auth/login'] + }, + { + label: 'Error', + icon: 'pi pi-fw pi-times-circle', + routerLink: ['/auth/error'] + }, + { + label: 'Access Denied', + icon: 'pi pi-fw pi-lock', + routerLink: ['/auth/access'] + } + ] + }, + { + label: 'Crud', + icon: 'pi pi-fw pi-pencil', + routerLink: ['/pages/crud'] + }, + { + label: 'Not Found', + icon: 'pi pi-fw pi-exclamation-circle', + routerLink: ['/pages/notfound'] + }, + { + label: 'Empty', + icon: 'pi pi-fw pi-circle-off', + routerLink: ['/pages/empty'] + }, + { + label: 'Layout', + icon: 'pi pi-fw pi-objects-column', + routerLink: ['/layout'] + } + ] + }, + { + label: 'Hierarchy', + items: [ + { + label: 'Submenu 1', + icon: 'pi pi-fw pi-bookmark', + items: [ + { + label: 'Submenu 1.1', + icon: 'pi pi-fw pi-bookmark', + items: [ + { label: 'Submenu 1.1.1', icon: 'pi pi-fw pi-bookmark' }, + { label: 'Submenu 1.1.2', icon: 'pi pi-fw pi-bookmark' }, + { label: 'Submenu 1.1.3', icon: 'pi pi-fw pi-bookmark' } + ] + }, + { + label: 'Submenu 1.2', + icon: 'pi pi-fw pi-bookmark', + items: [{ label: 'Submenu 1.2.1', icon: 'pi pi-fw pi-bookmark' }] + } + ] + }, + { + label: 'Submenu 2', + icon: 'pi pi-fw pi-bookmark', + items: [ + { + label: 'Submenu 2.1', + icon: 'pi pi-fw pi-bookmark', + items: [ + { label: 'Submenu 2.1.1', icon: 'pi pi-fw pi-bookmark' }, + { label: 'Submenu 2.1.2', icon: 'pi pi-fw pi-bookmark' } + ] + }, + { + label: 'Submenu 2.2', + icon: 'pi pi-fw pi-bookmark', + items: [{ label: 'Submenu 2.2.1', icon: 'pi pi-fw pi-bookmark' }] + } + ] + } + ] + }, + { + label: 'Get Started', + items: [ + { + label: 'Documentation', + icon: 'pi pi-fw pi-book', + routerLink: ['/documentation'] + }, + { + label: 'View Source', + icon: 'pi pi-fw pi-github', + url: 'https://github.com/primefaces/sakai-ng', + target: '_blank' + } + ] + } +]; + +@Injectable({ + providedIn: 'root' +}) +export class ConfigService { + private configState = signal({ + showcaseMenu: [], + theme: { + darkMode: false, + primaryColor: 'emerald', + surfaceColor: 'slate', + preset: 'Aura' + } + }); + + // Public signals + showcaseMenuConfig = signal([]); + + constructor() { + // Load config from localStorage on initialization + this.loadConfig(); + } + + private loadShowcaseMenuConfig(): MenuItem[] { + const stored = localStorage.getItem('demo-showcase-menu-config'); + if (stored) { + try { + return JSON.parse(stored); + } catch (e) { + console.error('Failed to parse showcase menu config from localStorage', e); + } + } + return [...DEFAULT_SHOWCASE_MENU_CONFIG]; + } + + private loadConfig(): void { + const stored = localStorage.getItem('demo-config'); + let config: ConfigState; + + if (stored) { + try { + config = JSON.parse(stored); + } catch (e) { + console.error('Failed to parse config from localStorage', e); + config = { + showcaseMenu: [...DEFAULT_SHOWCASE_MENU_CONFIG], + theme: { + darkMode: false, + primaryColor: 'emerald', + surfaceColor: 'slate', + preset: 'Aura' + } + }; + } + } else { + // Initialize with defaults if no stored config + config = { + showcaseMenu: [...DEFAULT_SHOWCASE_MENU_CONFIG], + theme: { + darkMode: false, + primaryColor: 'emerald', + surfaceColor: 'slate', + preset: 'Aura' + } + }; + } + + this.configState.set(config); + this.showcaseMenuConfig.set(config.showcaseMenu); + + // Ensure theme defaults exist + if (!config.theme) { + config.theme = { + darkMode: false, + primaryColor: 'emerald', + surfaceColor: 'slate', + preset: 'Aura' + }; + this.configState.set(config); + } + + // Save to localStorage if it was empty + if (!stored) { + this.saveConfig(); + } + } + + getThemeConfig() { + return this.configState().theme; + } + + updateThemeConfig(theme: Partial) { + this.configState.update(state => ({ + ...state, + theme: { ...state.theme, ...theme } + })); + this.saveConfig(); + } + + private saveConfig(): void { + const config = this.configState(); + console.log('Saving config to localStorage:', config); + localStorage.setItem('demo-config', JSON.stringify(config)); + localStorage.setItem('demo-showcase-menu-config', JSON.stringify(config.showcaseMenu)); + console.log('Config saved successfully'); + } + + getConfig(): ConfigState { + return this.configState(); + } + + getShowcaseMenuConfig(): MenuItem[] { + return this.showcaseMenuConfig(); + } + + updateShowcaseMenuConfig(menu: MenuItem[]): void { + this.configState.update(state => ({ ...state, showcaseMenu: menu })); + this.showcaseMenuConfig.set(menu); + this.saveConfig(); + } + + resetShowcaseMenuConfig(): void { + const defaultMenu = [...DEFAULT_SHOWCASE_MENU_CONFIG]; + this.updateShowcaseMenuConfig(defaultMenu); + } + + updateConfig(config: Partial): void { + this.configState.update(state => ({ ...state, ...config })); + if (config.showcaseMenu) { + this.showcaseMenuConfig.set(config.showcaseMenu); + } + this.saveConfig(); + } + + resetConfig(): void { + const defaultConfig: ConfigState = { + showcaseMenu: [...DEFAULT_SHOWCASE_MENU_CONFIG], + theme: { + darkMode: false, + primaryColor: 'emerald', + surfaceColor: 'slate', + preset: 'Aura' + } + }; + this.configState.set(defaultConfig); + this.showcaseMenuConfig.set(defaultConfig.showcaseMenu); + this.saveConfig(); + } + + exportConfigAsYaml(): string { + const config = this.configState(); + try { + return yaml.dump(config, { indent: 2, lineWidth: -1 }); + } catch (e) { + console.error('Failed to export YAML:', e); + return ''; + } + } + + importConfigFromYaml(yamlString: string): void { + try { + const config = yaml.load(yamlString) as ConfigState; + if (!config || typeof config !== 'object') { + throw new Error('Invalid YAML structure'); + } + this.configState.set(config); + if (config.showcaseMenu) { + this.showcaseMenuConfig.set(config.showcaseMenu); + } + this.saveConfig(); + } catch (e) { + throw new Error('Failed to parse YAML: ' + (e as Error).message); + } + } + + exportConfigAsJson(): string { + return JSON.stringify(this.configState(), null, 2); + } + + importConfigFromJson(json: string): void { + try { + const config = JSON.parse(json); + this.configState.set(config); + if (config.showcaseMenu) { + this.showcaseMenuConfig.set(config.showcaseMenu); + } + this.saveConfig(); + } catch (e) { + throw new Error('Failed to parse JSON: ' + (e as Error).message); + } + } +} diff --git a/src/assets b/src/assets deleted file mode 160000 index eaa70ece4..000000000 --- a/src/assets +++ /dev/null @@ -1 +0,0 @@ -Subproject commit eaa70ece4cfa9aeb8f60b36dce5a422b6bae8003 diff --git a/src/assets/demo/code.scss b/src/assets/demo/code.scss new file mode 100644 index 000000000..d0d2e9af4 --- /dev/null +++ b/src/assets/demo/code.scss @@ -0,0 +1,17 @@ +pre.app-code { + background-color: var(--code-background); + margin: 0 0 1rem 0; + padding: 0; + border-radius: var(--content-border-radius); + overflow: auto; + + code { + color: var(--code-color); + padding: 1rem; + margin: 0; + line-height: 1.5; + display: block; + font-weight: semibold; + font-family: monaco, Consolas, monospace; + } +} diff --git a/src/assets/demo/demo.scss b/src/assets/demo/demo.scss new file mode 100644 index 000000000..b8f47d4c5 --- /dev/null +++ b/src/assets/demo/demo.scss @@ -0,0 +1,2 @@ +@use './code.scss'; +@use './flags/flags'; diff --git a/src/assets/demo/flags/flags.scss b/src/assets/demo/flags/flags.scss new file mode 100755 index 000000000..44a437680 --- /dev/null +++ b/src/assets/demo/flags/flags.scss @@ -0,0 +1 @@ +span.flag{width:44px;height:30px;display:inline-block;}img.flag{width:30px}.flag{background:url(./flags_responsive.png) no-repeat;background-size:100%;vertical-align: middle;}.flag-ad{background-position:0 .413223%}.flag-ae{background-position:0 .826446%}.flag-af{background-position:0 1.239669%}.flag-ag{background-position:0 1.652893%}.flag-ai{background-position:0 2.066116%}.flag-al{background-position:0 2.479339%}.flag-am{background-position:0 2.892562%}.flag-an{background-position:0 3.305785%}.flag-ao{background-position:0 3.719008%}.flag-aq{background-position:0 4.132231%}.flag-ar{background-position:0 4.545455%}.flag-as{background-position:0 4.958678%}.flag-at{background-position:0 5.371901%}.flag-au{background-position:0 5.785124%}.flag-aw{background-position:0 6.198347%}.flag-az{background-position:0 6.61157%}.flag-ba{background-position:0 7.024793%}.flag-bb{background-position:0 7.438017%}.flag-bd{background-position:0 7.85124%}.flag-be{background-position:0 8.264463%}.flag-bf{background-position:0 8.677686%}.flag-bg{background-position:0 9.090909%}.flag-bh{background-position:0 9.504132%}.flag-bi{background-position:0 9.917355%}.flag-bj{background-position:0 10.330579%}.flag-bm{background-position:0 10.743802%}.flag-bn{background-position:0 11.157025%}.flag-bo{background-position:0 11.570248%}.flag-br{background-position:0 11.983471%}.flag-bs{background-position:0 12.396694%}.flag-bt{background-position:0 12.809917%}.flag-bv{background-position:0 13.22314%}.flag-bw{background-position:0 13.636364%}.flag-by{background-position:0 14.049587%}.flag-bz{background-position:0 14.46281%}.flag-ca{background-position:0 14.876033%}.flag-cc{background-position:0 15.289256%}.flag-cd{background-position:0 15.702479%}.flag-cf{background-position:0 16.115702%}.flag-cg{background-position:0 16.528926%}.flag-ch{background-position:0 16.942149%}.flag-ci{background-position:0 17.355372%}.flag-ck{background-position:0 17.768595%}.flag-cl{background-position:0 18.181818%}.flag-cm{background-position:0 18.595041%}.flag-cn{background-position:0 19.008264%}.flag-co{background-position:0 19.421488%}.flag-cr{background-position:0 19.834711%}.flag-cu{background-position:0 20.247934%}.flag-cv{background-position:0 20.661157%}.flag-cx{background-position:0 21.07438%}.flag-cy{background-position:0 21.487603%}.flag-cz{background-position:0 21.900826%}.flag-de{background-position:0 22.31405%}.flag-dj{background-position:0 22.727273%}.flag-dk{background-position:0 23.140496%}.flag-dm{background-position:0 23.553719%}.flag-do{background-position:0 23.966942%}.flag-dz{background-position:0 24.380165%}.flag-ec{background-position:0 24.793388%}.flag-ee{background-position:0 25.206612%}.flag-eg{background-position:0 25.619835%}.flag-eh{background-position:0 26.033058%}.flag-er{background-position:0 26.446281%}.flag-es{background-position:0 26.859504%}.flag-et{background-position:0 27.272727%}.flag-fi{background-position:0 27.68595%}.flag-fj{background-position:0 28.099174%}.flag-fk{background-position:0 28.512397%}.flag-fm{background-position:0 28.92562%}.flag-fo{background-position:0 29.338843%}.flag-fr{background-position:0 29.752066%}.flag-ga{background-position:0 30.165289%}.flag-gd{background-position:0 30.578512%}.flag-ge{background-position:0 30.991736%}.flag-gf{background-position:0 31.404959%}.flag-gh{background-position:0 31.818182%}.flag-gi{background-position:0 32.231405%}.flag-gl{background-position:0 32.644628%}.flag-gm{background-position:0 33.057851%}.flag-gn{background-position:0 33.471074%}.flag-gp{background-position:0 33.884298%}.flag-gq{background-position:0 34.297521%}.flag-gr{background-position:0 34.710744%}.flag-gs{background-position:0 35.123967%}.flag-gt{background-position:0 35.53719%}.flag-gu{background-position:0 35.950413%}.flag-gw{background-position:0 36.363636%}.flag-gy{background-position:0 36.77686%}.flag-hk{background-position:0 37.190083%}.flag-hm{background-position:0 37.603306%}.flag-hn{background-position:0 38.016529%}.flag-hr{background-position:0 38.429752%}.flag-ht{background-position:0 38.842975%}.flag-hu{background-position:0 39.256198%}.flag-id{background-position:0 39.669421%}.flag-ie{background-position:0 40.082645%}.flag-il{background-position:0 40.495868%}.flag-in{background-position:0 40.909091%}.flag-io{background-position:0 41.322314%}.flag-iq{background-position:0 41.735537%}.flag-ir{background-position:0 42.14876%}.flag-is{background-position:0 42.561983%}.flag-it{background-position:0 42.975207%}.flag-jm{background-position:0 43.38843%}.flag-jo{background-position:0 43.801653%}.flag-jp{background-position:0 44.214876%}.flag-ke{background-position:0 44.628099%}.flag-kg{background-position:0 45.041322%}.flag-kh{background-position:0 45.454545%}.flag-ki{background-position:0 45.867769%}.flag-km{background-position:0 46.280992%}.flag-kn{background-position:0 46.694215%}.flag-kp{background-position:0 47.107438%}.flag-kr{background-position:0 47.520661%}.flag-kw{background-position:0 47.933884%}.flag-ky{background-position:0 48.347107%}.flag-kz{background-position:0 48.760331%}.flag-la{background-position:0 49.173554%}.flag-lb{background-position:0 49.586777%}.flag-lc{background-position:0 50%}.flag-li{background-position:0 50.413223%}.flag-lk{background-position:0 50.826446%}.flag-lr{background-position:0 51.239669%}.flag-ls{background-position:0 51.652893%}.flag-lt{background-position:0 52.066116%}.flag-lu{background-position:0 52.479339%}.flag-lv{background-position:0 52.892562%}.flag-ly{background-position:0 53.305785%}.flag-ma{background-position:0 53.719008%}.flag-mc{background-position:0 54.132231%}.flag-md{background-position:0 54.545455%}.flag-me{background-position:0 54.958678%}.flag-mg{background-position:0 55.371901%}.flag-mh{background-position:0 55.785124%}.flag-mk{background-position:0 56.198347%}.flag-ml{background-position:0 56.61157%}.flag-mm{background-position:0 57.024793%}.flag-mn{background-position:0 57.438017%}.flag-mo{background-position:0 57.85124%}.flag-mp{background-position:0 58.264463%}.flag-mq{background-position:0 58.677686%}.flag-mr{background-position:0 59.090909%}.flag-ms{background-position:0 59.504132%}.flag-mt{background-position:0 59.917355%}.flag-mu{background-position:0 60.330579%}.flag-mv{background-position:0 60.743802%}.flag-mw{background-position:0 61.157025%}.flag-mx{background-position:0 61.570248%}.flag-my{background-position:0 61.983471%}.flag-mz{background-position:0 62.396694%}.flag-na{background-position:0 62.809917%}.flag-nc{background-position:0 63.22314%}.flag-ne{background-position:0 63.636364%}.flag-nf{background-position:0 64.049587%}.flag-ng{background-position:0 64.46281%}.flag-ni{background-position:0 64.876033%}.flag-nl{background-position:0 65.289256%}.flag-no{background-position:0 65.702479%}.flag-np{background-position:0 66.115702%}.flag-nr{background-position:0 66.528926%}.flag-nu{background-position:0 66.942149%}.flag-nz{background-position:0 67.355372%}.flag-om{background-position:0 67.768595%}.flag-pa{background-position:0 68.181818%}.flag-pe{background-position:0 68.595041%}.flag-pf{background-position:0 69.008264%}.flag-pg{background-position:0 69.421488%}.flag-ph{background-position:0 69.834711%}.flag-pk{background-position:0 70.247934%}.flag-pl{background-position:0 70.661157%}.flag-pm{background-position:0 71.07438%}.flag-pn{background-position:0 71.487603%}.flag-pr{background-position:0 71.900826%}.flag-pt{background-position:0 72.31405%}.flag-pw{background-position:0 72.727273%}.flag-py{background-position:0 73.140496%}.flag-qa{background-position:0 73.553719%}.flag-re{background-position:0 73.966942%}.flag-ro{background-position:0 74.380165%}.flag-rs{background-position:0 74.793388%}.flag-ru{background-position:0 75.206612%}.flag-rw{background-position:0 75.619835%}.flag-sa{background-position:0 76.033058%}.flag-sb{background-position:0 76.446281%}.flag-sc{background-position:0 76.859504%}.flag-sd{background-position:0 77.272727%}.flag-se{background-position:0 77.68595%}.flag-sg{background-position:0 78.099174%}.flag-sh{background-position:0 78.512397%}.flag-si{background-position:0 78.92562%}.flag-sj{background-position:0 79.338843%}.flag-sk{background-position:0 79.752066%}.flag-sl{background-position:0 80.165289%}.flag-sm{background-position:0 80.578512%}.flag-sn{background-position:0 80.991736%}.flag-so{background-position:0 81.404959%}.flag-sr{background-position:0 81.818182%}.flag-ss{background-position:0 82.231405%}.flag-st{background-position:0 82.644628%}.flag-sv{background-position:0 83.057851%}.flag-sy{background-position:0 83.471074%}.flag-sz{background-position:0 83.884298%}.flag-tc{background-position:0 84.297521%}.flag-td{background-position:0 84.710744%}.flag-tf{background-position:0 85.123967%}.flag-tg{background-position:0 85.53719%}.flag-th{background-position:0 85.950413%}.flag-tj{background-position:0 86.363636%}.flag-tk{background-position:0 86.77686%}.flag-tl{background-position:0 87.190083%}.flag-tm{background-position:0 87.603306%}.flag-tn{background-position:0 88.016529%}.flag-to{background-position:0 88.429752%}.flag-tp{background-position:0 88.842975%}.flag-tr{background-position:0 89.256198%}.flag-tt{background-position:0 89.669421%}.flag-tv{background-position:0 90.082645%}.flag-tw{background-position:0 90.495868%}.flag-ty{background-position:0 90.909091%}.flag-tz{background-position:0 91.322314%}.flag-ua{background-position:0 91.735537%}.flag-ug{background-position:0 92.14876%}.flag-gb,.flag-uk{background-position:0 92.561983%}.flag-um{background-position:0 92.975207%}.flag-us{background-position:0 93.38843%}.flag-uy{background-position:0 93.801653%}.flag-uz{background-position:0 94.214876%}.flag-va{background-position:0 94.628099%}.flag-vc{background-position:0 95.041322%}.flag-ve{background-position:0 95.454545%}.flag-vg{background-position:0 95.867769%}.flag-vi{background-position:0 96.280992%}.flag-vn{background-position:0 96.694215%}.flag-vu{background-position:0 97.107438%}.flag-wf{background-position:0 97.520661%}.flag-ws{background-position:0 97.933884%}.flag-ye{background-position:0 98.347107%}.flag-za{background-position:0 98.760331%}.flag-zm{background-position:0 99.173554%}.flag-zr{background-position:0 99.586777%}.flag-zw{background-position:0 100%} diff --git a/src/assets/demo/flags/flags_responsive.png b/src/assets/demo/flags/flags_responsive.png new file mode 100755 index 000000000..c27ce213f Binary files /dev/null and b/src/assets/demo/flags/flags_responsive.png differ diff --git a/src/assets/layout/_core.scss b/src/assets/layout/_core.scss new file mode 100644 index 000000000..8dee918c6 --- /dev/null +++ b/src/assets/layout/_core.scss @@ -0,0 +1,24 @@ +html { + height: 100%; + font-size: 14px; +} + +body { + font-family: 'Lato', sans-serif; + color: var(--text-color); + background-color: var(--surface-ground); + margin: 0; + padding: 0; + min-height: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + line-height: 1.2; +} + +a { + text-decoration: none; +} + +.layout-wrapper { + min-height: 100vh; +} diff --git a/src/assets/layout/_footer.scss b/src/assets/layout/_footer.scss new file mode 100644 index 000000000..27bcbf0e9 --- /dev/null +++ b/src/assets/layout/_footer.scss @@ -0,0 +1,8 @@ +.layout-footer { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 0 1rem 0; + gap: 0.5rem; + border-top: 1px solid var(--surface-border); +} diff --git a/src/assets/layout/_main.scss b/src/assets/layout/_main.scss new file mode 100644 index 000000000..162e95c7a --- /dev/null +++ b/src/assets/layout/_main.scss @@ -0,0 +1,17 @@ +.layout-main-container { + display: flex; + flex-direction: column; + min-height: 100vh; + justify-content: space-between; + padding: 6rem 2rem 0 2rem; + transition: margin-left var(--layout-section-transition-duration); +} + +.layout-main { + flex: 1 1 auto; + padding-bottom: 2rem; +} + +img { + max-width: none !important; +} diff --git a/src/assets/layout/_menu.scss b/src/assets/layout/_menu.scss new file mode 100644 index 000000000..3d22fb580 --- /dev/null +++ b/src/assets/layout/_menu.scss @@ -0,0 +1,160 @@ +@use 'mixins' as *; + +.layout-sidebar { + position: fixed; + width: 20rem; + height: calc(100vh - 8rem); + z-index: 999; + overflow-y: auto; + user-select: none; + top: 6rem; + left: 2rem; + transition: + transform var(--layout-section-transition-duration), + left var(--layout-section-transition-duration); + background-color: var(--surface-overlay); + border-radius: var(--content-border-radius); + padding: 0.5rem 1.5rem; +} + +.layout-menu { + margin: 0; + padding: 0; + list-style-type: none; + + .layout-root-menuitem { + > .layout-menuitem-root-text { + font-size: 0.857rem; + text-transform: uppercase; + font-weight: 700; + color: var(--text-color); + margin: 0.75rem 0; + } + + > a { + display: none; + } + } + + a { + user-select: none; + + &.active-menuitem { + > .layout-submenu-toggler { + transform: rotate(-180deg); + } + } + } + + li.active-menuitem { + > a { + .layout-submenu-toggler { + transform: rotate(-180deg); + } + } + } + + ul { + margin: 0; + padding: 0; + list-style-type: none; + + a { + display: flex; + align-items: center; + position: relative; + outline: 0 none; + color: var(--text-color); + cursor: pointer; + padding: 0.75rem 1rem; + border-radius: var(--content-border-radius); + transition: + background-color var(--element-transition-duration), + box-shadow var(--element-transition-duration); + + .layout-menuitem-icon { + margin-right: 0.5rem; + } + + .layout-submenu-toggler { + font-size: 75%; + margin-left: auto; + transition: transform var(--element-transition-duration); + } + + &.active-route { + font-weight: 700; + color: var(--primary-color); + } + + &:hover { + background-color: var(--surface-hover); + } + + &:focus { + @include focused-inset(); + } + } + + ul { + overflow: hidden; + border-radius: var(--content-border-radius); + + li { + a { + margin-left: 1rem; + } + + li { + a { + margin-left: 2rem; + } + + li { + a { + margin-left: 2.5rem; + } + + li { + a { + margin-left: 3rem; + } + + li { + a { + margin-left: 3.5rem; + } + + li { + a { + margin-left: 4rem; + } + } + } + } + } + } + } + } + } +} + +.layout-submenu-enter-from, +.layout-submenu-leave-to { + max-height: 0; +} + +.layout-submenu-enter-to, +.layout-submenu-leave-from { + max-height: 1000px; +} + +.layout-submenu-leave-active { + overflow: hidden; + transition: max-height 0.45s cubic-bezier(0, 1, 0, 1); +} + +.layout-submenu-enter-active { + overflow: hidden; + transition: max-height 1s ease-in-out; +} diff --git a/src/assets/layout/_mixins.scss b/src/assets/layout/_mixins.scss new file mode 100644 index 000000000..ad330b10b --- /dev/null +++ b/src/assets/layout/_mixins.scss @@ -0,0 +1,15 @@ +@mixin focused() { + outline-width: var(--focus-ring-width); + outline-style: var(--focus-ring-style); + outline-color: var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); + box-shadow: var(--focus-ring-shadow); + transition: + box-shadow var(--transition-duration), + outline-color var(--transition-duration); +} + +@mixin focused-inset() { + outline-offset: -1px; + box-shadow: inset var(--focus-ring-shadow); +} diff --git a/src/assets/layout/_preloading.scss b/src/assets/layout/_preloading.scss new file mode 100644 index 000000000..a81410444 --- /dev/null +++ b/src/assets/layout/_preloading.scss @@ -0,0 +1,47 @@ +.preloader { + position: fixed; + z-index: 999999; + background: #edf1f5; + width: 100%; + height: 100%; +} +.preloader-content { + border: 0 solid transparent; + border-radius: 50%; + width: 150px; + height: 150px; + position: absolute; + top: calc(50vh - 75px); + left: calc(50vw - 75px); +} + +.preloader-content:before, .preloader-content:after{ + content: ''; + border: 1em solid var(--primary-color); + border-radius: 50%; + width: inherit; + height: inherit; + position: absolute; + top: 0; + left: 0; + animation: loader 2s linear infinite; + opacity: 0; +} + +.preloader-content:before{ + animation-delay: 0.5s; +} + +@keyframes loader{ + 0%{ + transform: scale(0); + opacity: 0; + } + 50%{ + opacity: 1; + } + 100%{ + transform: scale(1); + opacity: 0; + } +} diff --git a/src/assets/layout/_responsive.scss b/src/assets/layout/_responsive.scss new file mode 100644 index 000000000..561d5f1b1 --- /dev/null +++ b/src/assets/layout/_responsive.scss @@ -0,0 +1,110 @@ +@media screen and (min-width: 1960px) { + .layout-main, + .landing-wrapper { + width: 1504px; + margin-left: auto !important; + margin-right: auto !important; + } +} + +@media (min-width: 992px) { + .layout-wrapper { + &.layout-overlay { + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + + .layout-sidebar { + transform: translateX(-100%); + left: 0; + top: 0; + height: 100vh; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-right: 1px solid var(--surface-border); + transition: + transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99), + left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99); + box-shadow: + 0px 3px 5px rgba(0, 0, 0, 0.02), + 0px 0px 2px rgba(0, 0, 0, 0.05), + 0px 1px 4px rgba(0, 0, 0, 0.08); + } + + &.layout-overlay-active { + .layout-sidebar { + transform: translateX(0); + } + } + } + + &.layout-static { + .layout-main-container { + margin-left: 22rem; + } + + &.layout-static-inactive { + .layout-sidebar { + transform: translateX(-100%); + left: 0; + } + + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + } + } + + .layout-mask { + display: none; + } + } +} + +@media (max-width: 991px) { + .blocked-scroll { + overflow: hidden; + } + + .layout-wrapper { + .layout-main-container { + margin-left: 0; + padding-left: 2rem; + } + + .layout-sidebar { + transform: translateX(-100%); + left: 0; + top: 0; + height: 100vh; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + transition: + transform 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99), + left 0.4s cubic-bezier(0.05, 0.74, 0.2, 0.99); + } + + .layout-mask { + display: none; + position: fixed; + top: 0; + left: 0; + z-index: 998; + width: 100%; + height: 100%; + background-color: var(--maskbg); + } + + &.layout-mobile-active { + .layout-sidebar { + transform: translateX(0); + } + + .layout-mask { + display: block; + } + } + } +} diff --git a/src/assets/layout/_topbar.scss b/src/assets/layout/_topbar.scss new file mode 100644 index 000000000..f5d239c18 --- /dev/null +++ b/src/assets/layout/_topbar.scss @@ -0,0 +1,160 @@ +@use 'mixins' as *; + +.layout-topbar { + position: fixed; + height: 4rem; + z-index: 997; + left: 0; + top: 0; + width: 100%; + padding: 0 2rem; + background-color: var(--surface-card); + transition: left var(--layout-section-transition-duration); + display: flex; + align-items: center; + + .layout-topbar-logo-container { + width: 20rem; + display: flex; + align-items: center; + } + + .layout-topbar-logo { + display: inline-flex; + align-items: center; + font-size: 1.5rem; + border-radius: var(--content-border-radius); + color: var(--text-color); + font-weight: 500; + gap: 0.5rem; + + svg { + width: 3rem; + } + + &:focus-visible { + @include focused(); + } + } + + .layout-topbar-action { + display: inline-flex; + justify-content: center; + align-items: center; + color: var(--text-color-secondary); + border-radius: 50%; + width: 2.5rem; + height: 2.5rem; + color: var(--text-color); + transition: background-color var(--element-transition-duration); + cursor: pointer; + + &:hover { + background-color: var(--surface-hover); + } + + &:focus-visible { + @include focused(); + } + + i { + font-size: 1.25rem; + } + + span { + font-size: 1rem; + display: none; + } + + &.layout-topbar-action-highlight { + background-color: var(--primary-color); + color: var(--primary-contrast-color); + } + } + + .layout-menu-button { + margin-right: 0.5rem; + } + + .layout-topbar-menu-button { + display: none; + } + + .layout-topbar-actions { + margin-left: auto; + display: flex; + gap: 1rem; + } + + .layout-topbar-menu-content { + display: flex; + gap: 1rem; + } + + .layout-config-menu { + display: flex; + gap: 1rem; + } +} + +@media (max-width: 991px) { + .layout-topbar { + padding: 0 2rem; + + .layout-topbar-logo-container { + width: auto; + } + + .layout-menu-button { + margin-left: 0; + margin-right: 0.5rem; + } + + .layout-topbar-menu-button { + display: inline-flex; + } + + .layout-topbar-menu { + position: absolute; + background-color: var(--surface-overlay); + transform-origin: top; + box-shadow: + 0px 3px 5px rgba(0, 0, 0, 0.02), + 0px 0px 2px rgba(0, 0, 0, 0.05), + 0px 1px 4px rgba(0, 0, 0, 0.08); + border-radius: var(--content-border-radius); + padding: 1rem; + right: 2rem; + top: 4rem; + min-width: 15rem; + border: 1px solid var(--surface-border); + + .layout-topbar-menu-content { + gap: 0.5rem; + } + + .layout-topbar-action { + display: flex; + width: 100%; + height: auto; + justify-content: flex-start; + border-radius: var(--content-border-radius); + padding: 0.5rem 1rem; + + i { + font-size: 1rem; + margin-right: 0.5rem; + } + + span { + font-weight: medium; + display: block; + } + } + } + + .layout-topbar-menu-content { + flex-direction: column; + } + } +} diff --git a/src/assets/layout/_typography.scss b/src/assets/layout/_typography.scss new file mode 100644 index 000000000..b17bbc2f9 --- /dev/null +++ b/src/assets/layout/_typography.scss @@ -0,0 +1,68 @@ +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 1.5rem 0 1rem 0; + font-family: inherit; + font-weight: 700; + line-height: 1.5; + color: var(--text-color); + + &:first-child { + margin-top: 0; + } +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 2rem; +} + +h3 { + font-size: 1.75rem; +} + +h4 { + font-size: 1.5rem; +} + +h5 { + font-size: 1.25rem; +} + +h6 { + font-size: 1rem; +} + +mark { + background: #fff8e1; + padding: 0.25rem 0.4rem; + border-radius: var(--content-border-radius); + font-family: monospace; +} + +blockquote { + margin: 1rem 0; + padding: 0 2rem; + border-left: 4px solid #90a4ae; +} + +hr { + border-top: solid var(--surface-border); + border-width: 1px 0 0 0; + margin: 1rem 0; +} + +p { + margin: 0 0 1rem 0; + line-height: 1.5; + + &:last-child { + margin-bottom: 0; + } +} diff --git a/src/assets/layout/_utils.scss b/src/assets/layout/_utils.scss new file mode 100644 index 000000000..6ccec88dd --- /dev/null +++ b/src/assets/layout/_utils.scss @@ -0,0 +1,25 @@ +/* Utils */ +.clearfix:after { + content: ' '; + display: block; + clear: both; +} + +.card { + background: var(--surface-card); + padding: 2rem; + margin-bottom: 2rem; + border-radius: var(--content-border-radius); + + &:last-child { + margin-bottom: 0; + } +} + +.p-toast { + &.p-toast-top-right, + &.p-toast-top-left, + &.p-toast-top-center { + top: 100px; + } +} diff --git a/src/assets/layout/layout.scss b/src/assets/layout/layout.scss new file mode 100644 index 000000000..ce93b988d --- /dev/null +++ b/src/assets/layout/layout.scss @@ -0,0 +1,13 @@ +@use './variables/_common'; +@use './variables/_light'; +@use './variables/_dark'; +@use './_mixins'; +@use './_preloading'; +@use './_core'; +@use './_main'; +@use './_topbar'; +@use './_menu'; +@use './_footer'; +@use './_responsive'; +@use './_utils'; +@use './_typography'; diff --git a/src/assets/layout/variables/_common.scss b/src/assets/layout/variables/_common.scss new file mode 100644 index 000000000..2a040c2c9 --- /dev/null +++ b/src/assets/layout/variables/_common.scss @@ -0,0 +1,20 @@ +:root { + --primary-color: var(--p-primary-color); + --primary-contrast-color: var(--p-primary-contrast-color); + --text-color: var(--p-text-color); + --text-color-secondary: var(--p-text-muted-color); + --surface-border: var(--p-content-border-color); + --surface-card: var(--p-content-background); + --surface-hover: var(--p-content-hover-background); + --surface-overlay: var(--p-overlay-popover-background); + --transition-duration: var(--p-transition-duration); + --maskbg: var(--p-mask-background); + --content-border-radius: var(--p-content-border-radius); + --layout-section-transition-duration: 0.2s; + --element-transition-duration: var(--p-transition-duration); + --focus-ring-width: var(--p-focus-ring-width); + --focus-ring-style: var(--p-focus-ring-style); + --focus-ring-color: var(--p-focus-ring-color); + --focus-ring-offset: var(--p-focus-ring-offset); + --focus-ring-shadow: var(--p-focus-ring-shadow); +} diff --git a/src/assets/layout/variables/_dark.scss b/src/assets/layout/variables/_dark.scss new file mode 100644 index 000000000..bb916050e --- /dev/null +++ b/src/assets/layout/variables/_dark.scss @@ -0,0 +1,5 @@ +:root[class*='app-dark'] { + --surface-ground: var(--p-surface-950); + --code-background: var(--p-surface-800); + --code-color: var(--p-surface-100); +} diff --git a/src/assets/layout/variables/_light.scss b/src/assets/layout/variables/_light.scss new file mode 100644 index 000000000..aa3403c92 --- /dev/null +++ b/src/assets/layout/variables/_light.scss @@ -0,0 +1,5 @@ +:root { + --surface-ground: var(--p-surface-100); + --code-background: var(--p-surface-900); + --code-color: var(--p-surface-200); +} diff --git a/src/assets/styles.scss b/src/assets/styles.scss new file mode 100644 index 000000000..0b502984a --- /dev/null +++ b/src/assets/styles.scss @@ -0,0 +1,5 @@ +/* You can add global styles to this file, and also import other style files */ +@use './tailwind.css'; +@use './layout/layout.scss'; +@use 'primeicons/primeicons.css'; +@use './demo/demo.scss'; diff --git a/src/assets/tailwind.css b/src/assets/tailwind.css new file mode 100644 index 000000000..a820cb7af --- /dev/null +++ b/src/assets/tailwind.css @@ -0,0 +1,11 @@ +@import 'tailwindcss'; +@import 'tailwindcss-primeui'; +@custom-variant dark (&:where(.app-dark, .app-dark *)); + +@theme { + --breakpoint-sm: 576px; + --breakpoint-md: 768px; + --breakpoint-lg: 992px; + --breakpoint-xl: 1200px; + --breakpoint-2xl: 1920px; +} \ No newline at end of file