Theming::getCss()   B
last analyzed

Complexity

Conditions 11
Paths 7

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 19
nc 7
nop 1
dl 0
loc 35
rs 7.3166
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
require_once __DIR__ . '/class.colors.php';
4
5
// The themes are moved to a different location when released
6
// so we will define these constants for their location
7
define('THEME_PATH_' . LOAD_SOURCE, 'client/zarafa/core/themes');
8
define('THEME_PATH_' . LOAD_DEBUG, 'client/themes');
9
define('THEME_PATH_' . LOAD_RELEASE, 'client/themes');
10
11
/**
12
 * This class provides some functionality for theming grommunio Web.
13
 */
14
class Theming {
15
	/**
16
	 * A hash that is used to cache if a theme is a json theme.
17
	 *
18
	 * @property
19
	 */
20
	private static $isJsonThemeCache = [];
21
22
	/**
23
	 * A hash that is used to cache the properties of json themes.
24
	 *
25
	 * @property
26
	 */
27
	private static $jsonThemePropsCache = [];
28
29
	/**
30
	 * Retrieves all installed json themes.
31
	 *
32
	 * @return array An array with the directory names of the json themes as keys and their display names
33
	 *               as values
34
	 */
35
	public static function getJsonThemes() {
36
		$themes = [];
37
		$directoryIterator = new DirectoryIterator(BASE_PATH . PATH_PLUGIN_DIR);
38
		foreach ($directoryIterator as $info) {
39
			if ($info->isDot() || !$info->isDir()) {
40
				continue;
41
			}
42
43
			if (!Theming::isJsonTheme($info->getFileName())) {
44
				continue;
45
			}
46
47
			$themeProps = Theming::getJsonThemeProps($info->getFileName());
48
			if (empty($themeProps)) {
49
				continue;
50
			}
51
52
			$themes[$info->getFileName()] = $themeProps['display-name'] ?? $info->getFileName();
53
		}
54
55
		return $themes;
56
	}
57
58
	/**
59
	 * Returns the name of the active theme if one was found, and false otherwise.
60
	 * The active theme can be set by the admin in the config.php, or by
61
	 * the user in his settings.
62
	 *
63
	 * @return bool|string
64
	 */
65
	public static function getActiveTheme() {
66
		$theme = false;
67
		$themePath = BASE_PATH . constant('THEME_PATH_' . DEBUG_LOADER);
68
69
		// First check if a theme was set by this user in his settings
70
		if (WebAppAuthentication::isAuthenticated()) {
71
			if (ENABLE_THEMES === false) {
0 ignored issues
show
introduced by
The condition ENABLE_THEMES === false is always false.
Loading history...
72
				$theme = THEME !== "" ? THEME : 'basic';
73
			}
74
			else {
75
				$theme = $GLOBALS['settings']->get('zarafa/v1/main/active_theme');
76
			}
77
78
			// If a theme was found, check if the theme is still installed
79
			// Remember that 'basic' is not a real theme, but the name for the default look of grommunio Web
80
			// Note 1: We will first try to find the a core theme with this name, only
81
			// when we don't find one, we will try to find a theme plugin.
82
			// Note 2: we do not use the pluginExists method of the PluginManager, because that
83
			// would not find packs with multiple plugins in it. So instead we just check if
84
			// the directory exists.
85
			if (
86
				isset($theme) && !empty($theme) && $theme !== 'basic' &&
87
				!is_dir($themePath . '/' . $theme) &&
88
				!is_dir(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme)
89
			) {
90
				$theme = false;
91
			}
92
		}
93
94
		// If a valid theme was not found in the settings of the user, let's see if a valid theme
95
		// was defined by the admin.
96
		if (!$theme && defined('THEME') && THEME) {
97
			$theme = is_dir($themePath . '/' . THEME) || is_dir(BASE_PATH . PATH_PLUGIN_DIR . '/' . THEME) ? THEME : false;
98
		}
99
100
		if (Theming::isJsonTheme($theme) && !is_array(Theming::getJsonThemeProps($theme))) {
0 ignored issues
show
introduced by
The condition is_array(Theming::getJsonThemeProps($theme)) is always true.
Loading history...
101
			// Someone made an error, we cannot read this json theme
102
			return false;
103
		}
104
105
		return $theme;
106
	}
107
108
	/**
109
	 * Returns the path to the favicon if included with the theme. If found the
110
	 * path to it will be returned. Otherwise false.
111
	 *
112
	 * @param string $theme the name of the theme for which the css will be returned.
113
	 *                      Note: This is the directory name of the theme plugin.
114
	 *
115
	 * 	 * @return bool|string
116
	 */
117
	public static function getFavicon($theme) {
118
		$themePath = constant('THEME_PATH_' . DEBUG_LOADER);
119
120
		// First check if we can find a core theme with this name
121
		if ($theme && is_dir(BASE_PATH . $themePath . '/' . $theme) && is_file(BASE_PATH . $themePath . '/' . $theme . '/favicon.ico')) {
122
			// Add a date as GET parameter, so we will fetch a new icon every day
123
			// This way themes can update the favicon and it will show the next day latest.
124
			return $themePath . '/' . $theme . '/favicon.ico?' . date('Ymd');
125
		}
126
127
		// If no core theme was found, let's try to find a theme plugin with this name
128
		if ($theme && is_dir(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme) && is_file(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme . '/favicon.ico')) {
129
			// Add a date as GET parameter, so we will fetch a new icon every day
130
			// This way themes can update the favicon and it will show the next day latest.
131
			return PATH_PLUGIN_DIR . '/' . $theme . '/favicon.ico?' . date('Ymd');
132
		}
133
134
		return false;
135
	}
136
137
	/**
138
	 * Returns the contents of the css files in the $theme as a string.
139
	 *
140
	 * @param string $theme the name of the theme for which the css will be returned.
141
	 *                      Note: This is the directory name of the theme plugin.
142
	 *
143
	 * @return string
144
	 */
145
	public static function getCss($theme) {
146
		$themePathCoreThemes = BASE_PATH . constant('THEME_PATH_' . DEBUG_LOADER);
147
		$cssFiles = [];
148
149
		// First check if this is a core theme, and if it isn't, check if it is a theme plugin
150
		if ($theme && is_dir($themePathCoreThemes . '/' . $theme)) {
151
			$themePath = $themePathCoreThemes . '/' . $theme;
152
		}
153
		elseif ($theme && is_dir(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme)) {
154
			if (Theming::isJsonTheme($theme)) {
155
				return [];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array() returns the type array which is incompatible with the documented return type string.
Loading history...
156
			}
157
			$themePath = BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme;
158
		}
159
160
		if (isset($themePath)) {
161
			// Use SPL iterators to recursively traverse the css directory and find all css files
162
			$directoryIterator = new RecursiveDirectoryIterator($themePath . '/css/', FilesystemIterator::SKIP_DOTS);
163
			$iterator = new RecursiveIteratorIterator($directoryIterator, RecursiveIteratorIterator::SELF_FIRST);
164
165
			// Always rewind an iterator before using it!!! See https://bugs.php.net/bug.php?id=62914 (it might save you a couple of hours debugging)
166
			$iterator->rewind();
167
			while ($iterator->valid()) {
168
				$fileName = $iterator->getFilename();
0 ignored issues
show
Bug introduced by
The method getFilename() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

168
				/** @scrutinizer ignore-call */ 
169
    $fileName = $iterator->getFilename();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
169
				if (!$iterator->isDir() && (strtolower($iterator->getExtension()) === 'css' || str_ends_with($fileName, '.css.php'))) {
0 ignored issues
show
Bug introduced by
The method isDir() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

169
				if (!$iterator->/** @scrutinizer ignore-call */ isDir() && (strtolower($iterator->getExtension()) === 'css' || str_ends_with($fileName, '.css.php'))) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getExtension() does not exist on RecursiveIteratorIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

169
				if (!$iterator->isDir() && (strtolower($iterator->/** @scrutinizer ignore-call */ getExtension()) === 'css' || str_ends_with($fileName, '.css.php'))) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
170
					$cssFiles[] = substr((string) $iterator->key(), strlen(BASE_PATH));
171
				}
172
				$iterator->next();
173
			}
174
		}
175
176
		// Sort the array alphabetically before adding the css
177
		sort($cssFiles);
178
179
		return $cssFiles;
180
	}
181
182
	/**
183
	 * Returns the value that is assigned to a property by the active theme
184
	 * or null otherwise.
185
	 * Currently only implemented for JSON themes.
186
	 *
187
	 * @param mixed $propName
188
	 *
189
	 * @return string the value that the active theme has set for the property,
190
	 *                or NULL
191
	 */
192
	public static function getThemeProperty($propName) {
193
		$theme = Theming::getActiveTheme();
194
		if (!Theming::isJsonTheme($theme)) {
195
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
196
		}
197
198
		$props = Theming::getJsonThemeProps($theme);
199
		if (!isset($props[$propName])) {
200
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
201
		}
202
203
		return $props[$propName];
204
	}
205
206
	/**
207
	 * Returns the color that the active theme has set for the primary color
208
	 * of the icons. Currently only supported for JSON themes.
209
	 * Note: Only SVG icons of an iconset that has defined the primary color
210
	 * can be 'recolored'.
211
	 *
212
	 * @return string the color that the active theme has set for the primary
213
	 *                color of the icons, or FALSE
214
	 */
215
	public static function getPrimaryIconColor() {
216
		$val = Theming::getThemeProperty('icons-primary-color');
217
218
		return $val ?? false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $val ?? false could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
219
	}
220
221
	/**
222
	 * Returns the color that the active theme has set for the secondary color
223
	 * of the icons. Currently only supported for JSON themes.
224
	 * Note: Only SVG icons of an iconset that has defined the secondary color
225
	 * can be 'recolored'.
226
	 *
227
	 * @return string the color that the active theme has set for the secondary
228
	 *                color of the icons, or FALSE
229
	 */
230
	public static function getSecondaryIconColor() {
231
		$val = Theming::getThemeProperty('icons-secondary-color');
232
233
		return $val ?? false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $val ?? false could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
234
	}
235
236
	/**
237
	 * Checks if a theme is a JSON theme. (Basically this means that it checks if a
238
	 * directory with the theme name exists and if that directory contains a file
239
	 * called theme.json).
240
	 *
241
	 * @param string $theme The name of the theme to check
242
	 *
243
	 * @return bool True if the theme is a json theme, false otherwise
244
	 */
245
	public static function isJsonTheme($theme) {
246
		if (empty($theme)) {
247
			return false;
248
		}
249
250
		if (!isset(Theming::$isJsonThemeCache[$theme])) {
251
			$themePathCoreThemes = BASE_PATH . constant('THEME_PATH_' . DEBUG_LOADER);
252
253
			// First check if this is a core theme, and if it isn't, check if it is a theme plugin
254
			if (is_dir($themePathCoreThemes . '/' . $theme)) {
255
				// We don't have core json themes, so return false
256
				Theming::$isJsonThemeCache[$theme] = false;
257
			}
258
			elseif (is_dir(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme) && is_file(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme . '/theme.json')) {
259
				Theming::$isJsonThemeCache[$theme] = true;
260
			}
261
			else {
262
				Theming::$isJsonThemeCache[$theme] = false;
263
			}
264
		}
265
266
		return Theming::$isJsonThemeCache[$theme];
267
	}
268
269
	/**
270
	 * Retrieves the properties set in the theme.json file of the theme.
271
	 *
272
	 * @param string $theme The theme for which the properties should be retrieved
273
	 *
274
	 * @return array The decoded array of properties defined in the theme.json file
275
	 */
276
	public static function getJsonThemeProps($theme) {
277
		if (!Theming::isJsonTheme($theme)) {
278
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type array.
Loading history...
279
		}
280
281
		// Check if we have the props in the cache before reading the file
282
		if (!isset(Theming::$jsonThemePropsCache[$theme])) {
283
			$json = file_get_contents(BASE_PATH . PATH_PLUGIN_DIR . '/' . $theme . '/theme.json');
284
			Theming::$jsonThemePropsCache[$theme] = json_decode($json, true);
285
286
			if (json_last_error() !== JSON_ERROR_NONE) {
287
				error_log("The theme '{$theme}' does not have a valid theme.json file. " . json_last_error_msg());
288
				Theming::$jsonThemePropsCache[$theme] = '';
289
			}
290
		}
291
292
		return Theming::$jsonThemePropsCache[$theme];
293
	}
294
295
	/**
296
	 * Normalizes all defined colors in a JSON theme to valid hex colors.
297
	 *
298
	 * @param array $themeProps A hash with the properties defined a theme.json file
299
	 */
300
	private static function normalizeColors($themeProps) {
301
		$colorKeys = [
302
			'primary-color',
303
			'primary-color:hover',
304
			'mainbar-text-color',
305
			'action-color',
306
			'action-color:hover',
307
			'selection-color',
308
			'selection-text-color',
309
			'focus-color',
310
		];
311
		foreach ($colorKeys as $ck) {
312
			$themeProps[$ck] = isset($themeProps[$ck]) ? Colors::getHexColorFromCssColor($themeProps[$ck]) : null;
313
		}
314
315
		return $themeProps;
316
	}
317
318
	/**
319
	 * Utility function to fix relative urls in JSON themes.
320
	 *
321
	 * @param string $url   the url to be fixed
322
	 * @param string $theme the name of the theme the url is part of
323
	 */
324
	private static function fixUrl($url, $theme) {
325
		// the url is absolute we don't have to fix anything
326
		if (preg_match('/^https?:\/\//', $url)) {
327
			return $url;
328
		}
329
330
		return PATH_PLUGIN_DIR . '/' . $theme . '/' . $url;
331
	}
332
333
	/**
334
	 * Retrieves the styles that should be added to the page for the json theme.
335
	 *
336
	 * @param string $theme The theme for which the properties should be retrieved
337
	 *
338
	 * @return string The styles (between <style> tags)
339
	 */
340
	public static function getStyles($theme) {
341
		$styles = '';
342
		if (!Theming::isJsonTheme($theme)) {
343
			$css = Theming::getCss($theme);
344
			foreach ($css as $file) {
0 ignored issues
show
Bug introduced by
The expression $css of type string is not traversable.
Loading history...
345
				$styles .= '<link rel="stylesheet" type="text/css" href="' . $file . '" />' . "\n";
346
			}
347
348
			return $styles;
349
		}
350
351
		// Convert the json theme to css styles
352
		$themeProps = Theming::getJsonThemeProps($theme);
353
		if (!$themeProps) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $themeProps of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
354
			return $styles;
355
		}
356
357
		$themeProps = Theming::normalizeColors($themeProps);
358
359
		if ($themeProps['primary-color']) {
360
			if (!$themeProps['primary-color:hover']) {
361
				[, , $l] = Colors::rgb2hsl(Colors::colorString2Object($themeProps['primary-color']));
362
				if ($l > 20) {
363
					$themeProps['primary-color:hover'] = Colors::darker($themeProps['primary-color'], 10);
364
				}
365
				else {
366
					$themeProps['primary-color:hover'] = Colors::lighter($themeProps['primary-color'], 20);
367
				}
368
			}
369
370
			if (!$themeProps['mainbar-text-color']) {
371
				// Check if the main bar is not too light for white text (i.e. the default color)
372
				if (Colors::getLuma($themeProps['primary-color']) > 155) {
373
					$themeProps['mainbar-text-color'] = '#000000';
374
				}
375
			}
376
377
			if (!$themeProps['selection-color']) {
378
				$themeProps['selection-color'] = Colors::setLuminance($themeProps['primary-color'], 80);
379
			}
380
		}
381
		if ($themeProps['action-color'] && !$themeProps['action-color:hover']) {
382
			$themeProps['action-color:hover'] = Colors::darker($themeProps['action-color'], 10);
383
		}
384
		if (isset($themeProps['selection-color']) && !isset($themeProps['selection-text-color'])) {
385
			// Set a text color for the selection-color
386
			$hsl = Colors::rgb2hsl($themeProps['selection-color']);
387
			if ($hsl['l'] > 50) {
388
				$hsl['l'] = 5;
389
			}
390
			else {
391
				$hsl['l'] = 95;
392
			}
393
			$themeProps['selection-text-color'] = Colors::colorObject2string(Colors::hsl2rgb($hsl));
394
		}
395
396
		if (isset($themeProps['background-image'])) {
397
			$themeProps['background-image'] = Theming::fixUrl($themeProps['background-image'], $theme);
398
		}
399
		if (isset($themeProps['logo-large'])) {
400
			$themeProps['logo-large'] = Theming::fixUrl($themeProps['logo-large'], $theme);
401
		}
402
		if (isset($themeProps['logo-small'])) {
403
			$themeProps['logo-small'] = Theming::fixUrl($themeProps['logo-small'], $theme);
404
		}
405
		if (isset($themeProps['logo-large']) && !isset($themeProps['logo-small'])) {
406
			$themeProps['logo-small'] = $themeProps['logo-large'];
407
		}
408
		if (isset($themeProps['spinner-image'])) {
409
			$themeProps['spinner-image'] = Theming::fixUrl($themeProps['spinner-image'], $theme);
410
		}
411
		$styles = '<style>';
412
		foreach ($themeProps as $k => $v) {
413
			if ($v && isset(Theming::$styles[$k])) {
414
				$styles .= str_replace("{{{$k}}}", htmlspecialchars((string) $v), Theming::$styles[$k]);
415
			}
416
		}
417
		$styles .= '</style>' . "\n";
418
419
		// Add the defined stylesheets
420
		if (isset($themeProps['stylesheets'])) {
421
			if (is_string($themeProps['stylesheets'])) {
422
				$stylesheets = explode(' ', $themeProps['stylesheets']);
423
			}
424
			elseif (is_array($themeProps['stylesheets'])) {
425
				$stylesheets = $themeProps['stylesheets'];
426
			}
427
			foreach ($stylesheets as $stylesheet) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $stylesheets does not seem to be defined for all execution paths leading up to this point.
Loading history...
428
				if (is_string($stylesheet)) {
429
					$stylesheet = trim($stylesheet);
430
					if (empty($stylesheet)) {
431
						continue;
432
					}
433
					$styles .= "\t\t" . '<link rel="stylesheet" type="text/css" href="' . htmlspecialchars((string) Theming::fixUrl($stylesheet, $theme)) . '" />' . "\n";
434
				}
435
			}
436
		}
