diff --git a/package-lock.json b/package-lock.json index 578a0f7a..4733353b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "eslint-plugin-standard": "^5.0.0", "jest": "^30.3.0", "jest-environment-jsdom": "^30.3.0", + "jest-preset-angular": "^16.1.2", "ng-packagr": "^21.2.2", "pa11y-ci": "^4.0.1", "prettier": "^3.4.2", @@ -5346,9 +5347,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5365,9 +5363,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5384,9 +5379,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5403,9 +5395,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5422,9 +5411,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5441,9 +5427,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5460,9 +5443,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5574,8 +5554,10 @@ "node_modules/@noble/hashes": { "version": "2.0.1", "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", - "extraneous": true, + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 20.19.0" }, @@ -6030,9 +6012,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6053,9 +6032,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6076,9 +6052,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6099,9 +6072,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6122,9 +6092,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6145,9 +6112,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6664,9 +6628,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6683,9 +6644,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6702,9 +6660,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6721,9 +6676,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6930,9 +6882,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6946,9 +6895,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6962,9 +6908,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6978,9 +6921,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6994,9 +6934,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7010,9 +6947,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7026,9 +6960,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7042,9 +6973,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7058,9 +6986,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7074,9 +6999,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7090,9 +7012,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7106,9 +7025,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -7122,9 +7038,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8188,9 +8101,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8204,9 +8114,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8220,9 +8127,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8236,9 +8140,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8252,9 +8153,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -8268,9 +8166,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8284,9 +8179,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -8300,9 +8192,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/package.json b/package.json index dfb0be32..11920e87 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "eslint-plugin-standard": "^5.0.0", "jest": "^30.3.0", "jest-environment-jsdom": "^30.3.0", + "jest-preset-angular": "^16.1.2", "ng-packagr": "^21.2.2", "pa11y-ci": "^4.0.1", "prettier": "^3.4.2", diff --git a/projects/composition/src/app/api-data/cps-loader.json b/projects/composition/src/app/api-data/cps-loader.json index 688310df..e6ddf4de 100644 --- a/projects/composition/src/app/api-data/cps-loader.json +++ b/projects/composition/src/app/api-data/cps-loader.json @@ -26,7 +26,7 @@ "optional": false, "readonly": false, "type": "string", - "default": "depth", + "default": "var(--cps-text-primary)", "description": "Color of the label." }, { diff --git a/projects/composition/src/app/api-data/cps-notification.json b/projects/composition/src/app/api-data/cps-notification.json index 93baf712..52ffc175 100644 --- a/projects/composition/src/app/api-data/cps-notification.json +++ b/projects/composition/src/app/api-data/cps-notification.json @@ -127,7 +127,7 @@ "optional": true, "readonly": false, "type": "number", - "description": "The duration (in milliseconds) that the notification will be displayed before automatically closing.\nValue 0 means that the notification is persistent and will not be automatically closed." + "description": "The duration (in milliseconds) that the notification will be displayed before automatically closing.\r\nValue 0 means that the notification is persistent and will not be automatically closed." }, { "name": "allowDuplicates", diff --git a/projects/composition/src/app/api-data/cps-progress-linear.json b/projects/composition/src/app/api-data/cps-progress-linear.json index 7ca8b869..0d2c554c 100644 --- a/projects/composition/src/app/api-data/cps-progress-linear.json +++ b/projects/composition/src/app/api-data/cps-progress-linear.json @@ -26,7 +26,7 @@ "optional": false, "readonly": false, "type": "string", - "default": "calm", + "default": "var(--cps-accent-primary)", "description": "Color of the progress bar." }, { diff --git a/projects/composition/src/app/api-data/cps-table.json b/projects/composition/src/app/api-data/cps-table.json index 2f546faf..3b7375d8 100644 --- a/projects/composition/src/app/api-data/cps-table.json +++ b/projects/composition/src/app/api-data/cps-table.json @@ -107,7 +107,7 @@ "readonly": false, "type": "boolean", "default": "true", - "description": "Determines whether the 'Remove' button should be displayed in the row menu.\nNote: This setting only takes effect if 'showRowMenu' is true and 'rowMenuItems' is not set." + "description": "Determines whether the 'Remove' button should be displayed in the row menu.\r\nNote: This setting only takes effect if 'showRowMenu' is true and 'rowMenuItems' is not set." }, { "name": "showRowEditButton", @@ -115,14 +115,14 @@ "readonly": false, "type": "boolean", "default": "true", - "description": "Determines whether the 'Edit' button should be displayed in the row menu.\nNote: This setting only takes effect if 'showRowMenu' is true and 'rowMenuItems' is not set." + "description": "Determines whether the 'Edit' button should be displayed in the row menu.\r\nNote: This setting only takes effect if 'showRowMenu' is true and 'rowMenuItems' is not set." }, { "name": "rowMenuItems", "optional": true, "readonly": false, "type": "CpsMenuItem[]", - "description": "Custom items to be displayed in the row menu.\nNote: This setting only takes effect if 'showRowMenu' is true." + "description": "Custom items to be displayed in the row menu.\r\nNote: This setting only takes effect if 'showRowMenu' is true." }, { "name": "reorderableRows", @@ -178,7 +178,7 @@ "readonly": false, "type": "boolean", "default": "true", - "description": "If true, automatically detects filter type based on values, otherwise sets 'text' filter type for all columns.\nNote: This setting only takes effect if 'filterableByColumns' is true." + "description": "If true, automatically detects filter type based on values, otherwise sets 'text' filter type for all columns.\r\nNote: This setting only takes effect if 'filterableByColumns' is true." }, { "name": "sortMode", @@ -570,7 +570,7 @@ "readonly": false, "type": "boolean", "default": "false", - "description": "Determines whether columns are resizable.\nIn case of using a custom template for columns, it is also needed to add cpsTColResizable directive to th elements." + "description": "Determines whether columns are resizable.\r\nIn case of using a custom template for columns, it is also needed to add cpsTColResizable directive to th elements." }, { "name": "columnResizeMode", @@ -578,7 +578,7 @@ "readonly": false, "type": "\"expand\" | \"fit\"", "default": "fit", - "description": "Determines how the columns are resized. It can be 'fit' (total width of the table stays the same) or 'expand' (total width of the table changes when resizing columns).\nNote: This setting only takes effect if 'resizableColumns' is true." + "description": "Determines how the columns are resized. It can be 'fit' (total width of the table stays the same) or 'expand' (total width of the table changes when resizing columns).\r\nNote: This setting only takes effect if 'resizableColumns' is true." } ] }, diff --git a/projects/composition/src/app/api-data/cps-theme.json b/projects/composition/src/app/api-data/cps-theme.json new file mode 100644 index 00000000..031d1f98 --- /dev/null +++ b/projects/composition/src/app/api-data/cps-theme.json @@ -0,0 +1,30 @@ +{ + "components": {}, + "name": "CpsThemeService", + "description": "CpsThemeService manages application theming including dark mode support.\r\n\r\nThis service provides:\r\n- Light and dark theme switching with smooth transitions\r\n- Automatic persistence of theme preference in localStorage\r\n- Reactive state management using Angular signals", + "types": { + "description": "Defines the custom types used by the module.", + "values": [ + { + "name": "CpsTheme", + "value": "\"light\" | \"dark\"", + "description": "Available theme options" + }, + { + "name": "CpsColorTheme", + "value": "\"neutral\" | \"calm\" | \"energy\" | \"passion\"", + "description": "Available color theme options" + }, + { + "name": "CpsBaseTheme", + "value": "\"default\" | \"graphite\" | \"midnight\" | \"aubergine\"", + "description": "Available dark-mode base theme options" + }, + { + "name": "CpsRadiusTheme", + "value": "\"none\" | \"compact\" | \"rounded\" | \"pill\"", + "description": "Available radius theme options" + } + ] + } +} diff --git a/projects/composition/src/app/api-data/cps-tree-table.json b/projects/composition/src/app/api-data/cps-tree-table.json index 04fbe608..145823c9 100644 --- a/projects/composition/src/app/api-data/cps-tree-table.json +++ b/projects/composition/src/app/api-data/cps-tree-table.json @@ -123,7 +123,7 @@ "readonly": false, "type": "boolean", "default": "true", - "description": "Determines whether the 'Remove' button should be displayed in the row menu.\nNote: This setting only takes effect if 'showRowMenu' is true and 'rowMenuItems' is not set." + "description": "Determines whether the 'Remove' button should be displayed in the row menu.\r\nNote: This setting only takes effect if 'showRowMenu' is true and 'rowMenuItems' is not set." }, { "name": "showRowEditButton", @@ -131,14 +131,14 @@ "readonly": false, "type": "boolean", "default": "true", - "description": "Determines whether the 'Edit' button should be displayed in the row menu.\nNote: This setting only takes effect if 'showRowMenu' is true and 'rowMenuItems' is not set." + "description": "Determines whether the 'Edit' button should be displayed in the row menu.\r\nNote: This setting only takes effect if 'showRowMenu' is true and 'rowMenuItems' is not set." }, { "name": "rowMenuItems", "optional": true, "readonly": false, "type": "CpsMenuItem[]", - "description": "Custom items to be displayed in the row menu.\nNote: This setting only takes effect if 'showRowMenu' is true." + "description": "Custom items to be displayed in the row menu.\r\nNote: This setting only takes effect if 'showRowMenu' is true." }, { "name": "loading", @@ -530,7 +530,7 @@ "readonly": false, "type": "boolean", "default": "true", - "description": "If true, automatically detects filter type based on values, otherwise sets 'text' filter type for all columns.\nNote: This setting only takes effect if 'filterableByColumns' is true." + "description": "If true, automatically detects filter type based on values, otherwise sets 'text' filter type for all columns.\r\nNote: This setting only takes effect if 'filterableByColumns' is true." }, { "name": "showExportBtn", @@ -586,7 +586,7 @@ "readonly": false, "type": "boolean", "default": "false", - "description": "Determines whether columns are resizable.\nIn case of using a custom template for columns, it is also needed to add cpsTTColResizable directive to th elements." + "description": "Determines whether columns are resizable.\r\nIn case of using a custom template for columns, it is also needed to add cpsTTColResizable directive to th elements." }, { "name": "columnResizeMode", @@ -594,7 +594,7 @@ "readonly": false, "type": "\"expand\" | \"fit\"", "default": "fit", - "description": "Determines how the columns are resized. It can be 'fit' (total width of the table stays the same) or 'expand' (total width of the table changes when resizing columns).\nNote: This setting only takes effect if 'resizableColumns' is true." + "description": "Determines how the columns are resized. It can be 'fit' (total width of the table stays the same) or 'expand' (total width of the table changes when resizing columns).\r\nNote: This setting only takes effect if 'resizableColumns' is true." } ] }, diff --git a/projects/composition/src/app/api-data/cron-validation.service.json b/projects/composition/src/app/api-data/cron-validation.service.json index 28b4e9af..77d68f60 100644 --- a/projects/composition/src/app/api-data/cron-validation.service.json +++ b/projects/composition/src/app/api-data/cron-validation.service.json @@ -1,7 +1,7 @@ { "components": {}, "name": "CronValidationService", - "description": "Service for validating 6-field cron expressions with extended features.\n\nThis service handles cron validation logic for extended cron expression formats\nthat support additional features beyond standard Unix cron for more flexible\nscheduling capabilities.\n\nFormat: minutes hours day-of-month month day-of-week year\n\nKey Features:\n- Wildcards: asterisk (any value), question mark (any value for day fields)\n- Ranges: 1-5, MON-FRI, JAN-MAR\n- Steps: asterisk/15, 5/10, 1-5/2\n- Lists: 1,3,5, MON,WED,FRI\n- Special chars: L (last), W (weekday), hash (nth occurrence)", + "description": "Service for validating 6-field cron expressions with extended features.\r\n\r\nThis service handles cron validation logic for extended cron expression formats\r\nthat support additional features beyond standard Unix cron for more flexible\r\nscheduling capabilities.\r\n\r\nFormat: minutes hours day-of-month month day-of-week year\r\n\r\nKey Features:\r\n- Wildcards: asterisk (any value), question mark (any value for day fields)\r\n- Ranges: 1-5, MON-FRI, JAN-MAR\r\n- Steps: asterisk/15, 5/10, 1-5/2\r\n- Lists: 1,3,5, MON,WED,FRI\r\n- Special chars: L (last), W (weekday), hash (nth occurrence)", "methods": { "description": "Methods used in service.", "values": [ diff --git a/projects/composition/src/app/api-data/types_map.json b/projects/composition/src/app/api-data/types_map.json index f247bb05..3abaf6e8 100644 --- a/projects/composition/src/app/api-data/types_map.json +++ b/projects/composition/src/app/api-data/types_map.json @@ -32,5 +32,9 @@ "CpsTooltipOpenOn": "tooltip", "CpsNotificationConfig": "notification", "CpsNotificationAppearance": "notification", - "CpsNotificationPosition": "notification" + "CpsNotificationPosition": "notification", + "CpsTheme": "theme", + "CpsColorTheme": "theme", + "CpsBaseTheme": "theme", + "CpsRadiusTheme": "theme" } \ No newline at end of file diff --git a/projects/composition/src/app/app.component.html b/projects/composition/src/app/app.component.html index 806ac9db..c85ffa7b 100644 --- a/projects/composition/src/app/app.component.html +++ b/projects/composition/src/app/app.component.html @@ -1,11 +1,12 @@
- + CPS UI Kit logo Consumer Products UI Kit @if (version) { v{{ version }} } +
{{ componentTitle }} diff --git a/projects/composition/src/app/app.component.scss b/projects/composition/src/app/app.component.scss index c3da3b32..2ba8863e 100644 --- a/projects/composition/src/app/app.component.scss +++ b/projects/composition/src/app/app.component.scss @@ -2,19 +2,38 @@ .top-toolbar { height: vars.$top-tbar-height; - background-color: white; + background-color: var(--cps-surface-body); display: flex; - padding-left: 20px; + padding: 0 14px; align-items: center; - border-bottom: 1px solid lightgrey; + border-bottom: 1px solid var(--cps-border-color); + gap: 8px; img { width: 42px; height: 42px; } + span { - margin-left: 16px; - font-size: 18px; - color: vars.$color-calm; + font-size: 13px; + color: var(--cps-text-secondary); + line-height: 1; + + &:first-of-type { + flex: 1; + color: var(--cps-text-primary); + font-weight: 600; + letter-spacing: 0.01em; + } + + b { + color: var(--cps-accent-primary); + font-weight: 700; + font-size: 15px; + } + } + + app-theme-toggle { + margin-left: auto; } } .composition-container { @@ -29,11 +48,12 @@ background-color: vars.$composition-background; .composition-body-toolbar { height: vars.$inner-tbar-height; - background: vars.$color-calm; + background: var(--cps-surface-elevated); + border-bottom: 1px solid var(--cps-border-color); display: flex; align-items: center; justify-content: space-between; - color: white; + color: var(--cps-text-primary); font-size: 20px; .composition-body-toolbar-title { margin: 0 auto; diff --git a/projects/composition/src/app/app.component.spec.ts b/projects/composition/src/app/app.component.spec.ts index 06e2ae99..198b2c59 100644 --- a/projects/composition/src/app/app.component.spec.ts +++ b/projects/composition/src/app/app.component.spec.ts @@ -1,8 +1,9 @@ import { TestBed } from '@angular/core/testing'; -import { AppComponent } from './app.component'; import { ActivatedRoute, RouterOutlet } from '@angular/router'; import { CpsIconComponent } from 'cps-ui-kit'; +import { AppComponent } from './app.component'; import { NavigationSidebarComponent } from './components/navigation-sidebar/navigation-sidebar.component'; +import { ThemeToggleComponent } from './components/theme-toggle/theme-toggle.component'; jest.mock('projects/cps-ui-kit/package.json', () => ({ version: '1.0.0' }), { virtual: true @@ -12,7 +13,12 @@ describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [AppComponent], - imports: [NavigationSidebarComponent, CpsIconComponent, RouterOutlet], + imports: [ + NavigationSidebarComponent, + CpsIconComponent, + ThemeToggleComponent, + RouterOutlet + ], providers: [{ provide: ActivatedRoute, useValue: {} }] }).compileComponents(); }); diff --git a/projects/composition/src/app/app.module.ts b/projects/composition/src/app/app.module.ts index 2821ea7b..b96364c7 100644 --- a/projects/composition/src/app/app.module.ts +++ b/projects/composition/src/app/app.module.ts @@ -4,12 +4,13 @@ import { // provideClientHydration } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { TitleStrategy } from '@angular/router'; +import { CpsIconComponent } from 'cps-ui-kit'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { NavigationSidebarComponent } from './components/navigation-sidebar/navigation-sidebar.component'; -import { TitleStrategy } from '@angular/router'; import { AppPrefixTitleStrategy } from './app.prefix-title-strategy'; -import { CpsIconComponent } from 'cps-ui-kit'; +import { NavigationSidebarComponent } from './components/navigation-sidebar/navigation-sidebar.component'; +import { ThemeToggleComponent } from './components/theme-toggle/theme-toggle.component'; @NgModule({ declarations: [AppComponent], @@ -18,7 +19,8 @@ import { CpsIconComponent } from 'cps-ui-kit'; BrowserAnimationsModule, AppRoutingModule, NavigationSidebarComponent, - CpsIconComponent + CpsIconComponent, + ThemeToggleComponent ], providers: [ { provide: TitleStrategy, useClass: AppPrefixTitleStrategy } diff --git a/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.html b/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.html index 826fe31a..9cee4d60 100644 --- a/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.html +++ b/projects/composition/src/app/components/component-docs-viewer/component-docs-viewer.component.html @@ -1,5 +1,5 @@ diff --git a/projects/composition/src/app/components/navigation-sidebar/navigation-sidebar.component.scss b/projects/composition/src/app/components/navigation-sidebar/navigation-sidebar.component.scss index 712d5b3e..403d634f 100644 --- a/projects/composition/src/app/components/navigation-sidebar/navigation-sidebar.component.scss +++ b/projects/composition/src/app/components/navigation-sidebar/navigation-sidebar.component.scss @@ -1,15 +1,15 @@ @use '../../../variables.scss' as vars; :host { - $item-hover-background: var(--cps-color-highlight-hover); - $item-active-background: var(--cps-color-highlight-active); - $item-border-color: var(--cps-color-line-light); + $item-hover-background: var(--cps-highlight-hover); + $item-active-background: var(--cps-highlight-active); + $item-border-color: var(--cps-color-line); border-right: 1px solid $item-border-color; - background-color: var(--cps-color-bg-light); + background-color: var(--cps-surface-body); .sidebar { transition: width 0.2s; - background-color: white; + background-color: var(--cps-surface-body); width: vars.$sidebar-width; height: calc(100vh - vars.$top-tbar-height - 20px); overflow: auto; @@ -17,7 +17,7 @@ &-title { margin: 0; padding: 12px 0 12px 12px; - color: vars.$color-text; + color: var(--cps-text-primary); } &-item { border-bottom: 1px solid $item-border-color; @@ -26,7 +26,7 @@ align-items: center; padding-left: 30px; text-decoration: none; - color: vars.$color-text; + color: var(--cps-text-primary); &:hover { background: $item-hover-background; } @@ -35,7 +35,7 @@ } } ._active { - color: white; + color: var(--cps-text-on-accent); font-weight: bold; background: vars.$color-calm; } diff --git a/projects/composition/src/app/components/theme-toggle/theme-toggle.component.html b/projects/composition/src/app/components/theme-toggle/theme-toggle.component.html new file mode 100644 index 00000000..e0b483d6 --- /dev/null +++ b/projects/composition/src/app/components/theme-toggle/theme-toggle.component.html @@ -0,0 +1,134 @@ +
+ @if (showCustomize) { + + } + @if (menuOpen) { + +
+
+
Appearance
+
Theme, radius and dark base
+
+
+

Theme

+
+ + + + +
+
+
+

Radius

+
+ + + + +
+
+
+

Base

+

Base affects dark mode surfaces.

+
+ + + + +
+
+
+ } + +
diff --git a/projects/composition/src/app/components/theme-toggle/theme-toggle.component.scss b/projects/composition/src/app/components/theme-toggle/theme-toggle.component.scss new file mode 100644 index 00000000..ba0f26dc --- /dev/null +++ b/projects/composition/src/app/components/theme-toggle/theme-toggle.component.scss @@ -0,0 +1,187 @@ +.theme-controls { + --appearance-radius-sm: 8px; + --appearance-radius-md: 14px; + display: flex; + align-items: center; + gap: 8px; + position: relative; +} +.theme-toggle-btn { + display: flex; + align-items: center; + gap: 8px; + height: 32px; + padding: 0 11px; + background: var(--cps-surface-elevated); + border: 1px solid var(--cps-border-color); + border-radius: var(--appearance-radius-sm); + color: var(--cps-text-primary); + cursor: pointer; + font-family: 'Source Sans Pro', sans-serif; + font-size: 12px; + font-weight: 600; + transition: all 0.2s; + &:hover { + background: var(--cps-highlight-hover); + border-color: var(--cps-border-focus); + } + &:active { + background: var(--cps-highlight-active); + } + &:focus-visible { + outline: 2px solid var(--cps-ring-color); + outline-offset: 2px; + } +} +.theme-toggle-caret { + color: var(--cps-text-muted); + font-size: 11px; + line-height: 1; +} +.menu-backdrop { + position: fixed; + inset: 0; + margin: 0; + padding: 0; + background: var(--cps-surface-overlay); + opacity: 0.22; + border: 0; + border-radius: 0; + appearance: none; + -webkit-appearance: none; + z-index: 10; +} +.theme-menu { + position: absolute; + top: calc(100% + 10px); + right: 0; + width: min(320px, calc(100vw - 16px)); + max-height: min(70vh, 520px); + overflow: auto; + background: var(--cps-surface-control); + border: 1px solid var(--cps-border-color); + border-radius: var(--appearance-radius-md); + box-shadow: var(--cps-shadow-md); + z-index: 20; + padding: 6px; + transform-origin: top right; + animation: menu-enter var(--cps-motion-fast) var(--cps-motion-easing); +} +@keyframes menu-enter { + from { + opacity: 0; + transform: translateY(-4px) scale(0.985); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} +@media (prefers-reduced-motion: reduce) { + .theme-menu { + animation: none; + } +} +.theme-menu-header { + padding: 10px 10px 9px; + border-bottom: 1px solid var(--cps-border-color); +} +.theme-menu-title { + color: var(--cps-text-primary); + font-family: 'Source Sans Pro', sans-serif; + font-size: 13px; + font-weight: 700; + line-height: 1.2; +} +.theme-menu-subtitle { + margin-top: 3px; + color: var(--cps-text-muted); + font-family: 'Source Sans Pro', sans-serif; + font-size: 11px; + line-height: 1.35; +} +.theme-section { + padding: 10px; +} +.theme-section + .theme-section { + border-top: 1px solid var(--cps-border-color); +} +.theme-section-title { + margin: 0 0 6px; + color: var(--cps-text-muted); + font-family: 'Source Sans Pro', sans-serif; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; +} +.theme-section-hint { + margin: -1px 0 8px; + color: var(--cps-text-muted); + font-family: 'Source Sans Pro', sans-serif; + font-size: 11px; + line-height: 1.35; +} +.theme-options { + display: flex; + flex-direction: column; + gap: 3px; +} +.theme-option { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + min-height: 32px; + padding: 7px 8px; + border: 1px solid transparent; + border-radius: var(--appearance-radius-sm); + background: transparent; + color: var(--cps-text-primary); + font-family: 'Source Sans Pro', sans-serif; + font-size: 12px; + line-height: 1; + text-align: left; + cursor: pointer; + &:hover { + background: var(--cps-highlight-hover); + } + &:focus-visible { + outline: 2px solid var(--cps-ring-color); + outline-offset: 2px; + } +} +.theme-option.selected { + background: var(--cps-highlight-active); + border-color: var(--cps-border-focus); +} +.theme-option.selected::after { + content: '✓'; + margin-left: auto; + color: var(--cps-accent-primary); + font-weight: 700; + font-size: 12px; +} +.option-dot { + width: 9px; + height: 9px; + border-radius: 9999px; + background: transparent; + border: 1px solid var(--cps-border-color); + flex: 0 0 auto; +} +.theme-section-theme .theme-option:nth-child(1) .option-dot { + background: var(--cps-text-muted); +} +.theme-section-theme .theme-option:nth-child(2) .option-dot { + background: var(--cps-color-calm); +} +.theme-section-theme .theme-option:nth-child(3) .option-dot { + background: var(--cps-color-energy); +} +.theme-section-theme .theme-option:nth-child(4) .option-dot { + background: var(--cps-color-passion); +} +.theme-option.selected .option-dot { + border-color: transparent; +} diff --git a/projects/composition/src/app/components/theme-toggle/theme-toggle.component.ts b/projects/composition/src/app/components/theme-toggle/theme-toggle.component.ts new file mode 100644 index 00000000..492c5130 --- /dev/null +++ b/projects/composition/src/app/components/theme-toggle/theme-toggle.component.ts @@ -0,0 +1,61 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { + CpsBaseTheme, + CpsColorTheme, + CpsIconComponent, + CpsRadiusTheme, + CpsThemeService +} from 'cps-ui-kit'; + +@Component({ + selector: 'app-theme-toggle', + imports: [CpsIconComponent], + templateUrl: './theme-toggle.component.html', + styleUrl: './theme-toggle.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '(document:keydown.escape)': 'onEscapeKey()' + } +}) +export class ThemeToggleComponent { + private themeService = inject(CpsThemeService); + + showCustomize = + new URLSearchParams(window.location.search).get('experimental') === 'true'; + + isDark = this.themeService.isDark; + colorTheme = this.themeService.colorTheme; + radiusTheme = this.themeService.radiusTheme; + baseTheme = this.themeService.baseTheme; + menuOpen = false; + + toggleTheme(): void { + this.themeService.toggleTheme(); + } + + toggleMenu(): void { + this.menuOpen = !this.menuOpen; + } + + closeMenu(): void { + this.menuOpen = false; + } + + onEscapeKey(): void { + if (this.menuOpen) { + this.closeMenu(); + } + } + + setColorTheme(value: CpsColorTheme): void { + this.themeService.setColorTheme(value); + } + + setRadiusTheme(value: CpsRadiusTheme): void { + this.themeService.setRadiusTheme(value); + } + + setBaseTheme(value: CpsBaseTheme): void { + this.themeService.setBaseTheme(value); + } +} diff --git a/projects/composition/src/app/pages/icons-page/icons-page/icons-page.component.html b/projects/composition/src/app/pages/icons-page/icons-page/icons-page.component.html index ab3bde80..85fa867e 100644 --- a/projects/composition/src/app/pages/icons-page/icons-page/icons-page.component.html +++ b/projects/composition/src/app/pages/icons-page/icons-page/icons-page.component.html @@ -12,7 +12,10 @@
@for (name of filteredIconsList; track name) {
- + {{ name }}
} diff --git a/projects/composition/src/app/pages/icons-page/icons-page/icons-page.component.scss b/projects/composition/src/app/pages/icons-page/icons-page/icons-page.component.scss index 4645cb80..6cbd8dca 100644 --- a/projects/composition/src/app/pages/icons-page/icons-page/icons-page.component.scss +++ b/projects/composition/src/app/pages/icons-page/icons-page/icons-page.component.scss @@ -17,6 +17,6 @@ cursor: pointer; span { margin-left: 16px; - color: vars.$color-calm; + color: var(--cps-text-primary); } } diff --git a/projects/composition/src/styles.scss b/projects/composition/src/styles.scss index 210f2849..97428f8e 100644 --- a/projects/composition/src/styles.scss +++ b/projects/composition/src/styles.scss @@ -41,16 +41,20 @@ body { font-weight: bold; text-align: left; padding: 0.75rem 1rem; - border-bottom: 1px solid var(--cps-color-line-light); + background-color: var(--cps-surface-elevated); + border-bottom: 1px solid var(--cps-color-line); } tr { transition: background-color 0.5s ease-in; + &:hover { + background-color: var(--cps-highlight-hover); + } } td { padding: 0.75rem 1rem; - border-bottom: 1px solid var(--cps-color-line-light); + border-bottom: 1px solid var(--cps-color-line); white-space: pre-line; span { @@ -61,18 +65,18 @@ body { &.highlighted-bg { span { - color: var(--cps-color-depth-darken4); - background-color: var(--cps-color-human-lighten5); + color: var(--cps-text-on-accent); + background-color: var(--cps-accent-primary); border-radius: 6px; padding: 0.2rem 0.5rem; } } &.highlighted-text { - color: var(--cps-color-calm); + color: var(--cps-text-primary); a { - color: var(--cps-color-calm); + color: var(--cps-text-primary); &:hover { text-decoration: none; @@ -92,8 +96,8 @@ body { Consolas, Liberation Mono, monospace; - background-color: var(--cps-color-bg-mid); - border: 1px solid var(--cps-color-line-light); + background-color: var(--cps-surface-elevated); + border: 1px solid var(--cps-color-line); } } } diff --git a/projects/composition/src/variables.scss b/projects/composition/src/variables.scss index ca663339..f26fb833 100644 --- a/projects/composition/src/variables.scss +++ b/projects/composition/src/variables.scss @@ -1,7 +1,7 @@ $sidebar-width: 300px; -$composition-background: var(--cps-color-bg-light); +$composition-background: var(--cps-background-color); $top-tbar-height: 64px; $inner-tbar-height: 45px; -$color-calm: var(--cps-color-calm); -$color-text: var(--cps-color-text-dark); +$color-calm: var(--cps-accent-primary); +$color-text: var(--cps-text-primary); diff --git a/projects/cps-ui-kit/assets/icons.svg b/projects/cps-ui-kit/assets/icons.svg index 7ac1852f..df8e0425 100644 --- a/projects/cps-ui-kit/assets/icons.svg +++ b/projects/cps-ui-kit/assets/icons.svg @@ -513,4 +513,11 @@ + + + + + + + diff --git a/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.html b/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.html index 1ca9034e..d10d5411 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.html +++ b/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.html @@ -1,7 +1,7 @@
@@ -129,6 +129,7 @@ #autocompleteInput class="cps-autocomplete-box-input" spellcheck="false" + [attr.aria-label]="label || placeholder || 'Autocomplete input'" [placeholder]=" (!multiple && isEmptyValue()) || (value?.length < 1 && multiple) ? placeholder @@ -283,6 +284,7 @@ spellcheck="false" [class]="inputClass" [style]="inputStyle" + [attr.aria-label]="label || placeholder || 'Autocomplete input'" [placeholder]=" (!multiple && isEmptyValue()) || (value?.length < 1 && multiple) ? placeholder diff --git a/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.scss b/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.scss index 1199eea0..ce8172b7 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.scss +++ b/projects/cps-ui-kit/src/lib/components/cps-autocomplete/cps-autocomplete.component.scss @@ -1,21 +1,21 @@ -$color-calm: var(--cps-color-calm); -$color-error: var(--cps-color-error); -$error-background: #fef3f2; -$autocomplete-placeholder-color: var(--cps-color-text-lightest); -$autocomplete-label-color: var(--cps-color-text-dark); -$autocomplete-label-disabled-color: var(--cps-color-text-mild); -$autocomplete-items-disabled-color: var(--cps-color-text-light); -$autocomplete-hint-color: var(--cps-color-text-mild); -$option-hover-background: var(--cps-color-highlight-hover); -$selected-option-background: var(--cps-color-highlight-selected); -$option-highlight-background: var(--cps-color-highlight-active); -$option-highlight-selected-background: var(--cps-color-highlight-selected-dark); -$autocomplete-option-info-color: var(--cps-color-text-light); -$autocomplete-option-value-color: var(--cps-color-text-dark); -$autocomplete-about-remove-color: var(--cps-color-text-light); -$autocomplete-about-remove-background: var(--cps-color-bg-mid); -$autocomplete-prefix-icon-color: var(--cps-color-text-dark); -$autocomplete-border-color: var(--cps-color-line-light); +$color-calm: var(--cps-accent-primary); +$color-error: var(--cps-state-error); +$error-background: var(--cps-error-background); +$autocomplete-placeholder-color: var(--cps-input-placeholder); +$autocomplete-label-color: var(--cps-text-primary); +$autocomplete-label-disabled-color: var(--cps-text-secondary); +$autocomplete-items-disabled-color: var(--cps-text-disabled); +$autocomplete-hint-color: var(--cps-text-muted); +$option-hover-background: var(--cps-highlight-hover); +$selected-option-background: var(--cps-highlight-selected); +$option-highlight-background: var(--cps-highlight-active); +$option-highlight-selected-background: var(--cps-highlight-selected); +$autocomplete-option-info-color: var(--cps-text-secondary); +$autocomplete-option-value-color: var(--cps-text-primary); +$autocomplete-about-remove-color: var(--cps-text-muted); +$autocomplete-about-remove-background: var(--cps-surface-muted); +$autocomplete-prefix-icon-color: var(--cps-text-secondary); +$autocomplete-border-color: var(--cps-border-color); $hover-transition-duration: 0.2s; @@ -26,7 +26,7 @@ $hover-transition-duration: 0.2s; position: relative; width: 100%; outline: none; - font-family: 'Source Sans Pro', sans-serif; + font-family: inherit; font-weight: normal; display: grid; @@ -40,7 +40,7 @@ $hover-transition-duration: 0.2s; &.focused { .cps-autocomplete-box { - background: white !important; + background: var(--cps-input-background) !important; } } @@ -62,6 +62,7 @@ $hover-transition-duration: 0.2s; &.active { .cps-autocomplete-box { border: 1px solid $color-calm; + box-shadow: 0 0 0 3px var(--cps-highlight-selected); .cps-autocomplete-box-area { .prefix-icon { color: $color-calm; @@ -79,6 +80,7 @@ $hover-transition-duration: 0.2s; display: inline-flex; margin-bottom: 0.2rem; color: $autocomplete-label-color; + background-color: var(--cps-surface-body); font-size: 0.875rem; font-weight: 600; .cps-autocomplete-label-info-circle { @@ -106,13 +108,16 @@ $hover-transition-duration: 0.2s; min-height: 38px; width: 100%; cursor: text; - background: white; + background: var(--cps-input-background); font-size: 1rem; outline: none; padding: 0 12px 0 12px; - border-radius: 4px; + border-radius: var(--cps-border-radius-medium); border: 1px solid $autocomplete-border-color; - transition-duration: $hover-transition-duration; + transition: + border-color $hover-transition-duration var(--cps-motion-easing), + box-shadow $hover-transition-duration var(--cps-motion-easing), + background-color $hover-transition-duration var(--cps-motion-easing); &-area { display: flex; @@ -135,7 +140,7 @@ $hover-transition-duration: 0.2s; color: $autocomplete-option-value-color; border-style: none; outline: none; - font-family: 'Source Sans Pro', sans-serif; + font-family: inherit; &::placeholder { color: $autocomplete-placeholder-color; font-style: italic; @@ -206,7 +211,7 @@ $hover-transition-duration: 0.2s; } &:hover { - border: 1px solid $color-calm; + border: 1px solid var(--cps-border-strong); .cps-autocomplete-box-area { .prefix-icon { color: $color-calm; @@ -248,6 +253,8 @@ $hover-transition-duration: 0.2s; .cps-autocomplete-hint { color: $autocomplete-hint-color; + background-color: var(--cps-surface-body); + display: inline-block; font-size: 0.75rem; min-height: 1.125rem; line-height: 1.125rem; @@ -267,7 +274,7 @@ $hover-transition-duration: 0.2s; &.disabled { pointer-events: none; .cps-autocomplete-box { - background: #f7f7f7; + background: var(--cps-background-disabled); &-items { color: $autocomplete-items-disabled-color; .text-group, @@ -297,8 +304,12 @@ $hover-transition-duration: 0.2s; } .cps-autocomplete-options { - font-family: 'Source Sans Pro', sans-serif; - background: white; + font-family: inherit; + background: var(--cps-popover-background); + color: var(--cps-popover-foreground); + border: 1px solid var(--cps-border-color); + border-radius: var(--cps-border-radius-medium); + box-shadow: var(--cps-shadow-md); overflow-x: hidden; max-height: 242px; overflow-y: auto; @@ -390,7 +401,7 @@ $hover-transition-duration: 0.2s; } .select-all-option { - border-bottom: 1px solid lightgrey; + border-bottom: 1px solid var(--cps-border-color); font-weight: 600; } diff --git a/projects/cps-ui-kit/src/lib/components/cps-chip/cps-chip.component.html b/projects/cps-ui-kit/src/lib/components/cps-chip/cps-chip.component.html index 70e42d96..1fdaf5e5 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-chip/cps-chip.component.html +++ b/projects/cps-ui-kit/src/lib/components/cps-chip/cps-chip.component.html @@ -11,7 +11,7 @@ class="cps-chip-close-icon" icon="close-x" size="xsmall" - color="text-darkest" + [color]="'var(--cps-text-primary)'" (click)="onCloseClick($event)"> }
diff --git a/projects/cps-ui-kit/src/lib/components/cps-chip/cps-chip.component.scss b/projects/cps-ui-kit/src/lib/components/cps-chip/cps-chip.component.scss index 7f3e2bc9..2c91bc6a 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-chip/cps-chip.component.scss +++ b/projects/cps-ui-kit/src/lib/components/cps-chip/cps-chip.component.scss @@ -7,8 +7,9 @@ .cps-chip { align-items: center; display: inline-flex; - background-color: var(--cps-color-bg-dark); - border-radius: 14px; + background-color: var(--cps-surface-elevated); + border: 1px solid var(--cps-border-color); + border-radius: var(--cps-radius-full); line-height: 16px; padding: 4px 12px; cursor: default; @@ -17,28 +18,28 @@ cursor: pointer; &:hover { ::ng-deep .cps-icon { - color: var(--cps-color-calm) !important; + color: var(--cps-accent-primary) !important; } } } &-label { font-size: 14px; - color: var(--cps-color-text-darkest); - font-family: 'Source Sans Pro', sans-serif; + color: var(--cps-text-primary); + font-family: inherit; font-style: normal; font-weight: 400; } &.cps-chip-disabled { pointer-events: none; - background-color: var(--cps-color-bg-mid); + background-color: var(--cps-background-disabled); .cps-chip-label { - color: var(--cps-color-text-light); + color: var(--cps-text-muted); } .cps-chip-icon, .cps-chip-close-icon { ::ng-deep .cps-icon { - color: var(--cps-color-text-light) !important; + color: var(--cps-text-muted) !important; } } } diff --git a/projects/cps-ui-kit/src/lib/components/cps-icon/cps-icon.component.ts b/projects/cps-ui-kit/src/lib/components/cps-icon/cps-icon.component.ts index 37308567..af01f768 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-icon/cps-icon.component.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-icon/cps-icon.component.ts @@ -7,8 +7,8 @@ import { Input, OnChanges } from '@angular/core'; -import { convertSize } from '../../utils/internal/size-utils'; import { getCSSColor } from '../../utils/colors-utils'; +import { convertSize } from '../../utils/internal/size-utils'; /** * Injection token that is used to provide the path to the icons. @@ -81,6 +81,7 @@ export const iconNames = [ 'issues', 'jpeg', 'json', + 'kafka', 'kris', 'last-seen-product', 'left', @@ -94,6 +95,7 @@ export const iconNames = [ 'menu-shrink', 'minimize', 'minus', + 'moon', 'move-grabber', 'open', 'ownership', @@ -120,6 +122,7 @@ export const iconNames = [ 'stepper-completed', 'success', 'suggestion', + 'sun', 'survivorship', 'table-row-error', 'table-row-success', diff --git a/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.html b/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.html index c078ce2a..492aeb2d 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.html +++ b/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.html @@ -2,7 +2,8 @@ @if (label) {
+ [class.cps-input-label-disabled]="disabled && !readonly" + [class.cps-input-label-error]="error"> @if (infoTooltip) { + [class.password]="type === 'password'" + [class.cps-input-wrap-error]="error" + [class.clearable]="clearable" + [class.persistent-clear]="persistentClear" + [class.borderless]="appearance === 'borderless'" + [class.underlined]="appearance === 'underlined'"> @if (!valueToDisplay) { + [class.password-show-btn-active]="currentType === 'text'"> diff --git a/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.scss b/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.scss index 75c985f1..4d4f0a94 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.scss +++ b/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.scss @@ -1,17 +1,17 @@ -$color-calm: var(--cps-color-calm); -$input-hint-color: var(--cps-color-text-mild); -$input-label-disabled-color: var(--cps-color-text-mild); -$input-pass-show-btn-color: var(--cps-color-text-mild); -$input-placeholder-color: var(--cps-color-text-lightest); -$input-border-color: var(--cps-color-line-light); -$input-label-color: var(--cps-color-text-dark); -$input-text-color: var(--cps-color-text-dark); -$input-text-disabled-color: var(--cps-color-text-light); -$input-prefix-text-color: var(--cps-color-text-mild); -$input-prefix-icon-color: var(--cps-color-text-dark); - -$color-error: var(--cps-color-error); -$error-background: #fef3f2; +$color-calm: var(--cps-accent-primary); +$input-hint-color: var(--cps-text-muted); +$input-label-disabled-color: var(--cps-text-muted); +$input-pass-show-btn-color: var(--cps-text-muted); +$input-placeholder-color: var(--cps-input-placeholder); +$input-border-color: var(--cps-border-color); +$input-label-color: var(--cps-text-primary); // --cps-color-text-dark +$input-text-color: var(--cps-text-primary); // --cps-color-text-dark +$input-text-disabled-color: var(--cps-text-disabled); +$input-prefix-text-color: var(--cps-text-muted); +$input-prefix-icon-color: var(--cps-text-primary); // --cps-color-text-dark + +$color-error: var(--cps-state-error); +$error-background: var(--cps-error-background); $hover-transition-duration: 0.2s; @@ -22,14 +22,14 @@ $hover-transition-duration: 0.2s; gap: 0.2rem !important; display: flex !important; flex-direction: column !important; - font-family: 'Source Sans Pro', sans-serif; + font-family: inherit; .cps-input-wrap { position: relative; overflow: hidden; &:hover { input:enabled:not(:read-only) { - border: 1px solid $color-calm; + border-color: var(--cps-border-strong); } } &-error { @@ -46,39 +46,37 @@ $hover-transition-duration: 0.2s; input { min-height: 38px; - font-family: 'Source Sans Pro', sans-serif; + font-family: inherit; font-size: 1rem; color: $input-text-color; - background: #ffffff; + background: var(--cps-input-background); padding: 0.375rem 0.75rem; line-height: 1.5; border: 1px solid $input-border-color; transition-duration: $hover-transition-duration; appearance: none; - border-radius: 4px; + border-radius: var(--cps-border-radius-medium); width: 100%; &:focus { outline: 0; } &:focus:not(:read-only) { - border: 1px solid $color-calm; + border-color: $color-calm; + box-shadow: 0 0 0 3px var(--cps-highlight-selected); } &:read-only { cursor: default; } &:disabled { - opacity: 1; + opacity: 0.7; } &:disabled:not([readonly]) { - color: $input-text-disabled-color; - background-color: #f7f7f7; + color: $input-text-disabled-color !important; + -webkit-text-fill-color: $input-text-disabled-color !important; + background-color: var(--cps-background-disabled); pointer-events: none; } - - &[type='password'] { - font-family: Verdana; - } } input:focus:not(:read-only) + .cps-input-prefix > .cps-input-prefix-icon, @@ -116,7 +114,7 @@ $hover-transition-duration: 0.2s; .clear-btn { display: flex; cursor: pointer; - color: $color-calm; + color: var(--cps-state-error); cps-icon { opacity: 0; transition-duration: $hover-transition-duration; @@ -188,6 +186,26 @@ $hover-transition-duration: 0.2s; &.underlined { input { border-bottom: 1px solid $input-border-color !important; + background: transparent; + box-shadow: none !important; + } + } + + &.borderless { + input { + background: var(--cps-surface-muted); + box-shadow: none !important; + } + + &:hover { + input:enabled:not(:read-only) { + background: var(--cps-highlight-hover); + } + } + + input:focus:not(:read-only) { + background: var(--cps-highlight-selected); + border-color: transparent !important; } } } @@ -213,6 +231,7 @@ $hover-transition-duration: 0.2s; .cps-input-hint { color: $input-hint-color; + font-family: inherit; font-size: 0.75rem; min-height: 1.125rem; line-height: 1.125rem; @@ -220,6 +239,7 @@ $hover-transition-duration: 0.2s; } .cps-input-error { color: $color-error; + font-family: inherit; font-weight: bold; font-size: 0.75rem; min-height: 1.125rem; @@ -240,9 +260,13 @@ $hover-transition-duration: 0.2s; &-disabled { color: $input-label-disabled-color; } + + &-error { + color: $color-error; + } } ::placeholder { - font-family: 'Source Sans Pro', sans-serif; + font-family: inherit; color: $input-placeholder-color; font-style: italic; opacity: 1; /* Firefox */ diff --git a/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.ts b/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.ts index 5995a28f..237d7f44 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-input/cps-input.component.ts @@ -14,16 +14,16 @@ import { ViewChild } from '@angular/core'; import { ControlValueAccessor, NgControl } from '@angular/forms'; +import { Subscription } from 'rxjs'; +import { CpsTooltipPosition } from '../../directives/cps-tooltip/cps-tooltip.directive'; +import { convertSize } from '../../utils/internal/size-utils'; import { CpsIconComponent, IconType, iconSizeType } from '../cps-icon/cps-icon.component'; -import { Subscription } from 'rxjs'; -import { convertSize } from '../../utils/internal/size-utils'; -import { CpsProgressLinearComponent } from '../cps-progress-linear/cps-progress-linear.component'; import { CpsInfoCircleComponent } from '../cps-info-circle/cps-info-circle.component'; -import { CpsTooltipPosition } from '../../directives/cps-tooltip/cps-tooltip.directive'; +import { CpsProgressLinearComponent } from '../cps-progress-linear/cps-progress-linear.component'; /** * CpsInputAppearanceType is used to define the border of the input field. diff --git a/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.scss b/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.scss index 98d14147..5905270b 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.scss +++ b/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.scss @@ -1,5 +1,5 @@ -$color-outer: var(--cps-color-calm); -$color-middle: var(--cps-color-warmth); +$color-outer: var(--cps-accent-primary); +$color-middle: var(--cps-accent-secondary); $color-inner: var(--cps-color-energy); :host { diff --git a/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.ts b/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.ts index 6a37f298..87128d90 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-loader/cps-loader.component.ts @@ -29,7 +29,7 @@ export class CpsLoaderComponent implements OnInit { * Color of the label. * @group Props */ - @Input() labelColor = 'depth'; + @Input() labelColor = 'var(--cps-text-primary)'; /** * Determines whether to show 'Loading...' label. @@ -37,7 +37,7 @@ export class CpsLoaderComponent implements OnInit { */ @Input() showLabel = true; - backgroundColor = 'rgba(0, 0, 0, 0.1)'; + backgroundColor = 'var(--cps-surface-overlay)'; // eslint-disable-next-line no-useless-constructor constructor(@Inject(DOCUMENT) private document: Document) {} diff --git a/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.ts b/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.ts index 7d853bec..af4fe6d4 100644 --- a/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.ts +++ b/projects/cps-ui-kit/src/lib/components/cps-progress-linear/cps-progress-linear.component.ts @@ -1,7 +1,7 @@ import { CommonModule, DOCUMENT } from '@angular/common'; import { Component, Inject, Input, OnInit } from '@angular/core'; -import { convertSize } from '../../utils/internal/size-utils'; import { getCSSColor } from '../../utils/colors-utils'; +import { convertSize } from '../../utils/internal/size-utils'; /** * CpsProgressLinearComponent is a process status indicator of a rectangular form. @@ -30,7 +30,7 @@ export class CpsProgressLinearComponent implements OnInit { * Color of the progress bar. * @group Props */ - @Input() color = 'calm'; + @Input() color = 'var(--cps-accent-primary)'; /** * Background color of the progress bar. diff --git a/projects/cps-ui-kit/src/lib/services/cps-theme/cps-theme.service.spec.ts b/projects/cps-ui-kit/src/lib/services/cps-theme/cps-theme.service.spec.ts new file mode 100644 index 00000000..76d0d036 --- /dev/null +++ b/projects/cps-ui-kit/src/lib/services/cps-theme/cps-theme.service.spec.ts @@ -0,0 +1,62 @@ +import { TestBed } from '@angular/core/testing'; +import { CpsThemeService } from './cps-theme.service'; + +describe('CpsThemeService', () => { + let service: CpsThemeService; + + beforeEach(() => { + localStorage.clear(); + TestBed.configureTestingModule({}); + service = TestBed.inject(CpsThemeService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should initialize with light theme by default', () => { + expect(service.theme()).toBe('light'); + }); + + it('should toggle theme', () => { + const initialTheme = service.theme(); + service.toggleTheme(); + const newTheme = service.theme(); + expect(newTheme).not.toBe(initialTheme); + }); + + it('should save theme preference to localStorage', () => { + service.setTheme('dark', false); + expect(localStorage.getItem('cps-theme-preference')).toBe('dark'); + }); + + it('should compute isDark correctly', () => { + service.setTheme('dark', false); + expect(service.isDark()).toBe(true); + service.setTheme('light', false); + expect(service.isDark()).toBe(false); + }); + + it('should initialize with calm color theme by default', () => { + expect(service.colorTheme()).toBe('calm'); + }); + + it('should save color theme preference to localStorage', () => { + service.setColorTheme('energy', false); + expect(localStorage.getItem('cps-color-theme-preference')).toBe('energy'); + }); + + it('should save base theme preference to localStorage', () => { + service.setBaseTheme('midnight', false); + expect(localStorage.getItem('cps-base-theme-preference')).toBe('midnight'); + }); + + it('should save radius theme preference to localStorage', () => { + service.setRadiusTheme('rounded', false); + expect(localStorage.getItem('cps-radius-theme-preference')).toBe('rounded'); + }); + + it('should initialize with compact radius theme by default', () => { + expect(service.radiusTheme()).toBe('compact'); + }); +}); diff --git a/projects/cps-ui-kit/src/lib/services/cps-theme/cps-theme.service.ts b/projects/cps-ui-kit/src/lib/services/cps-theme/cps-theme.service.ts new file mode 100644 index 00000000..d160cc6a --- /dev/null +++ b/projects/cps-ui-kit/src/lib/services/cps-theme/cps-theme.service.ts @@ -0,0 +1,322 @@ +import { DOCUMENT } from '@angular/common'; +import { computed, inject, Injectable, signal } from '@angular/core'; + +/** + * Available theme options + * @group Types + */ +export type CpsTheme = 'light' | 'dark'; + +/** + * Available color theme options + * @group Types + */ +export type CpsColorTheme = 'neutral' | 'calm' | 'energy' | 'passion'; + +/** + * Available dark-mode base theme options + * @group Types + */ +export type CpsBaseTheme = 'default' | 'graphite' | 'midnight' | 'aubergine'; + +/** + * Available radius theme options + * @group Types + */ +export type CpsRadiusTheme = 'none' | 'compact' | 'rounded' | 'pill'; + +/** + * CpsThemeService manages application theming including dark mode support. + * + * This service provides: + * - Light and dark theme switching with smooth transitions + * - Automatic persistence of theme preference in localStorage + * - Reactive state management using Angular signals + * + * @example + * ```typescript + * class MyComponent { + * private themeService = inject(CpsThemeService); + * + * isDark = this.themeService.isDark; + * + * toggleTheme() { + * this.themeService.toggleTheme(); + * } + * } + * ``` + * + * @group Services + */ +@Injectable({ + providedIn: 'root' +}) +export class CpsThemeService { + private document = inject(DOCUMENT); + private readonly THEME_STORAGE_KEY = 'cps-theme-preference'; + private readonly COLOR_THEME_STORAGE_KEY = 'cps-color-theme-preference'; + private readonly BASE_THEME_STORAGE_KEY = 'cps-base-theme-preference'; + private readonly RADIUS_THEME_STORAGE_KEY = 'cps-radius-theme-preference'; + private readonly TRANSITION_CLASS = 'cps-theme-transition'; + private readonly TRANSITION_DURATION = 500; + private transitionTimeout: ReturnType | null = null; + + private _theme = signal(this.getInitialTheme()); + private _colorTheme = signal(this.getInitialColorTheme()); + private _baseTheme = signal(this.getInitialBaseTheme()); + private _radiusTheme = signal(this.getInitialRadiusTheme()); + + /** + * Current active theme (readonly) + */ + readonly theme = this._theme.asReadonly(); + + /** + * Current active color theme (readonly) + */ + readonly colorTheme = this._colorTheme.asReadonly(); + + /** + * Current active base theme (readonly) + */ + readonly baseTheme = this._baseTheme.asReadonly(); + + /** + * Current active radius theme (readonly) + */ + readonly radiusTheme = this._radiusTheme.asReadonly(); + + /** + * Whether dark mode is currently active + */ + readonly isDark = computed(() => this._theme() === 'dark'); + + constructor() { + // Apply initial theme to DOM synchronously + this.applyCurrentTheme(); + + // Listen for system theme changes + this.watchSystemTheme(); + } + + /** + * Toggle between light and dark themes with smooth transition + */ + toggleTheme(): void { + const newTheme: CpsTheme = this._theme() === 'light' ? 'dark' : 'light'; + this.setTheme(newTheme); + } + + /** + * Set specific theme + * @param theme - Theme to apply ('light' or 'dark') + * @param animated - Whether to animate the transition (default: true) + */ + setTheme(theme: CpsTheme, animated = true): void { + if (this._theme() === theme) return; + + if (animated) { + this.enableTransition(); + } + + this._theme.set(theme); + this.saveThemePreference(theme); + this.applyCurrentTheme(); + + if (animated) { + this.scheduleDisableTransition(); + } + } + + /** + * Set specific color theme independently from mode + * @param colorTheme - Color theme to apply + * @param animated - Whether to animate the transition (default: true) + */ + setColorTheme(colorTheme: CpsColorTheme, animated = true): void { + if (this._colorTheme() === colorTheme) return; + + if (animated) { + this.enableTransition(); + } + + this._colorTheme.set(colorTheme); + this.saveColorThemePreference(colorTheme); + this.applyCurrentTheme(); + + if (animated) { + this.scheduleDisableTransition(); + } + } + + /** + * Set base background theme (primarily affects dark mode) + */ + setBaseTheme(baseTheme: CpsBaseTheme, animated = true): void { + if (this._baseTheme() === baseTheme) return; + + if (animated) { + this.enableTransition(); + } + + this._baseTheme.set(baseTheme); + this.saveBaseThemePreference(baseTheme); + this.applyCurrentTheme(); + + if (animated) { + this.scheduleDisableTransition(); + } + } + + /** + * Set radius profile + */ + setRadiusTheme(radiusTheme: CpsRadiusTheme, animated = true): void { + if (this._radiusTheme() === radiusTheme) return; + + if (animated) { + this.enableTransition(); + } + + this._radiusTheme.set(radiusTheme); + this.saveRadiusThemePreference(radiusTheme); + this.applyCurrentTheme(); + + if (animated) { + this.scheduleDisableTransition(); + } + } + + private applyCurrentTheme(): void { + this.document.documentElement.setAttribute('data-theme', this._theme()); + this.document.documentElement.setAttribute( + 'data-color-theme', + this._colorTheme() + ); + this.document.documentElement.setAttribute( + 'data-base-theme', + this._baseTheme() + ); + this.document.documentElement.setAttribute( + 'data-radius-theme', + this._radiusTheme() + ); + } + + private enableTransition(): void { + if (this.transitionTimeout) { + clearTimeout(this.transitionTimeout); + this.transitionTimeout = null; + } + this.document.documentElement.classList.add(this.TRANSITION_CLASS); + } + + private scheduleDisableTransition(): void { + this.transitionTimeout = setTimeout(() => { + this.document.documentElement.classList.remove(this.TRANSITION_CLASS); + this.transitionTimeout = null; + }, this.TRANSITION_DURATION); + } + + private getInitialTheme(): CpsTheme { + // Check saved preference first + const stored = localStorage.getItem( + this.THEME_STORAGE_KEY + ) as CpsTheme | null; + if (stored === 'light' || stored === 'dark') { + return stored; + } + + // Fall back to light mode + return 'light'; + } + + private getInitialColorTheme(): CpsColorTheme { + const stored = localStorage.getItem( + this.COLOR_THEME_STORAGE_KEY + ) as CpsColorTheme | null; + + if ( + stored === 'neutral' || + stored === 'calm' || + stored === 'energy' || + stored === 'passion' + ) { + return stored; + } + + return 'calm'; + } + + private getInitialBaseTheme(): CpsBaseTheme { + const stored = localStorage.getItem( + this.BASE_THEME_STORAGE_KEY + ) as CpsBaseTheme | null; + + if ( + stored === 'default' || + stored === 'graphite' || + stored === 'midnight' || + stored === 'aubergine' + ) { + return stored; + } + + return 'default'; + } + + private getInitialRadiusTheme(): CpsRadiusTheme { + const stored = localStorage.getItem( + this.RADIUS_THEME_STORAGE_KEY + ) as CpsRadiusTheme | null; + + if ( + stored === 'none' || + stored === 'compact' || + stored === 'rounded' || + stored === 'pill' + ) { + return stored; + } + + return 'compact'; + } + + // TODO: Use as fallback in getInitialTheme() once dark mode is fully supported across all components. + private getSystemTheme(): CpsTheme { + const win = this.document.defaultView; + if (!win?.matchMedia) return 'light'; + return win.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; + } + + // TODO: Enable system preference fallback once dark mode is fully supported across all components. + private watchSystemTheme(): void { + const win = this.document.defaultView; + if (!win?.matchMedia) return; + const mediaQuery = win.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', (e) => { + // Only auto-switch if user hasn't set a preference + if (!localStorage.getItem(this.THEME_STORAGE_KEY)) { + this.setTheme(e.matches ? 'dark' : 'light'); + } + }); + } + + private saveThemePreference(theme: CpsTheme): void { + localStorage.setItem(this.THEME_STORAGE_KEY, theme); + } + + private saveColorThemePreference(colorTheme: CpsColorTheme): void { + localStorage.setItem(this.COLOR_THEME_STORAGE_KEY, colorTheme); + } + + private saveBaseThemePreference(baseTheme: CpsBaseTheme): void { + localStorage.setItem(this.BASE_THEME_STORAGE_KEY, baseTheme); + } + + private saveRadiusThemePreference(radiusTheme: CpsRadiusTheme): void { + localStorage.setItem(this.RADIUS_THEME_STORAGE_KEY, radiusTheme); + } +} diff --git a/projects/cps-ui-kit/src/lib/utils/colors-utils.ts b/projects/cps-ui-kit/src/lib/utils/colors-utils.ts index 64d9701f..410e3167 100644 --- a/projects/cps-ui-kit/src/lib/utils/colors-utils.ts +++ b/projects/cps-ui-kit/src/lib/utils/colors-utils.ts @@ -20,12 +20,12 @@ const isDark = (color: string): boolean => { let g = 0; let b = 0; if (color.match(/^rgb/)) { - const colorMatched = color.match( - /^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/ - ) as any; - r = colorMatched[1]; - g = colorMatched[2]; - b = colorMatched[3]; + // Match both legacy rgba(r, g, b, a) and modern rgb(r g b / a) syntax + const colorMatched = color.match(/^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/); + if (!colorMatched) return false; + r = +colorMatched[1]; + g = +colorMatched[2]; + b = +colorMatched[3]; } else { const colorNum = +( '0x' + color.slice(1).replace(color.length < 5 && (/./g as any), '$&$&') @@ -42,6 +42,11 @@ const isDark = (color: string): boolean => { return hsp <= 127.5; }; +/** + * Collects all --cps-color-* CSS custom properties from :root rules only. + * Theme overrides (e.g. [data-theme='dark']) are excluded to avoid duplicates, + * since the Colors page serves as a base palette reference. + */ export const getCpsColors = (_document: Document): [string, string][] => [...(_document.styleSheets as any)] .filter((sheet: any) => @@ -50,16 +55,20 @@ export const getCpsColors = (_document: Document): [string, string][] => .reduce( (finalArr, sheet) => finalArr.concat( - [...sheet.cssRules].filter(isStyleRule).reduce((propValArr, rule) => { - const props = [...rule.style] - .map((propName) => [ - propName.trim(), - rule.style.getPropertyValue(propName).trim() - ]) - .filter(([propName]) => propName.indexOf('--cps-color') === 0); + [...sheet.cssRules] + .filter( + (rule: any) => isStyleRule(rule) && rule.selectorText === ':root' + ) + .reduce((propValArr, rule) => { + const props = [...rule.style] + .map((propName) => [ + propName.trim(), + rule.style.getPropertyValue(propName).trim() + ]) + .filter(([propName]) => propName.indexOf('--cps-color') === 0); - return [...propValArr, ...props]; - }, []) + return [...propValArr, ...props]; + }, []) ), [] ); diff --git a/projects/cps-ui-kit/src/public-api.ts b/projects/cps-ui-kit/src/public-api.ts index 0141399e..97b6370c 100644 --- a/projects/cps-ui-kit/src/public-api.ts +++ b/projects/cps-ui-kit/src/public-api.ts @@ -2,53 +2,53 @@ * Public API Surface of cps-ui-kit */ -export * from './lib/components/cps-icon/cps-icon.component'; -export * from './lib/components/cps-input/cps-input.component'; -export * from './lib/components/cps-select/cps-select.component'; -export * from './lib/components/cps-tree-select/cps-tree-select.component'; export * from './lib/components/cps-autocomplete/cps-autocomplete.component'; -export * from './lib/components/cps-tree-autocomplete/cps-tree-autocomplete.component'; -export * from './lib/components/cps-info-circle/cps-info-circle.component'; +export * from './lib/components/cps-button-toggle/cps-button-toggle.component'; export * from './lib/components/cps-button/cps-button.component'; export * from './lib/components/cps-checkbox/cps-checkbox.component'; -export * from './lib/components/cps-radio-group/cps-radio/cps-radio.component'; +export * from './lib/components/cps-chip/cps-chip.component'; +export * from './lib/components/cps-datepicker/cps-datepicker.component'; +export * from './lib/components/cps-divider/cps-divider.component'; +export * from './lib/components/cps-expansion-panel/cps-expansion-panel.component'; +export * from './lib/components/cps-file-upload/cps-file-upload.component'; +export * from './lib/components/cps-icon/cps-icon.component'; +export * from './lib/components/cps-info-circle/cps-info-circle.component'; +export * from './lib/components/cps-input/cps-input.component'; +export * from './lib/components/cps-loader/cps-loader.component'; +export * from './lib/components/cps-menu/cps-menu.component'; +export * from './lib/components/cps-paginator/cps-paginator.component'; +export * from './lib/components/cps-paginator/pipes/cps-paginate.pipe'; +export * from './lib/components/cps-progress-circular/cps-progress-circular.component'; +export * from './lib/components/cps-progress-linear/cps-progress-linear.component'; export * from './lib/components/cps-radio-group/cps-radio-group.component'; +export * from './lib/components/cps-radio-group/cps-radio/cps-radio.component'; +export * from './lib/components/cps-scheduler/cps-scheduler.component'; +export * from './lib/components/cps-select/cps-select.component'; +export * from './lib/components/cps-sidebar-menu/cps-sidebar-menu.component'; +export * from './lib/components/cps-switch/cps-switch.component'; +export * from './lib/components/cps-tab-group/cps-tab-group.component'; +export * from './lib/components/cps-tab-group/cps-tab/cps-tab.component'; +export * from './lib/components/cps-table/cps-column-filter-types'; export * from './lib/components/cps-table/cps-table.component'; -export * from './lib/components/cps-table/directives/cps-table-column-sortable.directive'; export * from './lib/components/cps-table/directives/cps-table-column-filter.directive'; export * from './lib/components/cps-table/directives/cps-table-column-resizable.directive'; +export * from './lib/components/cps-table/directives/cps-table-column-sortable.directive'; export * from './lib/components/cps-table/directives/cps-table-header-selectable.directive'; export * from './lib/components/cps-table/directives/cps-table-row-selectable.directive'; -export * from './lib/components/cps-table/cps-column-filter-types'; export * from './lib/components/cps-table/pipes/cps-table-detect-filter-type.pipe'; +export * from './lib/components/cps-tag/cps-tag.component'; +export * from './lib/components/cps-textarea/cps-textarea.component'; +export * from './lib/components/cps-timepicker/cps-timepicker.component'; +export * from './lib/components/cps-tree-autocomplete/cps-tree-autocomplete.component'; +export * from './lib/components/cps-tree-select/cps-tree-select.component'; export * from './lib/components/cps-tree-table/cps-tree-table.component'; -export * from './lib/components/cps-tree-table/directives/cps-tree-table-column-sortable.directive'; export * from './lib/components/cps-tree-table/directives/cps-tree-table-column-filter.directive'; export * from './lib/components/cps-tree-table/directives/cps-tree-table-column-resizable.directive'; -export * from './lib/components/cps-tree-table/directives/cps-tree-table-row-toggler.directive'; +export * from './lib/components/cps-tree-table/directives/cps-tree-table-column-sortable.directive'; export * from './lib/components/cps-tree-table/directives/cps-tree-table-header-selectable.directive'; export * from './lib/components/cps-tree-table/directives/cps-tree-table-row-selectable.directive'; +export * from './lib/components/cps-tree-table/directives/cps-tree-table-row-toggler.directive'; export * from './lib/components/cps-tree-table/pipes/cps-tree-table-detect-filter-type.pipe'; -export * from './lib/components/cps-tag/cps-tag.component'; -export * from './lib/components/cps-chip/cps-chip.component'; -export * from './lib/components/cps-menu/cps-menu.component'; -export * from './lib/components/cps-paginator/cps-paginator.component'; -export * from './lib/components/cps-paginator/pipes/cps-paginate.pipe'; -export * from './lib/components/cps-loader/cps-loader.component'; -export * from './lib/components/cps-expansion-panel/cps-expansion-panel.component'; -export * from './lib/components/cps-progress-circular/cps-progress-circular.component'; -export * from './lib/components/cps-progress-linear/cps-progress-linear.component'; -export * from './lib/components/cps-datepicker/cps-datepicker.component'; -export * from './lib/components/cps-sidebar-menu/cps-sidebar-menu.component'; -export * from './lib/components/cps-textarea/cps-textarea.component'; -export * from './lib/components/cps-button-toggle/cps-button-toggle.component'; -export * from './lib/components/cps-tab-group/cps-tab-group.component'; -export * from './lib/components/cps-tab-group/cps-tab/cps-tab.component'; -export * from './lib/components/cps-timepicker/cps-timepicker.component'; -export * from './lib/components/cps-file-upload/cps-file-upload.component'; -export * from './lib/components/cps-scheduler/cps-scheduler.component'; -export * from './lib/components/cps-switch/cps-switch.component'; -export * from './lib/components/cps-divider/cps-divider.component'; export * from './lib/directives/cps-tooltip/cps-tooltip.directive'; @@ -59,4 +59,5 @@ export * from './lib/services/cps-dialog/utils/cps-dialog-ref'; export * from './lib/services/cps-notification/cps-notification.service'; export * from './lib/services/cps-notification/utils/cps-notification-config'; +export * from './lib/services/cps-theme/cps-theme.service'; export * from './lib/utils/colors-utils'; diff --git a/projects/cps-ui-kit/styles/_colors-dark.scss b/projects/cps-ui-kit/styles/_colors-dark.scss new file mode 100644 index 00000000..2abc1d6f --- /dev/null +++ b/projects/cps-ui-kit/styles/_colors-dark.scss @@ -0,0 +1,184 @@ +// Dark theme color palette +// Only overrides variables that differ from the light theme (:root). +// All brand palette, darks, and state variant colors are inherited from :root. + +[data-theme='dark'] { + // Brand color overrides (base values only — variants inherited from :root) + --cps-color-energy: #f47721; + --cps-color-care: #f05a78; + + // States backgrounds + --cps-color-info-bg: rgba(73, 185, 255, 0.16); + --cps-color-success-bg: rgba(93, 211, 58, 0.16); + --cps-color-warn-bg: rgba(255, 186, 48, 0.16); + --cps-color-error-bg: rgba(255, 90, 103, 0.16); + + // Highlights + --cps-color-highlight-hover: rgba(251, 251, 251, 0.06); + --cps-color-highlight-active: rgba(251, 251, 251, 0.1); + --cps-color-highlight-selected: rgba(244, 119, 33, 0.16); + --cps-color-highlight-selected-dark: rgba(244, 119, 33, 0.24); + + // Backgrounds + --cps-color-bg-lightest: #2a2a31; + --cps-color-bg-light: #1f1f24; + --cps-color-bg-mid: #1b1b1e; + --cps-color-bg-dark: #151419; + + // Lines + --cps-color-line-light: #3f3f46; + --cps-color-line-mid: #50505a; + --cps-color-line-dark: #666670; + --cps-color-line-darkest: #7a7a86; + + // Text + --cps-color-text-lightest: #fbfbfb; + --cps-color-text-light: #d0d0d2; + --cps-color-text-mild: #a8a8ad; + --cps-color-text-dark: #878787; + --cps-color-text-darkest: #151419; + + // Semantic design tokens + --cps-accent-primary: var(--cps-color-energy); + --cps-accent-primary-contrast: #151419; + --cps-accent-secondary: #fbfbfb; + --cps-accent-secondary-contrast: #151419; + --cps-error-background: rgba(255, 90, 103, 0.16); + + // Lines and borders + --cps-color-line: rgba(251, 251, 251, 0.12); + --cps-surface-body: #151419; + --cps-surface-highlight: #1b1b1e; + --cps-surface-muted: #202028; + --cps-surface-elevated: #262626; + --cps-surface-control: #262626; + --cps-surface-overlay: rgba(21, 20, 25, 0.85); + --cps-background-color: #151419; + --cps-background-disabled: rgba(255, 255, 255, 0.05); + + // Tab component + --cps-tab-subtabs-background: var(--cps-surface-highlight); + --cps-tabs-subtabs-active-background: var(--cps-color-highlight-selected); + --cps-tabs-subtabs-background-hover: var(--cps-highlight-hover); + --cps-tabs-subtabs-text-hover: var(--cps-text-primary); + + // Text + --cps-text-primary: #fbfbfb; + --cps-text-secondary: #d0d0d2; + --cps-text-muted: #878787; + --cps-text-inverse: var(--cps-color-depth-darken4); + --cps-text-on-accent: #151419; + --cps-text-disabled: #6f6f74; + + --cps-border-color: #3a3a3a; + --cps-border-strong: #505050; + --cps-border-focus: var(--cps-accent-primary); + + --cps-highlight-hover: rgba(251, 251, 251, 0.1); + --cps-highlight-active: rgba(251, 251, 251, 0.16); + --cps-highlight-selected: var(--cps-color-highlight-selected-dark); + + --cps-state-info: #49b9ff; + --cps-state-info-contrast: #151419; + --cps-state-info-surface: rgba(73, 185, 255, 0.16); + --cps-state-success: #5dd33a; + --cps-state-success-contrast: #151419; + --cps-state-success-surface: rgba(93, 211, 58, 0.16); + --cps-state-warn: #ffba30; + --cps-state-warn-contrast: #151419; + --cps-state-warn-surface: rgba(255, 186, 48, 0.16); + --cps-state-error: var(--cps-color-error); + --cps-state-error-contrast: #ffffff; + --cps-state-error-surface: rgba(185, 28, 28, 0.28); + + // Modern semantic surfaces + --cps-card-background: var(--cps-surface-highlight); + --cps-card-foreground: var(--cps-text-primary); + --cps-popover-background: var(--cps-surface-elevated); + --cps-popover-foreground: var(--cps-text-primary); + --cps-input-background: var(--cps-surface-highlight); + --cps-input-foreground: var(--cps-text-primary); + --cps-input-placeholder: var(--cps-text-muted); + + // Focus ring and outlines + --cps-ring-color: var(--cps-border-focus); + --cps-ring-offset-color: var(--cps-surface-body); + + // Elevation shadows (darker than light theme) + --cps-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.3); + --cps-shadow-sm: 0 4px 10px rgba(0, 0, 0, 0.34); + --cps-shadow-md: 0 12px 24px rgba(0, 0, 0, 0.4); + --cps-shadow-lg: 0 24px 48px rgba(0, 0, 0, 0.5); +} + +[data-theme='dark'][data-color-theme='neutral'] { + --cps-accent-primary: #e4e4e7; + --cps-accent-primary-contrast: #151419; + --cps-accent-secondary: #f4f4f5; + --cps-accent-secondary-contrast: #151419; + --cps-border-focus: #e4e4e7; + --cps-highlight-selected: rgba(228, 228, 231, 0.22); +} + +[data-theme='dark'][data-color-theme='calm'] { + --cps-accent-primary: var(--cps-color-calm-lighten4); + --cps-accent-primary-contrast: #151419; + --cps-accent-secondary: var(--cps-color-care-lighten3); + --cps-accent-secondary-contrast: #151419; + --cps-border-focus: var(--cps-color-calm-lighten4); + --cps-highlight-selected: rgba(253, 128, 158, 0.28); +} + +[data-theme='dark'][data-color-theme='energy'] { + --cps-accent-primary: var(--cps-color-energy); + --cps-accent-primary-contrast: #151419; + --cps-accent-secondary: var(--cps-color-prepared-lighten4); + --cps-accent-secondary-contrast: #151419; + --cps-border-focus: var(--cps-color-energy); + --cps-highlight-selected: var(--cps-color-highlight-selected-dark); +} + +[data-theme='dark'][data-color-theme='passion'] { + --cps-accent-primary: var(--cps-color-passion-lighten3); + --cps-accent-primary-contrast: #151419; + --cps-accent-secondary: var(--cps-color-care-lighten4); + --cps-accent-secondary-contrast: #151419; + --cps-border-focus: var(--cps-color-passion-lighten3); + --cps-highlight-selected: rgba(255, 120, 121, 0.3); +} + +[data-theme='dark'][data-base-theme='graphite'] { + --cps-surface-body: #121216; + --cps-surface-highlight: #191a20; + --cps-surface-muted: #1d1f26; + --cps-surface-elevated: #23252f; + --cps-surface-control: #23252f; + --cps-background-color: #121216; + --cps-border-color: #3f424d; + --cps-border-strong: #555968; + --cps-ring-offset-color: #121216; +} + +[data-theme='dark'][data-base-theme='midnight'] { + --cps-surface-body: #0f1624; + --cps-surface-highlight: #172033; + --cps-surface-muted: #1b263c; + --cps-surface-elevated: #21304a; + --cps-surface-control: #21304a; + --cps-background-color: #0f1624; + --cps-border-color: #32445f; + --cps-border-strong: #4a6388; + --cps-ring-offset-color: #0f1624; +} + +[data-theme='dark'][data-base-theme='aubergine'] { + --cps-surface-body: #1a1320; + --cps-surface-highlight: #231a2c; + --cps-surface-muted: #2b2136; + --cps-surface-elevated: #332943; + --cps-surface-control: #332943; + --cps-background-color: #1a1320; + --cps-border-color: #534363; + --cps-border-strong: #6d5683; + --cps-ring-offset-color: #1a1320; +} diff --git a/projects/cps-ui-kit/styles/_colors.scss b/projects/cps-ui-kit/styles/_colors.scss index c63e637a..5a540f05 100644 --- a/projects/cps-ui-kit/styles/_colors.scss +++ b/projects/cps-ui-kit/styles/_colors.scss @@ -230,7 +230,7 @@ --cps-color-warn-darken3: #a15300; --cps-color-warn-darken4: #843b00; - --cps-color-error: #cc3333; + --cps-color-error: #b91c1c; --cps-color-error-highlighten: #f9e4e5; --cps-color-error-lighten5: #f5d9d9; --cps-color-error-lighten4: #f2cece; @@ -272,4 +272,162 @@ --cps-color-text-mild: #787272; --cps-color-text-dark: #524a4a; --cps-color-text-darkest: #2d2323; + + // Semantic design tokens (use these inside components) + --cps-accent-primary: var(--cps-color-passion); + --cps-accent-primary-contrast: #ffffff; + --cps-accent-secondary: var(--cps-color-energy); + --cps-accent-secondary-contrast: #2d2323; + --cps-error-background: #fef3f2; + + // Lines and borders + --cps-color-line: #e3e2e2; + + --cps-surface-body: var(--cps-color-bg-light); + --cps-surface-highlight: #ffffff; + --cps-surface-muted: var(--cps-color-bg-lightest); + --cps-surface-elevated: var(--cps-color-bg-mid); + --cps-surface-control: #ffffff; + --cps-surface-overlay: rgba(0, 0, 0, 0.45); + --cps-background-color: var(--cps-color-bg-lightest); + --cps-background-disabled: #f7f7f7; + + // Tab component + --cps-tab-subtabs-background: #d7d7d759; + --cps-tabs-subtabs-active-background: #fff; + --cps-tabs-subtabs-background-hover: var(--cps-highlight-active); + --cps-tabs-subtabs-text-hover: var(--cps-accent-primary); + + --cps-text-primary: var(--cps-color-text-darkest); + --cps-text-secondary: var(--cps-color-text-dark); + --cps-text-muted: var(--cps-color-text-mild); + --cps-text-inverse: var(--cps-color-depth-darken4); + --cps-text-on-accent: #ffffff; + --cps-text-disabled: var(--cps-color-text-light); + + --cps-border-color: var(--cps-color-line-mid); + --cps-border-strong: var(--cps-color-line-dark); + --cps-border-focus: var(--cps-color-passion); + + --cps-highlight-hover: var(--cps-color-highlight-hover); + --cps-highlight-active: var(--cps-color-highlight-active); + --cps-highlight-selected: var(--cps-color-highlight-selected); + + --cps-state-info: var(--cps-color-info); + --cps-state-info-contrast: #2d2323; + --cps-state-info-surface: var(--cps-color-info-bg); + --cps-state-success: var(--cps-color-success); + --cps-state-success-contrast: #2d2323; + --cps-state-success-surface: var(--cps-color-success-bg); + --cps-state-warn: var(--cps-color-warn); + --cps-state-warn-contrast: #3b2500; + --cps-state-warn-surface: var(--cps-color-warn-bg); + --cps-state-error: var(--cps-color-error); + --cps-state-error-contrast: #ffffff; + --cps-state-error-surface: rgba(185, 28, 28, 0.14); + + // Modern semantic surfaces + --cps-card-background: var(--cps-surface-highlight); + --cps-card-foreground: var(--cps-text-primary); + --cps-popover-background: var(--cps-surface-control); + --cps-popover-foreground: var(--cps-text-primary); + --cps-input-background: var(--cps-surface-control); + --cps-input-foreground: var(--cps-text-primary); + --cps-input-placeholder: var(--cps-text-muted); + + // Focus ring and outlines + --cps-ring-color: var(--cps-border-focus); + --cps-ring-offset-color: var(--cps-surface-body); + + // Radius scale + --cps-radius-xs: 2px; + --cps-radius-sm: 4px; + --cps-radius-md: 8px; + --cps-radius-lg: 12px; + --cps-radius-xl: 16px; + --cps-radius-full: 9999px; + + // Backward compatible radius aliases + --cps-border-radius-small: var(--cps-radius-sm); + --cps-border-radius-medium: var(--cps-radius-md); + --cps-border-radius-large: var(--cps-radius-lg); + --cps-border-radius-round: var(--cps-radius-full); + + // Elevation shadows + --cps-shadow-xs: 0 1px 2px rgba(45, 35, 35, 0.08); + --cps-shadow-sm: 0 2px 6px rgba(45, 35, 35, 0.12); + --cps-shadow-md: 0 8px 20px rgba(45, 35, 35, 0.14); + --cps-shadow-lg: 0 16px 40px rgba(45, 35, 35, 0.2); + + // Motion tokens + --cps-motion-fast: 120ms; + --cps-motion-base: 180ms; + --cps-motion-slow: 260ms; + --cps-motion-easing: cubic-bezier(0.2, 0, 0, 1); +} + +:root[data-color-theme='neutral'] { + --cps-accent-primary: #27272a; + --cps-accent-primary-contrast: #ffffff; + --cps-accent-secondary: #3f3f46; + --cps-accent-secondary-contrast: #ffffff; + --cps-border-focus: #27272a; + --cps-highlight-selected: rgba(39, 39, 42, 0.14); +} + +:root[data-color-theme='calm'] { + --cps-accent-primary: var(--cps-color-calm); + --cps-accent-primary-contrast: #ffffff; + --cps-accent-secondary: var(--cps-color-care); + --cps-accent-secondary-contrast: #2d2323; + --cps-border-focus: var(--cps-color-calm); + --cps-highlight-selected: rgba(135, 10, 60, 0.16); +} + +:root[data-color-theme='energy'] { + --cps-accent-primary: var(--cps-color-energy); + --cps-accent-primary-contrast: #2d2323; + --cps-text-on-accent: #2d2323; + --cps-accent-secondary: var(--cps-color-prepared); + --cps-accent-secondary-contrast: #2d2323; + --cps-border-focus: var(--cps-color-energy); + --cps-highlight-selected: var(--cps-color-highlight-selected); +} + +:root[data-color-theme='passion'] { + --cps-accent-primary: var(--cps-color-passion); + --cps-accent-primary-contrast: #ffffff; + --cps-accent-secondary: var(--cps-color-warmth); + --cps-accent-secondary-contrast: #ffffff; + --cps-border-focus: var(--cps-color-passion); + --cps-highlight-selected: rgba(220, 0, 50, 0.16); +} + +:root[data-radius-theme='none'] { + --cps-radius-xs: 0px; + --cps-radius-sm: 0px; + --cps-radius-md: 0px; + --cps-radius-lg: 0px; + --cps-radius-xl: 0px; +} + +:root[data-radius-theme='compact'] { + --cps-radius-sm: 3px; + --cps-radius-md: 6px; + --cps-radius-lg: 10px; + --cps-radius-xl: 14px; +} + +:root[data-radius-theme='rounded'] { + --cps-radius-sm: 6px; + --cps-radius-md: 10px; + --cps-radius-lg: 14px; + --cps-radius-xl: 20px; +} + +:root[data-radius-theme='pill'] { + --cps-radius-sm: 9999px; + --cps-radius-md: 9999px; + --cps-radius-lg: 9999px; + --cps-radius-xl: 9999px; } diff --git a/projects/cps-ui-kit/styles/styles.scss b/projects/cps-ui-kit/styles/styles.scss index 79e63a09..84dfb7d1 100644 --- a/projects/cps-ui-kit/styles/styles.scss +++ b/projects/cps-ui-kit/styles/styles.scss @@ -1,5 +1,6 @@ @use './_bootstrap-grid'; @use './_colors.scss'; +@use './_colors-dark.scss'; @use './_fonts.scss'; @use './_cps-tooltip-style.scss'; @use 'primeicons/primeicons.css'; @@ -18,3 +19,38 @@ position: absolute; top: -9999px; } + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} +::-webkit-scrollbar-track { + background: var(--cps-background-color); +} +::-webkit-scrollbar-thumb { + background: var(--cps-border-color); + border-radius: var(--cps-radius-sm); +} +::-webkit-scrollbar-thumb:hover { + background: var(--cps-accent-primary); +} + +html, +body { + font-family: 'Source Sans Pro', sans-serif; + background: var(--cps-background-color); + color: var(--cps-text-primary); +} + +// Theme transition hook used by CpsThemeService +.cps-theme-transition, +.cps-theme-transition *, +.cps-theme-transition *::before, +.cps-theme-transition *::after { + transition: + background-color var(--cps-motion-base) var(--cps-motion-easing), + border-color var(--cps-motion-base) var(--cps-motion-easing), + color var(--cps-motion-base) var(--cps-motion-easing), + box-shadow var(--cps-motion-base) var(--cps-motion-easing) !important; +}