├── .github └── workflows │ └── linting.yaml ├── .gitignore ├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── Controller └── CustomCssController.php ├── CustomCSSBundle.php ├── DependencyInjection └── CustomCSSExtension.php ├── Entity └── CustomCss.php ├── EventSubscriber ├── MenuSubscriber.php ├── PermissionSectionSubscriber.php └── ThemeEventSubscriber.php ├── Form └── CustomCssType.php ├── LICENSE ├── README.md ├── Repository └── CustomCssRepository.php ├── Resources ├── config │ ├── routes.yaml │ └── services.yaml ├── ruleset │ ├── general │ │ ├── hide-menu-calendar.css │ │ ├── hide-nav-icons.css │ │ └── remove-color-dots.css │ ├── login │ │ ├── no-title.css │ │ └── plain-black-bg.css │ └── timesheet │ │ ├── hide-overlapping-records.css │ │ └── highlight-active-records.css ├── translations │ ├── messages.de.xlf │ ├── messages.en.xlf │ ├── messages.hr.xlf │ ├── validators.de.xlf │ ├── validators.en.xlf │ └── validators.hr.xlf └── views │ └── index.html.twig ├── Validator └── Constraints │ ├── CustomCss.php │ └── CustomCssValidator.php ├── composer.json └── phpstan.neon /.github/workflows/linting.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: null 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | php: ['8.1', '8.2', '8.3'] 13 | 14 | name: Linting - PHP ${{ matrix.php }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{ matrix.php }} 20 | coverage: none 21 | extensions: intl 22 | - run: composer install --no-progress 23 | - run: composer validate --strict --no-check-version 24 | - run: composer codestyle-check 25 | - run: composer phpstan 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .disabled 3 | .php-cs-fixer.cache 4 | /vendor/ 5 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) 14 | ->setRiskyAllowed(true) 15 | ->setRules([ 16 | 'encoding' => true, 17 | 'full_opening_tag' => true, 18 | 'blank_line_after_namespace' => true, 19 | 'control_structure_braces' => true, 20 | 'control_structure_continuation_position' => ['position' => 'same_line'], 21 | 'declare_parentheses' => true, 22 | 'no_multiple_statements_per_line' => true, 23 | 'statement_indentation' => true, 24 | 'class_definition' => true, 25 | 'elseif' => true, 26 | 'function_declaration' => true, 27 | 'indentation_type' => true, 28 | 'line_ending' => true, 29 | 'constant_case' => ['case' => 'lower'], 30 | 'lowercase_keywords' => true, 31 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 32 | 'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'], 33 | 'no_php4_constructor' => true, 34 | 'ordered_imports' => true, 35 | 'no_break_comment' => true, 36 | 'no_closing_tag' => true, 37 | 'no_spaces_after_function_name' => true, 38 | 'spaces_inside_parentheses' => ['space' => 'none'], 39 | 'no_trailing_whitespace' => true, 40 | 'no_trailing_whitespace_in_comment' => true, 41 | 'single_blank_line_at_eof' => true, 42 | 'single_class_element_per_statement' => ['elements' => ['property']], 43 | 'single_import_per_statement' => true, 44 | 'single_line_after_imports' => true, 45 | 'switch_case_semicolon_to_colon' => true, 46 | 'switch_case_space' => true, 47 | 'array_syntax' => [ 48 | 'syntax' => 'short' 49 | ], 50 | 'binary_operator_spaces' => true, 51 | 'blank_line_after_opening_tag' => true, 52 | 'blank_line_before_statement' => [ 53 | 'statements' => ['return'], 54 | ], 55 | 'cast_spaces' => true, 56 | 'class_attributes_separation' => ['elements' => ['method' => 'one']], 57 | 'concat_space' => ['spacing' => 'one'], 58 | 'declare_equal_normalize' => true, 59 | 'type_declaration_spaces' => ['elements' => ['function', 'property']], 60 | 'include' => true, 61 | 'lowercase_cast' => true, 62 | 'lowercase_static_reference' => true, 63 | 'magic_constant_casing' => true, 64 | 'native_function_casing' => true, 65 | 'new_with_parentheses' => true, 66 | 'no_blank_lines_after_class_opening' => true, 67 | 'no_blank_lines_after_phpdoc' => true, 68 | 'no_empty_comment' => true, 69 | 'no_empty_phpdoc' => true, 70 | 'no_empty_statement' => true, 71 | 'no_extra_blank_lines' => ['tokens' => [ 72 | 'curly_brace_block', 73 | 'extra', 74 | 'parenthesis_brace_block', 75 | 'square_brace_block', 76 | 'throw', 77 | 'use', 78 | ]], 79 | 'no_leading_import_slash' => true, 80 | 'no_leading_namespace_whitespace' => true, 81 | 'no_mixed_echo_print' => ['use' => 'echo'], 82 | 'no_multiline_whitespace_around_double_arrow' => true, 83 | 'no_short_bool_cast' => true, 84 | 'no_singleline_whitespace_before_semicolons' => true, 85 | 'no_spaces_around_offset' => true, 86 | 'no_trailing_comma_in_singleline' => true, 87 | 'no_unneeded_braces' => true, 88 | 'no_unneeded_final_method' => true, 89 | 'no_unused_imports' => true, 90 | 'no_whitespace_before_comma_in_array' => true, 91 | 'no_whitespace_in_blank_line' => true, 92 | 'normalize_index_brace' => true, 93 | 'object_operator_without_whitespace' => true, 94 | 'php_unit_fqcn_annotation' => true, 95 | 'phpdoc_align' => [ 96 | 'align' => 'left', 97 | 'tags' => [ 98 | 'method', 99 | 'param', 100 | 'property', 101 | 'return', 102 | 'throws', 103 | 'type', 104 | 'var', 105 | ], 106 | ], 107 | 'phpdoc_annotation_without_dot' => true, 108 | 'phpdoc_indent' => true, 109 | 'phpdoc_inline_tag_normalizer' => true, 110 | 'phpdoc_no_access' => true, 111 | 'phpdoc_no_alias_tag' => true, 112 | 'phpdoc_no_empty_return' => false, 113 | 'phpdoc_no_package' => true, 114 | 'phpdoc_no_useless_inheritdoc' => true, 115 | 'phpdoc_return_self_reference' => true, 116 | 'phpdoc_scalar' => true, 117 | 'phpdoc_separation' => false, 118 | 'phpdoc_single_line_var_spacing' => true, 119 | 'phpdoc_summary' => false, 120 | 'phpdoc_to_comment' => true, 121 | 'phpdoc_trim' => true, 122 | 'phpdoc_types' => true, 123 | 'phpdoc_var_without_name' => true, 124 | 'protected_to_private' => true, 125 | 'return_type_declaration' => true, 126 | 'semicolon_after_instruction' => true, 127 | 'short_scalar_cast' => true, 128 | 'blank_lines_before_namespace' => ['min_line_breaks' => 2, 'max_line_breaks' => 2], 129 | 'single_line_comment_style' => [ 130 | 'comment_types' => ['hash'], 131 | ], 132 | 'single_quote' => true, 133 | 'space_after_semicolon' => [ 134 | 'remove_in_empty_for_expressions' => true, 135 | ], 136 | 'standardize_increment' => true, 137 | 'standardize_not_equals' => true, 138 | 'ternary_operator_spaces' => true, 139 | 'trailing_comma_in_multiline' => false, 140 | 'trim_array_spaces' => true, 141 | 'unary_operator_spaces' => true, 142 | 'whitespace_after_comma_in_array' => true, 143 | 'yoda_style' => false, 144 | 'ternary_to_null_coalescing' => true, 145 | 'visibility_required' => ['elements' => [ 146 | 'const', 147 | 'method', 148 | 'property', 149 | ]], 150 | 'native_function_invocation' => [ 151 | 'include' => [ 152 | '@compiler_optimized' 153 | ], 154 | 'scope' => 'namespaced' 155 | ], 156 | 'native_type_declaration_casing' => true, 157 | 'no_alias_functions' => [ 158 | 'sets' => [ 159 | '@internal' 160 | ] 161 | ], 162 | ]) 163 | ->setFinder( 164 | PhpCsFixer\Finder::create() 165 | ->in([ 166 | __DIR__ 167 | ])->exclude([ 168 | __DIR__ . '/Resources/', 169 | __DIR__ . '/vendor/', 170 | __DIR__ . '/.github/', 171 | ]) 172 | ) 173 | ->setFormat('checkstyle') 174 | ; 175 | 176 | return $fixer; 177 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 2.2.0 2 | 3 | Compatibility: requires minimum Kimai 2.17.0 4 | 5 | - Code modernization 6 | 7 | ## Version 2.1.0 8 | 9 | Compatibility: requires minimum Kimai 2.1.0 10 | 11 | - Fixed: route mapping type changed to attribute 12 | 13 | ## Version 2.0.2 14 | 15 | Compatibility: requires minimum Kimai 2.0 16 | 17 | - Fix `remove-color-dots` rule 18 | 19 | ## Version 2.0.1 20 | 21 | Compatibility: requires minimum Kimai 2.0 22 | 23 | - Updates existing CSS rules to be compatible with 2.0. 24 | - Remove (plugin) from permission/role header 25 | 26 | Hiding navigation items will only work with Kimai 2.0.24 27 | 28 | ## Version 2.0 29 | 30 | Compatibility: requires minimum Kimai 2.0 31 | 32 | - Fixed: compatibility with Kimai 2.0 33 | 34 | Attention: all existing rules from v1 are stored in var/data/custom-css-bundle.css. 35 | They will likely not work in v2 due to the new HTML structures and classes. 36 | Therefor you have to manually import them. 37 | The new storage file is: var/data/custom-css.css 38 | 39 | ## 1.7 40 | 41 | Compatibility: requires minimum Kimai 1.9 42 | 43 | - Prevent that HTML tags can be injected 44 | - Fix code styles 45 | 46 | ## 1.6 47 | 48 | Compatibility: requires minimum Kimai 1.9 49 | 50 | - updated install documentation 51 | - translated warning message for a rule inserted twice 52 | - added "page action" with link to documentation 53 | 54 | ## 1.5 55 | 56 | Compatibility: requires minimum Kimai 1.9 57 | 58 | - Use FileHelper to store and load custom CSS rules 59 | 60 | ## 1.4 61 | 62 | - Added permission `select_custom_css` to hide pre-made rules 63 | - Changed translation filenames from `*.xliff` to `*.xlf` 64 | - Minimal form change 65 | - Added rule for responsive tables everywhere 66 | - Added rule to hide calendar menu in navigation 67 | 68 | ## 1.3 69 | 70 | - Added rule to hide the red-lines between overlapping timesheet records 71 | - Added rule to highlight active timesheet records 72 | - Fixed "remove colored dot" rule for Kimai 1.9 HTML changes 73 | 74 | Best compatibility with Kimai 1.9 75 | 76 | ## 1.2 77 | 78 | - Added rule for switching save & close/reset buttons order 79 | 80 | Best compatibility with Kimai 1.9 81 | 82 | ## 1.1 83 | 84 | - Improved permission handling 85 | - New rule to disable colored dots 86 | 87 | Compatible with Kimai 1.4, but 1.6 is recommended 88 | 89 | ## 1.0 90 | 91 | - Added two login rules 92 | - Replace deprecated constant 93 | 94 | Compatible with Kimai 1.4 95 | 96 | ## 0.4 97 | 98 | - Added new rules: 99 | - `timesheet` / `responsive-timesheet` 100 | - `timesheet` / `wrap-label-timesheet` 101 | - Styling update in admin screen 102 | 103 | Compatible with Kimai 0.9 104 | -------------------------------------------------------------------------------- /Controller/CustomCssController.php: -------------------------------------------------------------------------------- 1 | getCustomCss(); 31 | 32 | $form = $this->createForm(CustomCssType::class, $entity, [ 33 | 'action' => $this->generateUrl('custom_css'), 34 | ]); 35 | 36 | $form->handleRequest($request); 37 | 38 | if ($form->isSubmitted() && $form->isValid()) { 39 | /** @var CustomCss $entity */ 40 | $entity = $form->getData(); 41 | try { 42 | $repository->saveCustomCss($entity); 43 | $this->flashSuccess('action.update.success'); 44 | } catch (\Exception $ex) { 45 | $this->flashUpdateException($ex); 46 | } 47 | } 48 | 49 | $rulesets = []; 50 | if ($this->isGranted('select_custom_css')) { 51 | $rulesets = $repository->getPredefinedStyles(); 52 | } 53 | 54 | $page = new PageSetup('Custom CSS'); 55 | $page->setHelp('plugin-custom-css.html'); 56 | 57 | return $this->render('@CustomCSS/index.html.twig', [ 58 | 'page_setup' => $page, 59 | 'entity' => $entity, 60 | 'form' => $form->createView(), 61 | 'rulesets' => $rulesets, 62 | ]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CustomCSSBundle.php: -------------------------------------------------------------------------------- 1 | load('services.yaml'); 30 | } 31 | 32 | public function prepend(ContainerBuilder $container): void 33 | { 34 | /* 35 | * @CloudRequired adapt if new permissions are added 36 | */ 37 | $container->prependExtensionConfig('kimai', [ 38 | 'permissions' => [ 39 | 'roles' => [ 40 | 'ROLE_SUPER_ADMIN' => [ 41 | 'edit_custom_css', 42 | 'select_custom_css', 43 | ], 44 | ], 45 | ], 46 | ]); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Entity/CustomCss.php: -------------------------------------------------------------------------------- 1 | customCss; 20 | } 21 | 22 | public function setCustomCss(?string $customCss = null): void 23 | { 24 | if (null === $customCss) { 25 | $customCss = ''; 26 | } 27 | 28 | $this->customCss = $customCss; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /EventSubscriber/MenuSubscriber.php: -------------------------------------------------------------------------------- 1 | security = $security; 25 | } 26 | 27 | public static function getSubscribedEvents(): array 28 | { 29 | return [ 30 | ConfigureMainMenuEvent::class => ['onMenuConfigure', 100], 31 | ]; 32 | } 33 | 34 | public function onMenuConfigure(ConfigureMainMenuEvent $event): void 35 | { 36 | $auth = $this->security; 37 | 38 | if (!$auth->isGranted('IS_AUTHENTICATED_REMEMBERED')) { 39 | return; 40 | } 41 | 42 | $menu = $event->getSystemMenu(); 43 | 44 | if ($auth->isGranted('edit_custom_css')) { 45 | $menu->addChild( 46 | new MenuItemModel('custom_css', 'Custom CSS', 'custom_css', [], 'fab fa-css3') 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /EventSubscriber/PermissionSectionSubscriber.php: -------------------------------------------------------------------------------- 1 | ['onEvent', 100], 25 | ]; 26 | } 27 | 28 | public function onEvent(PermissionSectionsEvent $event): void 29 | { 30 | $event->addSection(new PermissionSection(self::SECTION_TITLE, '_custom_css')); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /EventSubscriber/ThemeEventSubscriber.php: -------------------------------------------------------------------------------- 1 | ['renderStylesheet', 100], 27 | ]; 28 | } 29 | 30 | public function renderStylesheet(ThemeEvent $event): void 31 | { 32 | $css = $this->repository->getCustomCss()->getCustomCss(); 33 | if ($css === '') { 34 | return; 35 | } 36 | 37 | // the first two make sure that injected HTML will not be interpreted by the browser, the others are only 38 | // there to format/shrink the output size 39 | $css = str_replace(['' . trim($css) . ''; 41 | 42 | $event->addContent($css); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Form/CustomCssType.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class CustomCssType extends AbstractType 24 | { 25 | public function buildForm(FormBuilderInterface $builder, array $options): void 26 | { 27 | $builder 28 | ->add('customCss', TextareaType::class, [ 29 | 'label' => false, 30 | 'required' => false, 31 | 'attr' => [ 32 | 'rows' => '20', 33 | ], 34 | 'constraints' => [ 35 | new CustomCssConstraint() 36 | ] 37 | ]) 38 | ; 39 | } 40 | 41 | public function configureOptions(OptionsResolver $resolver): void 42 | { 43 | $resolver->setDefaults([ 44 | 'data_class' => CustomCss::class, 45 | 'csrf_protection' => true, 46 | 'csrf_field_name' => '_token', 47 | 'csrf_token_id' => 'edit_custom_css', 48 | 'method' => 'POST', 49 | ]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Kevin Papst - https://www.kevinpapst.de 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CustomCSSBundle 2 | 3 | A Kimai plugin, which allows editing custom CSS rules through an administration screen. 4 | 5 | It ships with some pre-defined CSS rules, which can be added with a button click. 6 | 7 | ## Installation 8 | 9 | This plugin is compatible with the following Kimai releases: 10 | 11 | | Bundle version | Minimum Kimai version | 12 | |----------------|-----------------------| 13 | | 2.1.0 | 2.1.0 | 14 | | 2.0 - 2.0.1 | 2.0.0 | 15 | | 1.5 - 1.7 | 1.9 | 16 | | 1.0 - 1.4 | 1.4 | 17 | 18 | You find the most notable changes between the versions in the file [CHANGELOG.md](CHANGELOG.md). 19 | 20 | Download and extract the [compatible release](https://github.com/Keleo/CustomCSSBundle/releases) in `var/plugins/` (see [plugin docs](https://www.kimai.org/documentation/plugin-management.html)). 21 | 22 | The file structure needs to look like this afterwards: 23 | 24 | ```bash 25 | var/plugins/ 26 | ├── CustomCSSBundle 27 | │ ├── CustomCSSBundle.php 28 | | └ ... more files and directories follow here ... 29 | ``` 30 | 31 | Then rebuild the cache: 32 | ```bash 33 | bin/console kimai:reload --env=prod 34 | ``` 35 | 36 | ## Permissions 37 | 38 | This bundle comes with the following permissions: 39 | 40 | - `edit_custom_css` - show the administration screen to edit custom css rules 41 | - `select_custom_css` - select from the pre-defined rules 42 | 43 | By default, it is assigned to each user with the role `ROLE_SUPER_ADMIN`. 44 | 45 | Read how to assign these permissions to your user roles in the [permission documentation](https://www.kimai.org/documentation/permissions.html). 46 | 47 | ## Storage 48 | 49 | This bundle stores the custom CSS rules in the file `var/data/custom-css-bundle.css`. 50 | Make sure its writable by your webserver and included in your backups. 51 | 52 | ## Screenshot 53 | 54 | Screenshots are available [in the store page](https://www.kimai.org/store/keleo-css-custom-bundle.html). 55 | -------------------------------------------------------------------------------- /Repository/CustomCssRepository.php: -------------------------------------------------------------------------------- 1 | ignoreUnreadableDirs()->ignoreVCS(true)->files()->name('*.css')->in($searchDir)->depth('< 2'); 31 | /* @var SplFileInfo $bundleDir */ 32 | foreach ($finder as $file) { 33 | $name = str_replace('.css', '', $file->getFilename()); 34 | $rules[$file->getRelativePath()][$name] = $file->getContents(); 35 | } 36 | 37 | asort($rules); 38 | 39 | return $rules; 40 | } 41 | 42 | private function getStorageFilename(): string 43 | { 44 | // pre v2 the file was called custom-css-bundle.css - the name was 45 | // changed because the old rules will not work in v2 46 | return $this->fileHelper->getDataDirectory() . '/custom-css.css'; 47 | } 48 | 49 | public function saveCustomCss(CustomCss $entity): void 50 | { 51 | $file = $this->getStorageFilename(); 52 | 53 | if (file_exists($file) && !is_writable($file)) { 54 | throw new \Exception('CSS file is not writable: ' . $file); 55 | } 56 | 57 | if (false === file_put_contents($file, $entity->getCustomCss())) { 58 | throw new \Exception('Failed saving custom css rules to file: ' . $file); 59 | } 60 | } 61 | 62 | public function getCustomCss(): CustomCss 63 | { 64 | $file = $this->getStorageFilename(); 65 | 66 | $entity = new CustomCss(); 67 | 68 | if (file_exists($file) && is_readable($file)) { 69 | $content = file_get_contents($file); 70 | if ($content !== false) { 71 | $entity->setCustomCss($content); 72 | } 73 | } 74 | 75 | return $entity; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Resources/config/routes.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: '@CustomCSSBundle/Controller/' 3 | type: attribute 4 | prefix: /{_locale} 5 | requirements: 6 | _locale: '%app_locales%' 7 | defaults: 8 | _locale: '%locale%' 9 | -------------------------------------------------------------------------------- /Resources/config/services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: false 6 | 7 | KimaiPlugin\CustomCSSBundle\: 8 | resource: '../../*' 9 | exclude: '../../{Resources}' 10 | 11 | KimaiPlugin\CustomCSSBundle\Controller\: 12 | resource: '../../Controller' 13 | tags: ['controller.service_arguments'] 14 | -------------------------------------------------------------------------------- /Resources/ruleset/general/hide-menu-calendar.css: -------------------------------------------------------------------------------- 1 | .navbar-menu-calendar { display:none !important; } 2 | -------------------------------------------------------------------------------- /Resources/ruleset/general/hide-nav-icons.css: -------------------------------------------------------------------------------- 1 | #navbar-menu li a .nav-link-icon { margin-right: 0; } 2 | #navbar-menu li a i { display:none; } 3 | -------------------------------------------------------------------------------- /Resources/ruleset/general/remove-color-dots.css: -------------------------------------------------------------------------------- 1 | .label-activity span.badge, .label-project span.badge, .label-customer span.badge {display:none !important;} 2 | -------------------------------------------------------------------------------- /Resources/ruleset/login/no-title.css: -------------------------------------------------------------------------------- 1 | body.login-page h1 { display: none; } 2 | -------------------------------------------------------------------------------- /Resources/ruleset/login/plain-black-bg.css: -------------------------------------------------------------------------------- 1 | body.login-page { background: #000000; } 2 | -------------------------------------------------------------------------------- /Resources/ruleset/timesheet/hide-overlapping-records.css: -------------------------------------------------------------------------------- 1 | table.dataTable tr.overlapping { border-top: none; } 2 | -------------------------------------------------------------------------------- /Resources/ruleset/timesheet/highlight-active-records.css: -------------------------------------------------------------------------------- 1 | tr.recording { background-color: #ffa059; } 2 | -------------------------------------------------------------------------------- /Resources/translations/messages.de.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom CSS 7 | Eigenes CSS 8 | 9 | 10 | custom_css.add_predefined_rule 11 | Vorgefertigte Regel einfügen 12 | 13 | 14 | custom_css.add_rule_help 15 | Fügen Sie ihre eigenen CSS Regeln hier ein 16 | 17 | 18 | custom_css.replace 19 | Die Regel "%name%" ist bereits in Ihrem eigenen CSS enthalten. Soll Sie ersetzt werden? Falls nicht, wird die Regel angehangen. 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Resources/translations/messages.en.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom CSS 7 | Custom CSS 8 | 9 | 10 | custom_css.add_predefined_rule 11 | Add pre-defined rule 12 | 13 | 14 | custom_css.add_rule_help 15 | Add all of your custom CSS rules here 16 | 17 | 18 | custom_css.replace 19 | The rule "%name%" is already included in your custom CSS. Replace it? If not, it will be appended. 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Resources/translations/messages.hr.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom CSS 7 | Prilagođeni CSS 8 | 9 | 10 | custom_css.subtitle 11 | Ova CSS pravila će biti uključena u svaku stranicu odgovora 12 | 13 | 14 | custom_css.add_predefined_rule 15 | Dodaj predodređeno pravilo 16 | 17 | 18 | custom_css.add_rule_help 19 | Dodaj sva svoja prilagođena CSS pravila ovdje 20 | 21 | 22 | custom_css.replace 23 | Pravilo „%name%” je već uključeno u tvoj prilagođeni CSS. Želiš li ga zamijeniti? Ako ne, bit će dodano. 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Resources/translations/validators.de.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | HTML_TAGS 7 | HTML Tags dürfen nicht enthalten sein 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Resources/translations/validators.en.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | HTML_TAGS 7 | HTML Tags are not allowed 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Resources/translations/validators.hr.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | HTML_TAGS 7 | HTML oznake nisu dozvoljene 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Resources/views/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | {% import "macros/widgets.html.twig" as widgets %} 3 | 4 | {% block main %} 5 | 6 | {% if rulesets is not empty %} 7 | 37 | 38 | {% embed '@theme/embeds/card.html.twig' with {boxtype: 'default'} %} 39 | {% block box_title %}{{ 'custom_css.add_predefined_rule' | trans }}{% endblock %} 40 | {% block box_body %} 41 |
42 | {% for groupName, rules in rulesets %} 43 | 53 | {% endfor %} 54 |
55 | {% endblock %} 56 | {% endembed %} 57 | {% endif %} 58 | 59 | {% embed 'default/_form.html.twig' with { 'title': 'custom_css.add_rule_help'|trans, 'form': form, 'reset': false } %} 60 | {% block form_body %} 61 | {{ form_rest(form) }} 62 | {% endblock %} 63 | {% endembed %} 64 | 65 | {% endblock %} 66 | -------------------------------------------------------------------------------- /Validator/Constraints/CustomCss.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | protected const ERROR_NAMES = [ 23 | self::TAGS_DISALLOWED => 'HTML_TAGS', 24 | ]; 25 | 26 | public string $message = 'Your custom css settings are invalid.'; 27 | 28 | public function getTargets(): string|array 29 | { 30 | return self::PROPERTY_CONSTRAINT; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Validator/Constraints/CustomCssValidator.php: -------------------------------------------------------------------------------- 1 | ') !== false || stripos($tmp, 'context->buildViolation(CustomCssConstraint::getErrorName(CustomCssConstraint::TAGS_DISALLOWED)) 38 | ->atPath('customCss') 39 | ->setTranslationDomain('validators') 40 | ->setCode(CustomCssConstraint::TAGS_DISALLOWED) 41 | ->addViolation(); 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keleo/custom-css-plugin", 3 | "description": "Edit custom CSS rules through an administration screen.", 4 | "homepage": "https://www.kimai.org/store/keleo-css-custom-bundle.html", 5 | "type": "kimai-plugin", 6 | "version": "2.2.0", 7 | "keywords": [ 8 | "kimai", 9 | "kimai-plugin" 10 | ], 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Kevin Papst", 15 | "homepage": "https://www.kimai.org" 16 | } 17 | ], 18 | "extra": { 19 | "kimai": { 20 | "require": 21700, 21 | "name": "Custom CSS" 22 | } 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "KimaiPlugin\\CustomCSSBundle\\": "" 27 | } 28 | }, 29 | "config": { 30 | "allow-plugins": { 31 | "symfony/flex": false, 32 | "symfony/runtime": false 33 | }, 34 | "platform": { 35 | "php": "8.1.3" 36 | }, 37 | "preferred-install": { 38 | "*": "dist" 39 | }, 40 | "sort-packages": true 41 | }, 42 | "scripts": { 43 | "codestyle": "vendor/bin/php-cs-fixer fix --dry-run --verbose --show-progress=none", 44 | "codestyle-fix": "vendor/bin/php-cs-fixer fix", 45 | "codestyle-check": "vendor/bin/php-cs-fixer fix --dry-run --verbose --using-cache=no --show-progress=none --format=checkstyle", 46 | "phpstan": "vendor/bin/phpstan analyse . --configuration=phpstan.neon", 47 | "linting": [ 48 | "composer validate --strict --no-check-version", 49 | "@codestyle-check", 50 | "@phpstan" 51 | ] 52 | }, 53 | "require-dev": { 54 | "friendsofphp/php-cs-fixer": "^3.0", 55 | "kimai/kimai": "dev-main", 56 | "phpstan/phpstan": "^2.0", 57 | "phpstan/phpstan-deprecation-rules": "^2.0", 58 | "phpstan/phpstan-doctrine": "^2.0", 59 | "phpstan/phpstan-strict-rules": "^2.0", 60 | "phpstan/phpstan-symfony": "^2.0", 61 | "symfony/console": "^6.0", 62 | "symfony/event-dispatcher": "^6.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - %rootDir%/../phpstan-symfony/extension.neon 3 | - %rootDir%/../phpstan-symfony/rules.neon 4 | - %rootDir%/../phpstan-doctrine/extension.neon 5 | - %rootDir%/../phpstan-doctrine/rules.neon 6 | - %rootDir%/../phpstan-deprecation-rules/rules.neon 7 | - %rootDir%/../phpstan-strict-rules/rules.neon 8 | - %rootDir%/../phpstan/conf/bleedingEdge.neon 9 | 10 | parameters: 11 | level: 9 12 | excludePaths: 13 | - vendor/(?) 14 | treatPhpDocTypesAsCertain: false 15 | inferPrivatePropertyTypeFromConstructor: true 16 | doctrine: 17 | allowNullablePropertyForRequiredField: true 18 | ignoreErrors: 19 | - 20 | identifier: missingType.iterableValue 21 | --------------------------------------------------------------------------------