437
438
		return $styles;
439
	}
440
441
	/**
442
	 * The templates of the styles that a json theme can add to the page.
443
	 *
444
	 * @property
445
	 */
446
	private static $styles = [
447
		'primary-color' => '
448
			/* The Sign in button of the login screen */
449
			body.login #form-container #submitbutton,
450
			#loading-mask #form-container #submitbutton {
451
				background: {{primary-color}};
452
			}
453
454
			/* The top bar of the Welcome dialog */
455
			.zarafa-welcome-body > .x-panel-bwrap > .x-panel-body div.zarafa-welcome-title {
456
				border-left: 1px solid {{primary-color}};
457
				border-right: 1px solid {{primary-color}};
458
				background: {{primary-color}};
459
			}
460
461
			/* The border line under the top menu bar */
462
			body #zarafa-mainmenu {
463
				border-color: {{primary-color}};
464
			}
465
			/* The background color of the top menu bar */
466
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct {
467
				background-color: {{primary-color}};
468
			}
469
			/* Unread items */
470
			.k-unreadborders .x-grid3-row.x-grid3-row-collapsed.mail_unread > table,
471
			.k-unreadborders .x-grid3-row.x-grid3-row-expanded.mail_unread > table {
472
		        	border-left: 4px solid {{primary-color}} !important;
473
			}
