├── .editorconfig ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── angular.json ├── commitlint.config.js ├── eslint.config.js ├── lint-staged.config.js ├── package.json ├── projects ├── dev-app │ ├── eslint.config.js │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.ts │ │ │ ├── app.config.ts │ │ │ ├── app.routes.ts │ │ │ ├── controls │ │ │ │ ├── controls.component.html │ │ │ │ ├── controls.component.scss │ │ │ │ └── controls.component.ts │ │ │ ├── examples │ │ │ │ ├── examples.component.html │ │ │ │ ├── examples.component.scss │ │ │ │ └── examples.component.ts │ │ │ ├── home │ │ │ │ ├── home.component.html │ │ │ │ ├── home.component.scss │ │ │ │ └── home.component.ts │ │ │ ├── layout │ │ │ │ ├── layout.component.html │ │ │ │ ├── layout.component.scss │ │ │ │ └── layout.component.ts │ │ │ ├── playground │ │ │ │ ├── playground.component.html │ │ │ │ ├── playground.component.scss │ │ │ │ └── playground.component.ts │ │ │ └── settings.service.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── styles.scss │ │ ├── theme-m2.scss │ │ └── theme.scss │ ├── tsconfig.app.json │ └── tsconfig.spec.json ├── docs │ ├── eslint.config.js │ ├── public │ │ ├── favicon.ico │ │ ├── icons │ │ │ ├── github.svg │ │ │ ├── invert_colors.svg │ │ │ └── menu.svg │ │ └── images │ │ │ ├── align_center.png │ │ │ ├── align_left.png │ │ │ ├── align_right.png │ │ │ ├── icon3d1.webp │ │ │ ├── icon3d2.webp │ │ │ └── icon3d3.webp │ ├── src │ │ ├── app │ │ │ ├── app.component.html │ │ │ ├── app.component.scss │ │ │ ├── app.component.ts │ │ │ ├── app.config.ts │ │ │ ├── app.routes.ts │ │ │ ├── layout │ │ │ │ ├── layout.component.html │ │ │ │ ├── layout.component.scss │ │ │ │ └── layout.component.ts │ │ │ ├── pages │ │ │ │ ├── basic-controls │ │ │ │ │ ├── basic-controls.component.html │ │ │ │ │ ├── basic-controls.component.scss │ │ │ │ │ └── basic-controls.component.ts │ │ │ │ ├── conditions │ │ │ │ │ ├── conditions.component.html │ │ │ │ │ ├── conditions.component.scss │ │ │ │ │ └── conditions.component.ts │ │ │ │ ├── getting-started │ │ │ │ │ ├── getting-started.component.html │ │ │ │ │ ├── getting-started.component.scss │ │ │ │ │ └── getting-started.component.ts │ │ │ │ ├── group-controls │ │ │ │ │ ├── group-controls.component.html │ │ │ │ │ ├── group-controls.component.scss │ │ │ │ │ └── group-controls.component.ts │ │ │ │ ├── home │ │ │ │ │ ├── home.component.html │ │ │ │ │ ├── home.component.scss │ │ │ │ │ └── home.component.ts │ │ │ │ ├── media-controls │ │ │ │ │ ├── media-controls.component.html │ │ │ │ │ ├── media-controls.component.scss │ │ │ │ │ └── media-controls.component.ts │ │ │ │ └── playground │ │ │ │ │ ├── playground.component.html │ │ │ │ │ ├── playground.component.scss │ │ │ │ │ └── playground.component.ts │ │ │ └── shared │ │ │ │ ├── example-viewer │ │ │ │ ├── example-viewer.component.html │ │ │ │ ├── example-viewer.component.scss │ │ │ │ └── example-viewer.component.ts │ │ │ │ ├── gradient-generator │ │ │ │ ├── gradient-generator.component.html │ │ │ │ ├── gradient-generator.component.scss │ │ │ │ ├── gradient-generator.component.ts │ │ │ │ └── gradient-presets.ts │ │ │ │ ├── index.ts │ │ │ │ ├── page-header │ │ │ │ ├── page-header.component.html │ │ │ │ ├── page-header.component.scss │ │ │ │ └── page-header.component.ts │ │ │ │ ├── style-manager │ │ │ │ └── style-manager.ts │ │ │ │ └── theme-picker │ │ │ │ ├── theme-picker.html │ │ │ │ ├── theme-picker.scss │ │ │ │ ├── theme-picker.ts │ │ │ │ └── theme-storage.ts │ │ ├── index.html │ │ ├── main.ts │ │ ├── styles.scss │ │ └── styles │ │ │ ├── _app-theme.scss │ │ │ └── _reboot.scss │ ├── tsconfig.app.json │ └── tsconfig.spec.json └── gui │ ├── _index.scss │ ├── button-toggle │ ├── button-toggle.html │ ├── button-toggle.scss │ ├── button-toggle.ts │ ├── icon.scss │ └── icon.ts │ ├── codearea │ ├── codearea-config.ts │ ├── codearea-dialog.html │ ├── codearea-dialog.scss │ ├── codearea-dialog.ts │ ├── codearea.html │ ├── codearea.scss │ └── codearea.ts │ ├── combobox │ ├── combobox.html │ ├── combobox.scss │ └── combobox.ts │ ├── eslint.config.js │ ├── field-group │ ├── field-group.html │ ├── field-group.scss │ └── field-group.ts │ ├── field-label │ ├── field-label.html │ ├── field-label.scss │ └── field-label.ts │ ├── file-uploader │ ├── file-uploader-config.ts │ ├── file-uploader.html │ ├── file-uploader.scss │ └── file-uploader.ts │ ├── fill │ ├── fill.html │ ├── fill.scss │ └── fill.ts │ ├── gui-form.html │ ├── gui-form.scss │ ├── gui-form.ts │ ├── gui-icons.ts │ ├── gui-module.ts │ ├── gui-utils.ts │ ├── icon-button-wrapper │ ├── icon-button-wrapper.html │ ├── icon-button-wrapper.scss │ └── icon-button-wrapper.ts │ ├── image-select │ ├── image-select.html │ ├── image-select.scss │ └── image-select.ts │ ├── inline-group │ ├── inline-group.html │ ├── inline-group.scss │ └── inline-group.ts │ ├── input-number │ ├── input-number.html │ ├── input-number.scss │ └── input-number.ts │ ├── input-text │ ├── input-text.html │ ├── input-text.scss │ └── input-text.ts │ ├── interface.ts │ ├── ng-package.json │ ├── package.json │ ├── public-api.ts │ ├── select │ ├── select.html │ ├── select.scss │ └── select.ts │ ├── slider │ ├── slider.html │ ├── slider.scss │ └── slider.ts │ ├── switch │ ├── switch.html │ ├── switch.scss │ └── switch.ts │ ├── textarea │ ├── textarea.html │ ├── textarea.scss │ └── textarea.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | /.nx/cache 34 | .sass-cache/ 35 | /connect.lock 36 | /coverage 37 | /libpeerconnection.log 38 | testem.log 39 | /typings 40 | 41 | # System files 42 | .DS_Store 43 | Thumbs.db 44 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no-install commitlint --edit "$1" 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no-install lint-staged 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # add files you wish to ignore here 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "ignore", 5 | "insertPragma": false, 6 | "jsxBracketSameLine": false, 7 | "jsxSingleQuote": false, 8 | "printWidth": 100, 9 | "proseWrap": "preserve", 10 | "quoteProps": "consistent", 11 | "requirePragma": false, 12 | "semi": true, 13 | "singleQuote": true, 14 | "tabWidth": 2, 15 | "trailingComma": "es5", 16 | "useTabs": false, 17 | "overrides": [ 18 | { 19 | "files": "*.html", 20 | "options": { 21 | "parser": "angular" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreFiles": [ 3 | "dist/**" 4 | ], 5 | "extends": [ 6 | "stylelint-config-standard", 7 | "stylelint-config-recommended-scss" 8 | ], 9 | "rules": { 10 | "alpha-value-notation": null, 11 | "annotation-no-unknown": null, 12 | "at-rule-empty-line-before": null, 13 | "block-no-empty": null, 14 | "color-function-notation": "legacy", 15 | "function-no-unknown": null, 16 | "import-notation": null, 17 | "no-descending-specificity": null, 18 | "no-empty-source": null, 19 | "media-query-no-invalid": null, 20 | "number-max-precision": null, 21 | "selector-pseudo-element-no-unknown": [ 22 | true, 23 | { 24 | "ignorePseudoElements": [ 25 | "ng-deep" 26 | ] 27 | } 28 | ], 29 | "selector-class-pattern": null, 30 | "selector-type-no-unknown": null, 31 | "value-keyword-case": null, 32 | "scss/at-extend-no-missing-placeholder": null, 33 | "scss/at-if-no-null": null, 34 | "scss/comment-no-empty": null, 35 | "scss/operator-no-unspaced": null, 36 | "scss/operator-no-newline-after": null 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": [ 4 | "angular.ng-template", 5 | "cyrilletuzi.angular-schematics", 6 | "esbenp.prettier-vscode", 7 | "stylelint.vscode-stylelint", 8 | "syler.sass-indented", 9 | "editorconfig.editorconfig", 10 | "mrmlnc.vscode-scss" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [100], 3 | "html.format.wrapLineLength": 100, 4 | "html.format.wrapAttributes": "preserve-aligned", 5 | "[javascript]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | }, 8 | "[typescript]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[jsonc]": { 12 | "editor.defaultFormatter": "esbenp.prettier-vscode" 13 | }, 14 | "[html]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode" 16 | }, 17 | "editor.codeActionsOnSave": { 18 | "source.fixAll.stylelint": "explicit" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Acrodata 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GUI 2 | 3 | [![npm](https://img.shields.io/npm/v/@acrodata/gui.svg)](https://www.npmjs.com/package/@acrodata/gui) 4 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/acrodata/gui/blob/main/LICENSE) 5 | 6 | ![cover](https://github.com/acrodata/gui/assets/20625845/275f7f08-7971-4a4d-8fe8-ff8ead67beac) 7 | 8 | JSON powered GUI for configurable panels. 9 | 10 | #### Quick links 11 | 12 | [Documentation](https://acrodata.github.io/gui/) | 13 | [Playground](https://acrodata.github.io/gui/playground) 14 | 15 | ## Compatibility 16 | 17 | | Angular | @acrodata/gui | Theming | 18 | | -------- | ------------- | ------- | 19 | | >=18.0.0 | 2.x | M2, M3 | 20 | | >=17.0.0 | 1.x | M2 | 21 | | >=16.0.0 | 0.x | M2 | 22 | 23 | ## Installation 24 | 25 | ```bash 26 | npm install @angular/material @ng-matero/extensions @acrodata/gui --save 27 | ``` 28 | 29 | ## Setup 30 | 31 | Define a theme with Angular Material's theming system. [More about theming](https://material.angular.io/guide/theming). 32 | 33 | ```scss 34 | @use '@angular/material' as mat; 35 | @use '@acrodata/gui' as gui; 36 | 37 | @include mat.core(); 38 | 39 | $theme: mat.define-theme(...); 40 | 41 | @include gui.all-control-themes($theme); 42 | ``` 43 | 44 | 🚨 If you use the Angular Material as default library and have included all component themes, there's no need to include the GUI themes anymore. 45 | 46 | ```diff 47 | + @include mat.all-component-themes($theme); 48 | + @include mtx.all-component-themes($theme); 49 | - @include gui.all-control-themes($theme); 50 | ``` 51 | 52 | ## Usage 53 | 54 | ```ts 55 | import { Component } from '@angular/core'; 56 | import { GuiFields, GuiForm } from '@acrodata/gui'; 57 | 58 | @Component({ 59 | selector: 'your-app', 60 | template: ``, 61 | standalone: true, 62 | imports: [GuiForm], 63 | }) 64 | export class YourAppComponent { 65 | config: GuiFields = { 66 | title: { 67 | type: 'text', 68 | name: 'Title', 69 | default: 'I am title', 70 | }, 71 | }; 72 | model = {}; 73 | form = new FormGroup({}); 74 | } 75 | ``` 76 | 77 | ## License 78 | 79 | MIT 80 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', 7 | [ 8 | 'build', 9 | 'ci', 10 | 'chore', 11 | 'docs', 12 | 'feat', 13 | 'fix', 14 | 'perf', 15 | 'refactor', 16 | 'revert', 17 | 'style', 18 | 'test', 19 | ], 20 | ], 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const eslint = require('@eslint/js'); 3 | const tseslint = require('typescript-eslint'); 4 | const angular = require('angular-eslint'); 5 | 6 | module.exports = tseslint.config( 7 | { 8 | files: ['**/*.ts'], 9 | extends: [ 10 | eslint.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | ...tseslint.configs.stylistic, 13 | ...angular.configs.tsRecommended, 14 | ], 15 | processor: angular.processInlineTemplates, 16 | rules: { 17 | 'max-len': [ 18 | 'warn', 19 | { 20 | code: 100, 21 | ignoreComments: true, 22 | ignoreStrings: true, 23 | ignoreTemplateLiterals: true, 24 | }, 25 | ], 26 | 'object-shorthand': ['warn', 'always', { avoidQuotes: true }], 27 | 'quote-props': ['warn', 'consistent-as-needed'], 28 | 'quotes': ['warn', 'single', { allowTemplateLiterals: true }], 29 | 'semi': ['warn', 'always'], 30 | '@typescript-eslint/no-empty-function': 'off', 31 | '@typescript-eslint/no-explicit-any': 'off', 32 | '@typescript-eslint/no-inferrable-types': 'off', 33 | '@typescript-eslint/no-unused-vars': 'off', 34 | '@angular-eslint/component-class-suffix': 'off', 35 | '@angular-eslint/directive-class-suffix': 'off', 36 | '@angular-eslint/no-empty-lifecycle-method': 'off', 37 | '@angular-eslint/no-output-native': 'off', 38 | }, 39 | }, 40 | { 41 | files: ['**/*.html'], 42 | extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility], 43 | rules: { 44 | '@angular-eslint/template/prefer-self-closing-tags': 'warn', 45 | }, 46 | } 47 | ); 48 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.ts': filenames => [ 3 | `eslint --fix ${filenames.join(' ')}`, 4 | `prettier --write ${filenames.join(' ')}`, 5 | ], 6 | '*.scss': filenames => `stylelint --fix ${filenames.join(' ')}`, 7 | '*.{html,css,js,json,md,yml}': filenames => `git add ${filenames.join(' ')}`, 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gui-srcs", 3 | "version": "0.0.0-NOT-USED", 4 | "engines": { 5 | "yarn": ">=1.22.0 <2", 6 | "npm": "Please use Yarn instead of NPM to install dependencies" 7 | }, 8 | "scripts": { 9 | "ng": "ng", 10 | "start": "ng serve dev-app", 11 | "build": "ng build dev-app --base-href=/gui/", 12 | "watch": "ng build dev-app --watch --configuration development", 13 | "test": "ng test dev-app", 14 | "serve:docs": "ng serve docs", 15 | "build:docs": "ng build docs --base-href=/gui/", 16 | "lint": "ng lint --fix && npm run lint:scss", 17 | "lint:ts": "eslint \"projects/**/*.ts\" --fix", 18 | "lint:scss": "stylelint \"projects/**/*.scss\" --fix", 19 | "build:lib": "ng build gui && cp README.md dist/gui", 20 | "publish": "npm run build:lib && cd dist/gui && npm publish", 21 | "deploy": "npm run build:docs && ng deploy", 22 | "prepare": "husky install" 23 | }, 24 | "private": true, 25 | "dependencies": { 26 | "@acrodata/code-editor": "^0.5.1", 27 | "@acrodata/rnd-dialog": "^0.3.2", 28 | "@angular/animations": "^18.2.0", 29 | "@angular/cdk": "^18.2.0", 30 | "@angular/common": "^18.2.0", 31 | "@angular/compiler": "^18.2.0", 32 | "@angular/core": "^18.2.0", 33 | "@angular/forms": "^18.2.0", 34 | "@angular/material": "^18.2.0", 35 | "@angular/platform-browser": "^18.2.0", 36 | "@angular/platform-browser-dynamic": "^18.2.0", 37 | "@angular/router": "^18.2.0", 38 | "@codemirror/language-data": "^6.5.1", 39 | "@mdi/font": "^7.4.47", 40 | "@ng-matero/extensions": "^18.5.0", 41 | "lodash-es": "^4.17.21", 42 | "ngx-highlightjs": "^12.0.0", 43 | "rxjs": "~7.8.0", 44 | "tslib": "^2.3.0", 45 | "zone.js": "~0.14.10" 46 | }, 47 | "devDependencies": { 48 | "@angular-devkit/build-angular": "^18.2.0", 49 | "@angular/cli": "^18.2.0", 50 | "@angular/compiler-cli": "^18.2.0", 51 | "@commitlint/cli": "^19.5.0", 52 | "@commitlint/config-conventional": "^19.5.0", 53 | "@types/jasmine": "~5.1.0", 54 | "@types/lodash-es": "^4.17.10", 55 | "@types/node": "^20.14.0", 56 | "angular-cli-ghpages": "^2.0.0", 57 | "angular-eslint": "^18.3.1", 58 | "eslint": "^9.12.0", 59 | "husky": "^9.1.6", 60 | "jasmine-core": "~5.1.0", 61 | "karma": "~6.4.0", 62 | "karma-chrome-launcher": "~3.2.0", 63 | "karma-coverage": "~2.2.0", 64 | "karma-jasmine": "~5.1.0", 65 | "karma-jasmine-html-reporter": "~2.1.0", 66 | "lint-staged": "^15.2.0", 67 | "ng-packagr": "^18.2.0", 68 | "prettier": "^3.3.0", 69 | "stylelint": "^16.7.0", 70 | "stylelint-config-recommended-scss": "^14.1.0", 71 | "stylelint-config-standard": "^36.0.0", 72 | "typescript": "~5.5.2", 73 | "typescript-eslint": "^8.9.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /projects/dev-app/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const tseslint = require('typescript-eslint'); 3 | const rootConfig = require('../../eslint.config.js'); 4 | 5 | module.exports = tseslint.config( 6 | ...rootConfig, 7 | { 8 | files: ['**/*.ts'], 9 | rules: { 10 | '@angular-eslint/directive-selector': [ 11 | 'error', 12 | { 13 | type: 'attribute', 14 | prefix: 'app', 15 | style: 'camelCase', 16 | }, 17 | ], 18 | '@angular-eslint/component-selector': [ 19 | 'error', 20 | { 21 | type: 'element', 22 | prefix: 'app', 23 | style: 'kebab-case', 24 | }, 25 | ], 26 | }, 27 | }, 28 | { 29 | files: ['**/*.html'], 30 | rules: {}, 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/dev-app/src/app/app.component.scss -------------------------------------------------------------------------------- /projects/dev-app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { RouterOutlet } from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | standalone: true, 7 | imports: [RouterOutlet], 8 | templateUrl: './app.component.html', 9 | styleUrl: './app.component.scss', 10 | }) 11 | export class AppComponent { 12 | title = 'dev-app'; 13 | } 14 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { provideHttpClient } from '@angular/common/http'; 2 | import { ApplicationConfig } from '@angular/core'; 3 | import { provideAnimations } from '@angular/platform-browser/animations'; 4 | import { provideRouter } from '@angular/router'; 5 | 6 | import { routes } from './app.routes'; 7 | 8 | export const appConfig: ApplicationConfig = { 9 | providers: [provideRouter(routes), provideAnimations(), provideHttpClient()], 10 | }; 11 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { LayoutComponent } from './layout/layout.component'; 3 | import { HomeComponent } from './home/home.component'; 4 | import { ExamplesComponent } from './examples/examples.component'; 5 | import { ControlsComponent } from './controls/controls.component'; 6 | import { PlaygroundComponent } from './playground/playground.component'; 7 | 8 | export const routes: Routes = [ 9 | { 10 | path: '', 11 | component: LayoutComponent, 12 | children: [ 13 | { path: '', pathMatch: 'full', redirectTo: 'home' }, 14 | { path: 'home', component: HomeComponent }, 15 | { path: 'examples', component: ExamplesComponent }, 16 | { path: 'controls', component: ControlsComponent }, 17 | { path: 'playground', component: PlaygroundComponent }, 18 | ], 19 | }, 20 | ]; 21 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/controls/controls.component.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 56 | 57 | 58 | 59 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/controls/controls.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/dev-app/src/app/controls/controls.component.scss -------------------------------------------------------------------------------- /projects/dev-app/src/app/controls/controls.component.ts: -------------------------------------------------------------------------------- 1 | import { GuiModule } from '@acrodata/gui'; 2 | import { Component } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-controls', 6 | standalone: true, 7 | imports: [GuiModule], 8 | templateUrl: './controls.component.html', 9 | styleUrl: './controls.component.scss', 10 | }) 11 | export class ControlsComponent {} 12 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/examples/examples.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/examples/examples.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | grid-template-columns: 320px 1fr; 4 | gap: 16px; 5 | height: 100%; 6 | } 7 | 8 | section { 9 | position: relative; 10 | border: 1px solid var(--mat-divider-color); 11 | overflow: auto; 12 | } 13 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |

Welcome to the development demos for GUI!

