| Name |
-
+
Name
{{ formArray.at(i).get('name')?.errors | errorMessage }}
@@ -21,7 +21,7 @@ NaturalAbstractEditableList
Description |
-
+
Description
{{ formArray.at(i).get('description')?.errors | errorMessage }}
@@ -46,12 +46,12 @@ NaturalAbstractEditableList
@for (element of dataSource.data; track element) {
-
+
Name
{{ formArray.at($index).get('name')?.errors | errorMessage }}
-
+
Description
{{ formArray.at($index).get('description')?.errors | errorMessage }}
diff --git a/src/app/editable-list/editable-list.component.scss b/src/app/editable-list/editable-list.component.scss
index e69de29b..1f580b29 100644
--- a/src/app/editable-list/editable-list.component.scss
+++ b/src/app/editable-list/editable-list.component.scss
@@ -0,0 +1,3 @@
+mat-form-field {
+ margin-block: 2px;
+}
diff --git a/src/app/editor/editor.component.scss b/src/app/editor/editor.component.scss
index 5f41e5f6..0d1ef73a 100644
--- a/src/app/editor/editor.component.scss
+++ b/src/app/editor/editor.component.scss
@@ -1,5 +1,5 @@
.preview {
border-radius: 8px;
- background: rgba(0, 0, 0, 0.1);
+ background-color: var(--mat-sys-surface-container-lowest);
padding: 20px 30px;
}
diff --git a/src/app/file/file.component.scss b/src/app/file/file.component.scss
index 4b235500..a85631fb 100644
--- a/src/app/file/file.component.scss
+++ b/src/app/file/file.component.scss
@@ -1,8 +1,4 @@
-a[naturalFileDrop] {
- border: 1px dashed #a541c1;
-}
-
.natural-file-over {
- border-color: pink;
- background-color: #00b0ff;
+ background-color: var(--mat-sys-secondary-container);
+ color: var(--mat-sys-on-secondary-container);
}
diff --git a/src/app/hierarchic/hierarchic.component.ts b/src/app/hierarchic/hierarchic.component.ts
index a8f31af4..622a28ef 100644
--- a/src/app/hierarchic/hierarchic.component.ts
+++ b/src/app/hierarchic/hierarchic.component.ts
@@ -84,7 +84,9 @@ export class HierarchicComponent {
.afterClosed()
.subscribe((result?: HierarchicDialogResult) => {
console.log('dialog usage', result);
- this.selected = result?.hierarchicSelection ?? {};
+ if (result !== undefined) {
+ this.selected = result.hierarchicSelection ?? {};
+ }
});
}
}
diff --git a/src/app/home/_home.theme.scss b/src/app/home/_home.theme.scss
deleted file mode 100644
index 87939057..00000000
--- a/src/app/home/_home.theme.scss
+++ /dev/null
@@ -1,35 +0,0 @@
-@use 'sass:map';
-@use '@angular/material' as mat;
-
-@mixin home($theme) {
- $primary: map.get($theme, primary);
- $accent: map.get($theme, accent);
- $warn: map.get($theme, warn);
-
- app-home {
- .mat-toolbar {
- background: #212121;
- color: #fff;
- }
-
- #menu {
- border-radius: 4px;
- background: mat.m2-get-color-from-palette($primary);
- background: -moz-linear-gradient(
- bottom,
- mat.m2-get-color-from-palette($primary) 50%,
- mat.m2-get-color-from-palette($accent) 150%
- ); /* FF3.6-15 */
- background: -webkit-linear-gradient(
- bottom,
- mat.m2-get-color-from-palette($primary) 50%,
- mat.m2-get-color-from-palette($accent) 150%
- ); /* Chrome10-25,Safari5.1-6 */
- background: linear-gradient(
- to bottom,
- mat.m2-get-color-from-palette($primary) 50%,
- mat.m2-get-color-from-palette($accent) 150%
- ); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
- }
- }
-}
diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html
index 54682d5e..132701c3 100644
--- a/src/app/home/home.component.html
+++ b/src/app/home/home.component.html
@@ -1,160 +1,141 @@
-
-
- @if (menu) {
-
- }
-
-
-
- @ecodev/natural
-
-
-
-
-
+
+
+
+ @ecodev/natural
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Home
+
+
+
+
+ Theme merger
+
+
+
+
+ Search
+
+
+
+
+ List A
+
+
+
+
+ List B
+
+
+
+
+ Editable list
+
+
+
+
+ Table style
+
+
+
+
+ Navigable list
+
+
+
+
+ Detail
+
+
+
+
+ Detail header
+
+
+
+
+ Select
+
+
+
+
+ Select hierarchic
+
+
+
+
+ Select Enum
+
+
+
+
+ Relation
+
+
+
+
+ Hierarchic
+
+
+
+
+ Panels
+
+
+
+
+ File
+
+
+
+
+ Alert service
+
+
+
+
+ Avatar
+
+
+
+
+ Editor
+
+
+
+
+ Other tools
+
+
+
+
+
+
+
diff --git a/src/app/home/home.component.scss b/src/app/home/home.component.scss
index 2ae26e71..b7a743f2 100644
--- a/src/app/home/home.component.scss
+++ b/src/app/home/home.component.scss
@@ -1,4 +1,55 @@
+@use '@angular/material' as mat;
+
:host {
display: flex;
flex-direction: column;
+ @include mat.sidenav-overrides(
+ (
+ container-width: auto,
+ )
+ );
+}
+
+.theme-choice {
+ border-radius: 999px;
+ background: var(--mat-sys-primary);
+ padding: 5px;
+
+ @include mat.theme-overrides(
+ (
+ on-surface-variant: var(--mat-sys-on-primary),
+ )
+ );
+}
+
+mat-toolbar {
+ @include mat.toolbar-overrides(
+ (
+ container-background-color: var(--mat-sys-surface-container),
+ )
+ );
+}
+
+natural-sidenav-content {
+ border-top-left-radius: var(--mat-sys-corner-large);
+ background-color: var(--mat-sys-surface-bright);
+ padding: 20px;
+ min-height: calc(100vh - 130px);
+ overflow: auto;
+}
+
+mat-nav-list {
+ margin: 0 20px;
+ .active {
+ @include mat.list-overrides(
+ (
+ list-item-container-color: var(--mat-sys-primary),
+ list-item-label-text-color: var(--mat-sys-on-primary),
+ list-item-hover-label-text-color: var(--mat-sys-on-primary),
+ list-item-focus-label-text-color: var(--mat-sys-on-primary),
+ list-item-leading-icon-color: var(--mat-sys-on-primary),
+ list-item-hover-leading-icon-color: var(--mat-sys-on-primary),
+ )
+ );
+ }
}
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts
index cea772db..e784341a 100644
--- a/src/app/home/home.component.ts
+++ b/src/app/home/home.component.ts
@@ -1,15 +1,14 @@
-import {Component, DOCUMENT, effect, inject} from '@angular/core';
+import {Component, inject} from '@angular/core';
import {MatButton, MatIconButton} from '@angular/material/button';
-import {MatAccordion, MatExpansionPanel} from '@angular/material/expansion';
import {MatIcon} from '@angular/material/icon';
-import {MatNavList, MatListItem, MatListItemIcon} from '@angular/material/list';
+import {MatListItem, MatListItemIcon, MatNavList} from '@angular/material/list';
import {MatToolbar} from '@angular/material/toolbar';
-import {RouterLink, RouterOutlet} from '@angular/router';
+import {Router, RouterLink, RouterLinkActive, RouterOutlet} from '@angular/router';
+import {NaturalColorSchemerComponent, NaturalThemeChangerComponent} from '@ecodev/natural';
import {NaturalIconDirective} from '../../../projects/natural/src/lib/modules/icon/icon.directive';
import {NaturalSidenavContainerComponent} from '../../../projects/natural/src/lib/modules/sidenav/sidenav-container/sidenav-container.component';
import {NaturalSidenavContentComponent} from '../../../projects/natural/src/lib/modules/sidenav/sidenav-content/sidenav-content.component';
import {NaturalSidenavComponent} from '../../../projects/natural/src/lib/modules/sidenav/sidenav/sidenav.component';
-import {allThemes, ThemeService} from '../shared/services/theme.service';
@Component({
selector: 'app-home',
@@ -21,32 +20,23 @@ import {allThemes, ThemeService} from '../shared/services/theme.service';
NaturalIconDirective,
NaturalSidenavContainerComponent,
NaturalSidenavComponent,
- MatAccordion,
- MatExpansionPanel,
MatNavList,
MatListItem,
MatListItemIcon,
RouterLink,
NaturalSidenavContentComponent,
RouterOutlet,
+ RouterLinkActive,
+ NaturalColorSchemerComponent,
+ NaturalThemeChangerComponent,
],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
})
export class HomeComponent {
- public readonly themeService = inject(ThemeService);
- private readonly document = inject(DOCUMENT);
+ public readonly router = inject(Router);
- public constructor() {
- effect(() => {
- // Remove old theme class
- allThemes.forEach(theme => {
- this.document.body.classList.remove(theme);
- });
-
- // set new theme class
- const newTheme = this.themeService.theme();
- this.document.body.classList.add(newTheme);
- });
+ protected isActive(url: string): boolean {
+ return this.router.url === url;
}
}
diff --git a/src/app/list/list.component.html b/src/app/list/list.component.html
index ffc7b003..5b394c98 100644
--- a/src/app/list/list.component.html
+++ b/src/app/list/list.component.html
@@ -25,6 +25,7 @@ NaturalAbstractList
+
@@ -41,11 +42,13 @@ NaturalAbstractList
(click)="$event.stopPropagation()"
/>
+ | |
id |
{{ element.id }} |
+ / |
@@ -53,20 +56,13 @@ NaturalAbstractList
|
+ name |
- description |
- {{
- element.description
- }} |
+ description |
+ {{ element.description }} |
+ description |
@@ -81,6 +77,7 @@ NaturalAbstractList
visible but hidden in dropdown |
+ hidden |
@@ -95,6 +92,7 @@ NaturalAbstractList
should never be visible, even if solicited by url
|
+ / |
@@ -114,7 +112,7 @@ NaturalAbstractList
(page)="pagination($event)"
/>
-
+
columnsForTable : {{ columnsForTable | json }}
selectedColumns : {{ selectedColumns | json }}
diff --git a/src/app/list/list.component.ts b/src/app/list/list.component.ts
index 6d3785d5..82e2fc41 100644
--- a/src/app/list/list.component.ts
+++ b/src/app/list/list.component.ts
@@ -15,6 +15,10 @@ import {
MatCell,
MatHeaderRow,
MatRow,
+ MatFooterCell,
+ MatFooterRow,
+ MatFooterCellDef,
+ MatFooterRowDef,
} from '@angular/material/table';
import {AvailableColumn, Button, NaturalAbstractList, Sorting, SortingOrder} from '@ecodev/natural';
import {NaturalColumnsPickerComponent} from '../../../projects/natural/src/lib/modules/columns-picker/columns-picker.component';
@@ -31,12 +35,16 @@ import {ItemService} from '../../../projects/natural/src/lib/testing/item.servic
MatTable,
MatHeaderCellDef,
MatHeaderRowDef,
+ MatFooterCellDef,
+ MatFooterRowDef,
MatColumnDef,
MatCellDef,
MatRowDef,
MatHeaderCell,
+ MatFooterCell,
MatCell,
MatHeaderRow,
+ MatFooterRow,
MatRow,
MatSort,
MatSortHeader,
diff --git a/src/app/relations/relations.component.html b/src/app/relations/relations.component.html
index e12ca4f9..5767d806 100644
--- a/src/app/relations/relations.component.html
+++ b/src/app/relations/relations.component.html
@@ -4,9 +4,9 @@ natural-relations
@if (isUpdatePage()) {
-
-
- Hierarchic selector
+
+
+ Hierarchic selector
natural-relations
(selectionChange)="relationsAdded('Hierarchic selector')"
/>
-
- Service
+
+ Service
natural-relations
(selectionChange)="relationsAdded('Service')"
/>
-
- No result service
+
+ No result service
natural-relations
(selectionChange)="relationsAdded('NoResultService')"
/>
-
- ErrorService
+
+ ErrorService
div {
- natural-relations {
- border-radius: 4px;
- background: rgba(0, 0, 0, 0.1);
- padding: 20px;
- }
- }
-}
diff --git a/src/app/search/search.component.html b/src/app/search/search.component.html
index bebf1ba4..730dc102 100644
--- a/src/app/search/search.component.html
+++ b/src/app/search/search.component.html
@@ -2,7 +2,7 @@ natural-search
(() => (this.isDark() ? 'defaultDark' : 'default'));
-
- public constructor() {
- const theme = this.storage.getItem('theme') as Theme | null;
- this.set(!!theme?.includes('Dark'));
- }
-
- public set(isDark: boolean): void {
- this.isDark.set(isDark);
- this.storage.setItem('theme', this.theme());
- }
-
- public toggle(): void {
- this.set(!this.isDark());
- }
-}
diff --git a/src/app/theme-merger/theme-merger.component.html b/src/app/theme-merger/theme-merger.component.html
new file mode 100644
index 00000000..c545fb6f
--- /dev/null
+++ b/src/app/theme-merger/theme-merger.component.html
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+ Primary
+
+
+
+ Secondary
+
+
+
+ Tertiary
+
+
+
+ link
+
+
+ @if (theme1) {
+
+ arrow_right_alt
+
+
+
+ Theme name
+
+
+
+ content_copy
+ Default
+
+
+ content_copy
+ Alternative
+
+
+
+ {{ showPropertyNames ? 'visibility_off' : 'visibility' }}
+
+ } @else {
+
+
+ upload
+ Pick theme
+
+
+ }
+
+
+
+@if (theme1) {
+
+ @if (showPropertyNames) {
+
+
+ @for (tokenName of getTokenNames(theme1); track $index) {
+ {{ camelToKebab(tokenName) }}
+ }
+
+ }
+
+
+
+
+ Theme 1 - Light
+
+
+
+ upload
+ {{ theme1.name }}
+
+
+ @for (variation of getVariationsForTheme(1); track variation) {
+
+ {{ getVariationLabel(variation) }}
+
+ }
+
+
+ @for (tokenName of getTokenNames(theme1); track $index) {
+
+ @if (tokenName) {
+ @for (item of getTokensForName(theme1, tokenName); track item.variation) {
+
+ }
+ }
+
+ }
+
+
+
+
+
+ {{ theme2 ? 'Theme 2' : 'Theme 1' }} - Dark
+
+
+
+
+ upload
+ {{ theme2?.name || 'Theme 2' }}
+
+ @if (theme2) {
+
+ close
+
+ }
+
+
+ @for (variation of getVariationsForTheme(2); track variation) {
+
+ {{ getVariationLabel(variation) }}
+
+ }
+
+
+
+ @for (tokenName of getTokenNames(getRightTheme()); track $index) {
+
+ @if (tokenName) {
+ @for (item of getTokensForNameRight(tokenName); track item.variation) {
+
+ }
+ }
+
+ }
+
+
+}
diff --git a/src/app/theme-merger/theme-merger.component.scss b/src/app/theme-merger/theme-merger.component.scss
new file mode 100644
index 00000000..8e6da4b3
--- /dev/null
+++ b/src/app/theme-merger/theme-merger.component.scss
@@ -0,0 +1,81 @@
+@use '@angular/material' as mat;
+
+.container {
+ border-radius: var(--mat-sys-corner-extra-large);
+ background: var(--mat-sys-surface-bright);
+}
+
+.theme-merger-container {
+ margin: 0 auto;
+ padding: 2rem;
+ max-width: 1400px;
+
+ h1 {
+ margin-bottom: 2rem;
+ text-align: center;
+ }
+}
+
+input[type='file'] {
+ display: none;
+}
+
+.generate-section {
+ margin-bottom: 2rem;
+ padding: 1.5rem;
+
+ .generate-controls {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+
+ label {
+ font-weight: 600;
+ font-size: 0.95rem;
+ }
+
+ .theme-name-input {
+ padding: 0.6rem 1rem;
+ min-width: 250px;
+ font-size: 0.95rem;
+
+ &:focus {
+ outline: none;
+ }
+ }
+ }
+}
+
+.tokens-display {
+ button {
+ margin-bottom: 0.5rem;
+ }
+
+ .tokens-column {
+ padding: 20px;
+ width: auto;
+ color: var(--mat-sys-on-surface);
+
+ &.color-column {
+ background: var(--mat-sys-surface);
+ }
+ .token-row {
+ display: flex;
+ gap: 5px;
+ margin-bottom: 5px;
+ min-height: 30px;
+ }
+
+ .token-name {
+ align-items: center;
+ font: var(--mat-sys-title-medium);
+ }
+
+ .column-header {
+ margin-bottom: 1rem;
+ height: 140px;
+ min-height: 4rem;
+ }
+ }
+}
diff --git a/src/app/theme-merger/theme-merger.component.ts b/src/app/theme-merger/theme-merger.component.ts
new file mode 100644
index 00000000..a3e0f398
--- /dev/null
+++ b/src/app/theme-merger/theme-merger.component.ts
@@ -0,0 +1,732 @@
+import {CommonModule} from '@angular/common';
+import {Component, DOCUMENT, ElementRef, inject, OnInit, viewChild} from '@angular/core';
+import {FormsModule} from '@angular/forms';
+import {MatButton, MatIconButton} from '@angular/material/button';
+import {MatCheckbox} from '@angular/material/checkbox';
+import {MatFormField, MatSuffix} from '@angular/material/form-field';
+import {MatIcon} from '@angular/material/icon';
+import {MatInput, MatLabel} from '@angular/material/input';
+import {MatTooltip} from '@angular/material/tooltip';
+import {copyToClipboard, LOCAL_STORAGE, NaturalAlertService} from '@ecodev/natural';
+
+type ThemeToken = {
+ name: string;
+ hex: string;
+};
+
+type SchemeVariation =
+ | 'light'
+ | 'light-medium-contrast'
+ | 'light-high-contrast'
+ | 'dark'
+ | 'dark-medium-contrast'
+ | 'dark-high-contrast';
+
+type MaterialTheme = {
+ seed?: string;
+ coreColors?: {
+ primary?: string;
+ secondary?: string;
+ tertiary?: string;
+ neutral?: string;
+ neutralVariant?: string;
+ };
+ schemes?: Record>;
+};
+
+type ThemeData = {
+ name: string;
+ seed?: string;
+ coreColors?: {
+ primary?: string;
+ secondary?: string;
+ tertiary?: string;
+ neutral?: string;
+ neutralVariant?: string;
+ };
+ schemes: Record>;
+ selectedVariations: Set;
+ tokensByVariation: Map;
+};
+
+@Component({
+ selector: 'app-theme-merger',
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatFormField,
+ MatInput,
+ MatLabel,
+ MatButton,
+ MatCheckbox,
+ MatIcon,
+ MatIconButton,
+ MatSuffix,
+ MatTooltip,
+ ],
+ templateUrl: './theme-merger.component.html',
+ styleUrl: './theme-merger.component.scss',
+})
+export class ThemeMergerComponent implements OnInit {
+ private readonly document = inject(DOCUMENT);
+ private readonly localStorage = inject(LOCAL_STORAGE);
+ protected readonly alertService = inject(NaturalAlertService);
+
+ protected readonly theme2Upload = viewChild>('theme2Upload');
+
+ protected theme1: ThemeData | null = null;
+ protected theme2: ThemeData | null = null;
+ private _themeName = 'natural';
+ protected theme1RightColumnSelectedVariations = new Set(['dark']);
+ protected showPropertyNames = true;
+ protected primary = '0086B2';
+ protected secondary = '';
+ protected tertiary = 'FF960B';
+ private _lightModeEnabled = true;
+ private _darkModeEnabled = true;
+
+ private readonly THEME_NAME_STORAGE_KEY = 'theme-merger-theme-name';
+ private readonly LIGHT_MODE_STORAGE_KEY = 'theme-merger-light-mode';
+ private readonly DARK_MODE_STORAGE_KEY = 'theme-merger-dark-mode';
+
+ protected get themeName(): string {
+ return this._themeName;
+ }
+
+ protected set themeName(value: string) {
+ this._themeName = value;
+ this.saveThemeNameToLocalStorage();
+ }
+
+ protected get lightModeEnabled(): boolean {
+ return this._lightModeEnabled;
+ }
+
+ protected set lightModeEnabled(value: boolean) {
+ this._lightModeEnabled = value;
+ this.saveLightModeToLocalStorage();
+ }
+
+ protected get darkModeEnabled(): boolean {
+ return this._darkModeEnabled;
+ }
+
+ protected set darkModeEnabled(value: boolean) {
+ this._darkModeEnabled = value;
+ this.saveDarkModeToLocalStorage();
+ }
+
+ public ngOnInit(): void {
+ this.loadThemeNameFromLocalStorage();
+ this.loadLightModeFromLocalStorage();
+ this.loadDarkModeFromLocalStorage();
+ }
+
+ private loadThemeNameFromLocalStorage(): void {
+ const savedThemeName = this.localStorage.getItem(this.THEME_NAME_STORAGE_KEY);
+ if (savedThemeName) {
+ this._themeName = savedThemeName;
+ }
+ }
+
+ private saveThemeNameToLocalStorage(): void {
+ this.localStorage.setItem(this.THEME_NAME_STORAGE_KEY, this._themeName);
+ }
+
+ private loadLightModeFromLocalStorage(): void {
+ const savedLightMode = this.localStorage.getItem(this.LIGHT_MODE_STORAGE_KEY);
+ if (savedLightMode !== null) {
+ this._lightModeEnabled = savedLightMode === 'true';
+ }
+ }
+
+ private saveLightModeToLocalStorage(): void {
+ this.localStorage.setItem(this.LIGHT_MODE_STORAGE_KEY, String(this._lightModeEnabled));
+ }
+
+ private loadDarkModeFromLocalStorage(): void {
+ const savedDarkMode = this.localStorage.getItem(this.DARK_MODE_STORAGE_KEY);
+ if (savedDarkMode !== null) {
+ this._darkModeEnabled = savedDarkMode === 'true';
+ }
+ }
+
+ private saveDarkModeToLocalStorage(): void {
+ this.localStorage.setItem(this.DARK_MODE_STORAGE_KEY, String(this._darkModeEnabled));
+ }
+
+ protected readonly lightVariations: SchemeVariation[] = ['light', 'light-medium-contrast', 'light-high-contrast'];
+ protected readonly darkVariations: SchemeVariation[] = ['dark', 'dark-medium-contrast', 'dark-high-contrast'];
+ protected readonly allVariations: SchemeVariation[] = [...this.lightVariations, ...this.darkVariations];
+
+ private readonly tokenOrderReference: readonly (string | null)[] = [
+ 'primary',
+ 'primary-container',
+ 'primary-fixed',
+ 'primary-fixed-dim',
+ 'on-primary',
+ 'on-primary-container',
+ 'on-primary-fixed',
+ 'on-primary-fixed-variant',
+ 'inverse-primary',
+ null,
+ 'secondary',
+ 'secondary-container',
+ 'secondary-fixed',
+ 'secondary-fixed-dim',
+ 'on-secondary',
+ 'on-secondary-container',
+ 'on-secondary-fixed',
+ 'on-secondary-fixed-variant',
+ null,
+ 'tertiary',
+ 'tertiary-container',
+ 'tertiary-fixed',
+ 'tertiary-fixed-dim',
+ 'on-tertiary',
+ 'on-tertiary-container',
+ 'on-tertiary-fixed',
+ 'on-tertiary-fixed-variant',
+ null,
+ 'error',
+ 'error-container',
+ 'on-error',
+ 'on-error-container',
+ null,
+ 'surface',
+ 'surface-variant',
+ 'on-surface',
+ 'on-surface-variant',
+ 'surface-dim',
+ 'surface-bright',
+ 'surface-container-lowest',
+ 'surface-container-low',
+ 'surface-container',
+ 'surface-container-high',
+ 'surface-container-highest',
+ 'surface-tint',
+ 'inverse-surface',
+ 'inverse-on-surface',
+ null,
+ 'background',
+ 'on-background',
+ null,
+ 'outline',
+ 'outline-variant',
+ 'shadow',
+ 'scrim',
+ ] as const;
+
+ protected clearTheme2(): void {
+ this.theme2 = null;
+ // Reset the file input so the same file can be selected again
+ const uploadInput = this.theme2Upload();
+ if (uploadInput) {
+ uploadInput.nativeElement.value = '';
+ }
+ }
+
+ protected onFileSelected(event: Event, themeNumber: 1 | 2): void {
+ const input = event.target as HTMLInputElement;
+ if (!input.files || input.files.length === 0) {
+ return;
+ }
+
+ const file = input.files[0];
+ const reader = new FileReader();
+
+ reader.onload = (e: ProgressEvent) => {
+ try {
+ const content = e.target?.result as string;
+ const theme = JSON.parse(content) as MaterialTheme;
+
+ if (theme.schemes) {
+ const tokensByVariation = new Map();
+
+ // Parse all available variations
+ this.allVariations.forEach(variation => {
+ if (theme.schemes![variation]) {
+ tokensByVariation.set(variation, this.parseTokens(theme.schemes![variation]));
+ }
+ });
+
+ // Default to 'light' for theme 1, 'dark' for theme 2
+ const defaultVariation = themeNumber === 1 ? 'light' : 'dark';
+
+ const themeData: ThemeData = {
+ name: file.name,
+ seed: theme.seed,
+ coreColors: theme.coreColors,
+ schemes: theme.schemes,
+ selectedVariations: new Set([defaultVariation]),
+ tokensByVariation,
+ };
+
+ if (themeNumber === 1) {
+ this.theme1 = themeData;
+ // Set primary and tertiary from theme1's coreColors
+ if (themeData.coreColors) {
+ if (themeData.coreColors.primary) {
+ this.primary = themeData.coreColors.primary.replace('#', '');
+ }
+ if (themeData.coreColors.tertiary) {
+ this.tertiary = themeData.coreColors.tertiary.replace('#', '');
+ }
+ }
+ } else {
+ this.theme2 = themeData;
+ }
+
+ // Auto-generate and copy SCSS after file upload
+ // Use setTimeout to ensure the view is updated first
+ setTimeout(() => {
+ this.generateAndCopyScss(true);
+ }, 0);
+ }
+ } catch (error) {
+ console.error('Error parsing JSON file:', error);
+ this.alertService.error(
+ 'Error parsing JSON file. Please ensure it is a valid Material Design 3 theme file.',
+ );
+ }
+ };
+
+ reader.readAsText(file);
+ }
+
+ private parseTokens(lightScheme: Record): ThemeToken[] {
+ return Object.entries(lightScheme).map(([name, hex]) => ({
+ name,
+ hex: hex.startsWith('#') ? hex : `#${hex}`,
+ }));
+ }
+
+ protected toggleVariation(theme: ThemeData, variation: SchemeVariation): void {
+ if (theme.schemes[variation]) {
+ if (theme.selectedVariations.has(variation)) {
+ theme.selectedVariations.delete(variation);
+ } else {
+ theme.selectedVariations.add(variation);
+ }
+ // Trigger change detection by creating a new Set
+ theme.selectedVariations = new Set(theme.selectedVariations);
+ }
+ }
+
+ protected isVariationSelected(theme: ThemeData, variation: SchemeVariation): boolean {
+ return theme.selectedVariations.has(variation);
+ }
+
+ protected getVariationLabel(variation: SchemeVariation): string {
+ const labels: Record = {
+ light: 'Light',
+ 'light-medium-contrast': 'Light Medium',
+ 'light-high-contrast': 'Light High',
+ dark: 'Dark',
+ 'dark-medium-contrast': 'Dark Medium',
+ 'dark-high-contrast': 'Dark High',
+ };
+ return labels[variation];
+ }
+
+ protected camelToKebab(str: string | null): string {
+ if (!str) {
+ return '';
+ }
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
+ }
+
+ protected getTokenNames(theme: ThemeData): (string | null)[] {
+ // Get token names from the first available variation
+ const firstVariation = Array.from(theme.tokensByVariation.keys())[0];
+ if (!firstVariation) {
+ return [];
+ }
+
+ const tokens = theme.tokensByVariation.get(firstVariation);
+ if (!tokens) {
+ return [];
+ }
+
+ // Create a map of kebab-case names to original camelCase names
+ const kebabToOriginal = new Map();
+ tokens.forEach(t => {
+ const kebabName = this.camelToKebab(t.name);
+ kebabToOriginal.set(kebabName, t.name);
+ });
+
+ // Return tokens in the order specified by tokenOrderReference
+ // Include null values for spacing
+ return this.tokenOrderReference
+ .map(tokenName => {
+ if (tokenName === null) {
+ return null;
+ }
+ // Check if this token exists in the theme (comparing kebab-case)
+ return kebabToOriginal.has(tokenName) ? kebabToOriginal.get(tokenName)! : null;
+ })
+ .filter(tokenName => tokenName !== null || this.tokenOrderReference.includes(tokenName));
+ }
+
+ protected getTokensForName(theme: ThemeData, tokenName: string): {variation: SchemeVariation; token: ThemeToken}[] {
+ const result: {variation: SchemeVariation; token: ThemeToken}[] = [];
+
+ // Return tokens only for selected variations, in the order of variations array
+ this.allVariations.forEach(variation => {
+ if (theme.selectedVariations.has(variation)) {
+ const tokens = theme.tokensByVariation.get(variation);
+ const token = tokens?.find(t => t.name === tokenName);
+ if (token) {
+ result.push({variation, token});
+ }
+ }
+ });
+
+ return result;
+ }
+
+ protected getVariationsForTheme(themeNumber: 1 | 2): SchemeVariation[] {
+ return themeNumber === 1 ? this.lightVariations : this.darkVariations;
+ }
+
+ protected getRightTheme(): ThemeData {
+ return this.theme2 ?? this.theme1!;
+ }
+
+ protected getRightThemeSelectedVariations(): Set {
+ return this.theme2 ? this.theme2.selectedVariations : this.theme1RightColumnSelectedVariations;
+ }
+
+ protected isVariationSelectedRight(variation: SchemeVariation): boolean {
+ return this.getRightThemeSelectedVariations().has(variation);
+ }
+
+ protected toggleVariationRight(variation: SchemeVariation): void {
+ const rightTheme = this.getRightTheme();
+ if (rightTheme.schemes[variation]) {
+ if (this.theme2) {
+ // Use theme2's own selected variations
+ this.toggleVariation(this.theme2, variation);
+ } else {
+ // Use separate state for theme1 as right column
+ if (this.theme1RightColumnSelectedVariations.has(variation)) {
+ this.theme1RightColumnSelectedVariations.delete(variation);
+ } else {
+ this.theme1RightColumnSelectedVariations.add(variation);
+ }
+ // Trigger change detection
+ this.theme1RightColumnSelectedVariations = new Set(this.theme1RightColumnSelectedVariations);
+ }
+ }
+ }
+
+ protected getTokensForNameRight(tokenName: string): {variation: SchemeVariation; token: ThemeToken}[] {
+ const result: {variation: SchemeVariation; token: ThemeToken}[] = [];
+ const rightTheme = this.getRightTheme();
+ const selectedVariations = this.getRightThemeSelectedVariations();
+
+ // Return tokens only for selected variations, in the order of variations array
+ this.allVariations.forEach(variation => {
+ if (selectedVariations.has(variation)) {
+ const tokens = rightTheme.tokensByVariation.get(variation);
+ const token = tokens?.find(t => t.name === tokenName);
+ if (token) {
+ result.push({variation, token});
+ }
+ }
+ });
+
+ return result;
+ }
+
+ protected generateAndCopyScss(isDefault: boolean): void {
+ if (!this.theme1) {
+ this.alertService.error('Please upload at least Theme 1.');
+ return;
+ }
+
+ const rightTheme = this.getRightTheme();
+
+ // Check if at least one mode is enabled
+ if (!this.lightModeEnabled && !this.darkModeEnabled) {
+ this.alertService.error('Please enable at least one mode (Light or Dark).');
+ return;
+ }
+
+ // Validate that exactly one variation is selected for each theme
+ if (this.theme1.selectedVariations.size !== 1) {
+ this.alertService.error('Please select exactly ONE variation for Theme 1.');
+ return;
+ }
+
+ const rightThemeSelectedVariations = this.getRightThemeSelectedVariations();
+ if (rightThemeSelectedVariations.size !== 1) {
+ this.alertService.error(
+ `Please select exactly ONE variation for ${this.theme2 ? 'Theme 2' : 'Theme 1 (right column)'}.`,
+ );
+ return;
+ }
+
+ // Get the selected variations
+ const theme1Variation = Array.from(this.theme1.selectedVariations)[0];
+ const theme2Variation = Array.from(rightThemeSelectedVariations)[0];
+
+ // Get tokens from the selected variations
+ const theme1Tokens = this.theme1.tokensByVariation.get(theme1Variation);
+ const theme2Tokens = rightTheme.tokensByVariation.get(theme2Variation);
+
+ if (!theme1Tokens || !theme2Tokens) {
+ this.alertService.error('Error retrieving tokens from selected variations.');
+ return;
+ }
+
+ // Create a map of kebab-case names to tokens
+ const kebabToTheme1Token = new Map();
+ const kebabToTheme2Token = new Map();
+ theme1Tokens.forEach(t => kebabToTheme1Token.set(this.camelToKebab(t.name), t));
+ theme2Tokens.forEach(t => kebabToTheme2Token.set(this.camelToKebab(t.name), t));
+
+ // Build the SCSS properties using tokenOrderReference for ordering
+ const properties: string[] = [];
+ this.tokenOrderReference.forEach(kebabName => {
+ // null values represent spacing
+ if (kebabName === null) {
+ properties.push('');
+ return;
+ }
+
+ // Only include tokens that exist in the required themes
+ const theme1Token = kebabToTheme1Token.get(kebabName);
+ const theme2Token = kebabToTheme2Token.get(kebabName);
+
+ // Generate CSS based on which modes are enabled
+ if (this.lightModeEnabled && this.darkModeEnabled) {
+ // Both modes: use light-dark()
+ if (theme1Token && theme2Token) {
+ properties.push(` ${kebabName}: light-dark(${theme1Token.hex}, ${theme2Token.hex})`);
+ }
+ } else if (this.lightModeEnabled) {
+ // Light mode only: use only theme1 color
+ if (theme1Token) {
+ properties.push(` ${kebabName}: ${theme1Token.hex}`);
+ }
+ } else if (this.darkModeEnabled) {
+ // Dark mode only: use only theme2 color
+ if (theme2Token) {
+ properties.push(` ${kebabName}: ${theme2Token.hex}`);
+ }
+ }
+ });
+
+ // Build comment with seed and coreColors
+ const commentLines: string[] = [];
+
+ // If using only one theme with different variations
+ if (!this.theme2) {
+ if (this.theme1.seed || this.theme1.coreColors) {
+ commentLines.push(`Theme 1 (${theme1Variation} + ${theme2Variation}):`);
+ if (this.theme1.seed) {
+ commentLines.push(` Seed: ${this.theme1.seed}`);
+ }
+ if (this.theme1.coreColors) {
+ commentLines.push(` Core Colors:`);
+ if (this.theme1.coreColors.primary) {
+ commentLines.push(` Primary: ${this.theme1.coreColors.primary}`);
+ }
+ if (this.theme1.coreColors.secondary) {
+ commentLines.push(` Secondary: ${this.theme1.coreColors.secondary}`);
+ }
+ if (this.theme1.coreColors.tertiary) {
+ commentLines.push(` Tertiary: ${this.theme1.coreColors.tertiary}`);
+ }
+ if (this.theme1.coreColors.neutral) {
+ commentLines.push(` Neutral: ${this.theme1.coreColors.neutral}`);
+ }
+ if (this.theme1.coreColors.neutralVariant) {
+ commentLines.push(` Neutral Variant: ${this.theme1.coreColors.neutralVariant}`);
+ }
+ }
+ }
+ } else {
+ // Using two different themes
+ // Add theme 1 info
+ if (this.theme1.seed || this.theme1.coreColors) {
+ commentLines.push(`Theme 1 (${theme1Variation}):`);
+ if (this.theme1.seed) {
+ commentLines.push(` Seed: ${this.theme1.seed}`);
+ }
+ if (this.theme1.coreColors) {
+ commentLines.push(` Core Colors:`);
+ if (this.theme1.coreColors.primary) {
+ commentLines.push(` Primary: ${this.theme1.coreColors.primary}`);
+ }
+ if (this.theme1.coreColors.secondary) {
+ commentLines.push(` Secondary: ${this.theme1.coreColors.secondary}`);
+ }
+ if (this.theme1.coreColors.tertiary) {
+ commentLines.push(` Tertiary: ${this.theme1.coreColors.tertiary}`);
+ }
+ if (this.theme1.coreColors.neutral) {
+ commentLines.push(` Neutral: ${this.theme1.coreColors.neutral}`);
+ }
+ if (this.theme1.coreColors.neutralVariant) {
+ commentLines.push(` Neutral Variant: ${this.theme1.coreColors.neutralVariant}`);
+ }
+ }
+ }
+
+ // Add theme 2 info
+ if (this.theme2.seed || this.theme2.coreColors) {
+ if (commentLines.length > 0) {
+ commentLines.push('');
+ }
+ commentLines.push(`Theme 2 (${theme2Variation}):`);
+ if (this.theme2.seed) {
+ commentLines.push(` Seed: ${this.theme2.seed}`);
+ }
+ if (this.theme2.coreColors) {
+ commentLines.push(` Core Colors:`);
+ if (this.theme2.coreColors.primary) {
+ commentLines.push(` Primary: ${this.theme2.coreColors.primary}`);
+ }
+ if (this.theme2.coreColors.secondary) {
+ commentLines.push(` Secondary: ${this.theme2.coreColors.secondary}`);
+ }
+ if (this.theme2.coreColors.tertiary) {
+ commentLines.push(` Tertiary: ${this.theme2.coreColors.tertiary}`);
+ }
+ if (this.theme2.coreColors.neutral) {
+ commentLines.push(` Neutral: ${this.theme2.coreColors.neutral}`);
+ }
+ if (this.theme2.coreColors.neutralVariant) {
+ commentLines.push(` Neutral Variant: ${this.theme2.coreColors.neutralVariant}`);
+ }
+ }
+ }
+ }
+
+ // Generate Material Theme Builder links
+ if (this.theme1.coreColors) {
+ const url1 = this.generateThemeBuilderUrl(this.theme1.coreColors);
+ if (url1) {
+ commentLines.push('');
+ commentLines.push(`Theme 1 Builder Link:`);
+ commentLines.push(` ${url1}`);
+ }
+ }
+ if (this.theme2?.coreColors) {
+ const url2 = this.generateThemeBuilderUrl(this.theme2.coreColors);
+ if (url2) {
+ commentLines.push('');
+ commentLines.push(`Theme 2 Builder Link:`);
+ commentLines.push(` ${url2}`);
+ }
+ }
+
+ const comment =
+ commentLines.length > 0 ? `/*\n${commentLines.map(line => ' * ' + line).join('\n')}\n */\n\n` : '';
+
+ // Generate SCSS - handle commas properly (don't add comma to empty lines)
+ const propertiesString = properties
+ .map((prop, index) => {
+ if (prop === '') {
+ // Empty line for spacing
+ return '';
+ }
+ // Add comma to all properties except the last non-empty one
+ const remainingProps = properties.slice(index + 1);
+ const hasMoreProps = remainingProps.some(p => p !== '');
+ return hasMoreProps ? `${prop},` : prop;
+ })
+ .join('\n');
+
+ // Build the selector based on whether a theme name is provided
+ let selector: string;
+ if (!this.themeName.trim()) {
+ // No theme name: use only :root, :host
+ selector = isDefault ? ':root, :host' : ':host';
+ } else {
+ // Theme name provided: include the data-theme selector
+ const defaultSelector = isDefault ? ':root, :host,' : '';
+ selector = defaultSelector + `\n[data-theme="${this.themeName.trim()}"]`;
+ }
+
+ const scss = `${comment}@use '@angular/material' as mat;
+/* prettier-ignore */
+${selector} {
+ @include mat.theme-overrides((
+ ${propertiesString}
+ ));
+}`;
+
+ // Copy to clipboard
+ try {
+ copyToClipboard(this.document, scss);
+ this.alertService.info('SCSS copied to clipboard!');
+ } catch (error) {
+ this.alertService.error('Failed to copy to clipboard. Please check browser permissions.');
+ }
+ }
+
+ private generateThemeBuilderUrl(coreColors: NonNullable): string {
+ const baseUrl = 'http://material-foundation.github.io/material-theme-builder/';
+ const params = new URLSearchParams();
+
+ if (coreColors.primary) {
+ params.append('primary', coreColors.primary.replace('#', ''));
+ }
+ if (coreColors.secondary) {
+ params.append('secondary', coreColors.secondary.replace('#', ''));
+ }
+ if (coreColors.tertiary) {
+ params.append('tertiary', coreColors.tertiary.replace('#', ''));
+ }
+ if (coreColors.neutral) {
+ params.append('neutral', coreColors.neutral.replace('#', ''));
+ }
+ if (coreColors.neutralVariant) {
+ params.append('neutralVariant', coreColors.neutralVariant.replace('#', ''));
+ }
+
+ return params.toString() ? `${baseUrl}?${params.toString()}` : '';
+ }
+
+ protected copyBuilderLink(): void {
+ if (!this.primary.trim() && !this.secondary.trim() && !this.tertiary.trim()) {
+ this.alertService.error('Please enter at least a primary, secondary, or tertiary color.');
+ return;
+ }
+
+ const baseUrl = 'http://material-foundation.github.io/material-theme-builder/';
+ const params = new URLSearchParams();
+
+ // Add primary color if provided
+ if (this.primary.trim()) {
+ params.append('primary', this.primary.replace('#', '').toUpperCase());
+ }
+
+ // Add secondary color if provided
+ if (this.secondary.trim()) {
+ params.append('secondary', this.secondary.replace('#', '').toUpperCase());
+ }
+
+ // Add tertiary color if provided
+ if (this.tertiary.trim()) {
+ params.append('tertiary', this.tertiary.replace('#', '').toUpperCase());
+ }
+
+ // Force neutral and neutralVariant to white
+ params.append('neutral', 'FFFFFF');
+ params.append('neutralVariant', 'FFFFFF');
+ params.append('colorMatch', 'true');
+
+ const url = `${baseUrl}?${params.toString()}`;
+
+ try {
+ copyToClipboard(this.document, url);
+ this.alertService.info('Theme Builder URL copied to clipboard!');
+ } catch (error) {
+ this.alertService.error('Failed to copy to clipboard. Please check browser permissions.');
+ }
+ }
+}
diff --git a/src/assets/github.svg b/src/assets/github.svg
index 49884fa6..3fc66cc4 100644
--- a/src/assets/github.svg
+++ b/src/assets/github.svg
@@ -1,6 +1,5 @@
- | |