474
		',
475
476
		'primary-color:hover' => '
477
			/* Hover state and active state of the Sign in button */
478
			body.login #form-container #submitbutton:hover,
479
			#loading-mask #form-container #submitbutton:hover,
480
			body.login #form-container #submitbutton:active,
481
			#loading-mask #form-container #submitbutton:active {
482
				background: {{primary-color:hover}};
483
			}
484
485
			/* Background color of the hover state of the buttons in the top menu bar */
486
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .x-btn.x-btn-over,
487
			/* Background color of the active state of the buttons (i.e. when the buttons get clicked) */
488
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .x-btn.x-btn-over.x-btn-click,
489
			/* Background color of the selected button */
490
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active,
491
			/* Background color of the hover state of selected button */
492
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active.x-btn-over,
493
			/* Background color of the active state of selected button */
494
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active.x-btn-over.x-btn-click {
495
				background-color: {{primary-color:hover}} !important;
496
			}
497
		',
498
499
		'mainbar-text-color' => '
500
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct,
501
			/* Text color of the buttons in the top menu bar */
502
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .x-btn button.x-btn-text,
503
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .x-btn-over button.x-btn-text,
504
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .x-btn-over.x-btn-click button.x-btn-text,
505
			/* Text color of the selected button in the top menu bar */
