diff --git a/live-editing/configs/GridConfigGenerator.ts b/live-editing/configs/GridConfigGenerator.ts
index 067596829..472080951 100644
--- a/live-editing/configs/GridConfigGenerator.ts
+++ b/live-editing/configs/GridConfigGenerator.ts
@@ -574,6 +574,11 @@ export class GridConfigGenerator implements IConfigGenerator {
additionalFiles: ['/src/app/directives/prevent-scroll.directive.ts', '/src/app/data/customers.ts', '/src/app/grid/grid-sample-pinning-styling/layout.scss'],
appConfig: BaseAppConfig
}));
+ configs.push(new Config({
+ component: 'GridThemingComponent',
+ additionalFiles: ['/src/app/directives/prevent-scroll.directive.ts', '/src/app/data/invoiceData.ts'],
+ appConfig: BaseAppConfig
+ }));
configs.push(new Config({
component: 'GridGroupByStylingComponent',
additionalFiles: ['/src/app/directives/prevent-scroll.directive.ts', '/src/app/data/invoiceData.ts', '/src/app/grid/grid-groupby-styling/layout.scss'],
diff --git a/live-editing/configs/PivotGridConfigGenerator.ts b/live-editing/configs/PivotGridConfigGenerator.ts
index 7f7fb25e2..c7c720c47 100644
--- a/live-editing/configs/PivotGridConfigGenerator.ts
+++ b/live-editing/configs/PivotGridConfigGenerator.ts
@@ -83,7 +83,7 @@ export class PivotGridConfigGenerator implements IConfigGenerator {
}));
configs.push(new Config({
- additionalFiles: ['/src/app/directives/prevent-scroll.directive.ts', '/src/app/data/dataToAnalyze.ts'],
+ additionalFiles: ['/src/app/data/pivot-data.ts', '/src/app/pivot-grid/pivot-styling/layout.scss'],
appConfig: BaseAppConfig,
component: 'PivotStylingComponent'
}));
diff --git a/live-editing/configs/ThemingConfigGenerator.ts b/live-editing/configs/ThemingConfigGenerator.ts
index 3c0a10d09..9d29d32b4 100644
--- a/live-editing/configs/ThemingConfigGenerator.ts
+++ b/live-editing/configs/ThemingConfigGenerator.ts
@@ -72,6 +72,22 @@ export class ThemingConfigGenerator implements IConfigGenerator {
shortenComponentPathBy: '/theming/angular/'
}));
+ configs.push(new Config({
+ component: 'GridThemePlaygroundComponent',
+ additionalFiles: [
+ '/src/app/theming/grid-theme-playground/theme-color-field.component.ts',
+ '/src/app/theming/grid-theme-playground/theme-color-field.component.html',
+ '/src/app/theming/grid-theme-playground/theme-color-field.component.scss',
+ '/src/app/theming/grid-theme-playground/color-utils.ts',
+ '/src/app/data/invoiceData.ts',
+ '/src/app/data/pivot-data.ts',
+ '/src/app/data/singersData.ts',
+ '/src/app/tree-grid/data/employees-flat-avatars.ts'
+ ],
+ appConfig: BaseAppConfig,
+ shortenComponentPathBy: '/theming/grid-theme-playground/'
+ }));
+
configs.push(new Config({
component: 'TailwindSampleComponent',
appConfig: BaseAppConfig,
diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts
index 85274dd7e..2960e6283 100644
--- a/src/app/app.routes.server.ts
+++ b/src/app/app.routes.server.ts
@@ -49,6 +49,16 @@ export const serverRoutes: ServerRoute[] = [
path: 'samples/lists/**',
renderMode: RenderMode.Client // TODO: Fails with Prerender
},
+ {
+ // Hosts grid previews, and grids fail under Prerender like every other
+ // grid route below. Must precede the theming/** globs.
+ path: 'theming/grid-theme-playground',
+ renderMode: RenderMode.Client
+ },
+ {
+ path: 'samples/theming/grid-theme-playground',
+ renderMode: RenderMode.Client
+ },
{
path: 'theming/**',
renderMode: RenderMode.Prerender
diff --git a/src/app/grid/grid-routes-data.ts b/src/app/grid/grid-routes-data.ts
index 57923487f..10860e02d 100644
--- a/src/app/grid/grid-routes-data.ts
+++ b/src/app/grid/grid-routes-data.ts
@@ -4,6 +4,7 @@ export const gridsRoutesData = {
'grid-groupby': { displayName: 'Grid GroupBy', parentName: 'Grid' },
'grid-cellMerge': { displayName: 'Grid Cell Merge', parentName: 'Grid' },
'grid-cellMerge-custom': { displayName: 'Grid Cell Merge Custom', parentName: 'Grid' },
+ 'grid-theming': { displayName: 'Grid Theming', parentName: 'Grid' },
'grid-groupby-custom': {displayName: 'Grid GroupBy Custom', parentName: 'Grid'},
'grid-groupby-paging': { displayName: 'Grid GroupBy Paging', parentName: 'Grid' },
'grid-groupby-styling': { displayName: 'Grid GroupBy Styling', parentName: 'Grid' },
diff --git a/src/app/grid/grid-theming/grid-theming.component.html b/src/app/grid/grid-theming/grid-theming.component.html
new file mode 100644
index 000000000..6a044abec
--- /dev/null
+++ b/src/app/grid/grid-theming/grid-theming.component.html
@@ -0,0 +1,64 @@
+
+
+
Pick a theme
+
+
+ @for (theme of themes; track theme.class) {
+
+ }
+
+
+
+ Custom themes, not built-in: each is a grid-theme() with its
+ own background and accent.
+
+
+
+
+
+
+ Invoices
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/grid/grid-theming/grid-theming.component.scss b/src/app/grid/grid-theming/grid-theming.component.scss
new file mode 100644
index 000000000..29578f9aa
--- /dev/null
+++ b/src/app/grid/grid-theming/grid-theming.component.scss
@@ -0,0 +1,145 @@
+@use "igniteui-angular/theming" as *;
+
+$studio-bg: #faf4ed;
+$studio-accent: #907aa9;
+$ledger-bg: #eceff4;
+$ledger-accent: #5e81ac;
+$editorial-bg: #333c43;
+$editorial-accent: #a7c080;
+$midnight-bg: #282a36;
+$midnight-accent: #bd93f9;
+
+:host {
+ display: block;
+ padding: 16px;
+}
+
+.theme-studio {
+ --ig-size: var(--ig-size-large);
+ --ig-radius-factor: 0.6;
+
+ @include tokens(grid-theme(
+ $background: $studio-bg,
+ $foreground: #575279,
+ $accent-color: $studio-accent,
+ $header-background: #fffaf3,
+ $header-border-color: #dfdad9,
+ $row-border-color: #f2e9e1,
+ $grid-border-color: #dfdad9,
+ $grid-shadow: (0 1px 3px rgba(87, 82, 121, 0.10), 0 1px 2px rgba(87, 82, 121, 0.06))
+ ));
+}
+
+.theme-ledger {
+ --ig-size: var(--ig-size-small);
+ --ig-radius-factor: 0;
+
+ @include tokens(grid-theme(
+ $background: $ledger-bg,
+ $foreground: #2e3440,
+ $accent-color: $ledger-accent,
+ $header-background: #d8dee9,
+ $row-odd-background: #eceff4,
+ $row-even-background: #e5e9f0,
+ $body-column-border-color-odd: #d8dee9,
+ $body-column-border-color-even: #d8dee9,
+ $row-border-color: #d8dee9,
+ $grid-border-color: #c8d0dc
+ ));
+}
+
+.theme-editorial {
+ --ig-size: var(--ig-size-large);
+ --ig-radius-factor: 0;
+
+ @include tokens(grid-theme(
+ $schema: $dark-material-schema,
+ $background: $editorial-bg,
+ $foreground: #d3c6aa,
+ $accent-color: $editorial-accent,
+ $header-background: #3a464c,
+ $row-border-color: #333c43,
+ $grid-border-color: #333c43
+ ));
+}
+
+.theme-midnight {
+ --ig-size: var(--ig-size-medium);
+ --ig-radius-factor: 0.25;
+
+ @include tokens(grid-theme(
+ $schema: $dark-material-schema,
+ $background: $midnight-bg,
+ $foreground: #f8f8f2,
+ $accent-color: $midnight-accent,
+ $header-background: #21222c,
+ $body-column-border-color-odd: #44475a,
+ $body-column-border-color-even: #44475a,
+ $row-border-color: #44475a,
+ $grid-border-color: #44475a
+ ));
+}
+
+.grid-theming-sample {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.theme-picker {
+ --ig-button-group-elevation: 0;
+
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ align-self: flex-start;
+}
+
+.theme-picker__label {
+ font-size: 12px;
+ font-weight: 500;
+ letter-spacing: 0.04em;
+ color: var(--ig-gray-700);
+}
+
+.theme-picker__hint {
+ margin: 0;
+ font-size: 12px;
+ line-height: 1.45;
+ color: var(--ig-gray-600);
+
+ code {
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
+ font-size: 11px;
+ }
+}
+
+.theme-swatch {
+ display: inline-block;
+ min-width: 14px;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ border: 1px solid var(--ig-gray-300);
+ vertical-align: -2px;
+ background: linear-gradient(135deg, var(--swatch-bg) 0 50%, var(--swatch-accent) 50% 100%);
+}
+
+.theme-swatch--studio {
+ --swatch-bg: #{$studio-bg};
+ --swatch-accent: #{$studio-accent};
+}
+
+.theme-swatch--ledger {
+ --swatch-bg: #{$ledger-bg};
+ --swatch-accent: #{$ledger-accent};
+}
+
+.theme-swatch--editorial {
+ --swatch-bg: #{$editorial-bg};
+ --swatch-accent: #{$editorial-accent};
+}
+
+.theme-swatch--midnight {
+ --swatch-bg: #{$midnight-bg};
+ --swatch-accent: #{$midnight-accent};
+}
diff --git a/src/app/grid/grid-theming/grid-theming.component.ts b/src/app/grid/grid-theming/grid-theming.component.ts
new file mode 100644
index 000000000..97fe513f4
--- /dev/null
+++ b/src/app/grid/grid-theming/grid-theming.component.ts
@@ -0,0 +1,53 @@
+import { Component, signal } from '@angular/core';
+import { IgxButtonGroupComponent } from 'igniteui-angular/button-group';
+import { IgxButtonDirective } from 'igniteui-angular/directives';
+import { IgxNumberSummaryOperand, IgxSummaryResult } from 'igniteui-angular/core';
+import { IgxColumnComponent, IgxGridToolbarActionsComponent, IgxGridToolbarComponent, IgxGridToolbarHidingComponent, IgxGridToolbarPinningComponent, IgxGridToolbarTitleComponent } from 'igniteui-angular/grids/core';
+import { IgxGridComponent } from 'igniteui-angular/grids/grid';
+import { IgxPaginatorComponent } from 'igniteui-angular/paginator';
+import { INVOICE_DATA } from '../../data/invoiceData';
+import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive';
+class CompactSummary extends IgxNumberSummaryOperand {
+ public override operate(data?: any[]): IgxSummaryResult[] {
+ return super.operate(data).filter(r => r.key === 'count' || r.key === 'sum');
+ }
+}
+
+@Component({
+ selector: 'app-grid-theming',
+ styleUrls: ['./grid-theming.component.scss'],
+ templateUrl: './grid-theming.component.html',
+ imports: [
+ IgxButtonDirective,
+ IgxButtonGroupComponent,
+ IgxColumnComponent,
+ IgxGridComponent,
+ IgxGridToolbarActionsComponent,
+ IgxGridToolbarComponent,
+ IgxGridToolbarHidingComponent,
+ IgxGridToolbarPinningComponent,
+ IgxGridToolbarTitleComponent,
+ IgxPaginatorComponent,
+ IgxPreventDocumentScrollDirective
+ ]
+})
+export class GridThemingComponent {
+ public data = INVOICE_DATA;
+ public compactSummary = CompactSummary;
+ public themes = [
+ { label: 'Studio', class: 'theme-studio', swatch: 'theme-swatch--studio' },
+ { label: 'Ledger', class: 'theme-ledger', swatch: 'theme-swatch--ledger' },
+ { label: 'Editorial', class: 'theme-editorial', swatch: 'theme-swatch--editorial' },
+ { label: 'Midnight', class: 'theme-midnight', swatch: 'theme-swatch--midnight' }
+ ];
+
+ public activeTheme = signal(this.themes[0].class);
+
+ public selectTheme(args: { index: number }) {
+ this.activeTheme.set(this.themes[args.index].class);
+ }
+
+ public formatCurrency(value: number) {
+ return '$' + value.toFixed(2);
+ }
+}
diff --git a/src/app/grid/grid-validation-style/grid-validation-style.component.ts b/src/app/grid/grid-validation-style/grid-validation-style.component.ts
index 89c22dfae..ccf4d404d 100644
--- a/src/app/grid/grid-validation-style/grid-validation-style.component.ts
+++ b/src/app/grid/grid-validation-style/grid-validation-style.component.ts
@@ -14,7 +14,7 @@ import { NgTemplateOutlet } from '@angular/common';
export class GridValidationStyleComponent {
@ViewChild('grid', { read: IgxGridComponent, static: true }) public grid: IgxGridComponent;
public data: any[];
- public rowStyles = {
+ public rowStyles: any = {
background: (row: RowType) => row.validation.status === 'INVALID' ? '#FF000033' : '#00000000'
};
public cellStyles = {
diff --git a/src/app/grid/grids.routes.ts b/src/app/grid/grids.routes.ts
index be9950dbc..c5ded8e02 100644
--- a/src/app/grid/grids.routes.ts
+++ b/src/app/grid/grids.routes.ts
@@ -31,6 +31,7 @@ import { GridGroupByCustomSampleComponent } from './grid-groupby-custom-sample/g
import { GridGroupByPagingSampleComponent } from './grid-group-by-paging-sample/grid-group-by-paging-sample.component';
import { GridGroupBySampleComponent } from './grid-groupby-sample/grid-groupby-sample.component';
import { GridGroupByStylingComponent } from './grid-groupby-styling/grid-groupby-styling.component';
+import { GridThemingComponent } from './grid-theming/grid-theming.component';
import { GridGroupBySummarySampleComponent } from './grid-groupby-summary-sample/grid-groupby-summary-sample.component';
import { GridGroupBySummaryStylingSampleComponent } from './grid-groupby-summary-styling-sample/grid-groupby-summary-styling-sample.component';
import { GridMovingSampleComponent } from './grid-moving-sample/grid-moving-sample.component';
@@ -168,6 +169,11 @@ export const GridsRoutes: Routes = [
data: gridsRoutesData['grid-cellMerge-custom'],
path: 'grid-cellMerge-custom'
},
+ {
+ component: GridThemingComponent,
+ data: gridsRoutesData['grid-theming'],
+ path: 'grid-theming'
+ },
{
component: GridGroupByStylingComponent,
data: gridsRoutesData['grid-groupby-styling'],
diff --git a/src/app/hierarchical-grid/hgrid-rowStyle-sample/hgrid-rowStyles.component.ts b/src/app/hierarchical-grid/hgrid-rowStyle-sample/hgrid-rowStyles.component.ts
index ae92b4259..cb950623c 100644
--- a/src/app/hierarchical-grid/hgrid-rowStyle-sample/hgrid-rowStyles.component.ts
+++ b/src/app/hierarchical-grid/hgrid-rowStyle-sample/hgrid-rowStyles.component.ts
@@ -18,12 +18,12 @@ export class HGridRowStylesSampleComponent implements OnInit {
private hierarchicalGrid: IgxHierarchicalGridComponent;
public localdata;
- public rowStyles = {
+ public rowStyles: any = {
background:(row: RowType) => row.data['HasGrammyAward'] ? '#eeddd3' : '#f0efeb',
'border-left': (row: RowType) => row.data['HasGrammyAward'] ? '2px solid #dda15e' : null
};
- public childRowStyles = {
+ public childRowStyles: any = {
'border-left': (row: RowType) => row.data['BillboardReview'] > 70 ? '3.5px solid #dda15e' : null
};
constructor() { }
diff --git a/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.html b/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.html
index 7028f9ada..8f27753f0 100644
--- a/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.html
+++ b/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.html
@@ -1,36 +1,67 @@
-
-
-
-
-
-
![]()
-
-
-
-
-
-
+
+
Pick a theme
-
-
-
-
-
-
-
-
-
-
-
-
+
+ @for (theme of themes; track theme.class) {
+
+ }
+
-
-
-
-
-
-
-
+
+ Custom themes, not built-in: each is a grid-theme() with its
+ own background and accent.
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.scss b/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.scss
index 2c0d35a3b..9a14ff29e 100644
--- a/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.scss
+++ b/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.scss
@@ -1,16 +1,141 @@
@use "layout.scss";
@use "igniteui-angular/theming" as *;
-$background: #292826;
-$foreground: #eeece1;
-$accent: #ffcd0f;
-
-$custom-grid: grid-theme(
- $background: $background,
- $foreground: $foreground,
- $accent-color: $accent,
-);
-
-:host {
- @include tokens($custom-grid);
+$studio-bg: #faf4ed;
+$studio-accent: #907aa9;
+$ledger-bg: #eceff4;
+$ledger-accent: #5e81ac;
+$editorial-bg: #333c43;
+$editorial-accent: #a7c080;
+$midnight-bg: #282a36;
+$midnight-accent: #bd93f9;
+
+.theme-studio {
+ --ig-size: var(--ig-size-large);
+ --ig-radius-factor: 0.6;
+
+ @include tokens(grid-theme(
+ $background: $studio-bg,
+ $foreground: #575279,
+ $accent-color: $studio-accent,
+ $header-background: #fffaf3,
+ $header-border-color: #dfdad9,
+ $row-border-color: #f2e9e1,
+ $grid-border-color: #dfdad9,
+ $grid-shadow: (0 1px 3px rgba(87, 82, 121, 0.10), 0 1px 2px rgba(87, 82, 121, 0.06))
+ ));
+}
+
+.theme-ledger {
+ --ig-size: var(--ig-size-small);
+ --ig-radius-factor: 0;
+
+ @include tokens(grid-theme(
+ $background: $ledger-bg,
+ $foreground: #2e3440,
+ $accent-color: $ledger-accent,
+ $header-background: #d8dee9,
+ $row-odd-background: #eceff4,
+ $row-even-background: #e5e9f0,
+ $body-column-border-color-odd: #d8dee9,
+ $body-column-border-color-even: #d8dee9,
+ $row-border-color: #d8dee9,
+ $grid-border-color: #c8d0dc
+ ));
+}
+
+.theme-editorial {
+ --ig-size: var(--ig-size-large);
+ --ig-radius-factor: 0;
+
+ @include tokens(grid-theme(
+ $schema: $dark-material-schema,
+ $background: $editorial-bg,
+ $foreground: #d3c6aa,
+ $accent-color: $editorial-accent,
+ $header-background: #3a464c,
+ $row-border-color: #333c43,
+ $grid-border-color: #333c43
+ ));
+}
+
+.theme-midnight {
+ --ig-size: var(--ig-size-medium);
+ --ig-radius-factor: 0.25;
+
+ @include tokens(grid-theme(
+ $schema: $dark-material-schema,
+ $background: $midnight-bg,
+ $foreground: #f8f8f2,
+ $accent-color: $midnight-accent,
+ $header-background: #21222c,
+ $body-column-border-color-odd: #44475a,
+ $body-column-border-color-even: #44475a,
+ $row-border-color: #44475a,
+ $grid-border-color: #44475a
+ ));
+}
+
+.grid__wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.theme-picker {
+ --ig-button-group-elevation: 0;
+
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ align-self: flex-start;
+}
+
+.theme-picker__label {
+ font-size: 12px;
+ font-weight: 500;
+ letter-spacing: 0.04em;
+ color: var(--ig-gray-700);
+}
+
+.theme-picker__hint {
+ margin: 0;
+ font-size: 12px;
+ line-height: 1.45;
+ color: var(--ig-gray-600);
+
+ code {
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
+ font-size: 11px;
+ }
+}
+
+.theme-swatch {
+ display: inline-block;
+ min-width: 14px;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ border: 1px solid var(--ig-gray-300);
+ vertical-align: -2px;
+ background: linear-gradient(135deg, var(--swatch-bg) 0 50%, var(--swatch-accent) 50% 100%);
+}
+
+.theme-swatch--studio {
+ --swatch-bg: #{$studio-bg};
+ --swatch-accent: #{$studio-accent};
+}
+
+.theme-swatch--ledger {
+ --swatch-bg: #{$ledger-bg};
+ --swatch-accent: #{$ledger-accent};
+}
+
+.theme-swatch--editorial {
+ --swatch-bg: #{$editorial-bg};
+ --swatch-accent: #{$editorial-accent};
+}
+
+.theme-swatch--midnight {
+ --swatch-bg: #{$midnight-bg};
+ --swatch-accent: #{$midnight-accent};
}
diff --git a/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.ts b/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.ts
index fcc915ddf..91bd07fa4 100644
--- a/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.ts
+++ b/src/app/hierarchical-grid/hierarchical-grid-styling/hierarchical-grid-styling.component.ts
@@ -1,4 +1,6 @@
-import { Component } from '@angular/core';
+import { Component, signal } from '@angular/core';
+import { IgxButtonGroupComponent } from 'igniteui-angular/button-group';
+import { IgxButtonDirective } from 'igniteui-angular/directives';
import { IgxCellTemplateDirective, IgxColumnComponent } from 'igniteui-angular/grids/core';
import { IgxHierarchicalGridComponent, IgxRowIslandComponent } from 'igniteui-angular/grids/hierarchical-grid';
import { SINGERS } from '../../data/singersData';
@@ -8,7 +10,7 @@ import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scro
selector: 'app-hierarchical-grid-styling',
styleUrls: ['./hierarchical-grid-styling.component.scss'],
templateUrl: 'hierarchical-grid-styling.component.html',
- imports: [IgxHierarchicalGridComponent, IgxPreventDocumentScrollDirective, IgxColumnComponent, IgxCellTemplateDirective, IgxRowIslandComponent]
+ imports: [IgxHierarchicalGridComponent, IgxPreventDocumentScrollDirective, IgxColumnComponent, IgxCellTemplateDirective, IgxRowIslandComponent, IgxButtonGroupComponent, IgxButtonDirective]
})
export class HGridStylingComponent {
@@ -16,11 +18,23 @@ export class HGridStylingComponent {
public col: IgxColumnComponent;
public pWidth: string;
public nWidth: string;
+ public themes = [
+ { label: 'Studio', class: 'theme-studio', swatch: 'theme-swatch--studio' },
+ { label: 'Ledger', class: 'theme-ledger', swatch: 'theme-swatch--ledger' },
+ { label: 'Editorial', class: 'theme-editorial', swatch: 'theme-swatch--editorial' },
+ { label: 'Midnight', class: 'theme-midnight', swatch: 'theme-swatch--midnight' }
+ ];
+
+ public activeTheme = signal(this.themes[0].class);
constructor() {
this.localdata = SINGERS;
}
+ public selectTheme(args: { index: number }) {
+ this.activeTheme.set(this.themes[args.index].class);
+ }
+
public onResize(event) {
this.col = event.column;
this.pWidth = event.prevWidth;
diff --git a/src/app/hierarchical-grid/hierarchical-grid-styling/layout.scss b/src/app/hierarchical-grid/hierarchical-grid-styling/layout.scss
index b620c7ade..e2bcf0bd0 100644
--- a/src/app/hierarchical-grid/hierarchical-grid-styling/layout.scss
+++ b/src/app/hierarchical-grid/hierarchical-grid-styling/layout.scss
@@ -1,12 +1,18 @@
+// Fits the smallest row density.
.photo {
vertical-align: middle;
- max-height: 62px;
+ max-block-size: 24px;
+ border-radius: 50%;
}
.cell__inner_2 {
margin: 1px
}
+:host {
+ display: block;
+}
+
.grid__wrapper{
padding: 10px;
}
diff --git a/src/app/hierarchical-grid/hierarchical-grid-validation-style/hierarchical-grid-validation-style.component.ts b/src/app/hierarchical-grid/hierarchical-grid-validation-style/hierarchical-grid-validation-style.component.ts
index 49871eb70..ffc614547 100644
--- a/src/app/hierarchical-grid/hierarchical-grid-validation-style/hierarchical-grid-validation-style.component.ts
+++ b/src/app/hierarchical-grid/hierarchical-grid-validation-style/hierarchical-grid-validation-style.component.ts
@@ -17,7 +17,7 @@ export class HGridValidationStyleComponent {
@ViewChild('hierarchicalGrid', { read: IgxHierarchicalGridComponent, static: true }) public hierarchicalGrid: IgxHierarchicalGridComponent;
@ViewChild('childGrid', { static: true }) private childGrid: IgxRowIslandComponent;
public localData: Singer[];
- public rowStyles = {
+ public rowStyles: any = {
background: (row: RowType) => row.validation.status === 'INVALID' ? '#FF000033' : '#00000000'
};
public cellStyles = {
diff --git a/src/app/index/index.component.scss b/src/app/index/index.component.scss
index f6cb2bd6a..5b2502768 100644
--- a/src/app/index/index.component.scss
+++ b/src/app/index/index.component.scss
@@ -3,19 +3,21 @@
}
.main {
+ display: flex;
+ flex-direction: column;
width: calc(100% - 300px);
+ height: 100%;
}
-.content {
- width: 100%;
- // height: calc(100% - 56px);
- height: 100%;
+.content {
+ width: 100%;
+ flex: 1 1 auto;
max-height: 1000px;
min-width: 300px;
min-height: 300px;
justify-content: center;
align-items: stretch;
- overflow: auto;
+ overflow: auto;
}
.navdrawer-ellipsis {
diff --git a/src/app/pivot-grid/pivot-styling/pivot-styling.component.html b/src/app/pivot-grid/pivot-styling/pivot-styling.component.html
index 08df70c02..f94ecf55a 100644
--- a/src/app/pivot-grid/pivot-styling/pivot-styling.component.html
+++ b/src/app/pivot-grid/pivot-styling/pivot-styling.component.html
@@ -1,2 +1,32 @@
-
-
\ No newline at end of file
+
+
+
Pick a theme
+
+
+ @for (theme of themes; track theme.class) {
+
+ }
+
+
+
+ Custom themes, not built-in: each is a grid-theme() with its
+ own background and accent.
+
+
+
+
+
+
diff --git a/src/app/pivot-grid/pivot-styling/pivot-styling.component.scss b/src/app/pivot-grid/pivot-styling/pivot-styling.component.scss
index 238acf973..d703a350a 100644
--- a/src/app/pivot-grid/pivot-styling/pivot-styling.component.scss
+++ b/src/app/pivot-grid/pivot-styling/pivot-styling.component.scss
@@ -1,16 +1,141 @@
@use "layout.scss";
@use "igniteui-angular/theming" as *;
-$background: #292826;
-$foreground: #eeece1;
-$accent: #ffcd0f;
-
-$custom-grid: grid-theme(
- $background: $background,
- $foreground: $foreground,
- $accent-color: $accent,
-);
-
-igx-pivot-grid {
- @include tokens($custom-grid);
+$studio-bg: #faf4ed;
+$studio-accent: #907aa9;
+$ledger-bg: #eceff4;
+$ledger-accent: #5e81ac;
+$editorial-bg: #333c43;
+$editorial-accent: #a7c080;
+$midnight-bg: #282a36;
+$midnight-accent: #bd93f9;
+
+.theme-studio {
+ --ig-size: var(--ig-size-large);
+ --ig-radius-factor: 0.6;
+
+ @include tokens(grid-theme(
+ $background: $studio-bg,
+ $foreground: #575279,
+ $accent-color: $studio-accent,
+ $header-background: #fffaf3,
+ $header-border-color: #dfdad9,
+ $row-border-color: #f2e9e1,
+ $grid-border-color: #dfdad9,
+ $grid-shadow: (0 1px 3px rgba(87, 82, 121, 0.10), 0 1px 2px rgba(87, 82, 121, 0.06))
+ ));
+}
+
+.theme-ledger {
+ --ig-size: var(--ig-size-small);
+ --ig-radius-factor: 0;
+
+ @include tokens(grid-theme(
+ $background: $ledger-bg,
+ $foreground: #2e3440,
+ $accent-color: $ledger-accent,
+ $header-background: #d8dee9,
+ $row-odd-background: #eceff4,
+ $row-even-background: #e5e9f0,
+ $body-column-border-color-odd: #d8dee9,
+ $body-column-border-color-even: #d8dee9,
+ $row-border-color: #d8dee9,
+ $grid-border-color: #c8d0dc
+ ));
+}
+
+.theme-editorial {
+ --ig-size: var(--ig-size-large);
+ --ig-radius-factor: 0;
+
+ @include tokens(grid-theme(
+ $schema: $dark-material-schema,
+ $background: $editorial-bg,
+ $foreground: #d3c6aa,
+ $accent-color: $editorial-accent,
+ $header-background: #3a464c,
+ $row-border-color: #333c43,
+ $grid-border-color: #333c43
+ ));
+}
+
+.theme-midnight {
+ --ig-size: var(--ig-size-medium);
+ --ig-radius-factor: 0.25;
+
+ @include tokens(grid-theme(
+ $schema: $dark-material-schema,
+ $background: $midnight-bg,
+ $foreground: #f8f8f2,
+ $accent-color: $midnight-accent,
+ $header-background: #21222c,
+ $body-column-border-color-odd: #44475a,
+ $body-column-border-color-even: #44475a,
+ $row-border-color: #44475a,
+ $grid-border-color: #44475a
+ ));
+}
+
+.pivot-theming-sample {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.theme-picker {
+ --ig-button-group-elevation: 0;
+
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ align-self: flex-start;
+}
+
+.theme-picker__label {
+ font-size: 12px;
+ font-weight: 500;
+ letter-spacing: 0.04em;
+ color: var(--ig-gray-700);
+}
+
+.theme-picker__hint {
+ margin: 0;
+ font-size: 12px;
+ line-height: 1.45;
+ color: var(--ig-gray-600);
+
+ code {
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
+ font-size: 11px;
+ }
+}
+
+.theme-swatch {
+ display: inline-block;
+ min-width: 14px;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ border: 1px solid var(--ig-gray-300);
+ vertical-align: -2px;
+ background: linear-gradient(135deg, var(--swatch-bg) 0 50%, var(--swatch-accent) 50% 100%);
+}
+
+.theme-swatch--studio {
+ --swatch-bg: #{$studio-bg};
+ --swatch-accent: #{$studio-accent};
+}
+
+.theme-swatch--ledger {
+ --swatch-bg: #{$ledger-bg};
+ --swatch-accent: #{$ledger-accent};
+}
+
+.theme-swatch--editorial {
+ --swatch-bg: #{$editorial-bg};
+ --swatch-accent: #{$editorial-accent};
+}
+
+.theme-swatch--midnight {
+ --swatch-bg: #{$midnight-bg};
+ --swatch-accent: #{$midnight-accent};
}
diff --git a/src/app/pivot-grid/pivot-styling/pivot-styling.component.ts b/src/app/pivot-grid/pivot-styling/pivot-styling.component.ts
index b85a70c4f..039ae0185 100644
--- a/src/app/pivot-grid/pivot-styling/pivot-styling.component.ts
+++ b/src/app/pivot-grid/pivot-styling/pivot-styling.component.ts
@@ -1,6 +1,8 @@
-import { Component, ChangeDetectionStrategy } from "@angular/core";
+import { Component, ChangeDetectionStrategy, signal } from "@angular/core";
import { DATA } from '../../data/pivot-data';
+import { IgxButtonGroupComponent } from 'igniteui-angular/button-group';
+import { IgxButtonDirective } from 'igniteui-angular/directives';
import { IPivotConfiguration, IgxPivotNumericAggregate } from 'igniteui-angular/grids/core';
import { IgxPivotGridComponent } from 'igniteui-angular/grids/pivot-grid';
@@ -9,24 +11,38 @@ import { IgxPivotGridComponent } from 'igniteui-angular/grids/pivot-grid';
styleUrls: ['./pivot-styling.component.scss'],
templateUrl: './pivot-styling.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
- imports: [IgxPivotGridComponent]
+ imports: [IgxPivotGridComponent, IgxButtonGroupComponent, IgxButtonDirective]
})
export class PivotStylingComponent {
public data = DATA;
+ public themes = [
+ { label: 'Studio', class: 'theme-studio', swatch: 'theme-swatch--studio' },
+ { label: 'Ledger', class: 'theme-ledger', swatch: 'theme-swatch--ledger' },
+ { label: 'Editorial', class: 'theme-editorial', swatch: 'theme-swatch--editorial' },
+ { label: 'Midnight', class: 'theme-midnight', swatch: 'theme-swatch--midnight' }
+ ];
+
+ public activeTheme = signal(this.themes[0].class);
+
public pivotConfigHierarchy: IPivotConfiguration = {
columns: [
{
memberName: 'Product',
memberFunction: (data) => data.Product.Name,
enabled: true
- }
+ }
],
rows: [
{
- memberName: 'Seller',
- memberFunction: (data) => data.Seller.Name,
- enabled: true
+ memberName: 'City',
+ memberFunction: (data) => data.Seller.City,
+ enabled: true,
+ childLevel: {
+ memberName: 'Seller',
+ memberFunction: (data) => data.Seller.Name,
+ enabled: true
+ }
}
],
values: [
@@ -42,4 +58,8 @@ export class PivotStylingComponent {
],
filters: null
};
+
+ public selectTheme(args: { index: number }) {
+ this.activeTheme.set(this.themes[args.index].class);
+ }
}
diff --git a/src/app/theming/grid-theme-playground/color-utils.ts b/src/app/theming/grid-theme-playground/color-utils.ts
new file mode 100644
index 000000000..9c3ed49e0
--- /dev/null
+++ b/src/app/theming/grid-theme-playground/color-utils.ts
@@ -0,0 +1,12 @@
+/** Normalizes free-typed color text to a 6-digit uppercase hex string, or null if invalid. */
+export function normalizeHexColor(raw: string): string | null {
+ let value = raw.trim();
+ if (!value) return null;
+ if (!value.startsWith('#')) value = '#' + value;
+
+ if (/^#[0-9a-fA-F]{3}$/.test(value)) {
+ value = '#' + value[1] + value[1] + value[2] + value[2] + value[3] + value[3];
+ }
+
+ return /^#[0-9a-fA-F]{6}$/.test(value) ? value.toUpperCase() : null;
+}
diff --git a/src/app/theming/grid-theme-playground/grid-theme-playground.component.html b/src/app/theming/grid-theme-playground/grid-theme-playground.component.html
new file mode 100644
index 000000000..fb3918857
--- /dev/null
+++ b/src/app/theming/grid-theme-playground/grid-theme-playground.component.html
@@ -0,0 +1,223 @@
+
+
+
+
+
+ @switch (preview()) {
+ @case ('grid') {
+
+
+
+
+
+
+
+
+ }
+ @case ('tree') {
+ @defer (on immediate) {
+
+
+
+
+
+ {{ cell.value }}
+
+
+
+
+
+
+
+ } @placeholder {
+
Loading preview…
+ }
+ }
+ @case ('hierarchical') {
+ @defer (on immediate) {
+
+
+
+
+
+
+
+
+
+
+
+ } @placeholder {
+
Loading preview…
+ }
+ }
+ @case ('pivot') {
+ @defer (on immediate) {
+
+
+ } @placeholder {
+
Loading preview…
+ }
+ }
+ }
+
+
+
+
+
+
+ Theme code
+
+
+
+
+
+
+ SCSS
+
+
+
+
+
+
+
+
+
+ CSS variables
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/theming/grid-theme-playground/grid-theme-playground.component.scss b/src/app/theming/grid-theme-playground/grid-theme-playground.component.scss
new file mode 100644
index 000000000..2a0e5ce63
--- /dev/null
+++ b/src/app/theming/grid-theme-playground/grid-theme-playground.component.scss
@@ -0,0 +1,173 @@
+@use "igniteui-angular/theming" as *;
+
+:host {
+ display: block;
+ block-size: 100%;
+}
+
+.playground {
+ display: flex;
+ align-items: stretch;
+ gap: 20px;
+ padding: 16px;
+ box-sizing: border-box;
+ block-size: 100%;
+ min-block-size: 520px;
+ overflow: hidden;
+}
+
+.playground__stage {
+ @include tokens(grid-theme(
+ $background: #08002e,
+ $accent-color: #ff7d52
+ ));
+}
+
+.playground__rail {
+ --ig-size: var(--ig-size-medium);
+ --ig-button-group-elevation: 0;
+
+ flex: 0 0 280px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ min-block-size: 0;
+ overflow-y: auto;
+ padding-inline-end: 4px;
+
+ igx-expansion-panel-title {
+ font-size: 13px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ color: var(--ig-gray-700);
+ }
+}
+
+.playground__hint {
+ margin: 0 0 10px;
+ font-size: 12px;
+ line-height: 1.45;
+ color: var(--ig-gray-600);
+}
+
+.playground__fields {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.playground__control {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.playground__label {
+ font-size: 12px;
+ color: var(--ig-gray-700);
+}
+
+.playground__actions {
+ position: sticky;
+ inset-block-end: 0;
+ z-index: 1;
+ display: flex;
+ gap: 8px;
+ padding-block: 8px;
+ background: var(--ig-surface-500);
+ border-block-start: 1px solid var(--ig-gray-200);
+
+ button {
+ flex: 1;
+ }
+}
+
+.playground__dialog-title {
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.playground__dialog-body {
+ --ig-size: var(--ig-size-medium);
+ --ig-icon-size: 18px;
+ --ig-h5-font-size: 14px;
+ --ig-h5-font-weight: 600;
+ --ig-expansion-panel-border-radius: 4px;
+ --ig-expansion-panel-header-background: var(--ig-gray-100);
+
+ inline-size: min(680px, 80vw);
+
+ ::ng-deep .igx-expansion-panel__body {
+ padding: 0;
+ }
+}
+
+.playground__copy {
+ margin-inline-start: auto;
+}
+
+.playground__export {
+ border: 1px solid var(--ig-gray-300);
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+.playground__code {
+ ::ng-deep pre.shiki {
+ margin: 0;
+ padding: 10px 12px;
+ font-size: 13px;
+ line-height: 1.5;
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
+ max-block-size: 55vh;
+ overflow: auto;
+ overscroll-behavior: contain;
+ }
+}
+
+.playground__loading {
+ display: grid;
+ place-items: center;
+ block-size: 100%;
+ color: var(--ig-gray-600);
+ font-size: 13px;
+}
+
+.playground__preview {
+ flex: 1 1 auto;
+ min-inline-size: 0;
+ min-block-size: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.playground__stage {
+ flex: 1 1 auto;
+ min-block-size: 0;
+ overflow: hidden;
+}
+
+.playground__cell {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ height: 100%;
+}
+
+@media (max-width: 860px) {
+ .playground {
+ flex-direction: column;
+ block-size: auto;
+ overflow: visible;
+ }
+
+ .playground__rail {
+ flex: 0 0 auto;
+ overflow: visible;
+ }
+
+ .playground__stage {
+ min-block-size: 460px;
+ }
+}
diff --git a/src/app/theming/grid-theme-playground/grid-theme-playground.component.ts b/src/app/theming/grid-theme-playground/grid-theme-playground.component.ts
new file mode 100644
index 000000000..1ed058dde
--- /dev/null
+++ b/src/app/theming/grid-theme-playground/grid-theme-playground.component.ts
@@ -0,0 +1,318 @@
+import { Component, ElementRef, afterNextRender, computed, inject, signal, viewChild } from '@angular/core';
+import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
+import { IgxAccordionComponent } from 'igniteui-angular/accordion';
+import { IgxAvatarComponent } from 'igniteui-angular/avatar';
+import { IgxButtonGroupComponent } from 'igniteui-angular/button-group';
+import { IgxNumberSummaryOperand, IgxSummaryResult } from 'igniteui-angular/core';
+import { IgxIconComponent } from 'igniteui-angular/icon';
+import { IgxDialogActionsDirective, IgxDialogComponent, IgxDialogTitleDirective } from 'igniteui-angular/dialog';
+import { IgxButtonDirective, IgxIconButtonDirective } from 'igniteui-angular/directives';
+import { IGX_EXPANSION_PANEL_DIRECTIVES } from 'igniteui-angular/expansion-panel';
+import { IgxCellTemplateDirective, IgxColumnComponent, IPivotConfiguration, IgxPivotNumericAggregate } from 'igniteui-angular/grids/core';
+import { IgxGridComponent } from 'igniteui-angular/grids/grid';
+import { IgxHierarchicalGridComponent, IgxRowIslandComponent } from 'igniteui-angular/grids/hierarchical-grid';
+import { IgxPivotGridComponent } from 'igniteui-angular/grids/pivot-grid';
+import { IgxTreeGridComponent } from 'igniteui-angular/grids/tree-grid';
+import { IgxPaginatorComponent } from 'igniteui-angular/paginator';
+import { IgxSwitchComponent } from 'igniteui-angular/switch';
+import { INVOICE_DATA } from '../../data/invoiceData';
+import { DATA as PIVOT_DATA } from '../../data/pivot-data';
+import { SINGERS } from '../../data/singersData';
+import { EMPLOYEE_FLAT_AVATARS_DATA } from '../../tree-grid/data/employees-flat-avatars';
+import { ThemeColorFieldComponent } from './theme-color-field.component';
+
+type TokenKey = 'background' | 'accentColor' | 'foreground' | 'headerBackground' | 'headerForeground';
+
+interface TokenDescriptor {
+ key: TokenKey;
+ cssVar: string;
+ scss: string;
+ label: string;
+}
+class CompactSummary extends IgxNumberSummaryOperand {
+ public override operate(data?: any[]): IgxSummaryResult[] {
+ return super.operate(data).filter(r => r.key === 'count' || r.key === 'sum');
+ }
+}
+const PRIMARY_TOKENS: TokenDescriptor[] = [
+ { key: 'background', cssVar: '--ig-grid-background', scss: '$background', label: 'Background' },
+ { key: 'accentColor', cssVar: '--ig-grid-accent-color', scss: '$accent-color', label: 'Accent' },
+ { key: 'foreground', cssVar: '--ig-grid-foreground', scss: '$foreground', label: 'Foreground' }
+];
+const HEADER_TOKENS: TokenDescriptor[] = [
+ { key: 'headerBackground', cssVar: '--ig-grid-header-background', scss: '$header-background', label: 'Header background' },
+ { key: 'headerForeground', cssVar: '--ig-grid-header-text-color', scss: '$header-text-color', label: 'Header foreground' }
+];
+
+const ALL_TOKENS = [...PRIMARY_TOKENS, ...HEADER_TOKENS];
+
+const EMPTY_COLORS: Record = {
+ background: '', accentColor: '', foreground: '', headerBackground: '', headerForeground: ''
+};
+/** What Sass emits for header text once $header-background is declared. */
+const HEADER_TEXT_ON_HEADER = 'hsla(from color(from var(--ig-grid-header-background) var(--y-contrast)) h 0 l/1)';
+const DIVIDER = 'hsl(from color-mix(in srgb, var(--ig-grid-foreground) 16%, var(--ig-grid-background)) h s l/0.38)';
+const ZEBRA = 'color-mix(in srgb, var(--ig-grid-foreground) 4%, var(--ig-grid-background))';
+const NO_DIVIDER = 'var(--ig-grid-background)';
+
+@Component({
+ selector: 'app-grid-theme-playground',
+ styleUrls: ['./grid-theme-playground.component.scss'],
+ templateUrl: './grid-theme-playground.component.html',
+ imports: [
+ IgxAccordionComponent, IGX_EXPANSION_PANEL_DIRECTIVES, IgxAvatarComponent, IgxButtonDirective,
+ IgxButtonGroupComponent, IgxCellTemplateDirective, IgxColumnComponent,
+ IgxDialogActionsDirective, IgxDialogComponent, IgxDialogTitleDirective, IgxGridComponent,
+ IgxHierarchicalGridComponent, IgxIconButtonDirective, IgxIconComponent, IgxPaginatorComponent, IgxPivotGridComponent, IgxRowIslandComponent,
+ IgxSwitchComponent, IgxTreeGridComponent, ThemeColorFieldComponent
+ ]
+})
+export class GridThemePlaygroundComponent {
+ protected readonly primaryTokens = PRIMARY_TOKENS;
+ protected readonly headerTokens = HEADER_TOKENS;
+ protected readonly compactSummary = CompactSummary;
+
+ protected readonly colors = signal>({ ...EMPTY_COLORS });
+ protected readonly size = signal<'small' | 'medium' | 'large'>('medium');
+ protected readonly radiusFactor = signal(0.4);
+ protected readonly horizontalDividers = signal(true);
+ protected readonly verticalDividers = signal(false);
+ protected readonly zebra = signal(false);
+ protected readonly preview = signal<'grid' | 'tree' | 'hierarchical' | 'pivot'>('grid');
+
+ protected readonly copied = signal<'scss' | 'css' | null>(null);
+
+ protected readonly sizes = [
+ { label: 'Small', selected: false },
+ { label: 'Medium', selected: true },
+ { label: 'Large', selected: false }
+ ];
+ protected readonly roundnessOptions = [
+ { label: 'Square', selected: false },
+ { label: 'Soft', selected: true },
+ { label: 'Round', selected: false }
+ ];
+ protected readonly previews = [
+ { label: 'Flat', selected: true },
+ { label: 'Tree', selected: false },
+ { label: 'Hierarchical', selected: false },
+ { label: 'Pivot', selected: false }
+ ];
+
+ public readonly data = INVOICE_DATA;
+ public readonly treeData = EMPLOYEE_FLAT_AVATARS_DATA();
+ public readonly hierarchicalData = SINGERS;
+ public readonly pivotData = PIVOT_DATA;
+
+ public pivotConfig: IPivotConfiguration = {
+ columns: [{ memberName: 'Product', memberFunction: (data) => data.Product.Name, enabled: true }],
+ rows: [{
+ memberName: 'City',
+ memberFunction: (data) => data.Seller.City,
+ enabled: true,
+ childLevel: { memberName: 'Seller', memberFunction: (data) => data.Seller.Name, enabled: true }
+ }],
+ values: [{
+ member: 'NumberOfUnits',
+ aggregate: { aggregator: IgxPivotNumericAggregate.sum, key: 'sum', label: 'Sum' },
+ enabled: true
+ }],
+ filters: null
+ };
+
+ private readonly sanitizer = inject(DomSanitizer);
+
+ protected readonly scssHtml = signal(null);
+ protected readonly cssHtml = signal(null);
+
+ private highlighter: { codeToHtml: (code: string, opts: { lang: string; theme: string }) => string } | null = null;
+
+ private readonly stage = viewChild.required>('stage');
+ /** The compiled theme, read from the stylesheet: the CSS export needs every
+ * token, not just the roots, to work in an app with no grid-theme(). */
+ private readonly themeTokens = signal<[string, string][]>([]);
+
+ constructor() {
+ afterNextRender(() => {
+ this.seedFromStylesheet();
+ this.readCompiledTheme();
+ });
+ }
+ protected readonly previewStyle = computed>(() => {
+ const style: Record = {};
+ const colors = this.colors();
+
+ for (const token of ALL_TOKENS) {
+ if (colors[token.key]) {
+ style[token.cssVar] = colors[token.key];
+ }
+ }
+ if (colors.headerBackground && !colors.headerForeground) {
+ style['--ig-grid-header-text-color'] = HEADER_TEXT_ON_HEADER;
+ }
+
+ style['--ig-size'] = `var(--ig-size-${this.size()})`;
+ style['--ig-radius-factor'] = `${this.radiusFactor()}`;
+ style['--ig-grid-row-border-color'] = this.horizontalDividers() ? DIVIDER : NO_DIVIDER;
+
+ const columnRule = this.verticalDividers() ? DIVIDER : NO_DIVIDER;
+ style['--ig-grid-body-column-border-color-odd'] = columnRule;
+ style['--ig-grid-body-column-border-color-even'] = columnRule;
+
+ if (this.zebra()) {
+ style['--ig-grid-row-even-background'] = ZEBRA;
+ }
+
+ return style;
+ });
+ protected readonly exportScss = computed(() => {
+ const colors = this.colors();
+ const themeLines = ALL_TOKENS
+ .filter(token => colors[token.key])
+ .map(token => ` ${token.scss}: ${colors[token.key]},`);
+
+ if (!this.horizontalDividers()) {
+ themeLines.push(` $row-border-color: ${NO_DIVIDER},`);
+ }
+ if (this.verticalDividers()) {
+ themeLines.push(` $body-column-border-color-odd: ${DIVIDER},`);
+ themeLines.push(` $body-column-border-color-even: ${DIVIDER},`);
+ }
+ if (this.zebra()) {
+ themeLines.push(` $row-even-background: ${ZEBRA},`);
+ }
+
+ const varLines = [
+ ` --ig-size: var(--ig-size-${this.size()});`,
+ ` --ig-radius-factor: ${this.radiusFactor()};`
+ ];
+
+ if (!themeLines.length) {
+ return `.my-grid {\n${varLines.join('\n')}\n}`;
+ }
+
+ return `.my-grid {\n${varLines.join('\n')}\n\n @include tokens(grid-theme(\n${themeLines.join('\n')}\n ));\n}`;
+ });
+
+ protected readonly exportCss = computed(() => {
+ const overrides = this.previewStyle();
+ const base = this.themeTokens();
+
+ if (!base.length) {
+ const lines = Object.entries(overrides).map(([name, value]) => ` ${name}: ${value};`);
+ return `/* Overrides only -- requires a grid-theme() base to derive from. */\n.my-grid {\n${lines.join('\n')}\n}`;
+ }
+
+ const emitted = new Set();
+ const lines = base.map(([name, value]) => {
+ emitted.add(name);
+ return ` ${name}: ${overrides[name] ?? value};`;
+ });
+ const extras = Object.entries(overrides)
+ .filter(([name]) => !emitted.has(name))
+ .map(([name, value]) => ` ${name}: ${value};`);
+
+ return `.my-grid {\n${[...extras, ...lines].join('\n')}\n}`;
+ });
+
+ protected setColor(key: TokenKey, value: string): void {
+ this.colors.update(current => ({ ...current, [key]: value }));
+ }
+
+ protected reset(): void {
+ this.colors.set({ ...EMPTY_COLORS });
+ this.size.set('medium');
+ this.radiusFactor.set(0.4);
+ this.horizontalDividers.set(true);
+ this.verticalDividers.set(false);
+ this.zebra.set(false);
+ this.seedFromStylesheet();
+ }
+
+ protected async showCode(dialog: IgxDialogComponent): Promise {
+ // Loaded on demand so shiki stays out of the initial bundle.
+ if (!this.highlighter) {
+ // shiki/core with explicit grammars: the bundled entry point resolves
+ // languages dynamically and emits a chunk for every one it ships.
+ const [core, engine, scss, css, theme] = await Promise.all([
+ import('shiki/core'),
+ import('shiki/engine/javascript'),
+ import('shiki/langs/scss.mjs'),
+ import('shiki/langs/css.mjs'),
+ import('shiki/themes/material-theme-lighter.mjs')
+ ]);
+
+ this.highlighter = await core.createHighlighterCore({
+ themes: [theme.default],
+ langs: [scss.default, css.default],
+ engine: engine.createJavaScriptRegexEngine()
+ });
+ }
+
+ const render = (code: string | null, lang: string) => code
+ ? this.sanitizer.bypassSecurityTrustHtml(this.highlighter.codeToHtml(code, { lang, theme: 'material-theme-lighter' }))
+ : null;
+
+ this.scssHtml.set(render(this.exportScss(), 'scss'));
+ this.cssHtml.set(render(this.exportCss(), 'css'));
+ dialog.open();
+ }
+
+ protected copy(code: string | null, format: 'scss' | 'css'): void {
+ if (!code) return;
+ navigator.clipboard.writeText(code).then(() => {
+ this.copied.set(format);
+ setTimeout(() => this.copied.set(null), 2000);
+ });
+ }
+
+ protected onSize(args: { index: number }): void {
+ this.size.set((['small', 'medium', 'large'] as const)[args.index]);
+ }
+
+ protected onRoundness(args: { index: number }): void {
+ this.radiusFactor.set([0, 0.4, 1][args.index]);
+ }
+
+ protected onPreview(args: { index: number }): void {
+ this.preview.set((['grid', 'tree', 'hierarchical', 'pivot'] as const)[args.index]);
+ }
+ private readCompiledTheme(): void {
+ const tokens: [string, string][] = [];
+
+ for (const sheet of Array.from(document.styleSheets)) {
+ let rules: CSSRuleList;
+
+ try {
+ rules = sheet.cssRules;
+ } catch {
+ continue; // cross-origin sheet, not ours
+ }
+
+ for (const rule of Array.from(rules)) {
+ if (!(rule instanceof CSSStyleRule) || !rule.selectorText.includes('playground__stage')) {
+ continue;
+ }
+
+ for (let i = 0; i < rule.style.length; i++) {
+ const name = rule.style.item(i);
+ if (name.startsWith('--ig-')) {
+ tokens.push([name, rule.style.getPropertyValue(name).trim()]);
+ }
+ }
+ }
+ }
+
+ this.themeTokens.set(tokens);
+ }
+
+ private seedFromStylesheet(): void {
+ const styles = getComputedStyle(this.stage().nativeElement);
+
+ this.colors.update(current => ({
+ ...current,
+ background: styles.getPropertyValue('--ig-grid-background').trim(),
+ accentColor: styles.getPropertyValue('--ig-grid-accent-color').trim()
+ }));
+ }
+}
diff --git a/src/app/theming/grid-theme-playground/theme-color-field.component.html b/src/app/theming/grid-theme-playground/theme-color-field.component.html
new file mode 100644
index 000000000..69afda312
--- /dev/null
+++ b/src/app/theming/grid-theme-playground/theme-color-field.component.html
@@ -0,0 +1,20 @@
+
+ {{ label() }}
+
+
+
+
+
+
+
+
+ @if (value()) {
+
+
+
+ }
+
+
diff --git a/src/app/theming/grid-theme-playground/theme-color-field.component.scss b/src/app/theming/grid-theme-playground/theme-color-field.component.scss
new file mode 100644
index 000000000..f06f7a1d6
--- /dev/null
+++ b/src/app/theming/grid-theme-playground/theme-color-field.component.scss
@@ -0,0 +1,76 @@
+.color-field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+
+ &__label {
+ font-size: 12px;
+ color: var(--ig-gray-700);
+ }
+
+ igx-input-group {
+ --ig-input-group-focused-border-color: var(--ig-gray-500);
+
+ [igxInput] {
+ font-size: 13px;
+ font-weight: 400;
+ }
+ }
+
+ &__swatch-wrap {
+ position: relative;
+ width: 28px;
+ height: 28px;
+ flex-shrink: 0;
+
+ input[type='color'] {
+ appearance: none;
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+ border: none;
+ padding: 0;
+ margin: 0;
+ }
+ }
+
+ &__swatch {
+ position: absolute;
+ inset: 0;
+ border-radius: 5px;
+ border: 1px solid var(--ig-gray-400);
+ pointer-events: none;
+ }
+
+ // "auto" -- the token is deriving rather than being overridden.
+ &__swatch--auto {
+ background: repeating-linear-gradient(
+ 45deg,
+ var(--ig-gray-200) 0 4px,
+ var(--ig-gray-100) 4px 8px
+ );
+ }
+
+ &__reset {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ border: none;
+ border-radius: 4px;
+ background: transparent;
+ color: var(--ig-gray-600);
+ font-size: 14px;
+ cursor: pointer;
+ transition: color 120ms ease, background 120ms ease;
+
+ &:hover {
+ color: var(--ig-gray-700);
+ background: var(--ig-gray-100);
+ }
+ }
+}
diff --git a/src/app/theming/grid-theme-playground/theme-color-field.component.ts b/src/app/theming/grid-theme-playground/theme-color-field.component.ts
new file mode 100644
index 000000000..47e532ae5
--- /dev/null
+++ b/src/app/theming/grid-theme-playground/theme-color-field.component.ts
@@ -0,0 +1,48 @@
+import { ChangeDetectionStrategy, Component, input, output } from '@angular/core';
+import { IgxInputGroupComponent, IgxInputDirective, IgxPrefixDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
+import { normalizeHexColor } from './color-utils';
+
+@Component({
+ selector: 'app-theme-color-field',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ templateUrl: './theme-color-field.component.html',
+ styleUrl: './theme-color-field.component.scss',
+ imports: [IgxInputGroupComponent, IgxInputDirective, IgxPrefixDirective, IgxSuffixDirective]
+})
+export class ThemeColorFieldComponent {
+ public readonly label = input.required();
+ public readonly value = input('');
+ /** Seed for the native colour wheel while the value is empty ("auto"). */
+ public readonly fallback = input('#808080');
+ public readonly valueChange = output();
+
+ private pendingFrame: number | null = null;
+ private latestPickedValue: string | null = null;
+ protected onPick(event: Event): void {
+ this.latestPickedValue = (event.target as HTMLInputElement).value;
+ if (this.pendingFrame !== null) return;
+
+ this.pendingFrame = requestAnimationFrame(() => {
+ this.pendingFrame = null;
+ if (this.latestPickedValue !== null) {
+ this.valueChange.emit(this.latestPickedValue);
+ this.latestPickedValue = null;
+ }
+ });
+ }
+
+ protected onTextChange(event: Event): void {
+ const inputEl = event.target as HTMLInputElement;
+ const normalized = normalizeHexColor(inputEl.value);
+ if (normalized) {
+ this.valueChange.emit(normalized);
+ inputEl.value = normalized;
+ } else {
+ inputEl.value = this.value();
+ }
+ }
+
+ protected reset(): void {
+ this.valueChange.emit('');
+ }
+}
diff --git a/src/app/theming/theming-routes-data.ts b/src/app/theming/theming-routes-data.ts
index a3a73cff1..91182ac67 100644
--- a/src/app/theming/theming-routes-data.ts
+++ b/src/app/theming/theming-routes-data.ts
@@ -12,5 +12,6 @@ export const themingRoutesData = {
'roundness': { displayName: 'Roundness', parentName: 'Themes' },
'angular-sample': { displayName: 'Angular Material Theming', parentName: 'Themes' },
'bootstrap-sample': { displayName: 'Bootstrap Theming', parentName: 'Themes' },
- 'card-sample-shadow': { displayName: 'card-sample-shadow', parentName: 'Shadows' }
+ 'card-sample-shadow': { displayName: 'card-sample-shadow', parentName: 'Shadows' },
+ 'grid-theme-playground': { displayName: 'Grid Theme Playground', parentName: 'Themes' }
};
diff --git a/src/app/theming/theming.routes.ts b/src/app/theming/theming.routes.ts
index 2a0cd5e31..b1e0d0aac 100644
--- a/src/app/theming/theming.routes.ts
+++ b/src/app/theming/theming.routes.ts
@@ -4,6 +4,7 @@ import { AnimationsSample2Component } from './animations/animations-sample-2/ani
import { AngularMaterialComponent } from './angular/angular-sample.component';
import { BootstrapComponent } from './bootstrap/bootstrap-sample.component';
import { DefaultThemeSampleComponent } from './default-theme-sample/default-theme-sample.component';
+import { GridThemePlaygroundComponent } from './grid-theme-playground/grid-theme-playground.component';
import { DisplayDensityComponent } from './display-density/display-density.component';
import { RoundnessComponent } from './roundness/roundness-sample.component';
import { CardSampleShadowComponent} from './shadows/card-sample-shadow/card-sample-shadow';
@@ -68,5 +69,10 @@ export const ThemingRoutes: Routes = [
component: TailwindSampleComponent,
data: themingRoutesData['tailwind-sample'],
path: 'tailwind-sample'
+ },
+ {
+ component: GridThemePlaygroundComponent,
+ data: themingRoutesData['grid-theme-playground'],
+ path: 'grid-theme-playground'
}
];
diff --git a/src/app/tree-grid/tree-grid-rowStyles-sample/tree-grid-rowStyle.component.ts b/src/app/tree-grid/tree-grid-rowStyles-sample/tree-grid-rowStyle.component.ts
index b18c2bcc1..324867fac 100644
--- a/src/app/tree-grid/tree-grid-rowStyles-sample/tree-grid-rowStyle.component.ts
+++ b/src/app/tree-grid/tree-grid-rowStyles-sample/tree-grid-rowStyle.component.ts
@@ -25,7 +25,7 @@ export class TreeGridRowStylesComponent implements OnInit {
row.data['Title'].includes('Senior') ? '#f8f9fa' : null;
// eslint-disable-next-line @typescript-eslint/member-ordering
- public rowStyles = {
+ public rowStyles: any = {
background: this.background,
'border-left': (row: RowType) => row.data['Title'] === 'CEO' || row.data['Title'].includes('President') ?
'2px solid' : null,
diff --git a/src/app/tree-grid/tree-grid-style/layout.scss b/src/app/tree-grid/tree-grid-style/layout.scss
index 0fda2fbb5..5c8872626 100644
--- a/src/app/tree-grid/tree-grid-style/layout.scss
+++ b/src/app/tree-grid/tree-grid-style/layout.scss
@@ -1,5 +1,9 @@
+:host {
+ display: block;
+}
+
.grid__wrapper {
- margin: 10px;
+ padding: 10px;
}
.cell__inner {
diff --git a/src/app/tree-grid/tree-grid-style/tree-grid-style.component.html b/src/app/tree-grid/tree-grid-style/tree-grid-style.component.html
index 9eeff6a07..751efd718 100644
--- a/src/app/tree-grid/tree-grid-style/tree-grid-style.component.html
+++ b/src/app/tree-grid/tree-grid-style/tree-grid-style.component.html
@@ -1,6 +1,38 @@
-
-
+
+
Pick a theme
+
+
+ @for (theme of themes; track theme.class) {
+
+ }
+
+
+
+ Custom themes, not built-in: each is a grid-theme() with its
+ own background and accent.
+
+
+
+
+
@@ -8,8 +40,8 @@
-
-
-
+
+
+
diff --git a/src/app/tree-grid/tree-grid-style/tree-grid-style.component.scss b/src/app/tree-grid/tree-grid-style/tree-grid-style.component.scss
index 2c0d35a3b..9a14ff29e 100644
--- a/src/app/tree-grid/tree-grid-style/tree-grid-style.component.scss
+++ b/src/app/tree-grid/tree-grid-style/tree-grid-style.component.scss
@@ -1,16 +1,141 @@
@use "layout.scss";
@use "igniteui-angular/theming" as *;
-$background: #292826;
-$foreground: #eeece1;
-$accent: #ffcd0f;
-
-$custom-grid: grid-theme(
- $background: $background,
- $foreground: $foreground,
- $accent-color: $accent,
-);
-
-:host {
- @include tokens($custom-grid);
+$studio-bg: #faf4ed;
+$studio-accent: #907aa9;
+$ledger-bg: #eceff4;
+$ledger-accent: #5e81ac;
+$editorial-bg: #333c43;
+$editorial-accent: #a7c080;
+$midnight-bg: #282a36;
+$midnight-accent: #bd93f9;
+
+.theme-studio {
+ --ig-size: var(--ig-size-large);
+ --ig-radius-factor: 0.6;
+
+ @include tokens(grid-theme(
+ $background: $studio-bg,
+ $foreground: #575279,
+ $accent-color: $studio-accent,
+ $header-background: #fffaf3,
+ $header-border-color: #dfdad9,
+ $row-border-color: #f2e9e1,
+ $grid-border-color: #dfdad9,
+ $grid-shadow: (0 1px 3px rgba(87, 82, 121, 0.10), 0 1px 2px rgba(87, 82, 121, 0.06))
+ ));
+}
+
+.theme-ledger {
+ --ig-size: var(--ig-size-small);
+ --ig-radius-factor: 0;
+
+ @include tokens(grid-theme(
+ $background: $ledger-bg,
+ $foreground: #2e3440,
+ $accent-color: $ledger-accent,
+ $header-background: #d8dee9,
+ $row-odd-background: #eceff4,
+ $row-even-background: #e5e9f0,
+ $body-column-border-color-odd: #d8dee9,
+ $body-column-border-color-even: #d8dee9,
+ $row-border-color: #d8dee9,
+ $grid-border-color: #c8d0dc
+ ));
+}
+
+.theme-editorial {
+ --ig-size: var(--ig-size-large);
+ --ig-radius-factor: 0;
+
+ @include tokens(grid-theme(
+ $schema: $dark-material-schema,
+ $background: $editorial-bg,
+ $foreground: #d3c6aa,
+ $accent-color: $editorial-accent,
+ $header-background: #3a464c,
+ $row-border-color: #333c43,
+ $grid-border-color: #333c43
+ ));
+}
+
+.theme-midnight {
+ --ig-size: var(--ig-size-medium);
+ --ig-radius-factor: 0.25;
+
+ @include tokens(grid-theme(
+ $schema: $dark-material-schema,
+ $background: $midnight-bg,
+ $foreground: #f8f8f2,
+ $accent-color: $midnight-accent,
+ $header-background: #21222c,
+ $body-column-border-color-odd: #44475a,
+ $body-column-border-color-even: #44475a,
+ $row-border-color: #44475a,
+ $grid-border-color: #44475a
+ ));
+}
+
+.grid__wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.theme-picker {
+ --ig-button-group-elevation: 0;
+
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ align-self: flex-start;
+}
+
+.theme-picker__label {
+ font-size: 12px;
+ font-weight: 500;
+ letter-spacing: 0.04em;
+ color: var(--ig-gray-700);
+}
+
+.theme-picker__hint {
+ margin: 0;
+ font-size: 12px;
+ line-height: 1.45;
+ color: var(--ig-gray-600);
+
+ code {
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
+ font-size: 11px;
+ }
+}
+
+.theme-swatch {
+ display: inline-block;
+ min-width: 14px;
+ aspect-ratio: 1;
+ border-radius: 50%;
+ border: 1px solid var(--ig-gray-300);
+ vertical-align: -2px;
+ background: linear-gradient(135deg, var(--swatch-bg) 0 50%, var(--swatch-accent) 50% 100%);
+}
+
+.theme-swatch--studio {
+ --swatch-bg: #{$studio-bg};
+ --swatch-accent: #{$studio-accent};
+}
+
+.theme-swatch--ledger {
+ --swatch-bg: #{$ledger-bg};
+ --swatch-accent: #{$ledger-accent};
+}
+
+.theme-swatch--editorial {
+ --swatch-bg: #{$editorial-bg};
+ --swatch-accent: #{$editorial-accent};
+}
+
+.theme-swatch--midnight {
+ --swatch-bg: #{$midnight-bg};
+ --swatch-accent: #{$midnight-accent};
}
diff --git a/src/app/tree-grid/tree-grid-style/tree-grid-style.component.ts b/src/app/tree-grid/tree-grid-style/tree-grid-style.component.ts
index 653f39c22..f2a247206 100644
--- a/src/app/tree-grid/tree-grid-style/tree-grid-style.component.ts
+++ b/src/app/tree-grid/tree-grid-style/tree-grid-style.component.ts
@@ -1,21 +1,35 @@
-import { Component, OnInit } from '@angular/core';
-import { EMPLOYEE_FLAT_AVATARS_DATA } from '../data/employees-flat-avatars';
-import { IgxTreeGridComponent } from 'igniteui-angular/grids/tree-grid';
-import { IgxCellTemplateDirective, IgxColumnComponent } from 'igniteui-angular/grids/core';
+import { Component, OnInit, signal } from '@angular/core';
import { IgxAvatarComponent } from 'igniteui-angular/avatar';
+import { IgxButtonGroupComponent } from 'igniteui-angular/button-group';
+import { IgxButtonDirective } from 'igniteui-angular/directives';
+import { IgxCellTemplateDirective, IgxColumnComponent } from 'igniteui-angular/grids/core';
+import { IgxTreeGridComponent } from 'igniteui-angular/grids/tree-grid';
+import { EMPLOYEE_FLAT_AVATARS_DATA } from '../data/employees-flat-avatars';
import { IgxPreventDocumentScrollDirective } from '../../directives/prevent-scroll.directive';
@Component({
selector: 'app-tree-grid-style',
styleUrls: ['./tree-grid-style.component.scss'],
templateUrl: './tree-grid-style.component.html',
- imports: [IgxTreeGridComponent, IgxPreventDocumentScrollDirective, IgxColumnComponent, IgxCellTemplateDirective, IgxAvatarComponent]
+ imports: [IgxTreeGridComponent, IgxPreventDocumentScrollDirective, IgxColumnComponent, IgxCellTemplateDirective, IgxAvatarComponent, IgxButtonGroupComponent, IgxButtonDirective]
})
export class TreeGridStyleComponent implements OnInit {
public data: any[];
+ public themes = [
+ { label: 'Studio', class: 'theme-studio', swatch: 'theme-swatch--studio' },
+ { label: 'Ledger', class: 'theme-ledger', swatch: 'theme-swatch--ledger' },
+ { label: 'Editorial', class: 'theme-editorial', swatch: 'theme-swatch--editorial' },
+ { label: 'Midnight', class: 'theme-midnight', swatch: 'theme-swatch--midnight' }
+ ];
+
+ public activeTheme = signal(this.themes[0].class);
public ngOnInit() {
this.data = EMPLOYEE_FLAT_AVATARS_DATA();
}
+
+ public selectTheme(args: { index: number }) {
+ this.activeTheme.set(this.themes[args.index].class);
+ }
}
diff --git a/src/app/tree-grid/tree-grid-validation-style/tree-grid-validation-style.component.ts b/src/app/tree-grid/tree-grid-validation-style/tree-grid-validation-style.component.ts
index dad7ada41..b6d515bcd 100644
--- a/src/app/tree-grid/tree-grid-validation-style/tree-grid-validation-style.component.ts
+++ b/src/app/tree-grid/tree-grid-validation-style/tree-grid-validation-style.component.ts
@@ -17,7 +17,7 @@ export class TreeGridValidationStyleComponent implements OnInit {
public data: IEmployee[];
public columns: any[];
public selectionMode = 'multiple';
- public rowStyles = {
+ public rowStyles: any = {
background: (row: RowType) => row.cells.find(c => c.validation.errors !== null && c.validation.errors !== undefined) ? '#FF000033' : '#00000000'
};
public cellStyles = {