2 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/home/home.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/dev-app/src/app/home/home.component.scss -------------------------------------------------------------------------------- /projects/dev-app/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | standalone: true, 6 | templateUrl: './home.component.html', 7 | styleUrl: './home.component.scss', 8 | }) 9 | export class HomeComponent {} 10 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/layout/layout.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 |
12 | 13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/layout/layout.component.scss: -------------------------------------------------------------------------------- 1 | mat-sidenav-container { 2 | height: 100%; 3 | } 4 | 5 | mat-toolbar { 6 | justify-content: space-between; 7 | } 8 | 9 | .demo-content { 10 | height: calc(100% - 64px); 11 | padding: 16px; 12 | } 13 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/layout/layout.component.ts: -------------------------------------------------------------------------------- 1 | import { GuiCodeareaConfig } from '@acrodata/gui'; 2 | import { Directionality } from '@angular/cdk/bidi'; 3 | import { DOCUMENT } from '@angular/common'; 4 | import { Component, Inject, OnInit, ViewEncapsulation } from '@angular/core'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatSidenavModule } from '@angular/material/sidenav'; 7 | import { MatToolbarModule } from '@angular/material/toolbar'; 8 | import { RouterModule } from '@angular/router'; 9 | import { LanguageDescription } from '@codemirror/language'; 10 | import { SettingsService } from '../settings.service'; 11 | import { json, jsonParseLinter } from '@codemirror/lang-json'; 12 | import { lintGutter, linter } from '@codemirror/lint'; 13 | 14 | export const CODEAREA_LANGUAGES = [ 15 | LanguageDescription.of({ 16 | name: 'JSON', 17 | alias: ['json5'], 18 | extensions: ['json', 'map'], 19 | load: () => import('@codemirror/lang-json').then(m => m.json()), 20 | }), 21 | LanguageDescription.of({ 22 | name: 'JavaScript', 23 | alias: ['ecmascript', 'js', 'node'], 24 | extensions: ['js', 'mjs', 'cjs'], 25 | load: () => import('@codemirror/lang-javascript').then(m => m.javascript()), 26 | }), 27 | ]; 28 | 29 | @Component({ 30 | selector: 'app-layout', 31 | standalone: true, 32 | imports: [RouterModule, MatSidenavModule, MatToolbarModule, MatButtonModule], 33 | templateUrl: './layout.component.html', 34 | styleUrl: './layout.component.scss', 35 | encapsulation: ViewEncapsulation.None, 36 | }) 37 | export class LayoutComponent implements OnInit { 38 | htmlElement!: HTMLHtmlElement; 39 | darkThemeClass = 'dark-theme'; 40 | isDark = false; 41 | isRtl = false; 42 | 43 | constructor( 44 | @Inject(DOCUMENT) private document: Document, 45 | private dir: Directionality, 46 | private settings: SettingsService, 47 | private codeareaCfg: GuiCodeareaConfig 48 | ) {} 49 | 50 | ngOnInit(): void { 51 | this.htmlElement = this.document.querySelector('html')!; 52 | this.isRtl = this.dir.value === 'rtl'; 53 | this.codeareaCfg.languages = CODEAREA_LANGUAGES; 54 | this.codeareaCfg.extensions = data => { 55 | return data.language == 'json' ? [lintGutter(), linter(jsonParseLinter())] : []; 56 | }; 57 | } 58 | 59 | toggleThemeClass() { 60 | this.isDark = !this.isDark; 61 | 62 | if (this.isDark) { 63 | this.htmlElement.classList.add(this.darkThemeClass); 64 | this.codeareaCfg.theme = 'dark'; 65 | } else { 66 | this.htmlElement.classList.remove(this.darkThemeClass); 67 | this.codeareaCfg.theme = 'light'; 68 | } 69 | 70 | this.codeareaCfg.changes.next(); 71 | this.settings.themeChange.next(this.isDark ? 'dark' : 'light'); 72 | } 73 | 74 | toggleRtl() { 75 | this.isRtl = !this.isRtl; 76 | 77 | if (this.isRtl) { 78 | this.htmlElement.dir = 'rtl'; 79 | } else { 80 | this.htmlElement.dir = 'ltr'; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/playground/playground.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/playground/playground.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: grid; 3 | grid-template-columns: 1fr 320px 1fr; 4 | gap: 16px; 5 | height: 100%; 6 | } 7 | 8 | section { 9 | position: relative; 10 | border: 1px solid var(--mat-divider-color); 11 | overflow: auto; 12 | } 13 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/playground/playground.component.ts: -------------------------------------------------------------------------------- 1 | import { CodeEditor, Theme } from '@acrodata/code-editor'; 2 | import { GuiFields, GuiForm } from '@acrodata/gui'; 3 | import { CommonModule } from '@angular/common'; 4 | import { Component, DestroyRef, OnInit } from '@angular/core'; 5 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 6 | import { FormGroup, FormsModule } from '@angular/forms'; 7 | import { json, jsonParseLinter } from '@codemirror/lang-json'; 8 | import { lintGutter, linter } from '@codemirror/lint'; 9 | import { SettingsService } from '../settings.service'; 10 | 11 | @Component({ 12 | selector: 'app-playground', 13 | standalone: true, 14 | imports: [CommonModule, FormsModule, CodeEditor, GuiForm], 15 | templateUrl: './playground.component.html', 16 | styleUrl: './playground.component.scss', 17 | }) 18 | export class PlaygroundComponent implements OnInit { 19 | form = new FormGroup({}); 20 | config: GuiFields = { 21 | title: { 22 | type: 'text', 23 | name: '标题', 24 | default: '我是标题', 25 | }, 26 | align: { 27 | type: 'buttonToggle', 28 | name: '对齐方式', 29 | options: [ 30 | { value: 'left', label: '左对齐' }, 31 | { value: 'center', label: '居中对齐' }, 32 | { value: 'right', label: '右对齐' }, 33 | ], 34 | default: 'center', 35 | }, 36 | size: { 37 | type: 'group', 38 | name: '尺寸', 39 | children: { 40 | width: { 41 | type: 'number', 42 | name: '宽度', 43 | default: 100, 44 | }, 45 | height: { 46 | type: 'number', 47 | name: '高度', 48 | default: 100, 49 | }, 50 | }, 51 | }, 52 | }; 53 | model: any = {}; 54 | 55 | configStr = ''; 56 | 57 | theme: Theme = 'light'; 58 | 59 | extensions = [json(), linter(jsonParseLinter()), lintGutter()]; 60 | 61 | constructor( 62 | private destroyRef: DestroyRef, 63 | private settings: SettingsService 64 | ) {} 65 | 66 | ngOnInit(): void { 67 | this.configStr = JSON.stringify(this.config, null, 2); 68 | 69 | this.form.valueChanges.subscribe(v => { 70 | console.log(v); 71 | }); 72 | 73 | this.settings.themeChange.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(theme => { 74 | this.theme = theme; 75 | }); 76 | } 77 | 78 | onConfigChange() { 79 | this.config = JSON.parse(this.configStr); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /projects/dev-app/src/app/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class SettingsService { 8 | themeChange = new Subject<'light' | 'dark'>(); 9 | 10 | constructor() {} 11 | } 12 | -------------------------------------------------------------------------------- /projects/dev-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/dev-app/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/dev-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/dev-app/src/favicon.ico -------------------------------------------------------------------------------- /projects/dev-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DevApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/dev-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); 6 | -------------------------------------------------------------------------------- /projects/dev-app/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | // @import '@angular/material/prebuilt-themes/indigo-pink.css'; 4 | // @import '@ng-matero/extensions/prebuilt-themes/indigo-pink.css'; 5 | 6 | @use './theme'; 7 | 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html, 15 | body { 16 | height: 100%; 17 | margin: 0; 18 | } 19 | 20 | textarea { 21 | display: block; 22 | width: 100%; 23 | height: 100%; 24 | font-size: 12px; 25 | border: none; 26 | outline: none; 27 | resize: vertical; 28 | background: transparent; 29 | color: inherit; 30 | } 31 | 32 | .gui-form { 33 | background-color: white; 34 | 35 | .dark-theme & { 36 | background-color: transparent; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /projects/dev-app/src/theme-m2.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @use '@ng-matero/extensions' as mtx; 3 | @use '../../gui' as gui; 4 | 5 | @include mat.core(); 6 | 7 | $my-primary: mat.m2-define-palette(mat.$m2-indigo-palette, 500); 8 | $my-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400); 9 | $my-theme: mat.m2-define-light-theme( 10 | ( 11 | color: ( 12 | primary: $my-primary, 13 | accent: $my-accent, 14 | ), 15 | typography: mat.m2-define-typography-config(), 16 | density: 0, 17 | ) 18 | ); 19 | 20 | @include mat.all-component-themes($my-theme); 21 | @include mtx.all-component-themes($my-theme); 22 | // @include gui.all-control-themes($my-theme); 23 | 24 | .dark-theme { 25 | $dark-primary: mat.m2-define-palette(mat.$m2-blue-grey-palette); 26 | $dark-accent: mat.m2-define-palette(mat.$m2-amber-palette, A200, A100, A400); 27 | $dark-theme: mat.m2-define-dark-theme( 28 | ( 29 | color: ( 30 | primary: $dark-primary, 31 | accent: $dark-accent, 32 | ), 33 | typography: mat.m2-define-typography-config(), 34 | density: 0, 35 | ) 36 | ); 37 | 38 | @include mat.all-component-colors($dark-theme); 39 | @include mtx.all-component-colors($dark-theme); 40 | // @include gui.all-control-colors($dark-theme); 41 | } 42 | -------------------------------------------------------------------------------- /projects/dev-app/src/theme.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @use '@ng-matero/extensions' as mtx; 3 | @use '../../gui' as gui; 4 | 5 | @include mat.core(); 6 | 7 | $light-config: ( 8 | color: ( 9 | theme-type: 'light', 10 | primary: mat.$azure-palette, 11 | tertiary: mat.$blue-palette, 12 | ), 13 | ); 14 | 15 | // Define the default (light) theme. 16 | $light-theme: mat.private-deep-merge-all( 17 | mat.define-theme($light-config), 18 | mtx.define-theme($light-config) 19 | ); 20 | 21 | html { 22 | @include mat.all-component-themes($light-theme); 23 | @include mtx.all-component-themes($light-theme); 24 | // @include gui.all-control-themes($light-theme); 25 | } 26 | 27 | $dark-config: ( 28 | color: ( 29 | theme-type: 'dark', 30 | primary: mat.$cyan-palette, 31 | tertiary: mat.$orange-palette, 32 | ), 33 | ); 34 | 35 | // Define an alternate dark theme. 36 | $dark-theme: mat.private-deep-merge-all( 37 | mat.define-theme($dark-config), 38 | mtx.define-theme($dark-config) 39 | ); 40 | 41 | .dark-theme { 42 | @include mat.all-component-colors($dark-theme); 43 | @include mtx.all-component-colors($dark-theme); 44 | // @include gui.all-control-colors($dark-theme); 45 | } 46 | -------------------------------------------------------------------------------- /projects/dev-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/dev-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/docs/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const tseslint = require("typescript-eslint"); 3 | const rootConfig = require("../../eslint.config.js"); 4 | 5 | module.exports = tseslint.config( 6 | ...rootConfig, 7 | { 8 | files: ["**/*.ts"], 9 | rules: { 10 | "@angular-eslint/directive-selector": [ 11 | "error", 12 | { 13 | type: "attribute", 14 | prefix: "app", 15 | style: "camelCase", 16 | }, 17 | ], 18 | "@angular-eslint/component-selector": [ 19 | "error", 20 | { 21 | type: "element", 22 | prefix: "app", 23 | style: "kebab-case", 24 | }, 25 | ], 26 | }, 27 | }, 28 | { 29 | files: ["**/*.html"], 30 | rules: {}, 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /projects/docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/public/favicon.ico -------------------------------------------------------------------------------- /projects/docs/public/icons/github.svg: -------------------------------------------------------------------------------- 1 | github 2 | -------------------------------------------------------------------------------- /projects/docs/public/icons/invert_colors.svg: -------------------------------------------------------------------------------- 1 | invert-colors 2 | -------------------------------------------------------------------------------- /projects/docs/public/icons/menu.svg: -------------------------------------------------------------------------------- 1 | menu 2 | -------------------------------------------------------------------------------- /projects/docs/public/images/align_center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/public/images/align_center.png -------------------------------------------------------------------------------- /projects/docs/public/images/align_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/public/images/align_left.png -------------------------------------------------------------------------------- /projects/docs/public/images/align_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/public/images/align_right.png -------------------------------------------------------------------------------- /projects/docs/public/images/icon3d1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/public/images/icon3d1.webp -------------------------------------------------------------------------------- /projects/docs/public/images/icon3d2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/public/images/icon3d2.webp -------------------------------------------------------------------------------- /projects/docs/public/images/icon3d3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/public/images/icon3d3.webp -------------------------------------------------------------------------------- /projects/docs/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/docs/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/src/app/app.component.scss -------------------------------------------------------------------------------- /projects/docs/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { MatIconRegistry } from '@angular/material/icon'; 3 | import { DomSanitizer } from '@angular/platform-browser'; 4 | import { RouterOutlet } from '@angular/router'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | standalone: true, 9 | imports: [RouterOutlet], 10 | templateUrl: './app.component.html', 11 | styleUrl: './app.component.scss', 12 | encapsulation: ViewEncapsulation.None, 13 | }) 14 | export class AppComponent { 15 | title = 'docs'; 16 | 17 | icons = ['menu', 'github', 'invert_colors']; 18 | 19 | constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) { 20 | for (const i of this.icons) { 21 | iconRegistry.addSvgIcon(i, sanitizer.bypassSecurityTrustResourceUrl(`icons/${i}.svg`)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projects/docs/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { GuiCodeareaConfig } from '@acrodata/gui'; 2 | import { provideHttpClient } from '@angular/common/http'; 3 | import { ApplicationConfig } from '@angular/core'; 4 | import { provideAnimations } from '@angular/platform-browser/animations'; 5 | import { provideRouter, withInMemoryScrolling } from '@angular/router'; 6 | import { LanguageDescription } from '@codemirror/language'; 7 | import { provideHighlightOptions } from 'ngx-highlightjs'; 8 | import { routes } from './app.routes'; 9 | 10 | export const CODEAREA_LANGUAGES = [ 11 | LanguageDescription.of({ 12 | name: 'JSON', 13 | alias: ['json5'], 14 | extensions: ['json', 'map'], 15 | load: () => import('@codemirror/lang-json').then(m => m.json()), 16 | }), 17 | LanguageDescription.of({ 18 | name: 'JavaScript', 19 | alias: ['ecmascript', 'js', 'node'], 20 | extensions: ['js', 'mjs', 'cjs'], 21 | load: () => import('@codemirror/lang-javascript').then(m => m.javascript()), 22 | }), 23 | LanguageDescription.of({ 24 | name: 'CSS', 25 | extensions: ['css'], 26 | load: () => import('@codemirror/lang-css').then(m => m.css()), 27 | }), 28 | LanguageDescription.of({ 29 | name: 'HTML', 30 | alias: ['xhtml'], 31 | extensions: ['html', 'htm', 'handlebars', 'hbs'], 32 | load: () => import('@codemirror/lang-html').then(m => m.html()), 33 | }), 34 | ]; 35 | 36 | export const appConfig: ApplicationConfig = { 37 | providers: [ 38 | provideRouter( 39 | routes, 40 | withInMemoryScrolling({ 41 | scrollPositionRestoration: 'enabled', 42 | anchorScrolling: 'enabled', 43 | }) 44 | ), 45 | provideAnimations(), 46 | provideHttpClient(), 47 | provideHighlightOptions({ 48 | coreLibraryLoader: () => import('highlight.js/lib/core'), 49 | languages: { 50 | typescript: () => import('highlight.js/lib/languages/typescript'), 51 | scss: () => import('highlight.js/lib/languages/scss'), 52 | xml: () => import('highlight.js/lib/languages/xml'), 53 | json: () => import('highlight.js/lib/languages/json'), 54 | diff: () => import('highlight.js/lib/languages/diff'), 55 | bash: () => import('highlight.js/lib/languages/bash'), 56 | }, 57 | }), 58 | { 59 | provide: GuiCodeareaConfig, 60 | useValue: { 61 | ...new GuiCodeareaConfig(), 62 | languages: CODEAREA_LANGUAGES, 63 | }, 64 | }, 65 | ], 66 | }; 67 | -------------------------------------------------------------------------------- /projects/docs/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { LayoutComponent } from './layout/layout.component'; 3 | import { HomeComponent } from './pages/home/home.component'; 4 | 5 | export const routes: Routes = [ 6 | { 7 | path: '', 8 | component: LayoutComponent, 9 | children: [ 10 | { path: '', redirectTo: 'home', pathMatch: 'full' }, 11 | { path: 'home', component: HomeComponent }, 12 | { 13 | path: 'getting-started', 14 | loadComponent: () => 15 | import('./pages/getting-started/getting-started.component').then( 16 | m => m.GettingStartedComponent 17 | ), 18 | }, 19 | { 20 | path: 'basic-controls', 21 | loadComponent: () => 22 | import('./pages/basic-controls/basic-controls.component').then( 23 | m => m.BasicControlsComponent 24 | ), 25 | }, 26 | { 27 | path: 'media-controls', 28 | loadComponent: () => 29 | import('./pages/media-controls/media-controls.component').then( 30 | m => m.MediaControlsComponent 31 | ), 32 | }, 33 | { 34 | path: 'group-controls', 35 | loadComponent: () => 36 | import('./pages/group-controls/group-controls.component').then( 37 | m => m.GroupControlsComponent 38 | ), 39 | }, 40 | { 41 | path: 'conditions', 42 | loadComponent: () => 43 | import('./pages/conditions/conditions.component').then(m => m.ConditionsComponent), 44 | }, 45 | { 46 | path: 'playground', 47 | loadComponent: () => 48 | import('./pages/playground/playground.component').then(m => m.PlaygroundComponent), 49 | }, 50 | ], 51 | }, 52 | { path: '**', redirectTo: 'home' }, 53 | ]; 54 | -------------------------------------------------------------------------------- /projects/docs/src/app/layout/layout.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 7 | 8 | Acrodata GUI 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 | 40 | 41 |
42 | 43 | 44 |
© 2023 Made with ❤️ by nzbin
45 |
46 |
47 | 48 |
49 | -------------------------------------------------------------------------------- /projects/docs/src/app/layout/layout.component.scss: -------------------------------------------------------------------------------- 1 | .container-wrap { 2 | display: block; 3 | max-width: 1280px; 4 | padding: 0 64px; 5 | margin: 0 auto; 6 | } 7 | 8 | .container { 9 | display: flex; 10 | gap: 32px; 11 | } 12 | 13 | main { 14 | flex: 1; 15 | width: 0; 16 | padding-top: 16px; 17 | } 18 | 19 | aside { 20 | position: sticky; 21 | top: 0; 22 | display: block; 23 | width: 240px; 24 | height: 100vh; 25 | padding: 16px 0 32px; 26 | overflow: auto; 27 | background-color: var(--bg-color); 28 | } 29 | 30 | header { 31 | position: relative; 32 | z-index: 100; 33 | display: flex; 34 | align-items: center; 35 | padding: 12px 0; 36 | border-bottom: 1px solid var(--border-color); 37 | background-color: var(--bg-translucent-color); 38 | backdrop-filter: blur(6px); 39 | } 40 | 41 | footer { 42 | padding: 32px 0; 43 | margin-top: 64px; 44 | font-size: 0.75rem; 45 | text-align: center; 46 | } 47 | 48 | .menu, 49 | .submenu { 50 | padding: 0; 51 | margin: 0; 52 | list-style-type: none; 53 | 54 | .menu-item { 55 | position: relative; 56 | display: block; 57 | padding: 8px 16px; 58 | border-radius: 8px; 59 | text-decoration: none; 60 | 61 | &:hover { 62 | background-color: var(--bg-secondary-color); 63 | } 64 | } 65 | } 66 | 67 | .menu > li + li { 68 | margin-top: 24px; 69 | } 70 | 71 | .menu-item { 72 | font-weight: 500; 73 | color: var(--fg-color); 74 | 75 | &.active { 76 | &::before { 77 | content: ''; 78 | position: absolute; 79 | top: 12px; 80 | left: 0; 81 | display: inline-block; 82 | width: 4px; 83 | height: 16px; 84 | background-color: currentColor; 85 | border-radius: 2px; 86 | } 87 | } 88 | 89 | .submenu & { 90 | color: var(--fg-secondary-color); 91 | } 92 | } 93 | 94 | .brand { 95 | display: inline-block; 96 | padding: 0 16px; 97 | color: var(--heading-color); 98 | font-size: 20px; 99 | font-weight: 600; 100 | line-height: 40px; 101 | } 102 | 103 | .spacer { 104 | flex-grow: 1; 105 | } 106 | 107 | @media (width <=960px) { 108 | .container-wrap { 109 | padding: 0 32px; 110 | } 111 | 112 | aside { 113 | position: fixed; 114 | top: 0; 115 | bottom: 0; 116 | left: 0; 117 | z-index: 200; 118 | transform: translateX(-100%); 119 | transition: transform 0.3s ease-in-out; 120 | 121 | &.show { 122 | transform: none; 123 | } 124 | } 125 | 126 | .backdrop { 127 | position: fixed; 128 | top: 0; 129 | left: 0; 130 | z-index: 190; 131 | width: 100vw; 132 | height: 100vh; 133 | visibility: hidden; 134 | transition-duration: 400ms; 135 | transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1); 136 | transition-property: background-color, visibility; 137 | 138 | &.show { 139 | background-color: rgba(0, 0, 0, 0.5); 140 | visibility: visible; 141 | } 142 | } 143 | 144 | header { 145 | position: sticky; 146 | top: 0; 147 | } 148 | 149 | .brand { 150 | padding: 0 8px; 151 | } 152 | } 153 | 154 | @media (width <=600px) { 155 | .container-wrap { 156 | padding: 0 16px; 157 | } 158 | } 159 | 160 | @media (width > 960px) { 161 | .menu-toggle { 162 | display: none; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /projects/docs/src/app/layout/layout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewEncapsulation } from '@angular/core'; 2 | import { MatIconModule } from '@angular/material/icon'; 3 | import { NavigationEnd, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; 4 | import { filter } from 'rxjs'; 5 | import { ThemePickerComponent } from '../shared'; 6 | 7 | @Component({ 8 | selector: 'app-layout', 9 | standalone: true, 10 | imports: [RouterOutlet, RouterLink, RouterLinkActive, MatIconModule, ThemePickerComponent], 11 | templateUrl: './layout.component.html', 12 | styleUrl: './layout.component.scss', 13 | host: { 14 | class: 'container-wrap', 15 | }, 16 | encapsulation: ViewEncapsulation.None, 17 | }) 18 | export class LayoutComponent { 19 | menu = [ 20 | { 21 | name: 'Getting started', 22 | route: 'getting-started', 23 | children: [], 24 | }, 25 | { 26 | name: 'Basic controls', 27 | route: 'basic-controls', 28 | children: [ 29 | { name: 'Text', route: 'text' }, 30 | { name: 'Number', route: 'number' }, 31 | { name: 'Switch', route: 'switch' }, 32 | { name: 'Slider', route: 'slider' }, 33 | { name: 'Fill', route: 'fill' }, 34 | { name: 'Select', route: 'select' }, 35 | { name: 'Combobox', route: 'combobox' }, 36 | { name: 'Button Toggle', route: 'button_toggle' }, 37 | { name: 'Image Select', route: 'image_select' }, 38 | { name: 'Textarea', route: 'textarea' }, 39 | { name: 'Codearea', route: 'codearea' }, 40 | { name: 'Hidden', route: 'hidden' }, 41 | ], 42 | }, 43 | { 44 | name: 'Group controls', 45 | route: 'group-controls', 46 | children: [ 47 | { name: 'Group', route: 'group' }, 48 | { name: 'Inline Group', route: 'inline_group' }, 49 | { name: 'Tabs', route: 'tabs' }, 50 | { name: 'Menu', route: 'menu' }, 51 | ], 52 | }, 53 | { 54 | name: 'Media controls', 55 | route: 'media-controls', 56 | children: [ 57 | { name: 'Upload Settings', route: 'upload_settings' }, 58 | { name: 'Image', route: 'image' }, 59 | { name: 'Video', route: 'video' }, 60 | { name: 'Audio', route: 'audio' }, 61 | { name: 'File', route: 'file' }, 62 | ], 63 | }, 64 | { 65 | name: 'Conditions', 66 | route: 'conditions', 67 | children: [], 68 | }, 69 | { 70 | name: 'Playground', 71 | route: 'playground', 72 | children: [], 73 | }, 74 | ]; 75 | 76 | menuOpened = false; 77 | 78 | constructor(private router: Router) { 79 | this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(e => { 80 | this.menuOpened = false; 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/basic-controls/basic-controls.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |

7 | Text 8 |

9 | 10 | 11 | 12 |

13 | Number 14 |

15 | 16 | 17 | 18 |

19 | Switch 20 |

21 | 22 | 23 | 24 |

25 | Slider 26 |

27 | 28 |
29 | 33 |
34 | 35 | 36 | 37 |

38 | Fill 39 |

40 | 41 | 42 | 43 |

44 | Select 45 |

46 | 47 |
48 | 52 |
53 | 54 | 55 | 56 |

57 | Combobox 58 |

59 | 60 |
61 | 65 |
66 | 67 | 68 | 69 |

70 | Button Toggle 71 |

72 | 73 |
74 | 82 | 83 | 93 | 103 | 113 |
114 | 115 | 116 | 117 |

118 | If you have lots of options, you can make a grid for options with the 119 | col 120 | attr. 121 |

122 | 123 | 124 | 125 |

126 | Image Select 127 |

128 | 129 | 130 | 131 |

132 | Textarea 133 |

134 | 135 | 136 | 137 |

138 | Codearea 139 |

140 | 141 |
142 | 143 | If you want to configure the language, you must define the 144 | languages 145 | at first. 146 | 147 | 148 |
159 |
160 | 161 | 162 | 163 |

164 | Hidden 165 |

166 | 167 | 168 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/basic-controls/basic-controls.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/src/app/pages/basic-controls/basic-controls.component.scss -------------------------------------------------------------------------------- /projects/docs/src/app/pages/basic-controls/basic-controls.component.ts: -------------------------------------------------------------------------------- 1 | import { GuiFields } from '@acrodata/gui'; 2 | import { Component } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HighlightModule } from 'ngx-highlightjs'; 5 | import { ExampleViewerComponent, PageHeaderComponent } from '../../shared'; 6 | 7 | @Component({ 8 | selector: 'app-basic-controls', 9 | standalone: true, 10 | imports: [ExampleViewerComponent, PageHeaderComponent, FormsModule, HighlightModule], 11 | templateUrl: './basic-controls.component.html', 12 | styleUrl: './basic-controls.component.scss', 13 | }) 14 | export class BasicControlsComponent { 15 | textConfig: GuiFields = { 16 | content: { 17 | type: 'text', 18 | name: 'Content', 19 | default: 'Hello world', 20 | description: 'I am description', 21 | prefix: '👋', 22 | suffix: '💖', 23 | }, 24 | }; 25 | 26 | numberConfig: GuiFields = { 27 | opacity: { 28 | type: 'number', 29 | name: 'Opacity', 30 | default: 0.33, 31 | min: 0, 32 | max: 1, 33 | step: 0.01, 34 | }, 35 | }; 36 | 37 | switchConfig: GuiFields = { 38 | visible: { 39 | type: 'switch', 40 | name: 'Visible', 41 | default: false, 42 | }, 43 | }; 44 | 45 | sliderConfig: GuiFields = { 46 | temperature: { 47 | type: 'slider', 48 | name: 'Temperature', 49 | mode: 'normal', 50 | default: 30, 51 | min: 0, 52 | max: 100, 53 | step: 5, 54 | suffix: '°C', 55 | }, 56 | }; 57 | isRangeSlider = false; 58 | toggleRangeSlider() { 59 | const { temperature } = this.sliderConfig; 60 | temperature.mode = this.isRangeSlider ? 'range' : 'normal'; 61 | temperature.default = this.isRangeSlider ? [20, 60] : 30; 62 | this.sliderConfig = { ...this.sliderConfig }; 63 | } 64 | 65 | fillConfig: GuiFields = { 66 | color: { 67 | type: 'fill', 68 | name: 'Color', 69 | default: '#ff0055', 70 | }, 71 | }; 72 | 73 | selectConfig: GuiFields = { 74 | font: { 75 | type: 'select', 76 | name: 'Font', 77 | default: 'arial', 78 | multiple: false, 79 | useFont: true, 80 | options: [ 81 | { value: 'arial', label: 'Arial' }, 82 | { value: 'fantasy', label: 'Fantasy' }, 83 | { value: 'monospace', label: 'Monospace' }, 84 | ], 85 | }, 86 | }; 87 | isMultiSelect = false; 88 | toggleMultiSelect() { 89 | const { font } = this.selectConfig; 90 | font.multiple = this.isMultiSelect; 91 | font.default = this.isMultiSelect ? [] : 'arial'; 92 | this.selectConfig = { ...this.selectConfig }; 93 | } 94 | 95 | comboboxConfig: GuiFields = { 96 | font: { 97 | type: 'combobox', 98 | name: 'Font', 99 | default: 'arial', 100 | multiple: false, 101 | useFont: true, 102 | options: [ 103 | { value: 'arial', label: 'Arial' }, 104 | { value: 'fantasy', label: 'Fantasy' }, 105 | { value: 'monospace', label: 'Monospace' }, 106 | ], 107 | }, 108 | }; 109 | isMultiCombobox = false; 110 | toggleMultiCombobox() { 111 | const { font } = this.comboboxConfig; 112 | font.multiple = this.isMultiCombobox; 113 | font.default = this.isMultiCombobox ? [] : 'arial'; 114 | this.comboboxConfig = { ...this.comboboxConfig }; 115 | } 116 | 117 | buttonToggleConfig: GuiFields = { 118 | textAlign: { 119 | type: 'buttonToggle', 120 | name: 'Align', 121 | default: 'right', 122 | multiple: false, 123 | options: [ 124 | { value: 'left', label: 'Left' }, 125 | { value: 'center', label: 'Center' }, 126 | { value: 'right', label: 'Right' }, 127 | ], 128 | }, 129 | }; 130 | isMultiButtonToggle = false; 131 | toggleMultiButtonToggle() { 132 | const { textAlign } = this.buttonToggleConfig; 133 | textAlign.multiple = this.isMultiButtonToggle; 134 | textAlign.default = this.isMultiButtonToggle ? [] : 'right'; 135 | this.buttonToggleConfig = { ...this.buttonToggleConfig }; 136 | } 137 | useIcon = 0; 138 | toggleIconButtonToggle() { 139 | const { textAlign } = this.buttonToggleConfig; 140 | textAlign.useIcon = this.useIcon > 0 ? true : false; 141 | textAlign.options = textAlign.options?.map(opt => { 142 | return { 143 | ...opt, 144 | src: 145 | this.useIcon == 1 146 | ? 'mdi mdi-format-align-' + opt.value 147 | : this.useIcon == 2 148 | ? './images/align_' + opt.value + '.png' 149 | : undefined, 150 | }; 151 | }); 152 | this.buttonToggleConfig = { ...this.buttonToggleConfig }; 153 | } 154 | 155 | buttonToggleConfig2: GuiFields = { 156 | direction: { 157 | type: 'buttonToggle', 158 | name: 'Direction', 159 | default: 'c', 160 | options: [ 161 | { value: 'nw', label: 'NW', col: 33.33 }, 162 | { value: 'n', label: 'N', col: 33.33 }, 163 | { value: 'ne', label: 'NE', col: 33.33 }, 164 | { value: 'w', label: 'W', col: 33.33 }, 165 | { value: 'c', label: 'C', col: 33.33 }, 166 | { value: 'e', label: 'E', col: 33.33 }, 167 | { value: 'sw', label: 'SW', col: 33.33 }, 168 | { value: 's', label: 'S', col: 33.33 }, 169 | { value: 'se', label: 'SE', col: 33.33 }, 170 | ], 171 | }, 172 | }; 173 | 174 | imageSelectConfig: GuiFields = { 175 | background: { 176 | type: 'imageSelect', 177 | name: 'Background', 178 | default: 'img1', 179 | options: [ 180 | { 181 | label: 'img1', 182 | value: 'img1', 183 | src: './images/icon3d1.webp', 184 | }, 185 | { 186 | label: 'img2', 187 | value: 'img2', 188 | src: './images/icon3d2.webp', 189 | }, 190 | { 191 | label: 'img3', 192 | value: 'img3', 193 | src: './images/icon3d3.webp', 194 | }, 195 | ], 196 | }, 197 | }; 198 | 199 | textareaConfig: GuiFields = { 200 | foo: { 201 | type: 'textarea', 202 | name: 'Foo', 203 | default: 'I am a textarea', 204 | rows: 3, 205 | }, 206 | }; 207 | 208 | codeareaConfig: GuiFields = { 209 | snippet: { 210 | type: 'codearea', 211 | name: 'Snippet', 212 | default: 'console.log("Hello, World!")', 213 | language: 'js', 214 | }, 215 | }; 216 | 217 | hiddenConfig: GuiFields = { 218 | id: { 219 | type: 'hidden', 220 | name: 'ID', 221 | default: 1, 222 | }, 223 | }; 224 | } 225 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/conditions/conditions.component.html: -------------------------------------------------------------------------------- 1 | 3 | 4 |

5 | Rule 6 |

7 | 8 |

9 | Add showIf to control the visibility of the field. 10 |

11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 88 | 89 | 90 |
ParameterDescription
path 25 | At first, it will find the condition form control from the parent form group of current 26 | field. 27 |
28 | If the control cannot be found, it will continue to find from the root form group. 29 |
operator 34 |
    35 |
  • 36 | $eq - 37 | equal 38 |
  • 39 |
  • 40 | $ne - 41 | not equal 42 |
  • 43 |
  • 44 | $gt - 45 | greater than 46 |
  • 47 |
  • 48 | $lt - 49 | less than 50 |
  • 51 |
  • 52 | $gte - 53 | greater than or equal 54 |
  • 55 |
  • 56 | $lte - 57 | less than or equal 58 |
  • 59 |
  • 60 | $in - 61 | in the set 62 |
  • 63 |
  • 64 | $nin - 65 | not in the set 66 |
  • 67 |
68 |
valueThe value of condition form control.
logicalType 77 |
    78 |
  • 79 | $and - 80 | All the conditions should be true 81 |
  • 82 |
  • 83 | $or - 84 | As long as one condition is true 85 |
  • 86 |
87 |
91 | 92 |

93 | Demo1 94 |

95 | 96 | 97 | 98 |

99 | Demo2 100 |

101 | 102 | 103 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/conditions/conditions.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/src/app/pages/conditions/conditions.component.scss -------------------------------------------------------------------------------- /projects/docs/src/app/pages/conditions/conditions.component.ts: -------------------------------------------------------------------------------- 1 | import { GuiFields } from '@acrodata/gui'; 2 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 3 | import { HighlightModule } from 'ngx-highlightjs'; 4 | import { ExampleViewerComponent, PageHeaderComponent } from '../../shared'; 5 | 6 | @Component({ 7 | selector: 'app-conditions', 8 | standalone: true, 9 | imports: [PageHeaderComponent, ExampleViewerComponent, HighlightModule], 10 | templateUrl: './conditions.component.html', 11 | styleUrl: './conditions.component.scss', 12 | changeDetection: ChangeDetectionStrategy.OnPush, 13 | }) 14 | export class ConditionsComponent { 15 | ruleJson = `{ 16 | ... 17 | "showIf": { 18 | "conditions": [ 19 | ["path", "operator", "value"], 20 | ... 21 | ], 22 | "logicalType": "$or" 23 | }, 24 | }`; 25 | 26 | demo1: GuiFields = { 27 | a: { 28 | type: 'group', 29 | name: 'groupA', 30 | children: { 31 | switchA: { 32 | type: 'switch', 33 | name: 'switchA', 34 | default: true, 35 | }, 36 | textA: { 37 | type: 'text', 38 | name: 'textA', 39 | default: 'A', 40 | showIf: { 41 | conditions: [ 42 | ['a.switchA', '$ne', false], 43 | ['b.switchB', '$eq', true], 44 | ], 45 | logicalType: '$or', 46 | }, 47 | }, 48 | }, 49 | expanded: true, 50 | }, 51 | b: { 52 | type: 'group', 53 | name: 'groupB', 54 | children: { 55 | switchB: { 56 | type: 'switch', 57 | name: 'switchB', 58 | default: false, 59 | }, 60 | textB: { 61 | type: 'text', 62 | name: 'textB', 63 | default: 'B', 64 | }, 65 | }, 66 | expanded: true, 67 | }, 68 | }; 69 | 70 | demo2: GuiFields = { 71 | array: { 72 | name: 'Array', 73 | type: 'tabs', 74 | default: [ 75 | { switch: true, text: 'A' }, 76 | { switch: true, text: 'B' }, 77 | ], 78 | template: { 79 | name: 'No.<%= i + 1%>', 80 | children: { 81 | switch: { 82 | type: 'switch', 83 | name: 'switch', 84 | default: true, 85 | }, 86 | text: { 87 | type: 'text', 88 | name: 'text', 89 | showIf: { 90 | conditions: [['switch', '$eq', true]], 91 | }, 92 | }, 93 | }, 94 | }, 95 | expanded: true, 96 | mode: 'list', 97 | }, 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/getting-started/getting-started.component.html: -------------------------------------------------------------------------------- 1 | 3 | 4 |

5 | Installation 6 |

7 | 8 |
10 | 11 |

12 | Setup 13 |

14 | 15 |

Define a theme with Angular Material's theming system. 16 | More about theming. 17 |

18 | 19 |
28 | 29 |

🚨 If you use the Angular Material as default library and have included all component themes, 30 | there's no need to include the GUI themes anymore.

31 | 32 |
36 | 37 |

38 | Usage 39 |

40 | 41 |
62 | 63 |

64 | Properties 65 |

66 | 67 |

68 | Inputs 69 |

70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
NameTypeRequiredDescription
configGuiFieldsyesThe field configurations for building the form.
modelanynoThe model to be represented by the form.
formFormGroupnoThe form instance which allow to track model value and validation status.
101 | 102 |

103 | Outputs 104 |

105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |
NameDescription
modelChangeFired on model value change.
120 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/getting-started/getting-started.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/src/app/pages/getting-started/getting-started.component.scss -------------------------------------------------------------------------------- /projects/docs/src/app/pages/getting-started/getting-started.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { PageHeaderComponent } from '../../shared'; 3 | import { HighlightModule } from 'ngx-highlightjs'; 4 | 5 | @Component({ 6 | selector: 'app-getting-started', 7 | standalone: true, 8 | imports: [HighlightModule, PageHeaderComponent], 9 | templateUrl: './getting-started.component.html', 10 | styleUrl: './getting-started.component.scss', 11 | }) 12 | export class GettingStartedComponent {} 13 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/group-controls/group-controls.component.html: -------------------------------------------------------------------------------- 1 | 3 | 4 |

5 | Group 6 |

7 | 8 | 9 | 10 |

11 | Inline Group 12 |

13 | 14 | 15 | 16 |

17 | Tabs 18 |

19 | 20 |

21 | Array of objects with template 22 |

23 | 24 | 25 | 26 |

27 | Array of objects without template 28 |

29 | 30 | 31 | 32 |

33 | Array of primitive value with template 34 |

35 | 36 | 37 | 38 |

39 | Array of primitive value without template 40 |

41 | 42 | 43 | 44 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/group-controls/group-controls.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/src/app/pages/group-controls/group-controls.component.scss -------------------------------------------------------------------------------- /projects/docs/src/app/pages/group-controls/group-controls.component.ts: -------------------------------------------------------------------------------- 1 | import { GuiFields } from '@acrodata/gui'; 2 | import { Component } from '@angular/core'; 3 | import { ExampleViewerComponent, PageHeaderComponent } from '../../shared'; 4 | 5 | @Component({ 6 | selector: 'app-group-controls', 7 | standalone: true, 8 | imports: [ExampleViewerComponent, PageHeaderComponent], 9 | templateUrl: './group-controls.component.html', 10 | styleUrl: './group-controls.component.scss', 11 | }) 12 | export class GroupControlsComponent { 13 | groupConfig: GuiFields = { 14 | size: { 15 | type: 'group', 16 | name: 'Size', 17 | children: { 18 | width: { 19 | name: 'Width', 20 | type: 'number', 21 | default: 1920, 22 | suffix: 'px', 23 | }, 24 | height: { 25 | name: 'Height', 26 | type: 'number', 27 | default: 1080, 28 | suffix: 'px', 29 | }, 30 | }, 31 | expanded: true, 32 | }, 33 | }; 34 | 35 | inlineConfig: GuiFields = { 36 | offset: { 37 | type: 'inline', 38 | name: 'Offset', 39 | children: { 40 | x: { 41 | name: 'X', 42 | type: 'number', 43 | default: 0, 44 | col: 50, 45 | }, 46 | y: { 47 | name: 'Y', 48 | type: 'number', 49 | default: 0, 50 | col: 50, 51 | }, 52 | }, 53 | }, 54 | }; 55 | 56 | tabs1Config: GuiFields = { 57 | series: { 58 | type: 'tabs', 59 | name: 'Series', 60 | description: 'Dynamic add/delete', 61 | default: [ 62 | { id: 1, name: 'bar' }, 63 | { id: 2, name: 'foo' }, 64 | ], 65 | template: { 66 | name: 'No.<%= i + 1 %>', 67 | children: { 68 | id: { 69 | type: 'number', 70 | name: 'ID', 71 | }, 72 | name: { 73 | type: 'text', 74 | name: 'Name', 75 | }, 76 | }, 77 | }, 78 | expanded: true, 79 | }, 80 | }; 81 | 82 | tabs2Config: GuiFields = { 83 | misc: { 84 | type: 'tabs', 85 | name: 'Misc', 86 | mode: 'list', 87 | children: [ 88 | { 89 | type: 'tab', 90 | name: 'Full Name', 91 | children: { 92 | firstName: { 93 | type: 'text', 94 | name: 'First Name', 95 | default: 'James', 96 | }, 97 | lastName: { 98 | type: 'text', 99 | name: 'Last Name', 100 | default: 'Bob', 101 | }, 102 | }, 103 | }, 104 | { 105 | type: 'tab', 106 | name: 'Contact', 107 | children: { 108 | phone: { 109 | type: 'text', 110 | name: 'Phone', 111 | default: '5550100', 112 | }, 113 | }, 114 | }, 115 | ], 116 | expanded: true, 117 | }, 118 | }; 119 | 120 | tabs3Config: GuiFields = { 121 | transitionProperty: { 122 | type: 'tabs', 123 | name: 'Transition property', 124 | default: ['color', 'width'], 125 | template: { 126 | type: 'text', 127 | name: 'Prop <%= i + 1 %>', 128 | }, 129 | expanded: true, 130 | }, 131 | }; 132 | 133 | tabs4Config: GuiFields = { 134 | coordinate: { 135 | name: 'Coordinate', 136 | type: 'tabs', 137 | mode: 'list', 138 | children: [ 139 | { 140 | type: 'slider', 141 | name: 'Latitude', 142 | min: -90, 143 | max: 90, 144 | step: 0.01, 145 | default: 39.92, 146 | }, 147 | { 148 | type: 'slider', 149 | name: 'Longitude', 150 | min: -180, 151 | max: 180, 152 | step: 0.01, 153 | default: 116.41, 154 | }, 155 | ], 156 | expanded: true, 157 | }, 158 | }; 159 | 160 | menuConfig: GuiFields = { 161 | options: { 162 | type: 'menu', 163 | name: 'Menu', 164 | children: { 165 | menuA: { 166 | type: 'menuItem', 167 | name: 'Menu A', 168 | children: { 169 | textarea: { 170 | type: 'textarea', 171 | name: 'Comments', 172 | default: 'Hello world', 173 | }, 174 | color: { 175 | type: 'fill', 176 | name: 'Color', 177 | default: '#50b4ff99', 178 | }, 179 | }, 180 | }, 181 | menuB: { 182 | type: 'menu', 183 | name: 'Menu B', 184 | children: { 185 | menuB1: { 186 | type: 'menuItem', 187 | name: 'Menu B1', 188 | children: { 189 | label: { 190 | type: 'text', 191 | name: 'Label', 192 | default: 'Hello', 193 | }, 194 | }, 195 | }, 196 | menuB2: { 197 | type: 'menuItem', 198 | name: 'Menu B2', 199 | children: { 200 | display: { 201 | type: 'switch', 202 | name: 'Display', 203 | default: true, 204 | }, 205 | }, 206 | }, 207 | }, 208 | }, 209 | }, 210 | }, 211 | }; 212 | } 213 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/home/home.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | Features 5 |

6 | 7 |
    8 |
  • Built on top of Angular 9 | Reactive Forms
  • 10 |
  • JSON powered config
  • 11 |
  • Easy to understand config and model
  • 12 |
  • Uses Angular Material as basic UI library
  • 13 |
  • A11y support
  • 14 |
  • RTL support
  • 15 |
16 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/home/home.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/src/app/pages/home/home.component.scss -------------------------------------------------------------------------------- /projects/docs/src/app/pages/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { GradientGeneratorComponent } from '../../shared'; 3 | 4 | @Component({ 5 | selector: 'app-home', 6 | standalone: true, 7 | imports: [GradientGeneratorComponent], 8 | templateUrl: './home.component.html', 9 | styleUrl: './home.component.scss', 10 | }) 11 | export class HomeComponent {} 12 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/media-controls/media-controls.component.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |

7 | Upload Settings 8 |

9 | 10 |

11 | If you want to use the upload feature, you should replace the 12 | GuiFileUploaderConfig 13 | with your own uploading logic. The returns of upload function must be a stream of file URL (or 14 | null). 15 |

16 | 17 |
47 | 48 |

49 | Image 50 |

51 | 52 | 53 | 54 |

55 | Video 56 |

57 | 58 | 59 | 60 |

61 | Audio 62 |

63 | 64 | 65 | 66 |

67 | File 68 |

69 | 70 | 71 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/media-controls/media-controls.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/docs/src/app/pages/media-controls/media-controls.component.scss -------------------------------------------------------------------------------- /projects/docs/src/app/pages/media-controls/media-controls.component.ts: -------------------------------------------------------------------------------- 1 | import { GuiFields } from '@acrodata/gui'; 2 | import { Component } from '@angular/core'; 3 | import { HighlightModule } from 'ngx-highlightjs'; 4 | import { ExampleViewerComponent, PageHeaderComponent } from '../../shared'; 5 | 6 | @Component({ 7 | selector: 'app-media-controls', 8 | standalone: true, 9 | imports: [ExampleViewerComponent, PageHeaderComponent, HighlightModule], 10 | templateUrl: './media-controls.component.html', 11 | styleUrl: './media-controls.component.scss', 12 | }) 13 | export class MediaControlsComponent { 14 | imageConfig: GuiFields = { 15 | image: { 16 | type: 'image', 17 | name: 'Upload Image', 18 | default: 19 | 'https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg', 20 | }, 21 | }; 22 | 23 | videoConfig: GuiFields = { 24 | video: { 25 | type: 'video', 26 | name: 'Upload Video', 27 | default: 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4', 28 | }, 29 | }; 30 | 31 | audioConfig: GuiFields = { 32 | audio: { 33 | type: 'audio', 34 | name: 'Upload Audio', 35 | default: 'https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3', 36 | }, 37 | }; 38 | 39 | fileConfig: GuiFields = { 40 | file: { 41 | type: 'file', 42 | name: 'Upload File', 43 | default: '', 44 | }, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/playground/playground.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/playground/playground.component.scss: -------------------------------------------------------------------------------- 1 | :host ::ng-deep { 2 | .mtx-split>.mtx-split-gutter { 3 | background-color: var(--border-color); 4 | } 5 | 6 | >mtx-split { 7 | position: relative; 8 | height: calc(100vh - 96px); 9 | border: 1px solid var(--border-color); 10 | border-radius: 8px; 11 | } 12 | } 13 | 14 | .preview { 15 | mtx-split-pane { 16 | padding: 8px; 17 | } 18 | 19 | textarea { 20 | display: block; 21 | width: 100%; 22 | height: 100%; 23 | border: none; 24 | outline: none; 25 | resize: none; 26 | font-size: 12px; 27 | color: var(--fg-color); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/docs/src/app/pages/playground/playground.component.ts: -------------------------------------------------------------------------------- 1 | import { CodeEditor } from '@acrodata/code-editor'; 2 | import { GuiFields, GuiForm } from '@acrodata/gui'; 3 | import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; 4 | import { CommonModule } from '@angular/common'; 5 | import { 6 | ChangeDetectionStrategy, 7 | ChangeDetectorRef, 8 | Component, 9 | DestroyRef, 10 | OnInit, 11 | inject, 12 | } from '@angular/core'; 13 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 14 | import { FormsModule } from '@angular/forms'; 15 | import { json, jsonParseLinter } from '@codemirror/lang-json'; 16 | import { lintGutter, linter } from '@codemirror/lint'; 17 | import { MtxSplitModule } from '@ng-matero/extensions/split'; 18 | 19 | @Component({ 20 | selector: 'app-playground', 21 | standalone: true, 22 | imports: [CommonModule, FormsModule, MtxSplitModule, CodeEditor, GuiForm], 23 | templateUrl: './playground.component.html', 24 | styleUrl: './playground.component.scss', 25 | changeDetection: ChangeDetectionStrategy.OnPush, 26 | }) 27 | export class PlaygroundComponent implements OnInit { 28 | config: GuiFields = { 29 | title: { 30 | type: 'text', 31 | name: 'Title', 32 | default: 'Hello, World!', 33 | }, 34 | align: { 35 | type: 'buttonToggle', 36 | name: 'Align', 37 | options: [ 38 | { value: 'left', label: 'Left' }, 39 | { value: 'center', label: 'Center' }, 40 | { value: 'right', label: 'Right' }, 41 | ], 42 | default: 'center', 43 | }, 44 | size: { 45 | type: 'group', 46 | name: 'Size', 47 | children: { 48 | width: { 49 | type: 'number', 50 | name: 'Width', 51 | default: 100, 52 | }, 53 | height: { 54 | type: 'number', 55 | name: 'Height', 56 | default: 100, 57 | }, 58 | }, 59 | }, 60 | }; 61 | model: any = {}; 62 | 63 | configStr = ''; 64 | 65 | isMobile = false; 66 | 67 | private readonly destroy = inject(DestroyRef); 68 | 69 | extensions = [json(), linter(jsonParseLinter()), lintGutter()]; 70 | 71 | constructor( 72 | private breakpointObserver: BreakpointObserver, 73 | private cdr: ChangeDetectorRef 74 | ) {} 75 | 76 | ngOnInit(): void { 77 | this.breakpointObserver 78 | .observe([Breakpoints.XSmall]) 79 | .pipe(takeUntilDestroyed(this.destroy)) 80 | .subscribe(result => { 81 | this.isMobile = result.matches; 82 | this.cdr.detectChanges(); 83 | }); 84 | 85 | this.configStr = JSON.stringify(this.config, null, 2); 86 | } 87 | 88 | onConfigChange() { 89 | this.config = JSON.parse(this.configStr); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/example-viewer/example-viewer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 | 8 |
9 | 13 |
14 |
15 |
16 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/example-viewer/example-viewer.component.scss: -------------------------------------------------------------------------------- 1 | .demo-wrapper { 2 | position: relative; 3 | display: flex; 4 | margin: 16px 0; 5 | background-color: var(--bg-secondary-color); 6 | border-radius: 8px; 7 | } 8 | 9 | .demo-code { 10 | flex: 1; 11 | max-height: 600px; 12 | overflow: auto; 13 | 14 | pre { 15 | margin: 0; 16 | } 17 | } 18 | 19 | .demo-result { 20 | width: 320px; 21 | padding: 32px; 22 | margin: 0 auto; 23 | } 24 | 25 | .demo-console { 26 | padding: 4px; 27 | margin-top: 16px; 28 | border: 1px solid var(--border-color); 29 | border-radius: 4px; 30 | 31 | textarea { 32 | display: block; 33 | width: 100%; 34 | padding: 0 4px; 35 | background-color: var(--console-bg-color); 36 | color: var(--console-color); 37 | border-width: 0; 38 | font-size: 12px; 39 | line-height: 20px; 40 | appearance: none; 41 | outline: none; 42 | } 43 | } 44 | 45 | @media (width < 600px){ 46 | .demo-wrapper { 47 | flex-direction: column; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/example-viewer/example-viewer.component.ts: -------------------------------------------------------------------------------- 1 | import { GuiFields, GuiModule } from '@acrodata/gui'; 2 | import { TextFieldModule } from '@angular/cdk/text-field'; 3 | import { CommonModule } from '@angular/common'; 4 | import { Component, Input } from '@angular/core'; 5 | import { HighlightModule } from 'ngx-highlightjs'; 6 | 7 | @Component({ 8 | selector: 'app-example-viewer', 9 | standalone: true, 10 | imports: [CommonModule, GuiModule, TextFieldModule, HighlightModule], 11 | templateUrl: './example-viewer.component.html', 12 | styleUrl: './example-viewer.component.scss', 13 | }) 14 | export class ExampleViewerComponent { 15 | @Input() config: GuiFields = {}; 16 | 17 | model = {}; 18 | } 19 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/gradient-generator/gradient-generator.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Acrodata GUI

4 |

JSON powered GUI for configurable panels

5 |
6 | 7 |
8 |
Gradient Generator
9 | 10 |
11 |
12 | 13 | @for (m of presets; track m; let i = $index) { 14 | 17 | } 18 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/gradient-generator/gradient-generator.component.scss: -------------------------------------------------------------------------------- 1 | .jumbotron { 2 | display: flex; 3 | min-height: 30vw; 4 | padding: 64px; 5 | margin-bottom: 24px; 6 | border-radius: 8px; 7 | } 8 | 9 | .text { 10 | flex: 1; 11 | } 12 | 13 | h1, 14 | p { 15 | color: rgba(255, 255, 255, 1); 16 | text-shadow: 4px 4px 6px rgba(0, 0, 0, 0.85); 17 | } 18 | 19 | .gui-wrapper { 20 | width: 260px; 21 | } 22 | 23 | .gui-heading { 24 | position: relative; 25 | z-index: 1; 26 | padding: 0 12px; 27 | border-radius: 4px 4px 0 0; 28 | background-color: var(--bg-color); 29 | border-bottom: 1px solid var(--border-color); 30 | text-align: center; 31 | line-height: 32px; 32 | font-size: 12px; 33 | } 34 | 35 | .thumbnail { 36 | width: 64px; 37 | height: 36px; 38 | background-color: transparent; 39 | border: 1px solid var(--border-color); 40 | border-radius: 4px; 41 | cursor: pointer; 42 | } 43 | 44 | @media (width <= 600px) { 45 | .jumbotron { 46 | flex-direction: column; 47 | align-items: center; 48 | padding: 16px; 49 | } 50 | 51 | .text { 52 | margin-bottom: 32px; 53 | text-align: center; 54 | } 55 | } 56 | 57 | button + button { 58 | margin-left: 8px; 59 | } 60 | 61 | :host ::ng-deep { 62 | .gui-form { 63 | border-top-left-radius: 0; 64 | border-top-right-radius: 0; 65 | 66 | .mat-expansion-panel { 67 | position: relative; 68 | padding-left: 4px; 69 | 70 | &::before { 71 | content: ''; 72 | position: absolute; 73 | top: 10px; 74 | bottom: 10px; 75 | left: 2px; 76 | z-index: 2; 77 | display: block; 78 | width: 4px; 79 | border: 2px solid var(--mat-expansion-container-text-color); 80 | border-right: none; 81 | opacity: 0.4; 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/gradient-generator/gradient-presets.ts: -------------------------------------------------------------------------------- 1 | export interface IBackground { 2 | gradients: IGradient[]; 3 | blendMode: string[]; 4 | repeat: string; 5 | } 6 | 7 | export interface IGradient { 8 | type: 'linear' | 'radial' | 'conic'; 9 | repeating?: boolean; 10 | reverse?: boolean; 11 | angle?: number; 12 | radialBase?: string; 13 | conicBase?: string; 14 | stops?: IColorStop[]; 15 | position?: { x?: string; y?: string }; 16 | size?: { w?: string; h?: string }; 17 | } 18 | 19 | export interface IColorStop { 20 | color: string; 21 | offset: string; 22 | } 23 | 24 | export const gradientPresets: IBackground[] = [ 25 | { 26 | gradients: [ 27 | { 28 | type: 'linear', 29 | repeating: true, 30 | reverse: false, 31 | angle: 45, 32 | stops: [ 33 | { color: 'rgba(75, 75, 75, 0.5)', offset: '0%' }, 34 | { color: 'rgba(220, 235, 255, 0.75)', offset: '50%' }, 35 | ], 36 | position: {}, 37 | size: { w: '100px', h: '100px' }, 38 | }, 39 | { 40 | type: 'linear', 41 | repeating: true, 42 | reverse: false, 43 | angle: 135, 44 | stops: [ 45 | { color: 'rgba(5, 30, 50, 0.75)', offset: '0%' }, 46 | { color: 'rgba(115, 150, 255, 0.5)', offset: '50%' }, 47 | ], 48 | position: {}, 49 | size: {}, 50 | }, 51 | ], 52 | blendMode: ['overlay'], 53 | repeat: 'repeat', 54 | }, 55 | { 56 | gradients: [ 57 | { 58 | type: 'conic', 59 | repeating: true, 60 | conicBase: '', 61 | stops: [ 62 | { color: '#023047', offset: '0 25%' }, 63 | { color: '#00000000', offset: '0 50%' }, 64 | ], 65 | position: { x: '0', y: '0' }, 66 | size: { w: '50px', h: '86.5px' }, 67 | }, 68 | { 69 | type: 'conic', 70 | repeating: true, 71 | conicBase: 'from -30deg', 72 | stops: [ 73 | { color: '#fb8500', offset: '0 16.67%' }, 74 | { color: '#023047', offset: '0 50%' }, 75 | ], 76 | position: { x: '0', y: '0' }, 77 | size: { w: '25px', h: '43.25px' }, 78 | }, 79 | ], 80 | blendMode: ['normal'], 81 | repeat: 'repeat', 82 | }, 83 | { 84 | gradients: [ 85 | { 86 | type: 'conic', 87 | conicBase: 'from -60deg at 50% 33.33%', 88 | stops: [ 89 | { color: '#d9d9d9', offset: '0 120deg' }, 90 | { color: '#00000000', offset: '0' }, 91 | ], 92 | position: {}, 93 | size: { w: '142.024px', h: '82px' }, 94 | }, 95 | { 96 | type: 'conic', 97 | conicBase: 'from 120deg at 50% 66.67%', 98 | stops: [ 99 | { color: '#d9d9d9', offset: '0 120deg' }, 100 | { color: '#00000000', offset: '0' }, 101 | ], 102 | position: {}, 103 | size: {}, 104 | }, 105 | { 106 | type: 'conic', 107 | conicBase: 'from 60deg at 66.67%', 108 | stops: [ 109 | { color: '#d9d9d9', offset: '60deg' }, 110 | { color: '#ffffff', offset: '0 120deg' }, 111 | { color: '#00000000', offset: '0' }, 112 | ], 113 | position: {}, 114 | size: {}, 115 | }, 116 | { 117 | type: 'conic', 118 | conicBase: 'from 180deg at 33.33%', 119 | stops: [ 120 | { color: '#b2b2b2', offset: '60deg' }, 121 | { color: '#d9d9d9', offset: '0 120deg' }, 122 | { color: '#00000000', offset: '0' }, 123 | ], 124 | position: {}, 125 | size: {}, 126 | }, 127 | { 128 | type: 'linear', 129 | angle: 90, 130 | stops: [ 131 | { color: '#b2b2b2', offset: '16.7%' }, 132 | { color: '#ffffff', offset: '0 50%' }, 133 | { color: '#b2b2b2', offset: '0 83.33%' }, 134 | { color: '#ffffff', offset: '0' }, 135 | ], 136 | position: {}, 137 | size: {}, 138 | }, 139 | ], 140 | blendMode: ['normal'], 141 | repeat: 'repeat', 142 | }, 143 | { 144 | gradients: [ 145 | { 146 | type: 'linear', 147 | repeating: true, 148 | angle: 45, 149 | stops: [ 150 | { color: 'rgba(0,0,0,.2)', offset: '0' }, 151 | { color: 'transparent', offset: '5px 50px' }, 152 | ], 153 | position: {}, 154 | size: {}, 155 | }, 156 | { 157 | type: 'linear', 158 | repeating: true, 159 | angle: -45, 160 | stops: [ 161 | { color: 'rgba(0,0,0,.2)', offset: '0' }, 162 | { color: 'transparent', offset: '5px 50px' }, 163 | ], 164 | position: {}, 165 | size: {}, 166 | }, 167 | { 168 | type: 'linear', 169 | repeating: true, 170 | angle: 45, 171 | stops: [ 172 | { color: '#000', offset: '0 10px' }, 173 | { color: '#333', offset: '0 20px' }, 174 | { color: '#d79033', offset: '0 30px' }, 175 | { color: '#d7d7d3', offset: '0 40px' }, 176 | { color: '#e9e9ea', offset: '0 50px' }, 177 | ], 178 | position: {}, 179 | size: {}, 180 | }, 181 | { 182 | type: 'linear', 183 | repeating: true, 184 | angle: -45, 185 | stops: [ 186 | { color: '#000', offset: '0 10px' }, 187 | { color: '#333', offset: '0 20px' }, 188 | { color: '#d79033', offset: '0 30px' }, 189 | { color: '#d7d7d3', offset: '0 40px' }, 190 | { color: '#e9e9ea', offset: '0 50px' }, 191 | ], 192 | position: {}, 193 | size: {}, 194 | }, 195 | ], 196 | blendMode: ['multiply', 'multiply', 'lighten'], 197 | repeat: 'repeat', 198 | }, 199 | ]; 200 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from './example-viewer/example-viewer.component'; 2 | export * from './page-header/page-header.component'; 3 | export * from './gradient-generator/gradient-generator.component'; 4 | export * from './theme-picker/theme-picker'; 5 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/page-header/page-header.component.html: -------------------------------------------------------------------------------- 1 |
2 | @if (pageTitle) { 3 |

{{pageTitle}}

4 | } 5 | @if (pageContent) { 6 |

{{pageContent}}

7 | } 8 |
9 | @if (config) { 10 |
11 | } 12 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/page-header/page-header.component.scss: -------------------------------------------------------------------------------- 1 | .page-header { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | justify-content: space-between; 6 | padding: 64px; 7 | background-color: var(--bg-secondary-color); 8 | border-radius: 8px; 9 | } 10 | 11 | .page-header-text { 12 | h1 { 13 | margin: 0; 14 | } 15 | 16 | p { 17 | margin-top: 16px; 18 | margin-bottom: 0; 19 | } 20 | } 21 | 22 | @media (width < 600px){ 23 | .page-header { 24 | flex-direction: column; 25 | padding: 64px 16px; 26 | text-align: center; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/page-header/page-header.component.ts: -------------------------------------------------------------------------------- 1 | import { GuiFields } from '@acrodata/gui'; 2 | import { Component, Input, ViewEncapsulation } from '@angular/core'; 3 | 4 | @Component({ 5 | selector: 'app-page-header', 6 | standalone: true, 7 | templateUrl: './page-header.component.html', 8 | styleUrl: './page-header.component.scss', 9 | encapsulation: ViewEncapsulation.None, 10 | host: { 11 | class: 'page-header', 12 | }, 13 | }) 14 | export class PageHeaderComponent { 15 | @Input() pageTitle = ''; 16 | @Input() pageContent = ''; 17 | @Input() config: GuiFields | null = null; 18 | } 19 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/style-manager/style-manager.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | /** 4 | * Class for managing stylesheets. Stylesheets are loaded into named slots so that they can be 5 | * removed or changed later. 6 | */ 7 | @Injectable({ providedIn: 'root' }) 8 | export class StyleManager { 9 | /** 10 | * Set the stylesheet with the specified key. 11 | */ 12 | setStyle(key: string, href: string) { 13 | getLinkElementForKey(key).setAttribute('href', href); 14 | } 15 | 16 | /** 17 | * Remove the stylesheet with the specified key. 18 | */ 19 | removeStyle(key: string) { 20 | const existingLinkElement = getExistingLinkElementByKey(key); 21 | if (existingLinkElement) { 22 | document.head.removeChild(existingLinkElement); 23 | } 24 | } 25 | } 26 | 27 | function getLinkElementForKey(key: string) { 28 | return getExistingLinkElementByKey(key) || createLinkElementWithKey(key); 29 | } 30 | 31 | function getExistingLinkElementByKey(key: string) { 32 | return document.head.querySelector(`link[rel="stylesheet"].${getClassNameForKey(key)}`); 33 | } 34 | 35 | function createLinkElementWithKey(key: string) { 36 | const linkEl = document.createElement('link'); 37 | linkEl.setAttribute('rel', 'stylesheet'); 38 | linkEl.classList.add(getClassNameForKey(key)); 39 | document.head.appendChild(linkEl); 40 | return linkEl; 41 | } 42 | 43 | function getClassNameForKey(key: string) { 44 | return `style-manager-${key}`; 45 | } 46 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/theme-picker/theme-picker.html: -------------------------------------------------------------------------------- 1 | 5 | 6 |
    7 | @for (theme of themes; track theme) { 8 |
  • 9 | 11 |
  • 12 | } 13 |
14 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/theme-picker/theme-picker.scss: -------------------------------------------------------------------------------- 1 | .theme-picker-toggle { 2 | min-width: 64px; 3 | 4 | mat-icon { 5 | vertical-align: middle; 6 | } 7 | 8 | &::after { 9 | display: inline-block; 10 | margin-left: .4em; 11 | vertical-align: .1em; 12 | content: ''; 13 | border-top: .3em solid; 14 | border-right: .3em solid transparent; 15 | border-bottom: 0; 16 | border-left: .3em solid transparent; 17 | } 18 | } 19 | 20 | .theme-picker { 21 | position: relative; 22 | } 23 | 24 | .theme-picker-menu { 25 | position: absolute; 26 | right: 0; 27 | z-index: 100; 28 | display: none; 29 | padding: 4px; 30 | margin: 0; 31 | list-style: none; 32 | border: 1px solid var(--border-color); 33 | background-color: var(--bg-color); 34 | border-radius: 8px; 35 | box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15); 36 | 37 | &.show { 38 | display: block; 39 | } 40 | 41 | li+li { 42 | margin-top: 2px; 43 | } 44 | } 45 | 46 | .theme-picker-item { 47 | display: flex; 48 | gap: 4px; 49 | width: 100%; 50 | padding: 4px 16px 4px 8px; 51 | border-radius: 4px; 52 | white-space: nowrap; 53 | font-size: 14px; 54 | background-color: transparent; 55 | color: inherit; 56 | border: none; 57 | cursor: pointer; 58 | 59 | &:hover { 60 | background-color: var(--bg-secondary-color); 61 | } 62 | 63 | &::before { 64 | content: ''; 65 | display: inline-block; 66 | width: 20px; 67 | text-align: center; 68 | } 69 | 70 | &.active::before { 71 | content: '✔️'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/theme-picker/theme-picker.ts: -------------------------------------------------------------------------------- 1 | import { GuiCodeareaConfig } from '@acrodata/gui'; 2 | import { LiveAnnouncer } from '@angular/cdk/a11y'; 3 | import { 4 | ChangeDetectionStrategy, 5 | ChangeDetectorRef, 6 | Component, 7 | OnDestroy, 8 | OnInit, 9 | ViewEncapsulation, 10 | } from '@angular/core'; 11 | import { MatIconModule } from '@angular/material/icon'; 12 | import { ActivatedRoute, ParamMap } from '@angular/router'; 13 | import { fromEvent, Subscription } from 'rxjs'; 14 | import { filter, map } from 'rxjs/operators'; 15 | import { StyleManager } from '../style-manager/style-manager'; 16 | import { DocsSiteTheme, ThemeStorage } from './theme-storage'; 17 | 18 | @Component({ 19 | selector: 'app-theme-picker', 20 | templateUrl: 'theme-picker.html', 21 | styleUrl: 'theme-picker.scss', 22 | host: { 23 | class: 'theme-picker', 24 | }, 25 | changeDetection: ChangeDetectionStrategy.OnPush, 26 | encapsulation: ViewEncapsulation.None, 27 | standalone: true, 28 | imports: [MatIconModule], 29 | }) 30 | export class ThemePickerComponent implements OnInit, OnDestroy { 31 | private queryParamSubscription = Subscription.EMPTY; 32 | currentTheme: DocsSiteTheme | undefined; 33 | 34 | // The below colors need to align with the themes defined in theme-picker.scss 35 | themes: DocsSiteTheme[] = [ 36 | { 37 | color: '#ffd9e1', 38 | displayName: '🌞 M3 (Rose & Red)', 39 | name: 'rose-red', 40 | }, 41 | { 42 | color: '#d7e3ff', 43 | displayName: '🌞 M3 (Azure & Blue)', 44 | name: 'azure-blue', 45 | isDefault: true, 46 | }, 47 | { 48 | color: '#810081', 49 | displayName: '🌚 M3 (Magenta & Violet)', 50 | name: 'magenta-violet', 51 | isDark: true, 52 | }, 53 | { 54 | color: '#004f4f', 55 | displayName: '🌚 M3 (Cyan & Orange)', 56 | name: 'cyan-orange', 57 | isDark: true, 58 | }, 59 | { 60 | color: '#673AB7', 61 | displayName: '🌞 M2 (Deep Purple & Amber)', 62 | name: 'deeppurple-amber', 63 | }, 64 | { 65 | color: '#3F51B5', 66 | displayName: '🌞 M2 (Indigo & Pink)', 67 | name: 'indigo-pink', 68 | }, 69 | { 70 | color: '#E91E63', 71 | displayName: '🌚 M2 (Pink & Blue-grey)', 72 | name: 'pink-bluegrey', 73 | isDark: true, 74 | }, 75 | { 76 | color: '#9C27B0', 77 | displayName: '🌚 M2 (Purple & Green)', 78 | name: 'purple-green', 79 | isDark: true, 80 | }, 81 | ]; 82 | 83 | showMenu = false; 84 | 85 | constructor( 86 | private styleManager: StyleManager, 87 | private themeStorage: ThemeStorage, 88 | private activatedRoute: ActivatedRoute, 89 | private liveAnnouncer: LiveAnnouncer, 90 | private cdr: ChangeDetectorRef, 91 | private codeareaCfg: GuiCodeareaConfig 92 | ) { 93 | const themeName = this.themeStorage.getStoredThemeName(); 94 | if (themeName) { 95 | this.selectTheme(themeName); 96 | } else { 97 | this.themes.find(theme => { 98 | if (theme.isDefault === true) { 99 | this.selectTheme(theme.name); 100 | } 101 | }); 102 | } 103 | } 104 | 105 | ngOnInit() { 106 | this.queryParamSubscription = this.activatedRoute.queryParamMap 107 | .pipe(map((params: ParamMap) => params.get('theme'))) 108 | .subscribe((themeName: string | null) => { 109 | if (themeName) { 110 | this.selectTheme(themeName); 111 | } 112 | }); 113 | 114 | fromEvent(document, 'click') 115 | .pipe(filter(event => event.target != document.querySelector('.theme-picker-menu'))) 116 | .subscribe(v => { 117 | this.showMenu = false; 118 | this.cdr.detectChanges(); 119 | }); 120 | } 121 | 122 | ngOnDestroy() { 123 | this.queryParamSubscription.unsubscribe(); 124 | } 125 | 126 | selectTheme(themeName: string) { 127 | const theme = 128 | this.themes.find(currentTheme => currentTheme.name === themeName) || 129 | this.themes.find(currentTheme => currentTheme.isDefault)!; 130 | 131 | this.currentTheme = theme; 132 | 133 | this.styleManager.setStyle('theme1', `themes/material/${theme.name}.css`); 134 | this.styleManager.setStyle('theme2', `themes/extensions/${theme.name}.css`); 135 | 136 | if (this.currentTheme) { 137 | this.liveAnnouncer.announce(`${theme.displayName} theme selected.`, 'polite', 3000); 138 | this.themeStorage.storeTheme(this.currentTheme); 139 | } 140 | 141 | this.codeareaCfg.theme = theme.isDark ? 'dark' : 'light'; 142 | this.codeareaCfg.changes.next(); 143 | 144 | this.showMenu = false; 145 | } 146 | 147 | toggleMenu(e: MouseEvent) { 148 | e.stopPropagation(); 149 | this.showMenu = !this.showMenu; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /projects/docs/src/app/shared/theme-picker/theme-storage.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, EventEmitter } from '@angular/core'; 2 | 3 | export interface DocsSiteTheme { 4 | name: string; 5 | displayName?: string; 6 | color: string; 7 | isDefault?: boolean; 8 | isDark?: boolean; 9 | } 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class ThemeStorage { 13 | static storageKey = 'docs-theme'; 14 | 15 | onThemeUpdate: EventEmitter = new EventEmitter(); 16 | 17 | storeTheme(theme: DocsSiteTheme) { 18 | try { 19 | window.localStorage[ThemeStorage.storageKey] = theme.name; 20 | } catch { 21 | // 22 | } 23 | 24 | this.onThemeUpdate.emit(theme); 25 | } 26 | 27 | getStoredThemeName(): string | null { 28 | try { 29 | return window.localStorage[ThemeStorage.storageKey] || null; 30 | } catch { 31 | return null; 32 | } 33 | } 34 | 35 | clearStorage() { 36 | try { 37 | window.localStorage.removeItem(ThemeStorage.storageKey); 38 | } catch { 39 | // 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projects/docs/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Acrodata GUI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /projects/docs/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch(err => console.error(err)); 6 | -------------------------------------------------------------------------------- /projects/docs/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | @use './styles/reboot'; 4 | // @use './styles/app-theme'; 5 | -------------------------------------------------------------------------------- /projects/docs/src/styles/_app-theme.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @use '@ng-matero/extensions' as mtx; 3 | 4 | @include mat.core(); 5 | 6 | $primary: mat.m2-define-palette(mat.$m2-indigo-palette, 500); 7 | $accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400); 8 | $theme: mat.m2-define-light-theme(( 9 | color: ( 10 | primary: $primary, 11 | accent: $accent, 12 | ), 13 | typography: mat.m2-define-typography-config(), 14 | density: 0, 15 | )); 16 | 17 | @include mat.all-component-themes($theme); 18 | @include mtx.all-component-themes($theme); 19 | -------------------------------------------------------------------------------- /projects/docs/src/styles/_reboot.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-color: white; 3 | --bg-translucent-color: rgba(255, 255, 255, 0.9); 4 | --bg-secondary-color: rgba(240, 240, 240, 1); 5 | --fg-color: rgba(85, 85, 90, 1); 6 | --fg-secondary-color: rgba(85, 85, 90, .7); 7 | --border-color: rgba(0, 0, 0, .15); 8 | --heading-color: rgba(50, 50, 55, 1); 9 | --console-color: rgba(0, 0, 0, .4); 10 | --console-bg-color: rgba(0, 0, 0, .04); 11 | --a-color: rgba(65, 105, 190, 1); 12 | --hl-keyword: rgb(85, 140, 180); 13 | --hl-string: rgb(115, 105, 175); 14 | --hl-number: rgb(50, 160, 85); 15 | --hl-comment: rgba(100, 100, 100); 16 | --hl-title: rgb(165, 120, 20); 17 | --hl-inserted-text: rgb(17, 99, 41); 18 | --hl-inserted-bg: rgb(218, 251, 225); 19 | --hl-deleted-text: rgb(130, 7, 30); 20 | --hl-deleted-bg: rgb(255, 235, 233); 21 | } 22 | 23 | *, 24 | *::before, 25 | *::after { 26 | box-sizing: border-box; 27 | } 28 | 29 | body { 30 | padding: 0; 31 | margin: 0; 32 | color: var(--fg-color); 33 | background-color: var(--bg-color); 34 | line-height: 1.5; 35 | font-family: system-ui, -apple-system, "Helvetica Neue", sans-serif; 36 | } 37 | 38 | h1, 39 | h2, 40 | h3, 41 | h4, 42 | h5, 43 | h6 { 44 | color: var(--heading-color); 45 | 46 | a { 47 | color: var(--heading-color); 48 | } 49 | } 50 | 51 | a { 52 | color: var(--a-color); 53 | text-decoration: none; 54 | 55 | &:hover { 56 | text-decoration: underline; 57 | } 58 | } 59 | 60 | code, 61 | kbd, 62 | pre, 63 | samp { 64 | font-family: "Roboto Mono", monospace; 65 | } 66 | 67 | pre { 68 | padding: 32px; 69 | border-radius: 8px; 70 | background-color: var(--bg-secondary-color); 71 | line-height: 1.2; 72 | overflow: auto; 73 | 74 | code { 75 | display: inline; 76 | padding: 0; 77 | } 78 | } 79 | 80 | @media (width < 600px) { 81 | pre { 82 | padding: 16px; 83 | } 84 | } 85 | 86 | code { 87 | display: inline-block; 88 | padding: 0 4px; 89 | font-size: 0.75rem; 90 | word-wrap: break-word; 91 | background-color: var(--bg-secondary-color); 92 | border-radius: 4px; 93 | } 94 | 95 | table { 96 | width: 100%; 97 | border-spacing: 0; 98 | 99 | tr { 100 | th { 101 | border-top: 1px solid var(--border-color); 102 | } 103 | 104 | th, 105 | td { 106 | padding: 8px; 107 | border-right: 1px solid var(--border-color); 108 | border-bottom: 1px solid var(--border-color); 109 | 110 | &:first-child { 111 | border-left: 1px solid var(--border-color); 112 | } 113 | } 114 | 115 | &:first-child { 116 | th { 117 | &:first-child { 118 | border-top-left-radius: 8px; 119 | } 120 | 121 | &:last-child { 122 | border-top-right-radius: 8px; 123 | } 124 | } 125 | } 126 | 127 | &:last-child { 128 | td { 129 | &:first-child { 130 | border-bottom-left-radius: 8px; 131 | } 132 | 133 | &:last-child { 134 | border-bottom-right-radius: 8px; 135 | } 136 | } 137 | } 138 | } 139 | } 140 | 141 | .gui-form { 142 | background-color: var(--mat-expansion-container-background-color); 143 | color: var(--mat-expansion-container-text-color); 144 | box-shadow: 0 2px 4px rgba(0, 0, 0, .2); 145 | border-radius: 4px; 146 | 147 | .mat-expansion-panel { 148 | border-radius: 4px; 149 | } 150 | } 151 | 152 | .hljs-keyword { 153 | color: var(--hl-keyword); 154 | } 155 | 156 | .hljs-number { 157 | color: var(--hl-number); 158 | } 159 | 160 | .hljs-string { 161 | color: var(--hl-string); 162 | } 163 | 164 | .hljs-comment, 165 | .hljs-quote { 166 | color: var(--hl-comment); 167 | } 168 | 169 | .hljs-title { 170 | color: var(--hl-title); 171 | } 172 | 173 | .hljs-addition { 174 | color: var(--hl-inserted-text); 175 | background-color: var(--hl-inserted-bg); 176 | } 177 | 178 | .hljs-deletion { 179 | color: var(--hl-deleted-text); 180 | background-color: var(--hl-deleted-bg); 181 | } 182 | 183 | .btn { 184 | height: 40px; 185 | padding: 8px; 186 | background-color: transparent; 187 | border: none; 188 | border-radius: 20px; 189 | cursor: pointer; 190 | color: var(--fg-color); 191 | 192 | &:hover, 193 | &:focus { 194 | background-color: var(--bg-secondary-color); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /projects/docs/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/docs/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/gui/_index.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @use '@ng-matero/extensions' as mtx; 3 | 4 | @mixin all-control-themes($theme) { 5 | @include mat.option-theme($theme); 6 | @include mat.optgroup-theme($theme); 7 | @include mat.pseudo-checkbox-theme($theme); 8 | @include mat.form-field-theme($theme); 9 | @include mat.input-theme($theme); 10 | @include mat.button-toggle-theme($theme); 11 | @include mat.slide-toggle-theme($theme); 12 | @include mat.select-theme($theme); 13 | @include mat.slider-theme($theme); 14 | @include mat.icon-theme($theme); 15 | @include mat.icon-button-theme($theme); 16 | @include mat.button-theme($theme); 17 | @include mat.tabs-theme($theme); 18 | @include mat.expansion-theme($theme); 19 | @include mat.tooltip-theme($theme); 20 | @include mtx.colorpicker-theme($theme); 21 | @include mtx.select-theme($theme); 22 | } 23 | 24 | @mixin all-control-colors($theme) { 25 | @include mat.option-color($theme); 26 | @include mat.optgroup-color($theme); 27 | @include mat.pseudo-checkbox-color($theme); 28 | @include mat.form-field-color($theme); 29 | @include mat.input-color($theme); 30 | @include mat.button-toggle-color($theme); 31 | @include mat.slide-toggle-color($theme); 32 | @include mat.select-color($theme); 33 | @include mat.slider-color($theme); 34 | @include mat.icon-color($theme); 35 | @include mat.icon-button-color($theme); 36 | @include mat.button-color($theme); 37 | @include mat.tabs-color($theme); 38 | @include mat.expansion-color($theme); 39 | @include mat.tooltip-color($theme); 40 | @include mtx.colorpicker-color($theme); 41 | @include mtx.select-color($theme); 42 | } 43 | -------------------------------------------------------------------------------- /projects/gui/button-toggle/button-toggle.html: -------------------------------------------------------------------------------- 1 | 9 | @for (opt of config.options; track opt) { 10 | 18 | @if (config.useIcon) { 19 | 20 | } @else { 21 | {{ opt.label }} 22 | } 23 | 24 | } 25 | 26 | @if (config.parentType === 'inline') { 27 | 28 | 29 | 30 | } 31 | -------------------------------------------------------------------------------- /projects/gui/button-toggle/button-toggle.scss: -------------------------------------------------------------------------------- 1 | .gui-button-toggle { 2 | --mat-standard-button-toggle-shape: 0.25rem; 3 | --mat-standard-button-toggle-height: 1.5rem; 4 | --mat-standard-button-toggle-label-text-size: 0.75rem; 5 | --mat-standard-button-toggle-label-text-weight: 400; 6 | --mat-standard-button-toggle-label-text-line-height: var(--mat-standard-button-toggle-height); 7 | 8 | .mat-button-toggle-group { 9 | flex-wrap: wrap; 10 | border-width: 0; 11 | } 12 | 13 | .mat-button-toggle-group-appearance-standard { 14 | .mat-button-toggle-appearance-standard+.mat-button-toggle-appearance-standard { 15 | border-width: 0; 16 | } 17 | } 18 | 19 | .mat-button-toggle-appearance-standard { 20 | background-color: var(--mdc-filled-text-field-container-color); 21 | } 22 | } 23 | 24 | .gui-icon-toggle { 25 | &.mat-button-toggle { 26 | .mat-button-toggle-label-content { 27 | padding: 0; 28 | line-height: normal; 29 | } 30 | } 31 | 32 | &.mat-button-toggle-checked img { 33 | opacity: .64; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/gui/button-toggle/button-toggle.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | forwardRef, 6 | Input, 7 | ViewEncapsulation, 8 | } from '@angular/core'; 9 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 10 | import { MatButtonToggle, MatButtonToggleGroup } from '@angular/material/button-toggle'; 11 | import { MatHint } from '@angular/material/form-field'; 12 | import { GuiFieldLabel } from '../field-label/field-label'; 13 | import { GuiFlexDirective } from '../gui-utils'; 14 | import { GuiBasicValue, GuiControl } from '../interface'; 15 | import { GuiIcon } from './icon'; 16 | 17 | @Component({ 18 | selector: 'gui-button-toggle', 19 | templateUrl: './button-toggle.html', 20 | styleUrl: './button-toggle.scss', 21 | host: { 22 | class: 'gui-field gui-button-toggle', 23 | }, 24 | encapsulation: ViewEncapsulation.None, 25 | changeDetection: ChangeDetectionStrategy.OnPush, 26 | providers: [ 27 | { 28 | provide: NG_VALUE_ACCESSOR, 29 | useExisting: forwardRef(() => GuiButtonToggle), 30 | multi: true, 31 | }, 32 | ], 33 | standalone: true, 34 | imports: [ 35 | FormsModule, 36 | MatButtonToggleGroup, 37 | MatButtonToggle, 38 | MatHint, 39 | GuiFlexDirective, 40 | GuiIcon, 41 | GuiFieldLabel, 42 | ], 43 | }) 44 | export class GuiButtonToggle implements ControlValueAccessor { 45 | @Input() config: Partial = {}; 46 | @Input() disabled = false; 47 | 48 | value: GuiBasicValue | GuiBasicValue[] = ''; 49 | 50 | private onChange: (value: GuiBasicValue | GuiBasicValue[]) => void = () => {}; 51 | private onTouched: () => void = () => {}; 52 | 53 | constructor(private cdr: ChangeDetectorRef) {} 54 | 55 | writeValue(value: GuiBasicValue | GuiBasicValue[]) { 56 | this.value = value; 57 | this.cdr.markForCheck(); 58 | } 59 | 60 | registerOnChange(fn: (value: GuiBasicValue | GuiBasicValue[]) => void) { 61 | this.onChange = fn; 62 | } 63 | 64 | registerOnTouched(fn: () => void) { 65 | this.onTouched = fn; 66 | } 67 | 68 | setDisabledState(isDisabled: boolean) { 69 | this.disabled = isDisabled; 70 | this.cdr.markForCheck(); 71 | } 72 | 73 | onValueChange() { 74 | this.onChange(this.value); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /projects/gui/button-toggle/icon.scss: -------------------------------------------------------------------------------- 1 | .gui-icon { 2 | display: inline-flex; 3 | 4 | img, 5 | i { 6 | display: block; 7 | width: var(--mat-standard-button-toggle-height); 8 | height: var(--mat-standard-button-toggle-height); 9 | line-height: var(--mat-standard-button-toggle-height); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/gui/button-toggle/icon.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'gui-icon', 5 | template: ` 6 | @if (isUrl()) { 7 | 8 | } @else { 9 | 10 | } 11 | `, 12 | styleUrl: './icon.scss', 13 | host: { 14 | class: 'gui-icon', 15 | }, 16 | encapsulation: ViewEncapsulation.None, 17 | changeDetection: ChangeDetectionStrategy.OnPush, 18 | standalone: true, 19 | }) 20 | export class GuiIcon { 21 | @Input() src = ''; 22 | 23 | isUrl() { 24 | return /^(https?:\/\/|\.?\/)\w+/.test(this.src); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/gui/codearea/codearea-config.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@acrodata/code-editor'; 2 | import { Injectable } from '@angular/core'; 3 | import { LanguageDescription } from '@codemirror/language'; 4 | import { Extension } from '@codemirror/state'; 5 | import { Subject } from 'rxjs'; 6 | import { GuiCodeareaDialogData } from './codearea-dialog'; 7 | 8 | @Injectable({ 9 | providedIn: 'root', 10 | }) 11 | export class GuiCodeareaConfig { 12 | readonly changes: Subject = new Subject(); 13 | 14 | theme: Theme = 'light'; 15 | 16 | languages: LanguageDescription[] = []; 17 | 18 | extensions: Extension[] | ((data: GuiCodeareaDialogData) => Extension[]) = []; 19 | } 20 | -------------------------------------------------------------------------------- /projects/gui/codearea/codearea-dialog.html: -------------------------------------------------------------------------------- 1 |
7 | {{ title }} 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 27 | 28 |
29 | 30 | 33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /projects/gui/codearea/codearea-dialog.scss: -------------------------------------------------------------------------------- 1 | .gui-codearea-dialog-panel { 2 | --rnd-dialog-container-color: var(--mdc-dialog-container-color); 3 | } 4 | 5 | .gui-codearea-dialog { 6 | --mdc-outlined-button-label-text-size: 0.75rem; 7 | --mdc-outlined-button-container-height: 1.5rem; 8 | --mat-outlined-button-horizontal-padding: 0.75rem; 9 | --mat-outlined-button-touch-target-display: none; 10 | --mdc-filled-button-label-text-size: 0.75rem; 11 | --mdc-filled-button-container-height: 1.5rem; 12 | --mat-filled-button-horizontal-padding: 0.75rem; 13 | --mat-filled-button-touch-target-display: none; 14 | 15 | display: flex; 16 | flex-direction: column; 17 | height: 100%; 18 | overflow: hidden; 19 | border-radius: inherit; 20 | font-size: 0.75rem; 21 | } 22 | 23 | .gui-codearea-dialog-header { 24 | display: flex; 25 | gap: 0.5rem; 26 | align-items: center; 27 | padding: 0.5rem; 28 | cursor: grab; 29 | border-bottom: 1px solid rgba(0, 0, 0, 0.24); 30 | box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.24); 31 | 32 | &.dragging { 33 | cursor: grabbing; 34 | } 35 | } 36 | 37 | .gui-codearea-dialog-title { 38 | overflow: hidden; 39 | text-overflow: ellipsis; 40 | white-space: nowrap; 41 | } 42 | 43 | .gui-codearea-dialog-spacer { 44 | flex: 1; 45 | } 46 | 47 | .gui-codearea-dialog-content { 48 | position: relative; 49 | flex: 1; 50 | overflow: auto; 51 | 52 | .code-editor { 53 | height: 100%; 54 | overflow: auto; 55 | border: 1px solid transparent; 56 | } 57 | 58 | &:hover { 59 | .gui-codearea-btns { 60 | display: block; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /projects/gui/codearea/codearea-dialog.ts: -------------------------------------------------------------------------------- 1 | import { CodeEditor } from '@acrodata/code-editor'; 2 | import { RndDialogDragHandle } from '@acrodata/rnd-dialog'; 3 | import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog'; 4 | import { 5 | ChangeDetectionStrategy, 6 | ChangeDetectorRef, 7 | Component, 8 | DestroyRef, 9 | Inject, 10 | ViewEncapsulation, 11 | } from '@angular/core'; 12 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 13 | import { FormsModule } from '@angular/forms'; 14 | import { MatButton, MatIconButton } from '@angular/material/button'; 15 | import { MatIcon } from '@angular/material/icon'; 16 | import { GuiIconsRegistry } from '../gui-icons'; 17 | import { GuiIconButtonWrapper } from '../icon-button-wrapper/icon-button-wrapper'; 18 | import { GuiCodeareaConfig } from './codearea-config'; 19 | 20 | export interface GuiCodeareaDialogData { 21 | value: string; 22 | disabled?: boolean; 23 | readonly?: boolean; 24 | language?: string; 25 | title?: string; 26 | } 27 | 28 | @Component({ 29 | selector: 'gui-codearea-dialog', 30 | standalone: true, 31 | imports: [ 32 | FormsModule, 33 | MatButton, 34 | MatIconButton, 35 | MatIcon, 36 | RndDialogDragHandle, 37 | CodeEditor, 38 | GuiIconButtonWrapper, 39 | ], 40 | templateUrl: './codearea-dialog.html', 41 | styleUrl: './codearea-dialog.scss', 42 | host: { 43 | class: 'gui-codearea-dialog', 44 | }, 45 | encapsulation: ViewEncapsulation.None, 46 | changeDetection: ChangeDetectionStrategy.OnPush, 47 | }) 48 | export class GuiCodeareaDialog { 49 | get languages() { 50 | return this.codeareaCfg.languages; 51 | } 52 | 53 | get theme() { 54 | return this.codeareaCfg.theme; 55 | } 56 | 57 | get extensions() { 58 | return typeof this.codeareaCfg.extensions === 'function' 59 | ? this.codeareaCfg.extensions(this.data) 60 | : this.codeareaCfg.extensions; 61 | } 62 | 63 | langDesc = this.codeareaCfg.languages.find( 64 | lang => this.data.language && lang.alias.includes(this.data.language.toLowerCase()) 65 | ); 66 | 67 | title = `${this.data.title || ''} (${this.langDesc?.name || 'Plain Text'})`; 68 | 69 | lineWrapping = false; 70 | 71 | constructor( 72 | private dialogRef: DialogRef, 73 | @Inject(DIALOG_DATA) public data: GuiCodeareaDialogData, 74 | private cdr: ChangeDetectorRef, 75 | private destroyRef: DestroyRef, 76 | private codeareaCfg: GuiCodeareaConfig, 77 | iconsRegistry: GuiIconsRegistry 78 | ) { 79 | iconsRegistry.add('wrap'); 80 | 81 | this.codeareaCfg.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { 82 | this.cdr.markForCheck(); 83 | }); 84 | } 85 | 86 | toggleLineWrapping() { 87 | this.lineWrapping = !this.lineWrapping; 88 | } 89 | 90 | save() { 91 | this.dialogRef.close(this.data.value); 92 | } 93 | 94 | close() { 95 | this.dialogRef.close(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /projects/gui/codearea/codearea.html: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 15 | 18 | 19 |
20 | 21 | @if (config.parentType === 'inline') { 22 | 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /projects/gui/codearea/codearea.scss: -------------------------------------------------------------------------------- 1 | .gui-codearea { 2 | position: relative; 3 | overflow: auto; 4 | border: 1px solid var(--mdc-outlined-text-field-outline-color); 5 | border-radius: var(--mdc-outlined-text-field-container-shape); 6 | 7 | .code-editor { 8 | width: 100%; 9 | min-height: 1.5rem; 10 | overflow: auto; 11 | resize: vertical; 12 | } 13 | 14 | &:hover { 15 | .gui-codearea-btns { 16 | display: block; 17 | } 18 | } 19 | } 20 | 21 | .gui-codearea-btns { 22 | position: absolute; 23 | top: 0; 24 | right: 0; 25 | display: none; 26 | margin: 2px; 27 | 28 | [dir='rtl'] & { 29 | left: 0; 30 | right: auto; 31 | } 32 | 33 | .gui-icon-button-wrapper { 34 | .mat-mdc-icon-button, 35 | .mat-icon { 36 | width: 1.25rem; 37 | height: 1.25rem; 38 | } 39 | } 40 | 41 | .mat-mdc-icon-button { 42 | backdrop-filter: blur(8px); 43 | border-radius: 0.25rem; 44 | 45 | .mat-mdc-button-persistent-ripple { 46 | border-radius: inherit; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /projects/gui/codearea/codearea.ts: -------------------------------------------------------------------------------- 1 | import { CodeEditor, Setup } from '@acrodata/code-editor'; 2 | import { RndDialog } from '@acrodata/rnd-dialog'; 3 | import { coerceCssPixelValue } from '@angular/cdk/coercion'; 4 | import { 5 | ChangeDetectionStrategy, 6 | ChangeDetectorRef, 7 | Component, 8 | DestroyRef, 9 | forwardRef, 10 | Input, 11 | ViewEncapsulation, 12 | } from '@angular/core'; 13 | import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; 14 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 15 | import { MatIconButton } from '@angular/material/button'; 16 | import { MatHint } from '@angular/material/form-field'; 17 | import { MatIcon } from '@angular/material/icon'; 18 | import { GuiFieldLabel } from '../field-label/field-label'; 19 | import { GuiIconsRegistry } from '../gui-icons'; 20 | import { GuiIconButtonWrapper } from '../icon-button-wrapper/icon-button-wrapper'; 21 | import { GuiControl } from '../interface'; 22 | import { GuiCodeareaConfig } from './codearea-config'; 23 | import { GuiCodeareaDialog, GuiCodeareaDialogData } from './codearea-dialog'; 24 | 25 | @Component({ 26 | selector: 'gui-codearea', 27 | templateUrl: './codearea.html', 28 | styleUrl: './codearea.scss', 29 | host: { 30 | class: 'gui-field gui-codearea', 31 | }, 32 | encapsulation: ViewEncapsulation.None, 33 | changeDetection: ChangeDetectionStrategy.OnPush, 34 | providers: [ 35 | { 36 | provide: NG_VALUE_ACCESSOR, 37 | useExisting: forwardRef(() => GuiCodearea), 38 | multi: true, 39 | }, 40 | ], 41 | standalone: true, 42 | imports: [ 43 | FormsModule, 44 | MatIcon, 45 | MatIconButton, 46 | MatHint, 47 | CodeEditor, 48 | GuiFieldLabel, 49 | GuiIconButtonWrapper, 50 | ], 51 | }) 52 | export class GuiCodearea implements ControlValueAccessor { 53 | @Input() config: Partial = {}; 54 | @Input() disabled = false; 55 | 56 | @Input() setup: Setup = 'minimal'; 57 | 58 | @Input() 59 | get height() { 60 | return coerceCssPixelValue(this.config.height || this._height); 61 | } 62 | set height(value: string | number) { 63 | this._height = value; 64 | } 65 | private _height: string | number = 120; 66 | 67 | @Input() 68 | get language() { 69 | return this.config.language || this._language; 70 | } 71 | set language(value: string) { 72 | this._language = value; 73 | } 74 | private _language = ''; 75 | 76 | get languages() { 77 | return this.codeareaCfg.languages; 78 | } 79 | 80 | get theme() { 81 | return this.codeareaCfg.theme; 82 | } 83 | 84 | get dialogData(): GuiCodeareaDialogData { 85 | return { 86 | value: this.value, 87 | disabled: this.disabled, 88 | language: this.language, 89 | }; 90 | } 91 | 92 | get extensions() { 93 | return typeof this.codeareaCfg.extensions === 'function' 94 | ? this.codeareaCfg.extensions(this.dialogData) 95 | : this.codeareaCfg.extensions; 96 | } 97 | 98 | value = ''; 99 | private oldValue = ''; 100 | 101 | private onChange: (value: string) => void = () => {}; 102 | private onTouched: () => void = () => {}; 103 | 104 | constructor( 105 | private rndDialog: RndDialog, 106 | private cdr: ChangeDetectorRef, 107 | private destroyRef: DestroyRef, 108 | private codeareaCfg: GuiCodeareaConfig, 109 | iconsRegistry: GuiIconsRegistry 110 | ) { 111 | iconsRegistry.add('expand'); 112 | 113 | this.codeareaCfg.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { 114 | this.cdr.markForCheck(); 115 | }); 116 | } 117 | 118 | writeValue(value: any) { 119 | if (typeof value === 'string' || value == null) { 120 | this.value = value || ''; 121 | } else { 122 | this.value = value.toString(); 123 | } 124 | this.oldValue = this.value; 125 | this.cdr.markForCheck(); 126 | } 127 | 128 | registerOnChange(fn: (value: string) => void) { 129 | this.onChange = fn; 130 | } 131 | 132 | registerOnTouched(fn: () => void) { 133 | this.onTouched = fn; 134 | } 135 | 136 | setDisabledState(isDisabled: boolean) { 137 | this.disabled = isDisabled; 138 | this.cdr.markForCheck(); 139 | } 140 | 141 | onValueChange() { 142 | if (this.value !== this.oldValue) { 143 | this.onChange(this.value); 144 | this.oldValue = this.value; 145 | } 146 | } 147 | 148 | onExpand() { 149 | const dialogRef = this.rndDialog.open( 150 | GuiCodeareaDialog, 151 | { 152 | panelClass: 'gui-codearea-dialog-panel', 153 | hasBackdrop: false, 154 | width: '600px', 155 | data: this.dialogData, 156 | } 157 | ); 158 | 159 | dialogRef.closed.subscribe(newValue => { 160 | if (newValue) { 161 | this.value = newValue; 162 | this.cdr.detectChanges(); 163 | this.onValueChange(); 164 | } 165 | }); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /projects/gui/combobox/combobox.html: -------------------------------------------------------------------------------- 1 | 2 | @if (config.prefix) { 3 | {{ config.prefix }} 4 | } 5 | 19 | 20 |
21 | {{ item.label }} 22 |
23 |
24 |
25 | @if (config.suffix) { 26 | {{ config.suffix }} 27 | } 28 | @if (config.parentType === 'inline') { 29 | 30 | 31 | 32 | } 33 |
34 | -------------------------------------------------------------------------------- /projects/gui/combobox/combobox.scss: -------------------------------------------------------------------------------- 1 | .gui-combobox { 2 | .ng-select { 3 | padding-left: 0.5rem; 4 | padding-right: 0.5rem; 5 | margin-left: -0.5rem; 6 | margin-right: -0.5rem; 7 | 8 | &.ng-select-multiple .ng-value { 9 | flex-direction: row-reverse; 10 | } 11 | 12 | .ng-value-icon { 13 | font-family: math; 14 | font-size: 10px; 15 | } 16 | 17 | .ng-clear-wrapper .ng-clear { 18 | font-family: math; 19 | font-size: 12px; 20 | } 21 | 22 | .ng-arrow { 23 | vertical-align: -1px; 24 | } 25 | } 26 | 27 | &.ng-dropdown-panel { 28 | padding: 0.5rem 0; 29 | 30 | .ng-dropdown-panel-items { 31 | .ng-option { 32 | padding: 0 0.5rem; 33 | line-height: var(--mat-option-label-text-line-height); 34 | 35 | .ng-tag-label { 36 | line-height: 1; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /projects/gui/combobox/combobox.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ChangeDetectionStrategy, 4 | ChangeDetectorRef, 5 | Component, 6 | forwardRef, 7 | Input, 8 | ViewChild, 9 | ViewEncapsulation, 10 | } from '@angular/core'; 11 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 12 | import { MatFormField, MatHint, MatPrefix, MatSuffix } from '@angular/material/form-field'; 13 | import { MtxSelect, MtxSelectOptionTemplate } from '@ng-matero/extensions/select'; 14 | import { GuiFieldLabel } from '../field-label/field-label'; 15 | import { GuiBasicValue, GuiControl } from '../interface'; 16 | 17 | @Component({ 18 | selector: 'gui-combobox', 19 | templateUrl: './combobox.html', 20 | styleUrl: './combobox.scss', 21 | host: { 22 | class: 'gui-field gui-combobox', 23 | }, 24 | encapsulation: ViewEncapsulation.None, 25 | changeDetection: ChangeDetectionStrategy.OnPush, 26 | providers: [ 27 | { 28 | provide: NG_VALUE_ACCESSOR, 29 | useExisting: forwardRef(() => GuiCombobox), 30 | multi: true, 31 | }, 32 | ], 33 | standalone: true, 34 | imports: [ 35 | FormsModule, 36 | MatFormField, 37 | MatPrefix, 38 | MatSuffix, 39 | MatHint, 40 | MtxSelect, 41 | MtxSelectOptionTemplate, 42 | GuiFieldLabel, 43 | ], 44 | }) 45 | export class GuiCombobox implements ControlValueAccessor, AfterViewInit { 46 | @ViewChild(MtxSelect) mtxSelect!: MtxSelect; 47 | 48 | @Input() config: Partial = {}; 49 | @Input() disabled = false; 50 | @Input() appendTo = 'body'; 51 | 52 | value: GuiBasicValue | GuiBasicValue[] = ''; 53 | 54 | private onChange: (value: GuiBasicValue | GuiBasicValue[]) => void = () => {}; 55 | private onTouched: () => void = () => {}; 56 | 57 | constructor(private cdr: ChangeDetectorRef) {} 58 | 59 | ngAfterViewInit(): void { 60 | // Add additional class for ng-select's dropdown panel 61 | const { ngSelect } = this.mtxSelect; 62 | ngSelect.classes = (ngSelect.classes || '') + ' gui-combobox'; 63 | } 64 | 65 | writeValue(value: GuiBasicValue | GuiBasicValue[]) { 66 | this.value = value; 67 | this.cdr.markForCheck(); 68 | } 69 | 70 | registerOnChange(fn: (value: GuiBasicValue | GuiBasicValue[]) => void) { 71 | this.onChange = fn; 72 | } 73 | 74 | registerOnTouched(fn: () => void) { 75 | this.onTouched = fn; 76 | } 77 | 78 | setDisabledState(isDisabled: boolean) { 79 | this.disabled = isDisabled; 80 | this.cdr.markForCheck(); 81 | } 82 | 83 | onValueChange() { 84 | this.onChange(this.value); 85 | } 86 | 87 | addTagFn(label: string) { 88 | return { label, value: label }; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /projects/gui/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const tseslint = require('typescript-eslint'); 3 | const rootConfig = require('../../eslint.config.js'); 4 | 5 | module.exports = tseslint.config( 6 | ...rootConfig, 7 | { 8 | files: ['**/*.ts'], 9 | rules: { 10 | '@angular-eslint/directive-selector': [ 11 | 'error', 12 | { 13 | type: 'attribute', 14 | prefix: '', 15 | style: 'camelCase', 16 | }, 17 | ], 18 | '@angular-eslint/component-selector': [ 19 | 'error', 20 | { 21 | type: 'element', 22 | prefix: 'gui', 23 | style: 'kebab-case', 24 | }, 25 | ], 26 | }, 27 | }, 28 | { 29 | files: ['**/*.html'], 30 | rules: {}, 31 | } 32 | ); 33 | -------------------------------------------------------------------------------- /projects/gui/field-group/field-group.html: -------------------------------------------------------------------------------- 1 | @if (config.parentType !== 'inline' && config.name) { 2 | 3 | } 4 | 5 | -------------------------------------------------------------------------------- /projects/gui/field-group/field-group.scss: -------------------------------------------------------------------------------- 1 | .gui-field-group { 2 | display: flex; 3 | padding: 0.25rem 0.5rem; 4 | } 5 | 6 | .gui-field { 7 | --mat-form-field-container-vertical-padding: 0.25rem; 8 | --mat-form-field-container-text-line-height: 1rem; 9 | --mat-form-field-container-text-size: 0.75rem; 10 | 11 | display: inline-flex; 12 | flex: 1; 13 | flex-wrap: wrap; 14 | 15 | >.mat-mdc-form-field { 16 | flex: 1; 17 | width: 0; 18 | max-width: 100%; 19 | } 20 | 21 | .mat-mdc-text-field-wrapper, 22 | .mat-mdc-form-field-hint-wrapper { 23 | position: relative; 24 | padding: 0 0.5rem; 25 | border-radius: var(--mdc-filled-text-field-container-shape); 26 | 27 | [dir='rtl'] & { 28 | padding: 0 0.5rem; 29 | } 30 | } 31 | 32 | .mat-mdc-form-field-infix { 33 | min-height: auto; 34 | } 35 | 36 | .mat-mdc-form-field-hint-spacer, 37 | .mat-mdc-form-field-bottom-align::before, 38 | .mdc-line-ripple { 39 | display: none; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /projects/gui/field-group/field-group.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | DoCheck, 6 | Input, 7 | KeyValueDiffer, 8 | KeyValueDiffers, 9 | OnInit, 10 | ViewEncapsulation, 11 | } from '@angular/core'; 12 | import { GuiFieldLabel } from '../field-label/field-label'; 13 | import { GuiControl } from '../interface'; 14 | 15 | @Component({ 16 | selector: 'gui-field-group', 17 | templateUrl: './field-group.html', 18 | styleUrl: './field-group.scss', 19 | host: { 20 | class: 'gui-field-group', 21 | }, 22 | encapsulation: ViewEncapsulation.None, 23 | changeDetection: ChangeDetectionStrategy.OnPush, 24 | standalone: true, 25 | imports: [GuiFieldLabel], 26 | }) 27 | export class GuiFieldGroup implements OnInit, DoCheck { 28 | @Input() config: Partial = {}; 29 | 30 | private configDiffer?: KeyValueDiffer; 31 | 32 | constructor( 33 | private differs: KeyValueDiffers, 34 | private cdr: ChangeDetectorRef 35 | ) {} 36 | 37 | ngOnInit(): void { 38 | this.configDiffer = this.differs.find(this.config).create(); 39 | } 40 | 41 | ngDoCheck(): void { 42 | const changes = this.configDiffer?.diff(this.config); 43 | changes?.forEachChangedItem(record => { 44 | this.cdr.markForCheck(); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /projects/gui/field-label/field-label.html: -------------------------------------------------------------------------------- 1 | @if (!config.description) { 2 | {{ title }} 3 | } @else { 4 | 10 | {{ title }} 11 | 12 | } 13 | -------------------------------------------------------------------------------- /projects/gui/field-label/field-label.scss: -------------------------------------------------------------------------------- 1 | .gui-field-label { 2 | display: inline-block; 3 | width: 4rem; 4 | height: 1.5rem; 5 | padding: 0 0.25rem; 6 | line-height: 1.5rem; 7 | overflow: hidden; 8 | white-space: nowrap; 9 | text-overflow: ellipsis; 10 | } 11 | 12 | .gui-field-label-with-description { 13 | text-decoration: underline dotted; 14 | cursor: help; 15 | } 16 | 17 | .gui-field-label-tooltip { 18 | white-space: pre-wrap; 19 | } 20 | -------------------------------------------------------------------------------- /projects/gui/field-label/field-label.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | Component, 4 | Input, 5 | OnChanges, 6 | ViewEncapsulation, 7 | } from '@angular/core'; 8 | import { MatTooltip } from '@angular/material/tooltip'; 9 | import { ejsTmpl } from '../gui-utils'; 10 | import { GuiControl } from '../interface'; 11 | 12 | @Component({ 13 | selector: 'gui-field-label', 14 | templateUrl: './field-label.html', 15 | styleUrl: './field-label.scss', 16 | host: { 17 | '[class.gui-field-label]': '!styless', 18 | '[title]': 'title', 19 | }, 20 | encapsulation: ViewEncapsulation.None, 21 | changeDetection: ChangeDetectionStrategy.OnPush, 22 | standalone: true, 23 | imports: [MatTooltip], 24 | }) 25 | export class GuiFieldLabel implements OnChanges { 26 | @Input() config: Partial = {}; 27 | 28 | @Input() index?: number; 29 | 30 | title = ''; 31 | 32 | styless = false; 33 | 34 | ngOnChanges(): void { 35 | const { index, name, parentType, type } = this.config; 36 | this.title = index != null && !isNaN(index) ? ejsTmpl(name || '', { i: index }) : name; 37 | this.styless = 38 | (parentType === 'inline' && type !== 'inline') || type === 'group' || type === 'tabs'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /projects/gui/file-uploader/file-uploader-config.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpResponse } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { map } from 'rxjs'; 4 | import { GuiControl } from '../interface'; 5 | 6 | export interface FileUploadResponseBody { 7 | bytes: number; 8 | mime: string; 9 | url: string; 10 | } 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class GuiFileUploaderConfig { 16 | constructor(protected http: HttpClient) {} 17 | 18 | /** 19 | * The file upload URL 20 | */ 21 | url = ''; 22 | 23 | /** 24 | * The File upload API 25 | * 26 | * @param formData The FormData with file binary 27 | * @param config The custom upload config that passed from component input 28 | * @returns The uploaded file url stream 29 | */ 30 | upload(formData: FormData, config: Partial) { 31 | return this.http 32 | .post(this.url, formData, { 33 | reportProgress: true, 34 | observe: 'events', 35 | }) 36 | .pipe( 37 | map(res => { 38 | if (res instanceof HttpResponse && res.body) { 39 | return res.body.url; 40 | } 41 | return null; 42 | }) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /projects/gui/file-uploader/file-uploader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | @if (url) { 17 | 18 | 21 | 22 | } 23 | 24 | 25 |
26 | @if (url) { 27 | @switch (type) { 28 | @case ('image') { 29 | 30 | } 31 | @case ('video') { 32 | 33 | } 34 | @case ('audio') { 35 | 36 | } 37 | @default { 38 | 39 | } 40 | } 41 | } @else { 42 |
43 | 44 |
45 | } 46 | 47 | 55 |
56 | 57 | @if (config.parentType === 'inline') { 58 | 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /projects/gui/file-uploader/file-uploader.scss: -------------------------------------------------------------------------------- 1 | .gui-file-uploader { 2 | display: block; 3 | overflow: auto; 4 | 5 | .mat-mdc-form-field { 6 | width: 100%; 7 | } 8 | 9 | .gui-file-content { 10 | position: relative; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | height: 7.5rem; 15 | margin: 0.25rem 0 0; 16 | padding: 0.25rem; 17 | background-color: var(--mdc-filled-text-field-container-color); 18 | border: 1px solid transparent; 19 | border-radius: 0.25rem; 20 | 21 | &:hover, 22 | &:focus-within { 23 | border-color: currentColor; 24 | } 25 | 26 | img, 27 | video { 28 | width: 100%; 29 | height: 100%; 30 | object-fit: contain; 31 | } 32 | 33 | input[type='file'] { 34 | position: absolute; 35 | top: 0; 36 | left: 0; 37 | width: 100%; 38 | height: 100%; 39 | opacity: 0; 40 | cursor: pointer; 41 | } 42 | 43 | .mat-icon svg { 44 | width: 1.5rem; 45 | height: 1.5rem; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /projects/gui/file-uploader/file-uploader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | ElementRef, 6 | EventEmitter, 7 | Input, 8 | OnChanges, 9 | Output, 10 | SimpleChanges, 11 | ViewChild, 12 | ViewEncapsulation, 13 | forwardRef, 14 | } from '@angular/core'; 15 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 16 | import { MatIconButton } from '@angular/material/button'; 17 | import { MatFormField, MatHint, MatPrefix, MatSuffix } from '@angular/material/form-field'; 18 | import { MatIcon } from '@angular/material/icon'; 19 | import { MatInput } from '@angular/material/input'; 20 | import { finalize } from 'rxjs/operators'; 21 | 22 | import { GuiFieldLabel } from '../field-label/field-label'; 23 | import { GuiIconsRegistry } from '../gui-icons'; 24 | import { GuiIconButtonWrapper } from '../icon-button-wrapper/icon-button-wrapper'; 25 | import { GuiControl } from '../interface'; 26 | import { GuiFileUploaderConfig } from './file-uploader-config'; 27 | 28 | export type FileUploadType = 'image' | 'video' | 'audio' | '*'; 29 | 30 | export interface FileUploadContent { 31 | data: File; 32 | progress: number; 33 | inProgress: boolean; 34 | } 35 | 36 | @Component({ 37 | selector: 'gui-file-uploader', 38 | templateUrl: './file-uploader.html', 39 | styleUrl: './file-uploader.scss', 40 | host: { 41 | class: 'gui-field gui-file-uploader', 42 | }, 43 | encapsulation: ViewEncapsulation.None, 44 | changeDetection: ChangeDetectionStrategy.OnPush, 45 | providers: [ 46 | { 47 | provide: NG_VALUE_ACCESSOR, 48 | useExisting: forwardRef(() => GuiFileUploader), 49 | multi: true, 50 | }, 51 | ], 52 | standalone: true, 53 | imports: [ 54 | FormsModule, 55 | MatFormField, 56 | MatIcon, 57 | MatPrefix, 58 | MatInput, 59 | MatIconButton, 60 | MatSuffix, 61 | MatHint, 62 | GuiFieldLabel, 63 | GuiIconButtonWrapper, 64 | ], 65 | }) 66 | export class GuiFileUploader implements ControlValueAccessor, OnChanges { 67 | @ViewChild('fileInput') fileInput!: ElementRef; 68 | 69 | @Input() config: Partial = {}; 70 | @Input() disabled = false; 71 | @Input() type: FileUploadType = '*'; 72 | @Input() name = ''; 73 | @Input() accept = ''; 74 | 75 | @Output() fileChange = new EventEmitter(); 76 | 77 | // file url that returned from the server 78 | url = ''; 79 | 80 | // file to upload 81 | fileUpload!: FileUploadContent; 82 | 83 | private onChange: (value: string) => void = () => {}; 84 | private onTouched: () => void = () => {}; 85 | 86 | constructor( 87 | private fileUploaderCfg: GuiFileUploaderConfig, 88 | private cdr: ChangeDetectorRef, 89 | iconsRegistry: GuiIconsRegistry 90 | ) { 91 | iconsRegistry.add('link', 'clear', 'file', 'upload'); 92 | } 93 | 94 | ngOnChanges(changes: SimpleChanges): void { 95 | if (changes['config'] || changes['accept'] || changes['type']) { 96 | this.accept = this.config.accept || this.accept || this.type + '/*'; 97 | } 98 | } 99 | 100 | writeValue(value: string) { 101 | this.url = value; 102 | this.cdr.markForCheck(); 103 | } 104 | 105 | registerOnChange(fn: (value: string) => void) { 106 | this.onChange = fn; 107 | } 108 | 109 | registerOnTouched(fn: () => void) { 110 | this.onTouched = fn; 111 | } 112 | 113 | setDisabledState(isDisabled: boolean) { 114 | this.disabled = isDisabled; 115 | this.cdr.markForCheck(); 116 | } 117 | 118 | upload(fileUpload: FileUploadContent) { 119 | const formData = new FormData(); 120 | formData.append('file', fileUpload.data); 121 | 122 | fileUpload.inProgress = true; 123 | 124 | this.fileUploaderCfg 125 | .upload(formData, this.config) 126 | .pipe( 127 | finalize(() => { 128 | fileUpload.inProgress = false; 129 | }) 130 | ) 131 | .subscribe(result => { 132 | if (result) { 133 | this.url = result; 134 | this.cdr.detectChanges(); 135 | 136 | this.onChange(this.url); 137 | this.onTouched(); 138 | 139 | this.fileChange.emit(this.url); 140 | } 141 | }); 142 | } 143 | 144 | onUrlChange(e: Event) { 145 | this.url = (e.target as HTMLInputElement).value; 146 | 147 | this.onChange(this.url); 148 | 149 | this.fileChange.emit(this.url); 150 | } 151 | 152 | onFileChange(e: Event) { 153 | this.fileUpload = { 154 | data: (e.target as HTMLInputElement).files![0], 155 | inProgress: false, 156 | progress: 0, 157 | }; 158 | 159 | this.upload(this.fileUpload); 160 | 161 | // reset input value 162 | (e.target as HTMLInputElement).value = ''; 163 | } 164 | 165 | onBlur() { 166 | this.onTouched(); 167 | } 168 | 169 | onClear() { 170 | this.url = ''; 171 | 172 | this.onChange(this.url); 173 | this.onTouched(); 174 | 175 | this.fileChange.emit(this.url); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /projects/gui/fill/fill.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | @if (config.parentType === 'inline') { 22 | 23 | 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /projects/gui/fill/fill.scss: -------------------------------------------------------------------------------- 1 | .gui-color-block, 2 | .gui-color-block-empty { 3 | position: relative; 4 | display: inline-block; 5 | width: 1.5rem; 6 | height: 1.5rem; 7 | } 8 | 9 | .gui-color-block-empty { 10 | position: absolute; 11 | background-color: #fff; 12 | background-image: 13 | linear-gradient(45deg, #ccc 25%, transparent 25%), 14 | linear-gradient(-45deg, #ccc 25%, transparent 25%), 15 | linear-gradient(45deg, transparent 75%, #ccc 75%), 16 | linear-gradient(-45deg, transparent 75%, #ccc 75%); 17 | background-position: 0 0, 0 0.25rem, 0.25rem -0.25rem, -0.25rem 0; 18 | background-size: 0.5rem 0.5rem; 19 | } 20 | 21 | // use the pseudo-elements `::after` to simulate the border for color-block 22 | .gui-color-block::after { 23 | display: inline-block; 24 | width: 100%; 25 | height: 100%; 26 | box-sizing: border-box; 27 | border: 1px solid currentColor; 28 | border-top-left-radius: var(--mdc-filled-text-field-container-shape); 29 | border-bottom-left-radius: var(--mdc-filled-text-field-container-shape); 30 | opacity: .12; 31 | content: ''; 32 | 33 | [dir='rtl'] & { 34 | border-radius: 0; 35 | border-top-right-radius: var(--mdc-filled-text-field-container-shape); 36 | border-bottom-right-radius: var(--mdc-filled-text-field-container-shape); 37 | } 38 | } 39 | 40 | // fix the vertical alignment for icon 41 | .gui-fill { 42 | .mat-mdc-icon-button { 43 | display: inline-flex; 44 | justify-content: center; 45 | align-items: center; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /projects/gui/fill/fill.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | forwardRef, 6 | Input, 7 | ViewEncapsulation, 8 | } from '@angular/core'; 9 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 10 | import { MatFormField, MatHint, MatPrefix, MatSuffix } from '@angular/material/form-field'; 11 | import { MatInput } from '@angular/material/input'; 12 | import { 13 | MtxColorpicker, 14 | MtxColorpickerInput, 15 | MtxColorpickerToggle, 16 | } from '@ng-matero/extensions/colorpicker'; 17 | import { GuiFieldLabel } from '../field-label/field-label'; 18 | import { GuiIconButtonWrapper } from '../icon-button-wrapper/icon-button-wrapper'; 19 | import { GuiControl } from '../interface'; 20 | 21 | @Component({ 22 | selector: 'gui-fill', 23 | templateUrl: './fill.html', 24 | styleUrl: './fill.scss', 25 | host: { 26 | class: 'gui-field gui-fill', 27 | }, 28 | encapsulation: ViewEncapsulation.None, 29 | changeDetection: ChangeDetectionStrategy.OnPush, 30 | providers: [ 31 | { 32 | provide: NG_VALUE_ACCESSOR, 33 | useExisting: forwardRef(() => GuiFill), 34 | multi: true, 35 | }, 36 | ], 37 | standalone: true, 38 | imports: [ 39 | FormsModule, 40 | MatFormField, 41 | MatPrefix, 42 | MatInput, 43 | MatSuffix, 44 | MatHint, 45 | MtxColorpickerInput, 46 | MtxColorpicker, 47 | MtxColorpickerToggle, 48 | GuiFieldLabel, 49 | GuiIconButtonWrapper, 50 | ], 51 | }) 52 | export class GuiFill implements ControlValueAccessor { 53 | @Input() config: Partial = {}; 54 | @Input() disabled = false; 55 | 56 | value = ''; 57 | 58 | private onChange: (value: string) => void = () => {}; 59 | private onTouched: () => void = () => {}; 60 | 61 | constructor(private cdr: ChangeDetectorRef) {} 62 | 63 | writeValue(value: string) { 64 | if (typeof value === 'string') { 65 | this.value = value; 66 | this.cdr.markForCheck(); 67 | } 68 | } 69 | 70 | registerOnChange(fn: (value: string) => void) { 71 | this.onChange = fn; 72 | } 73 | 74 | registerOnTouched(fn: () => void) { 75 | this.onTouched = fn; 76 | } 77 | 78 | setDisabledState(isDisabled: boolean) { 79 | this.disabled = isDisabled; 80 | this.cdr.markForCheck(); 81 | } 82 | 83 | onValueChange() { 84 | this.onChange(this.value); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /projects/gui/gui-form.scss: -------------------------------------------------------------------------------- 1 | .gui-form { 2 | --mat-expansion-header-text-size: 0.75rem; 3 | --mat-expansion-header-collapsed-state-height: 2rem; 4 | --mat-expansion-header-expanded-state-height: 2rem; 5 | --mat-expansion-container-text-size: 0.75rem; 6 | --mat-expansion-container-shape: 0; 7 | 8 | position: relative; 9 | display: block; 10 | font-size: 0.75rem; 11 | 12 | .mat-expansion-panel:not([class*="mat-elevation-z"]) { 13 | box-shadow: none; 14 | } 15 | 16 | .mat-expansion-panel-body { 17 | padding: 0; 18 | } 19 | 20 | .mat-expansion-panel-header { 21 | padding: 0 0.75rem; 22 | 23 | .mat-content { 24 | align-items: center; 25 | padding-right: 0.5rem; 26 | 27 | [dir='rtl'] & { 28 | padding-right: 0; 29 | padding-left: 0.5rem; 30 | } 31 | 32 | gui-field-label { 33 | flex: 1; 34 | overflow: hidden; 35 | white-space: nowrap; 36 | text-overflow: ellipsis; 37 | } 38 | } 39 | } 40 | 41 | .mat-expansion-indicator { 42 | svg { 43 | width: 1.5rem; 44 | height: 1.5rem; 45 | margin: 0 -0.5rem; 46 | } 47 | 48 | &::after { 49 | vertical-align: 0.125rem; 50 | } 51 | } 52 | 53 | .mat-mdc-tab-header { 54 | --mat-tab-header-label-text-size: 0.75rem; 55 | --mdc-secondary-navigation-tab-container-height: 1.5rem; 56 | } 57 | 58 | .mat-mdc-tab-header-pagination { 59 | min-width: var(--mdc-secondary-navigation-tab-container-height); 60 | } 61 | 62 | .mat-mdc-tab { 63 | min-width: auto; 64 | padding: 0 0.75rem; 65 | } 66 | } 67 | 68 | .gui-list-item-heading { 69 | display: flex; 70 | align-items: center; 71 | position: relative; 72 | padding: 0 0.75rem; 73 | line-height: 1.5rem; 74 | border-bottom: var(--mat-tab-header-divider-height) solid transparent; 75 | 76 | &::before { 77 | position: absolute; 78 | left: 0; 79 | width: 0.125rem; 80 | height: 1rem; 81 | background-color: var(--mat-expansion-header-text-color); 82 | content: ''; 83 | 84 | [dir='rtl'] & { 85 | left: auto; 86 | right: 0; 87 | } 88 | } 89 | } 90 | 91 | .gui-list-item-title { 92 | display: flex; 93 | flex: 1; 94 | padding: 0; 95 | background-color: transparent; 96 | border: none; 97 | color: inherit; 98 | font-size: inherit; 99 | font-family: inherit; 100 | letter-spacing: inherit; 101 | line-height: inherit; 102 | cursor: pointer; 103 | 104 | .gui-list-item-active & { 105 | font-weight: bold; 106 | } 107 | } 108 | 109 | .gui-menu { 110 | .mat-mdc-tab-header { 111 | --mdc-tab-indicator-active-indicator-shape: 0.25rem; 112 | 113 | padding: 0.25rem; 114 | background-color: var(--mdc-filled-text-field-container-color); 115 | 116 | .mdc-tab__ripple::before { 117 | border-radius: var(--mdc-tab-indicator-active-indicator-shape); 118 | } 119 | } 120 | 121 | .mat-mdc-tab-labels { 122 | gap: 0.25rem; 123 | } 124 | 125 | .mdc-tab-indicator .mdc-tab-indicator__content { 126 | height: 100%; 127 | background-color: var(--mdc-tab-indicator-active-indicator-color); 128 | } 129 | 130 | .mdc-tab-indicator--active .mdc-tab-indicator__content { 131 | opacity: 0.24; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /projects/gui/gui-icons.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, InjectionToken, Optional } from '@angular/core'; 2 | import { MatIconRegistry } from '@angular/material/icon'; 3 | import { DomSanitizer } from '@angular/platform-browser'; 4 | 5 | export const svgIcons = { 6 | horizontal: ` 7 | 8 | 9 | 10 | `, 11 | vertical: ` 12 | 13 | 14 | 15 | `, 16 | add: ` 17 | 18 | 19 | 20 | `, 21 | delete: ` 22 | 23 | 24 | 25 | `, 26 | copy: ` 27 | 28 | 29 | 30 | `, 31 | link: ` 32 | 33 | 34 | 35 | `, 36 | clear: ` 37 | 38 | 39 | 40 | `, 41 | file: ` 42 | 43 | 44 | 45 | `, 46 | upload: ` 47 | 48 | 49 | 50 | `, 51 | expand: ` 52 | 53 | 54 | `, 55 | wrap: ` 56 | 57 | 58 | `, 59 | }; 60 | 61 | export type GuiIconType = keyof typeof svgIcons; 62 | 63 | export type GuiIconsConfig = Record; 64 | 65 | /** Injection token that can be used to provide the default icons. */ 66 | export const GUI_ICONS_CONFIG = new InjectionToken('gui-icons-config'); 67 | 68 | @Injectable({ providedIn: 'root' }) 69 | export class GuiIconsRegistry { 70 | constructor( 71 | private _iconRegistry: MatIconRegistry, 72 | private _sanitizer: DomSanitizer, 73 | @Optional() @Inject(GUI_ICONS_CONFIG) private _defaultIcons?: GuiIconsConfig 74 | ) {} 75 | 76 | add(...iconNames: GuiIconType[]) { 77 | const icons = Object.assign(svgIcons, this._defaultIcons); 78 | iconNames.forEach(k => { 79 | this._iconRegistry.addSvgIconLiteral(k, this._sanitizer.bypassSecurityTrustHtml(icons[k])); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /projects/gui/gui-module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule, FormsModule } from '@angular/forms'; 4 | 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatInputModule } from '@angular/material/input'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatSelectModule } from '@angular/material/select'; 9 | import { MatSliderModule } from '@angular/material/slider'; 10 | import { MatSlideToggleModule } from '@angular/material/slide-toggle'; 11 | import { MatButtonToggleModule } from '@angular/material/button-toggle'; 12 | import { MatButtonModule } from '@angular/material/button'; 13 | import { MatTabsModule } from '@angular/material/tabs'; 14 | import { MatExpansionModule } from '@angular/material/expansion'; 15 | import { MatTooltipModule } from '@angular/material/tooltip'; 16 | import { MtxColorpickerModule } from '@ng-matero/extensions/colorpicker'; 17 | import { MtxSelectModule } from '@ng-matero/extensions/select'; 18 | 19 | import { GuiForm } from './gui-form'; 20 | import { GuiFieldGroup } from './field-group/field-group'; 21 | import { GuiFieldLabel } from './field-label/field-label'; 22 | import { GuiInlineGroup } from './inline-group/inline-group'; 23 | import { GuiInputText } from './input-text/input-text'; 24 | import { GuiInputNumber } from './input-number/input-number'; 25 | import { GuiSelect } from './select/select'; 26 | import { GuiSwitch } from './switch/switch'; 27 | import { GuiSlider } from './slider/slider'; 28 | import { GuiIcon } from './button-toggle/icon'; 29 | import { GuiButtonToggle } from './button-toggle/button-toggle'; 30 | import { GuiFill } from './fill/fill'; 31 | import { GuiImageSelect } from './image-select/image-select'; 32 | import { GuiCombobox } from './combobox/combobox'; 33 | import { GuiTextarea } from './textarea/textarea'; 34 | import { GuiCodearea } from './codearea/codearea'; 35 | import { GuiCodeareaDialog } from './codearea/codearea-dialog'; 36 | import { GuiFileUploader } from './file-uploader/file-uploader'; 37 | import { GuiIconButtonWrapper } from './icon-button-wrapper/icon-button-wrapper'; 38 | import { GuiEjsPipe, GuiFlexDirective } from './gui-utils'; 39 | 40 | @NgModule({ 41 | imports: [ 42 | CommonModule, 43 | ReactiveFormsModule, 44 | FormsModule, 45 | MatFormFieldModule, 46 | MatInputModule, 47 | MatSelectModule, 48 | MatSliderModule, 49 | MatSlideToggleModule, 50 | MatButtonToggleModule, 51 | MatIconModule, 52 | MatButtonModule, 53 | MatTabsModule, 54 | MatExpansionModule, 55 | MatTooltipModule, 56 | MtxColorpickerModule, 57 | MtxSelectModule, 58 | GuiForm, 59 | GuiInputText, 60 | GuiInputNumber, 61 | GuiSelect, 62 | GuiSwitch, 63 | GuiSlider, 64 | GuiIcon, 65 | GuiButtonToggle, 66 | GuiFill, 67 | GuiFieldGroup, 68 | GuiFieldLabel, 69 | GuiInlineGroup, 70 | GuiImageSelect, 71 | GuiCombobox, 72 | GuiTextarea, 73 | GuiCodearea, 74 | GuiCodeareaDialog, 75 | GuiFileUploader, 76 | GuiIconButtonWrapper, 77 | GuiEjsPipe, 78 | GuiFlexDirective, 79 | ], 80 | exports: [ 81 | GuiForm, 82 | GuiInputText, 83 | GuiInputNumber, 84 | GuiSelect, 85 | GuiSwitch, 86 | GuiSlider, 87 | GuiIcon, 88 | GuiButtonToggle, 89 | GuiFill, 90 | GuiFieldGroup, 91 | GuiFieldLabel, 92 | GuiInlineGroup, 93 | GuiImageSelect, 94 | GuiCombobox, 95 | GuiTextarea, 96 | GuiCodearea, 97 | GuiCodeareaDialog, 98 | GuiFileUploader, 99 | GuiIconButtonWrapper, 100 | GuiEjsPipe, 101 | GuiFlexDirective, 102 | ], 103 | }) 104 | export class GuiModule {} 105 | -------------------------------------------------------------------------------- /projects/gui/gui-utils.ts: -------------------------------------------------------------------------------- 1 | import { Directive, ElementRef, Input, OnInit, Pipe, PipeTransform } from '@angular/core'; 2 | import { GuiDefaultValue, GuiOperator } from './interface'; 3 | 4 | /** 5 | * Lightweight EJS template engine 6 | * 7 | * @param str template string 8 | * @param data data passed to the template 9 | * @returns 10 | * 11 | * ### Example 12 | * 13 | * ```ts 14 | * const people = ['geddy', 'neil', 'alex']; 15 | * const res = ejsTmpl('<%= people.join(", ") %>', {people: people}); 16 | * console.log(res); 17 | * // => 'geddy, neil, alex' 18 | * ``` 19 | * 20 | */ 21 | export function ejsTmpl(str: string, data: any) { 22 | const fn = new Function( 23 | 'obj', 24 | 'var p=[],print=function(){p.push.apply(p,arguments);};' + 25 | // Introduce the data as local variables using with(){} 26 | 'with(obj){p.push("' + 27 | // Convert the template into pure JavaScript 28 | str 29 | .replace(/[\r\t\n]/g, ' ') 30 | .split('<%') 31 | .join('\t') 32 | .replace(/((^|%>)[^\t]*)'/g, '$1\r') 33 | .replace(/\t=(.*?)%>/g, '",$1,"') 34 | .split('\t') 35 | .join('");') 36 | .split('%>') 37 | .join('p.push("') 38 | .split('\r') 39 | .join('"') + 40 | '");}return p.join("");' 41 | ); 42 | 43 | // Provide some basic currying to the user 44 | return data ? fn(data) : fn; 45 | } 46 | 47 | @Pipe({ 48 | name: 'ejs', 49 | standalone: true, 50 | }) 51 | export class GuiEjsPipe implements PipeTransform { 52 | transform(value: string, data = {}): string { 53 | return ejsTmpl(value, data); 54 | } 55 | } 56 | 57 | @Directive({ 58 | selector: '[flex]', 59 | standalone: true, 60 | }) 61 | export class GuiFlexDirective implements OnInit { 62 | @Input() flex: number | undefined = 100; 63 | 64 | constructor(private el: ElementRef) {} 65 | 66 | ngOnInit(): void { 67 | this.el.nativeElement.style.flex = `1 1 ${this.flex}%`; 68 | this.el.nativeElement.style.maxWidth = `${this.flex}%`; 69 | } 70 | } 71 | 72 | export function compareValues(a: GuiDefaultValue, b: GuiDefaultValue, operator: GuiOperator) { 73 | switch (operator) { 74 | case '$eq': 75 | return a === b; 76 | case '$ne': 77 | return a !== b; 78 | case '$gt': 79 | return (a ?? 0) > (b ?? 0); 80 | case '$lt': 81 | return (a ?? 0) < (b ?? 0); 82 | case '$gte': 83 | return (a ?? 0) >= (b ?? 0); 84 | case '$lte': 85 | return (a ?? 0) <= (b ?? 0); 86 | case '$in': 87 | return Array.isArray(b) && b.includes(a); 88 | case '$nin': 89 | return Array.isArray(b) && !b.includes(a); 90 | default: 91 | return false; 92 | } 93 | } 94 | 95 | export function getValueByPath(obj: Record, path: string) { 96 | return path.split('.').reduce((acc: Record | undefined, key) => { 97 | return acc?.['children']?.[key] ? acc['children'][key] : acc?.[key]; 98 | }, obj); 99 | } 100 | -------------------------------------------------------------------------------- /projects/gui/icon-button-wrapper/icon-button-wrapper.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/gui/icon-button-wrapper/icon-button-wrapper.scss: -------------------------------------------------------------------------------- 1 | .gui-icon-button-wrapper { 2 | display: block; 3 | line-height: 1; 4 | 5 | .mat-mdc-icon-button { 6 | --mdc-icon-button-state-layer-size: 1.5rem; 7 | --mdc-icon-button-icon-size: 0.875rem; 8 | --mat-icon-button-touch-target-display: none; 9 | 10 | padding: 0; 11 | } 12 | 13 | .mat-icon { 14 | display: inline-flex; 15 | justify-content: center; 16 | align-items: center; 17 | width: 1.5rem; 18 | height: 1.5rem; 19 | 20 | svg { 21 | width: 0.875rem; 22 | height: 0.875rem; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/gui/icon-button-wrapper/icon-button-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'gui-icon-button-wrapper', 5 | standalone: true, 6 | templateUrl: './icon-button-wrapper.html', 7 | styleUrl: './icon-button-wrapper.scss', 8 | host: { 9 | class: 'gui-icon-button-wrapper', 10 | }, 11 | encapsulation: ViewEncapsulation.None, 12 | changeDetection: ChangeDetectionStrategy.OnPush, 13 | }) 14 | export class GuiIconButtonWrapper {} 15 | -------------------------------------------------------------------------------- /projects/gui/image-select/image-select.html: -------------------------------------------------------------------------------- 1 | 2 | @if (config.prefix) { 3 | {{ config.prefix }} 4 | } 5 | 14 | 15 | 16 | {{ opt.label }} 17 | 18 | 19 | 20 | {{ opt.label }} 21 | 22 | 23 | @if (config.suffix) { 24 | {{ config.suffix }} 25 | } 26 | @if (config.parentType === 'inline') { 27 | 28 | 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /projects/gui/image-select/image-select.scss: -------------------------------------------------------------------------------- 1 | %img { 2 | height: 100%; 3 | max-width: 7.5rem; 4 | object-fit: contain; 5 | border-radius: 0.25rem; 6 | background-color: var(--mat-option-hover-state-layer-color); 7 | } 8 | 9 | .gui-image-select { 10 | --mat-form-field-container-vertical-padding: 0; 11 | 12 | .ng-select { 13 | padding: 0 0.5rem; 14 | margin: 0 -0.5rem; 15 | 16 | .ng-value-container { 17 | height: 4rem; 18 | } 19 | 20 | .ng-value { 21 | display: inline-flex; 22 | align-items: center; 23 | gap: 0.5rem; 24 | height: 100%; 25 | padding: 0.5rem 0; 26 | 27 | img { 28 | @extend %img; 29 | } 30 | } 31 | 32 | .ng-clear-wrapper .ng-clear { 33 | font-family: math; 34 | font-size: 12px; 35 | } 36 | 37 | .ng-arrow { 38 | vertical-align: -1px; 39 | } 40 | } 41 | 42 | &.ng-dropdown-panel { 43 | .ng-dropdown-panel-items { 44 | .ng-option { 45 | display: flex; 46 | align-items: center; 47 | gap: 0.5rem; 48 | height: 4rem; 49 | padding: 0.5rem; 50 | 51 | img { 52 | @extend %img; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /projects/gui/image-select/image-select.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ChangeDetectionStrategy, 4 | ChangeDetectorRef, 5 | Component, 6 | forwardRef, 7 | Input, 8 | ViewChild, 9 | ViewEncapsulation, 10 | } from '@angular/core'; 11 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 12 | import { MatFormField, MatHint, MatPrefix, MatSuffix } from '@angular/material/form-field'; 13 | import { 14 | MtxSelect, 15 | MtxSelectLabelTemplate, 16 | MtxSelectOptionTemplate, 17 | } from '@ng-matero/extensions/select'; 18 | import { GuiFieldLabel } from '../field-label/field-label'; 19 | import { GuiControl } from '../interface'; 20 | 21 | @Component({ 22 | selector: 'gui-image-select', 23 | templateUrl: './image-select.html', 24 | styleUrl: './image-select.scss', 25 | host: { 26 | class: 'gui-field gui-image-select', 27 | }, 28 | encapsulation: ViewEncapsulation.None, 29 | changeDetection: ChangeDetectionStrategy.OnPush, 30 | providers: [ 31 | { 32 | provide: NG_VALUE_ACCESSOR, 33 | useExisting: forwardRef(() => GuiImageSelect), 34 | multi: true, 35 | }, 36 | ], 37 | standalone: true, 38 | imports: [ 39 | FormsModule, 40 | MatFormField, 41 | MatPrefix, 42 | MatSuffix, 43 | MatHint, 44 | MtxSelect, 45 | MtxSelectLabelTemplate, 46 | MtxSelectOptionTemplate, 47 | GuiFieldLabel, 48 | ], 49 | }) 50 | export class GuiImageSelect implements ControlValueAccessor, AfterViewInit { 51 | @ViewChild(MtxSelect) mtxSelect!: MtxSelect; 52 | 53 | @Input() config: Partial = {}; 54 | @Input() disabled = false; 55 | @Input() appendTo = 'body'; 56 | 57 | value: unknown; 58 | 59 | private onChange: (value: unknown) => void = () => {}; 60 | private onTouched: () => void = () => {}; 61 | 62 | constructor(private cdr: ChangeDetectorRef) {} 63 | 64 | ngAfterViewInit(): void { 65 | // Add additional class for ng-select's dropdown panel 66 | const { ngSelect } = this.mtxSelect; 67 | ngSelect.classes = (ngSelect.classes || '') + ' gui-image-select'; 68 | } 69 | 70 | writeValue(value: unknown) { 71 | this.value = value; 72 | this.cdr.markForCheck(); 73 | } 74 | 75 | registerOnChange(fn: (value: unknown) => void) { 76 | this.onChange = fn; 77 | } 78 | 79 | registerOnTouched(fn: () => void) { 80 | this.onTouched = fn; 81 | } 82 | 83 | setDisabledState(isDisabled: boolean) { 84 | this.disabled = isDisabled; 85 | this.cdr.markForCheck(); 86 | } 87 | 88 | onValueChange() { 89 | this.onChange(this.value); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /projects/gui/inline-group/inline-group.html: -------------------------------------------------------------------------------- 1 | @if (config.name) { 2 | 3 | } 4 |
5 | 6 |
7 | -------------------------------------------------------------------------------- /projects/gui/inline-group/inline-group.scss: -------------------------------------------------------------------------------- 1 | .gui-inline-group { 2 | display: flex; 3 | padding: 0.25rem 0.5rem; 4 | 5 | mat-hint { 6 | width: 100%; 7 | line-height: var(--mat-form-field-subscript-text-line-height); 8 | -webkit-font-smoothing: antialiased; 9 | } 10 | } 11 | 12 | .gui-inline-group-content { 13 | display: inline-flex; 14 | flex: 1; 15 | flex-flow: row wrap; 16 | margin: 0 -0.25rem; 17 | 18 | >.gui-field-group, 19 | >.gui-inline-group, 20 | >.mat-expansion-panel, 21 | >.gui-menu { 22 | width: 100%; 23 | padding: 0 0.25rem; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /projects/gui/inline-group/inline-group.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input, ViewEncapsulation } from '@angular/core'; 2 | import { GuiFieldLabel } from '../field-label/field-label'; 3 | import { GuiControl } from '../interface'; 4 | 5 | @Component({ 6 | selector: 'gui-inline-group', 7 | templateUrl: './inline-group.html', 8 | styleUrl: './inline-group.scss', 9 | host: { 10 | class: 'gui-inline-group', 11 | }, 12 | encapsulation: ViewEncapsulation.None, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | standalone: true, 15 | imports: [GuiFieldLabel], 16 | }) 17 | export class GuiInlineGroup { 18 | @Input() config: Partial = {}; 19 | } 20 | -------------------------------------------------------------------------------- /projects/gui/input-number/input-number.html: -------------------------------------------------------------------------------- 1 | 2 | @if (config.prefix) { 3 | {{ config.prefix }} 4 | } 5 | 16 | @if (config.suffix) { 17 | {{ config.suffix }} 18 | } 19 | @if (config.parentType === 'inline') { 20 | 21 | 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /projects/gui/input-number/input-number.scss: -------------------------------------------------------------------------------- 1 | .gui-input-number { 2 | } 3 | -------------------------------------------------------------------------------- /projects/gui/input-number/input-number.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | forwardRef, 6 | Input, 7 | ViewEncapsulation, 8 | } from '@angular/core'; 9 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 10 | import { MatFormField, MatHint, MatPrefix, MatSuffix } from '@angular/material/form-field'; 11 | import { MatInput } from '@angular/material/input'; 12 | import { GuiFieldLabel } from '../field-label/field-label'; 13 | import { GuiControl } from '../interface'; 14 | 15 | @Component({ 16 | selector: 'gui-input-number', 17 | templateUrl: './input-number.html', 18 | styleUrl: './input-number.scss', 19 | host: { 20 | class: 'gui-field gui-input-number', 21 | }, 22 | encapsulation: ViewEncapsulation.None, 23 | changeDetection: ChangeDetectionStrategy.OnPush, 24 | providers: [ 25 | { 26 | provide: NG_VALUE_ACCESSOR, 27 | useExisting: forwardRef(() => GuiInputNumber), 28 | multi: true, 29 | }, 30 | ], 31 | standalone: true, 32 | imports: [FormsModule, MatFormField, MatPrefix, MatInput, MatSuffix, MatHint, GuiFieldLabel], 33 | }) 34 | export class GuiInputNumber implements ControlValueAccessor { 35 | @Input() config: Partial = {}; 36 | @Input() disabled = false; 37 | 38 | value!: number; 39 | 40 | private onChange: (value: number) => void = () => {}; 41 | private onTouched: () => void = () => {}; 42 | 43 | constructor(private cdr: ChangeDetectorRef) {} 44 | 45 | writeValue(value: number) { 46 | this.value = value; 47 | this.cdr.markForCheck(); 48 | } 49 | 50 | registerOnChange(fn: (value: number) => void) { 51 | this.onChange = fn; 52 | } 53 | 54 | registerOnTouched(fn: () => void) { 55 | this.onTouched = fn; 56 | } 57 | 58 | setDisabledState(isDisabled: boolean) { 59 | this.disabled = isDisabled; 60 | this.cdr.markForCheck(); 61 | } 62 | 63 | onValueChange() { 64 | this.onChange(this.value); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /projects/gui/input-text/input-text.html: -------------------------------------------------------------------------------- 1 | 2 | @if (config.prefix) { 3 | {{ config.prefix }} 4 | } 5 | 13 | @if (config.suffix) { 14 | {{ config.suffix }} 15 | } 16 | @if (config.parentType === 'inline') { 17 | 18 | 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /projects/gui/input-text/input-text.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acrodata/gui/cb79cb68ecc0af7663e677743690feffc7effd8a/projects/gui/input-text/input-text.scss -------------------------------------------------------------------------------- /projects/gui/input-text/input-text.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | forwardRef, 6 | Input, 7 | ViewEncapsulation, 8 | } from '@angular/core'; 9 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 10 | import { MatFormField, MatHint, MatPrefix, MatSuffix } from '@angular/material/form-field'; 11 | import { MatInput } from '@angular/material/input'; 12 | import { GuiFieldLabel } from '../field-label/field-label'; 13 | import { GuiControl } from '../interface'; 14 | 15 | @Component({ 16 | selector: 'gui-input-text', 17 | templateUrl: './input-text.html', 18 | styleUrl: './input-text.scss', 19 | host: { 20 | class: 'gui-field gui-input-text', 21 | }, 22 | encapsulation: ViewEncapsulation.None, 23 | changeDetection: ChangeDetectionStrategy.OnPush, 24 | providers: [ 25 | { 26 | provide: NG_VALUE_ACCESSOR, 27 | useExisting: forwardRef(() => GuiInputText), 28 | multi: true, 29 | }, 30 | ], 31 | standalone: true, 32 | imports: [FormsModule, MatFormField, MatPrefix, MatInput, MatSuffix, MatHint, GuiFieldLabel], 33 | }) 34 | export class GuiInputText implements ControlValueAccessor { 35 | @Input() config: Partial = {}; 36 | @Input() disabled = false; 37 | 38 | value = ''; 39 | 40 | private onChange: (value: string) => void = () => {}; 41 | private onTouched: () => void = () => {}; 42 | 43 | constructor(private cdr: ChangeDetectorRef) {} 44 | 45 | writeValue(value: string) { 46 | this.value = value; 47 | this.cdr.markForCheck(); 48 | } 49 | 50 | registerOnChange(fn: (value: string) => void) { 51 | this.onChange = fn; 52 | } 53 | 54 | registerOnTouched(fn: () => void) { 55 | this.onTouched = fn; 56 | } 57 | 58 | setDisabledState(isDisabled: boolean) { 59 | this.disabled = isDisabled; 60 | this.cdr.markForCheck(); 61 | } 62 | 63 | onValueChange() { 64 | this.onChange(this.value); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /projects/gui/interface.ts: -------------------------------------------------------------------------------- 1 | export type GuiFieldType = 2 | | 'group' 3 | | 'inline' 4 | | 'tabs' 5 | | 'tab' 6 | | 'menu' 7 | | 'menuItem' 8 | | 'text' 9 | | 'number' 10 | | 'switch' 11 | | 'slider' 12 | | 'fill' 13 | | 'select' 14 | | 'buttonToggle' 15 | | 'imageSelect' 16 | | 'combobox' 17 | | 'textarea' 18 | | 'codearea' 19 | | 'hidden' 20 | | 'image' 21 | | 'video' 22 | | 'audio' 23 | | 'file'; 24 | 25 | export type GuiFillMode = 'flat' | 'gradient' | 'image'; 26 | 27 | export type GuiSliderMode = 'normal' | 'range'; 28 | 29 | export type GuiTabsMode = 'normal' | 'list'; 30 | 31 | export type GuiBasicValue = boolean | number | string; 32 | 33 | export type GuiDefaultValue = boolean | number | string | any[] | Record | null; 34 | 35 | export type GuiOperator = '$eq' | '$ne' | '$gt' | '$lt' | '$gte' | '$lte' | '$in' | '$nin'; 36 | 37 | export type GuiCondition = [string, GuiOperator, GuiDefaultValue]; 38 | 39 | export interface GuiFieldShowIf { 40 | conditions: GuiCondition[]; 41 | logicalType?: '$and' | '$or'; 42 | } 43 | 44 | export interface GuiField { 45 | type: GuiFieldType; 46 | name: string; 47 | default?: GuiDefaultValue; 48 | description?: string; 49 | placeholder?: string; 50 | disabled?: boolean; 51 | col?: number; 52 | showIf?: GuiFieldShowIf; 53 | show?: boolean; 54 | // group & menu & tabs & inline 55 | children?: GuiFields | GuiField[]; 56 | // group & tabs 57 | expanded?: boolean; 58 | // number & slider 59 | min?: number; 60 | max?: number; 61 | step?: number; 62 | // text & number & slider & select 63 | prefix?: string; 64 | suffix?: string; 65 | // select & buttonToggle & imageSelect & combobox 66 | options?: GuiFieldOption[]; 67 | // select & combobox 68 | useFont?: boolean; 69 | // select & buttonToggle & combobox 70 | multiple?: boolean; 71 | // buttonToggle 72 | useIcon?: boolean; 73 | // tabs 74 | template?: Partial; 75 | addable?: boolean; 76 | // fill & slider & tabs 77 | mode?: GuiFillMode | GuiSliderMode | GuiTabsMode; 78 | // textarea 79 | rows?: number; 80 | // image & video & audio & file 81 | accept?: string; 82 | // codearea 83 | height?: string | number; 84 | language?: string; 85 | } 86 | 87 | export type GuiFields = Record; 88 | 89 | export interface GuiFieldOption { 90 | label: string; 91 | value: string | number | boolean; 92 | disabled?: boolean; 93 | src?: string; 94 | col?: number; 95 | } 96 | 97 | export interface GuiControl extends GuiField { 98 | _type: 'control' | 'group' | 'array'; 99 | key: string; 100 | parentType: GuiFieldType; 101 | model?: any; 102 | default?: any; 103 | index?: number; 104 | children?: GuiControl[]; 105 | template?: Partial; 106 | selectedIndex?: number; 107 | } 108 | 109 | export type GuiControls = Record; 110 | -------------------------------------------------------------------------------- /projects/gui/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/gui", 4 | "assets": [ 5 | "**/*.scss" 6 | ], 7 | "lib": { 8 | "entryFile": "public-api.ts" 9 | }, 10 | "allowedNonPeerDependencies": [ 11 | "@acrodata/code-editor", 12 | "@acrodata/rnd-dialog" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/gui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@acrodata/gui", 3 | "version": "2.5.1", 4 | "description": "JSON powered GUI for configurable panels.", 5 | "publishConfig": { 6 | "access": "public" 7 | }, 8 | "author": "nzbin", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/acrodata/gui.git" 13 | }, 14 | "homepage": "https://acrodata.github.io/gui/", 15 | "keywords": [ 16 | "gui", 17 | "angular", 18 | "typescript", 19 | "creative-coding" 20 | ], 21 | "exports": { 22 | ".": { 23 | "sass": "./_index.scss" 24 | } 25 | }, 26 | "peerDependencies": { 27 | "@angular/cdk": ">=18.0.0", 28 | "@angular/common": ">=18.0.0", 29 | "@angular/core": ">=18.0.0", 30 | "@angular/forms": ">=18.0.0", 31 | "@angular/material": ">=18.0.0", 32 | "@ng-matero/extensions": ">=18.0.0" 33 | }, 34 | "dependencies": { 35 | "@acrodata/code-editor": "^0.5.0", 36 | "@acrodata/rnd-dialog": "^0.3.0", 37 | "tslib": "^2.3.0" 38 | }, 39 | "sideEffects": false 40 | } 41 | -------------------------------------------------------------------------------- /projects/gui/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of gui 3 | */ 4 | 5 | export * from './gui-module'; 6 | export * from './gui-form'; 7 | export * from './button-toggle/button-toggle'; 8 | export * from './button-toggle/icon'; 9 | export * from './codearea/codearea'; 10 | export * from './codearea/codearea-dialog'; 11 | export * from './codearea/codearea-config'; 12 | export * from './combobox/combobox'; 13 | export * from './field-group/field-group'; 14 | export * from './field-label/field-label'; 15 | export * from './file-uploader/file-uploader'; 16 | export * from './file-uploader/file-uploader-config'; 17 | export * from './fill/fill'; 18 | export * from './icon-button-wrapper/icon-button-wrapper'; 19 | export * from './image-select/image-select'; 20 | export * from './inline-group/inline-group'; 21 | export * from './input-number/input-number'; 22 | export * from './input-text/input-text'; 23 | export * from './select/select'; 24 | export * from './slider/slider'; 25 | export * from './switch/switch'; 26 | export * from './textarea/textarea'; 27 | export * from './gui-utils'; 28 | export * from './gui-icons'; 29 | export * from './interface'; 30 | -------------------------------------------------------------------------------- /projects/gui/select/select.html: -------------------------------------------------------------------------------- 1 | 2 | @if (config.prefix) { 3 | {{ config.prefix }} 4 | } 5 | 15 | @for (opt of config.options; track opt) { 16 | 17 | {{ opt.label }} 18 | 19 | } 20 | 21 | @if (config.suffix) { 22 | {{ config.suffix }} 23 | } 24 | @if (config.parentType === 'inline') { 25 | 26 | 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /projects/gui/select/select.scss: -------------------------------------------------------------------------------- 1 | .gui-select { 2 | --mat-select-trigger-text-line-height: 1rem; 3 | --mat-select-trigger-text-size: 0.75rem; 4 | --mat-option-label-text-size: 0.75rem; 5 | 6 | .mat-mdc-option { 7 | min-height: var(--mat-option-label-text-line-height); 8 | padding: 0 0.5rem; 9 | 10 | .mat-pseudo-checkbox-full { 11 | margin-right: 0.5rem; 12 | 13 | [dir='rtl'] & { 14 | margin-right: 0; 15 | margin-left: 0.5rem; 16 | } 17 | } 18 | } 19 | 20 | .mat-mdc-select-arrow-wrapper { 21 | height: 1rem; 22 | } 23 | 24 | .mat-pseudo-checkbox { 25 | width: 1rem; 26 | height: 1rem; 27 | border-width: 1px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/gui/select/select.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | forwardRef, 6 | Input, 7 | ViewEncapsulation, 8 | } from '@angular/core'; 9 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 10 | import { MatOption } from '@angular/material/core'; 11 | import { MatFormField, MatHint, MatPrefix, MatSuffix } from '@angular/material/form-field'; 12 | import { MatSelect } from '@angular/material/select'; 13 | import { GuiFieldLabel } from '../field-label/field-label'; 14 | import { GuiBasicValue, GuiControl } from '../interface'; 15 | 16 | @Component({ 17 | selector: 'gui-select', 18 | templateUrl: './select.html', 19 | styleUrl: './select.scss', 20 | host: { 21 | class: 'gui-field gui-select', 22 | }, 23 | encapsulation: ViewEncapsulation.None, 24 | changeDetection: ChangeDetectionStrategy.OnPush, 25 | providers: [ 26 | { 27 | provide: NG_VALUE_ACCESSOR, 28 | useExisting: forwardRef(() => GuiSelect), 29 | multi: true, 30 | }, 31 | ], 32 | standalone: true, 33 | imports: [ 34 | FormsModule, 35 | MatFormField, 36 | MatPrefix, 37 | MatSelect, 38 | MatOption, 39 | MatSuffix, 40 | MatHint, 41 | GuiFieldLabel, 42 | ], 43 | }) 44 | export class GuiSelect implements ControlValueAccessor { 45 | @Input() config: Partial = {}; 46 | @Input() disabled = false; 47 | 48 | value: GuiBasicValue | GuiBasicValue[] = ''; 49 | 50 | private onChange: (value: GuiBasicValue | GuiBasicValue[]) => void = () => {}; 51 | private onTouched: () => void = () => {}; 52 | 53 | constructor(private cdr: ChangeDetectorRef) {} 54 | 55 | writeValue(value: GuiBasicValue | GuiBasicValue[]) { 56 | this.value = value; 57 | this.cdr.markForCheck(); 58 | } 59 | 60 | registerOnChange(fn: (value: GuiBasicValue | GuiBasicValue[]) => void) { 61 | this.onChange = fn; 62 | } 63 | 64 | registerOnTouched(fn: () => void) { 65 | this.onTouched = fn; 66 | } 67 | 68 | setDisabledState(isDisabled: boolean) { 69 | this.disabled = isDisabled; 70 | this.cdr.markForCheck(); 71 | } 72 | 73 | onValueChange() { 74 | this.onChange(this.value); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /projects/gui/slider/slider.html: -------------------------------------------------------------------------------- 1 | @if (config.mode !== 'range') { 2 | 3 | 4 | 5 | 6 | 7 | @if (config.prefix) { 8 | {{ config.prefix }} 9 | } 10 | 21 | @if (config.suffix) { 22 | {{ config.suffix }} 23 | } 24 | 25 | } @else { 26 | 27 | 28 | 29 | 30 | 31 | 32 | @if (config.prefix) { 33 | {{ config.prefix }} 34 | } 35 | 46 | @if (config.suffix) { 47 | {{ config.suffix }} 48 | } 49 | 50 | 51 | @if (config.prefix) { 52 | {{ config.prefix }} 53 | } 54 | 65 | @if (config.suffix) { 66 | {{ config.suffix }} 67 | } 68 | 69 | } 70 | 71 | @if (config.parentType === 'inline') { 72 | 73 | 74 | 75 | } 76 | -------------------------------------------------------------------------------- /projects/gui/slider/slider.scss: -------------------------------------------------------------------------------- 1 | .gui-slider { 2 | .mat-mdc-slider { 3 | --mdc-slider-handle-width: 0.75rem; 4 | --mdc-slider-handle-height: 0.75rem; 5 | --mdc-slider-inactive-track-height: 0.125rem; 6 | --mdc-slider-active-track-height: 0.125rem; 7 | 8 | flex: 3; 9 | min-width: auto; 10 | height: calc(var(--mdc-slider-handle-width) * 2); 11 | margin: 0 calc(var(--mdc-slider-handle-width) / 2); 12 | 13 | +.mat-mdc-form-field { 14 | flex: 2; 15 | } 16 | 17 | &.mdc-slider--range { 18 | +.mat-mdc-form-field, 19 | +.mat-mdc-form-field+.mat-mdc-form-field { 20 | flex: 1.5; 21 | } 22 | } 23 | 24 | .mdc-slider__input { 25 | top: 0; 26 | height: calc(var(--mdc-slider-handle-width) * 2); 27 | } 28 | 29 | .mdc-slider__thumb { 30 | left: calc(var(--mdc-slider-handle-width) * -1); 31 | width: calc(var(--mdc-slider-handle-width) * 2); 32 | height: calc(var(--mdc-slider-handle-width) * 2); 33 | border-radius: 50%; 34 | overflow: hidden; 35 | } 36 | } 37 | 38 | .mat-mdc-form-field { 39 | margin-left: 0.375rem; 40 | 41 | [dir='rtl'] & { 42 | margin-left: 0; 43 | margin-right: 0.375rem; 44 | } 45 | 46 | input::-webkit-outer-spin-button, 47 | input::-webkit-inner-spin-button { 48 | appearance: none; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /projects/gui/slider/slider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | forwardRef, 6 | Input, 7 | ViewEncapsulation, 8 | } from '@angular/core'; 9 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 10 | import { MatFormField, MatHint, MatPrefix, MatSuffix } from '@angular/material/form-field'; 11 | import { MatInput } from '@angular/material/input'; 12 | import { MatSlider, MatSliderRangeThumb, MatSliderThumb } from '@angular/material/slider'; 13 | import { GuiFieldLabel } from '../field-label/field-label'; 14 | import { GuiControl } from '../interface'; 15 | 16 | @Component({ 17 | selector: 'gui-slider', 18 | templateUrl: './slider.html', 19 | styleUrl: './slider.scss', 20 | host: { 21 | class: 'gui-field gui-slider', 22 | }, 23 | encapsulation: ViewEncapsulation.None, 24 | changeDetection: ChangeDetectionStrategy.OnPush, 25 | providers: [ 26 | { 27 | provide: NG_VALUE_ACCESSOR, 28 | useExisting: forwardRef(() => GuiSlider), 29 | multi: true, 30 | }, 31 | ], 32 | standalone: true, 33 | imports: [ 34 | FormsModule, 35 | MatSlider, 36 | MatSliderThumb, 37 | MatFormField, 38 | MatPrefix, 39 | MatInput, 40 | MatSuffix, 41 | MatSliderRangeThumb, 42 | MatHint, 43 | GuiFieldLabel, 44 | ], 45 | }) 46 | export class GuiSlider implements ControlValueAccessor { 47 | @Input() config: Partial = {}; 48 | @Input() disabled = false; 49 | 50 | value!: number | number[]; 51 | 52 | // The input binding value for range slider 53 | minValue!: number; 54 | maxValue!: number; 55 | 56 | private onChange: (value: number | number[]) => void = () => {}; 57 | private onTouched: () => void = () => {}; 58 | 59 | constructor(private cdr: ChangeDetectorRef) {} 60 | 61 | writeValue(value: number) { 62 | this.value = value; 63 | this.setInputValue(); 64 | this.cdr.markForCheck(); 65 | } 66 | 67 | registerOnChange(fn: (value: number | number[]) => void) { 68 | this.onChange = fn; 69 | } 70 | 71 | registerOnTouched(fn: () => void) { 72 | this.onTouched = fn; 73 | } 74 | 75 | setDisabledState(isDisabled: boolean) { 76 | this.disabled = isDisabled; 77 | this.cdr.markForCheck(); 78 | } 79 | 80 | onValueChange() { 81 | this.onChange(this.value); 82 | } 83 | 84 | onRangeSliderChange() { 85 | this.setInputValue(); 86 | this.onValueChange(); 87 | } 88 | 89 | onRangeInputChange() { 90 | this.value = [this.minValue, this.maxValue]; 91 | this.onValueChange(); 92 | } 93 | 94 | setInputValue() { 95 | if (Array.isArray(this.value)) { 96 | this.minValue = this.value[0]; 97 | this.maxValue = this.value[1]; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /projects/gui/switch/switch.html: -------------------------------------------------------------------------------- 1 | 2 | @if (config.parentType === 'inline') { 3 | 4 | 5 | 6 | } 7 | -------------------------------------------------------------------------------- /projects/gui/switch/switch.scss: -------------------------------------------------------------------------------- 1 | .gui-switch { 2 | .mat-mdc-slide-toggle { 3 | --mat-switch-unselected-handle-size: 0.5rem; 4 | --mat-switch-selected-handle-size: 0.75rem; 5 | --mat-switch-pressed-handle-size: 0.75rem; 6 | --mat-switch-selected-handle-horizontal-margin: 0; 7 | --mat-switch-selected-pressed-handle-horizontal-margin: 0; 8 | --mat-switch-unselected-handle-horizontal-margin: 0 0.25rem; 9 | --mat-switch-unselected-pressed-handle-horizontal-margin: 0 0.125rem; 10 | --mat-switch-track-outline-width: 1px; 11 | --mdc-switch-track-width: 1.75rem; 12 | --mdc-switch-track-height: 1rem; 13 | --mdc-switch-track-shape: 0.5rem; 14 | --mdc-switch-handle-width: 0.875rem; 15 | --mdc-switch-state-layer-size: 1.5rem; 16 | 17 | line-height: var(--mdc-switch-state-layer-size); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /projects/gui/switch/switch.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | forwardRef, 6 | Input, 7 | ViewEncapsulation, 8 | } from '@angular/core'; 9 | import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 10 | import { MatHint } from '@angular/material/form-field'; 11 | import { MatSlideToggle } from '@angular/material/slide-toggle'; 12 | import { GuiFieldLabel } from '../field-label/field-label'; 13 | import { GuiControl } from '../interface'; 14 | 15 | @Component({ 16 | selector: 'gui-switch', 17 | templateUrl: './switch.html', 18 | styleUrl: './switch.scss', 19 | host: { 20 | class: 'gui-field gui-switch', 21 | }, 22 | encapsulation: ViewEncapsulation.None, 23 | changeDetection: ChangeDetectionStrategy.OnPush, 24 | providers: [ 25 | { 26 | provide: NG_VALUE_ACCESSOR, 27 | useExisting: forwardRef(() => GuiSwitch), 28 | multi: true, 29 | }, 30 | ], 31 | standalone: true, 32 | imports: [FormsModule, MatSlideToggle, MatHint, GuiFieldLabel], 33 | }) 34 | export class GuiSwitch implements ControlValueAccessor { 35 | @Input() config: Partial = {}; 36 | @Input() disabled = false; 37 | 38 | value = false; 39 | 40 | private onChange: (value: boolean) => void = () => {}; 41 | private onTouched: () => void = () => {}; 42 | 43 | constructor(private cdr: ChangeDetectorRef) {} 44 | 45 | writeValue(value: boolean) { 46 | this.value = value; 47 | this.cdr.markForCheck(); 48 | } 49 | 50 | registerOnChange(fn: (value: boolean) => void) { 51 | this.onChange = fn; 52 | } 53 | 54 | registerOnTouched(fn: () => void) { 55 | this.onTouched = fn; 56 | } 57 | 58 | setDisabledState(isDisabled: boolean) { 59 | this.disabled = isDisabled; 60 | this.cdr.markForCheck(); 61 | } 62 | 63 | onValueChange() { 64 | this.onChange(this.value); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /projects/gui/textarea/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | @if (config.prefix) { 3 | {{ config.prefix }} 4 | } 5 | 13 | @if (config.suffix) { 14 | {{ config.suffix }} 15 | } 16 | @if (config.parentType === 'inline') { 17 | 18 | 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /projects/gui/textarea/textarea.scss: -------------------------------------------------------------------------------- 1 | .gui-textarea { 2 | } 3 | -------------------------------------------------------------------------------- /projects/gui/textarea/textarea.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | forwardRef, 6 | Input, 7 | ViewEncapsulation, 8 | } from '@angular/core'; 9 | import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; 10 | import { MatFormField, MatHint, MatPrefix, MatSuffix } from '@angular/material/form-field'; 11 | import { MatInput } from '@angular/material/input'; 12 | import { GuiFieldLabel } from '../field-label/field-label'; 13 | import { GuiControl } from '../interface'; 14 | 15 | @Component({ 16 | selector: 'gui-textarea', 17 | templateUrl: './textarea.html', 18 | styleUrl: './textarea.scss', 19 | host: { 20 | class: 'gui-field gui-textarea', 21 | }, 22 | encapsulation: ViewEncapsulation.None, 23 | changeDetection: ChangeDetectionStrategy.OnPush, 24 | providers: [ 25 | { 26 | provide: NG_VALUE_ACCESSOR, 27 | useExisting: forwardRef(() => GuiTextarea), 28 | multi: true, 29 | }, 30 | ], 31 | standalone: true, 32 | imports: [FormsModule, MatFormField, MatPrefix, MatInput, MatSuffix, MatHint, GuiFieldLabel], 33 | }) 34 | export class GuiTextarea { 35 | @Input() config: Partial = {}; 36 | @Input() disabled = false; 37 | 38 | value = ''; 39 | 40 | private onChange: (value: string) => void = () => {}; 41 | private onTouched: () => void = () => {}; 42 | 43 | constructor(private cdr: ChangeDetectorRef) {} 44 | 45 | writeValue(value: string) { 46 | this.value = value; 47 | this.cdr.markForCheck(); 48 | } 49 | 50 | registerOnChange(fn: (value: string) => void) { 51 | this.onChange = fn; 52 | } 53 | 54 | registerOnTouched(fn: () => void) { 55 | this.onTouched = fn; 56 | } 57 | 58 | setDisabledState(isDisabled: boolean) { 59 | this.disabled = isDisabled; 60 | this.cdr.markForCheck(); 61 | } 62 | 63 | onValueChange() { 64 | this.onChange(this.value); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /projects/gui/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/gui/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/gui/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.spec.ts", 12 | "**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ], 26 | "paths": { 27 | "@acrodata/gui": [ 28 | "projects/gui/public-api" 29 | ], 30 | } 31 | }, 32 | "angularCompilerOptions": { 33 | "enableI18nLegacyMessageIdFormat": false, 34 | "strictInjectionParameters": true, 35 | "strictInputAccessModifiers": true, 36 | "strictTemplates": true 37 | } 38 | } 39 | --------------------------------------------------------------------------------