506
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active button.x-btn-text,
507
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active.x-btn-over button.x-btn-text,
508
			body #zarafa-mainmenu.zarafa-maintabbar > .x-toolbar-ct .zarafa-maintabbar-maintab-active.x-btn-over.x-btn-click button.x-btn-text {
509
				color: {{mainbar-text-color}} !important;
510
			}
511
		',
512
513
		'action-color' => '
514
			/****************************************************************************
515
			 *  Action color
516
			 * ===============
517
			 * Some elements have a different color than the default color of these elements
518
			 * to get extra attention, e.g. "call-to-action buttons", the current day
519
			 * in the calendar, etc.
520
			 ****************************************************************************/
521
			/* Buttons, normal state */
522
			.x-btn.zarafa-action .x-btn-small,
523
			.x-btn.zarafa-action .x-btn-medium,
524
			.x-btn.zarafa-action .x-btn-large,
525
			/* Buttons, active state */
526
			.x-btn.zarafa-action.x-btn-over.x-btn-click .x-btn-small,
527
			.x-btn.zarafa-action.x-btn-over.x-btn-click .x-btn-medium,
528
			.x-btn.zarafa-action.x-btn-over.x-btn-click .x-btn-large,
529
			.x-btn.zarafa-action.x-btn-click .x-btn-small,
530
			.x-btn.zarafa-action.x-btn-click .x-btn-medium,
531
			.x-btn.zarafa-action.x-btn-click .x-btn-large,
532
			/* Special case: Popup, Windows, or Messageboxes (first button is by default styled as the action button) */
533
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-small,
534
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-medium,
535
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-large,
536
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-small,
537
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-medium,
538
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-large,
539
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-small,
540
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-medium,
541
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-large,
542
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-small,
543
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-medium,
544
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-large,
545
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-small,
546
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-medium,
547
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-large,
548
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-small,
549
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-medium,
550
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-large,
551
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-small,
552
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-medium,
553
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-large,
554
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-small,
555
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-medium,
556
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-large,
557
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-small,
558
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-medium,
559
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-large,
560
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-small,
561
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-medium,
562
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-large,
563
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-small,
564
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-medium,
565
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-large,
566
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-small,
567
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-medium,
568
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-large,
569
			/* action button in reminder popout */
570
			.k-reminderpanel .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn:not(.zarafa-normal) .x-btn-small,
571
			.k-reminderpanel .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-click:not(.zarafa-normal) .x-btn-small,
572
			.k-reminderpanel .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over.x-btn-click:not(.zarafa-normal) .x-btn-small,
573
			/* Current day in the calendar */
574
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-header .x-freebusy-header-body .x-freebusy-timeline-day.x-freebusy-timeline-day-current,
575
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-header .x-freebusy-header-body .x-freebusy-timeline-day.x-freebusy-timeline-day-current table,
576
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-header .x-freebusy-header-body .x-freebusy-timeline-day.x-freebusy-timeline-day-current table tr.x-freebusy-timeline-day td,
577
			/* The date pickers */
578
			.x-date-picker .x-date-inner td.x-date-today a,
579
			.x-date-picker .x-date-mp table td.x-date-mp-sel a,
580
			.x-date-picker .x-date-mp table tr.x-date-mp-btns td button.x-date-mp-ok {
581
				background: {{action-color}} !important;
582
			}
583
			/* Focused Action button */
584
			.x-btn.zarafa-action.x-btn-focus .x-btn-small, .x-btn.zarafa-action.x-btn-focus .x-btn-medium, .x-btn.zarafa-action.x-btn-focus .x-btn-large {
585
				background: {{action-color}} !important;
586
			}
587
			/* Selected calendar */
588
			.zarafa-calendar-tabarea-stroke.zarafa-calendar-tab-selected {
589
				border-top-color: {{action-color}};
590
			}
591
			.x-date-picker .x-date-inner td.x-date-weeknumber a,
592
			.zarafa-hierarchy-node-total-count span.zarafa-hierarchy-node-counter,
593
			.zarafa-hierarchy-node-unread-count span.zarafa-hierarchy-node-counter {
594
				color: {{action-color}};
595
			}
596
			.x-date-picker .x-date-inner td.x-date-today a {
597
				border-color: {{action-color}};
598
			}
599
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-header .x-freebusy-header-body .x-freebusy-timeline-day.x-freebusy-timeline-day-current,
600
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-body .x-freebusy-background .x-freebusy-timeline-day.x-freebusy-timeline-day-current {
601
				border-right-color: {{action-color}};
602
			}
603
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-header .x-freebusy-header-body .x-freebusy-timeline-day.x-freebusy-timeline-day-current table tr.x-freebusy-timeline-hour td:first-child,
604
			.zarafa-freebusy-panel .x-freebusy-timeline-container .x-freebusy-body .x-freebusy-background .x-freebusy-timeline-day.x-freebusy-timeline-day-current td:first-child {
605
				border-left-color: {{action-color}};
606
			}
607
		',
608
609
		'action-color:hover' => '
610
			/* Buttons, hover state */
611
			.x-btn.zarafa-action.x-btn-over .x-btn-small,
612
			.x-btn.zarafa-action.x-btn-over .x-btn-medium,
613
			.x-btn.zarafa-action.x-btn-over .x-btn-large,
614
			/* Special case: Popup, Windows, or Messageboxes */
615
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-small,
616
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-medium,
617
			.x-window .x-panel-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-large,
618
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-small,
619
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-medium,
620
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-large,
621
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-small,
622
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-medium,
623
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-large,
624
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-small,
625
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-medium,
626
			.x-window .x-window-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-large,
627
			/* action button in reminder popout */
628
			.k-reminderpanel .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-over:not(.zarafa-normal) .x-btn-small,
629
			/* The date pickers */
630
			.x-date-picker .x-date-mp table tr.x-date-mp-btns td button.x-date-mp-ok:hover {
631
				background: {{action-color:hover}} !important;
632
			}
633
		',
634
635
		'selection-color' => '
636
			/*********************************************************************
637
			 * Selected items in grids and trees
638
			 * =================================
639
			 * The background color of the selected items in grids and trees can
640
			 * be changed to better suit the theme.
641
			 *********************************************************************/
642
			/* selected item in grids */
643
			.x-grid3-row.x-grid3-row-selected,
644
			.x-grid3 .x-grid3-row-selected .zarafa-grid-button-container,
645
			/* selected item in tree hierarchies */
646
			.x-tree-node .zarafa-hierarchy-node.x-tree-selected,
647
			/* selected items in boxfields (e.g. the recipient fields) */
648
			.x-zarafa-boxfield ul .x-zarafa-boxfield-item-focus,
649
			.x-zarafa-boxfield ul .x-zarafa-boxfield-recipient-item.x-zarafa-boxfield-item-focus,
650
			/* selected items in card view of Contacts context */
651
			div.zarafa-contact-cardview-selected,
652
			/* selected items in icon view of Notes context */
653
			.zarafa-note-iconview-selected,
654
			/* selected category in the Settings context */
655
			#zarafa-mainpanel-contentpanel-settings .zarafa-settings-category-panel .zarafa-settings-category-tab-active,
656
			/* selected date in date pickers */
657
			.x-date-picker .x-date-inner td.x-date-selected:not(.x-date-today) a,
658
			.x-date-picker .x-date-inner td.x-date-selected:not(.x-date-today) a:hover {
659
				background-color: {{selection-color}} !important;
660
				border-color: {{selection-color}};
661
			}
662
663
			/* Selected x-menu */
664
			.x-menu-item-selected {
665
			background-color: {{selection-color}};
666
			}
667
668
			/*********************************************************************
669
			 * Extra information about items
670
			 * =================================
671
			 * Sometimes extra information is shown in opened items. (e.g. "You replied
672
			 * to this message etc"). This can be styled with the following rules.
673
			 *********************************************************************/
674
			.preview-header-extrainfobox,
675
			.preview-header-extrainfobox-item,
676
			.k-appointmentcreatetab .zarafa-calendar-appointment-extrainfo div,
677
			.k-taskgeneraltab .zarafa-calendar-appointment-extrainfo div,
678
			.zarafa-mailcreatepanel > .x-panel-bwrap > .x-panel-body .zarafa-mailcreatepanel-extrainfo div {
679
				background: {{selection-color}} !important;
680
			}
681
682
			/* Selected mail item */
683
			.k-unreadborders .x-grid3-row.x-grid3-row-expanded.mail_read.x-grid3-row-selected > table {
684
			        border-left: 4px solid {{selection-color}} !important;
685
			}
686
687
			/* Hover selected item */
688
			.k-unreadborders .x-grid3-row.x-grid3-row-expanded.mail_read.x-grid3-row-selected.x-grid3-row-over > table,
689
			.k-unreadborders .x-grid3-row.x-grid3-row-collapsed.mail_read.x-grid3-row-selected > table {
690
			        border-left: 4px solid {{selection-color}} !important;
691
			}
692
		',
693
694
		'selection-text-color' => '
695
			/*********************************************************************
696
			 * Extra information about items
697
			 * =================================
698
			 * Sometimes extra information is shown in opened items. (e.g. "You replied
699
			 * to this message etc"). This can be styled with the following rules.
700
			 *********************************************************************/
701
			.preview-header-extrainfobox,
702
			.preview-header-extrainfobox-item,
703
			.k-appointmentcreatetab .zarafa-calendar-appointment-extrainfo div,
704
			.k-taskgeneraltab .zarafa-calendar-appointment-extrainfo div,
705
			.zarafa-mailcreatepanel > .x-panel-bwrap > .x-panel-body .zarafa-mailcreatepanel-extrainfo div {
706
				color: {{selection-text-color}};
707
			}
708
		',
709
710
		'focus-color' => '
711
			/*********************************************************************
712
			 * Focused items
713
			 * =================================
714
			 *********************************************************************/
715
			/* Normal button */
716
			.x-window .x-window-footer .x-toolbar-left-row .x-toolbar-cell:not(.x-hide-offsets) ~ .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-over):not(.x-btn-click) .x-btn-small,
717
			.x-window .x-panel-footer .x-toolbar-right-row .x-toolbar-cell:not(.x-hide-offsets) ~ .x-toolbar-cell:not(.x-hide-offsets) .x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-over):not(.x-btn-click) .x-btn-small,
718
			.x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-click) .x-btn-small,
719
			.x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-click) .x-btn-medium,
720
			.x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-click) .x-btn-large,
721
			.x-toolbar .x-btn.x-btn-focus:not(.zarafa-action):not(.x-btn-noicon) .x-btn-small {
722
			border: 1px solid {{focus-color}} !important;
723
			}
724
			/* Login */
725
			body.login #form-container input:focus,
726
			#loading-mask #form-container input:focus {
727
			border-color: {{focus-color}};
728
			}
729
			input:focus {
730
				border-color: {{focus-color}};
731
			}
732
			/* Form elements */
733
			.x-form-text.x-form-focus:not(.x-trigger-noedit) {
734
				border-color: {{focus-color}} !important;
735
			}
736
			.x-form-field-wrap.x-trigger-wrap-focus:not(.x-freebusy-userlist-container) {
737
				border-color: {{focus-color}};
738
			}
739
			input.x-form-text.x-form-field.x-form-focus {
740
				border-color: {{focus-color}} !important;
741
			}
742
			.x-form-field-wrap.x-trigger-wrap-focus:not(.x-freebusy-userlist-container) input.x-form-text.x-form-field.x-form-focus {
743
			border-color: {{focus-color}} !important;
744
			}
745
		',
746
747
		'logo-large' => '
748
			/* The logo in the Login screen. Maximum size of the logo image is 220x60px. */
749
			body.login #form-container #logo,
750
			#loading-mask #form-container #logo {
751
				background: url({{logo-large}}) no-repeat right center;
752
				background-size: contain;
753
			}
754
		',
755
756
		'logo-small' => '
757
			/****************************************************************************
758
			 * The logo (shown on the right below the top bar)
759
			 * ===============================================
760
			 * The maximum height of the image that can be shown is 45px.
761
			 ****************************************************************************/
762
			.zarafa-maintoolbar {
763
				background-image: url({{logo-small}});
764
				background-size: auto 38px;
765
			}
766
		',
767
768
		'background-image' => '
769
			/*********************************************************************************************
770
			 * The Login screen and the Welcome screen
771
			 * =======================================
772
			 ********************************************************************************************/
773
			/* Background image of the login screen */
774
			body.login,
775
			#loading-mask,
776
			#bg,
777
			/* Background image of the Welcome screen */
778
			body.zarafa-welcome {
779
				background: url({{background-image}}) no-repeat center center;
780
				background-size: cover;
781
			}
782
		',
783
784
		'spinner-image' => '
785
			/* The spinner of the login/loading screen */
786
			body.login #form-container.loading .right,
787
			#loading-mask #form-container.loading .right {
788
				background: url({{spinner-image}}) no-repeat center center;
789
			}
790
		',
791
	];
792
}
793