This is an automated email from the ASF dual-hosted git repository. riemer pushed a commit to branch add-data-preview-table in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 603ee55c4b3caea9a79ea3edde9fc97e8a508a8b Author: Dominik Riemer <[email protected]> AuthorDate: Fri Feb 27 09:51:00 2026 +0100 feat: Add data preview --- .../chart-container/chart-container.component.ts | 14 +- .../base/base-data-explorer-widget.directive.ts | 9 + .../models/dataview-dashboard.model.ts | 1 + .../chart-view/chart-view.component.html | 75 +++++--- .../chart-view/chart-view.component.scss | 37 ---- .../components/chart-view/chart-view.component.ts | 9 + .../chart-data-preview.component.html | 100 ++++++++++ .../chart-data-preview.component.scss | 151 +++++++++++++++ .../chart-data-preview.component.ts | 204 +++++++++++++++++++++ ui/src/scss/sp/_variables.scss | 33 ++++ 10 files changed, 573 insertions(+), 60 deletions(-) diff --git a/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts b/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts index 0c62028bab..a9f4384877 100644 --- a/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts +++ b/ui/src/app/chart-shared/components/chart-container/chart-container.component.ts @@ -38,6 +38,7 @@ import { ExtendedTimeSettings, QuickTimeSelection, SpLogMessage, + SpQueryResult, TimeSelectionConstants, TimeSettings, } from '@streampipes/platform-services'; @@ -152,6 +153,8 @@ export class ChartContainerComponent @Output() deleteCallback: EventEmitter<number> = new EventEmitter<number>(); @Output() startEditModeEmitter: EventEmitter<DataExplorerWidgetModel> = new EventEmitter<DataExplorerWidgetModel>(); + @Output() queryResultsEmitter: EventEmitter<SpQueryResult[]> = + new EventEmitter<SpQueryResult[]>(); title = ''; widgetLoaded = false; @@ -352,7 +355,15 @@ export class ChartContainerComponent this.handleTimer(ev), ); const error$ = this.componentRef.instance.errorCallback.subscribe( - ev => (this.errorMessage = ev), + ev => { + this.errorMessage = ev; + if (ev) { + this.queryResultsEmitter.emit([]); + } + }, + ); + const data$ = this.componentRef.instance.dataReceivedCallback.subscribe( + results => this.queryResultsEmitter.emit(results), ); this.componentRef.onDestroy(destroy => { @@ -360,6 +371,7 @@ export class ChartContainerComponent remove$?.unsubscribe(); timer$?.unsubscribe(); error$?.unsubscribe(); + data$?.unsubscribe(); }); } diff --git a/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts b/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts index db21d8b508..a1f862a65b 100644 --- a/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts +++ b/ui/src/app/chart-shared/components/charts/base/base-data-explorer-widget.directive.ts @@ -70,6 +70,11 @@ export abstract class BaseDataExplorerWidgetDirective< errorCallback: EventEmitter<SpLogMessage> = new EventEmitter<SpLogMessage>(); + @Output() + dataReceivedCallback: EventEmitter<SpQueryResult[]> = new EventEmitter< + SpQueryResult[] + >(); + @Input() editMode: boolean; @Input() kioskMode: boolean; @Input() dataViewMode: boolean; @@ -141,6 +146,7 @@ export abstract class BaseDataExplorerWidgetDirective< catchError(err => { this.timerCallback.emit(false); this.errorCallback.emit(err.error); + this.dataReceivedCallback.emit([]); return []; }), ); @@ -271,11 +277,14 @@ export abstract class BaseDataExplorerWidgetDirective< const spQueryResult = spQueryResults[0]; if (spQueryResult.total === 0) { + this.dataReceivedCallback.emit([]); this.setShownComponents(true, false, false, false); } else if (spQueryResult['spQueryStatus'] === 'TOO_MUCH_DATA') { this.amountOfTooMuchEvents = spQueryResult.total; + this.dataReceivedCallback.emit([]); this.setShownComponents(false, false, false, true); } else { + this.dataReceivedCallback.emit(spQueryResults); this.onDataReceived(spQueryResults); } } diff --git a/ui/src/app/chart-shared/models/dataview-dashboard.model.ts b/ui/src/app/chart-shared/models/dataview-dashboard.model.ts index e792765b6f..3f6e310754 100644 --- a/ui/src/app/chart-shared/models/dataview-dashboard.model.ts +++ b/ui/src/app/chart-shared/models/dataview-dashboard.model.ts @@ -36,6 +36,7 @@ export interface BaseWidgetData<T extends DataExplorerWidgetModel> { removeWidgetCallback: EventEmitter<boolean>; timerCallback: EventEmitter<boolean>; errorCallback: EventEmitter<SpLogMessage>; + dataReceivedCallback: EventEmitter<SpQueryResult[]>; editMode: boolean; kioskMode: boolean; diff --git a/ui/src/app/chart/components/chart-view/chart-view.component.html b/ui/src/app/chart/components/chart-view/chart-view.component.html index d34c1dab33..cdfb4ae0af 100644 --- a/ui/src/app/chart/components/chart-view/chart-view.component.html +++ b/ui/src/app/chart/components/chart-view/chart-view.component.html @@ -45,7 +45,10 @@ </div> } @else { <mat-drawer-container - class="designer-panel-container h-100 dashboard-grid" + fxFlex="100" + fxLayout="column" + class="h-100" + style="width: 100%; min-height: 0" > <mat-drawer [opened]="editMode" @@ -70,27 +73,55 @@ </div> </sp-sidebar-resize> </mat-drawer> - <mat-drawer-content class="h-100 dashboard-grid"> - <div #panel fxFlex="100" fxLayout="column"> - @if ( - dataView && - dataView.dataConfig?.sourceConfigs?.length > 0 - ) { - <sp-chart-container - fxFlex - [dataViewMode]="true" - [editMode]="editMode" - [configuredWidget]="dataView" - [timeSettings]="timeSettings" - [observableGenerator]="observableGenerator" - [dataLakeMeasure]=" - dataView.dataConfig.sourceConfigs[0] - .measure - " - (startEditModeEmitter)="editDataView()" - > - </sp-chart-container> - } + <mat-drawer-content + class="h-100" + fxFlex + fxLayout="column" + style="min-height: 0" + > + <div + fxFlex + fxLayout="column" + style="min-height: 0; overflow: hidden" + > + <div + #panel + fxLayout="column" + fxFlex + style="min-height: 0; overflow: hidden" + > + @if ( + dataView && + dataView.dataConfig?.sourceConfigs?.length > + 0 + ) { + <sp-chart-container + fxFlex + style="display: block; min-height: 0" + [dataViewMode]="true" + [editMode]="editMode" + [configuredWidget]="dataView" + [timeSettings]="timeSettings" + [observableGenerator]=" + observableGenerator + " + [dataLakeMeasure]=" + dataView.dataConfig.sourceConfigs[0] + .measure + " + (startEditModeEmitter)="editDataView()" + (queryResultsEmitter)=" + onQueryResultsChanged($event) + " + > + </sp-chart-container> + } + </div> + + <sp-chart-data-preview + [queryResults]="latestQueryResults" + > + </sp-chart-data-preview> </div> </mat-drawer-content> </mat-drawer-container> diff --git a/ui/src/app/chart/components/chart-view/chart-view.component.scss b/ui/src/app/chart/components/chart-view/chart-view.component.scss index b286235804..22e68622e7 100644 --- a/ui/src/app/chart/components/chart-view/chart-view.component.scss +++ b/ui/src/app/chart/components/chart-view/chart-view.component.scss @@ -16,38 +16,6 @@ * */ -.fixed-height { - height: 50px; -} - -.data-explorer-options { - padding: 0px; -} - -.data-explorer-options-item { - display: inline; - margin-right: 10px; -} - -.m-20 { - margin: 20px; -} - -.h-100 { - height: 100%; -} - -.dashboard-grid { - display: flex; - flex-direction: column; - flex: 1 1 100%; -} - -.designer-panel-container { - width: 100%; - height: 100%; -} - .designer-panel { border: 1px solid var(--color-tab-border); width: auto; @@ -55,8 +23,3 @@ display: inline-block; overflow-x: hidden; } - -.widget-title-text { - white-space: nowrap; - margin-right: 12px; -} diff --git a/ui/src/app/chart/components/chart-view/chart-view.component.ts b/ui/src/app/chart/components/chart-view/chart-view.component.ts index 43a3461383..26df223397 100644 --- a/ui/src/app/chart/components/chart-view/chart-view.component.ts +++ b/ui/src/app/chart/components/chart-view/chart-view.component.ts @@ -31,6 +31,7 @@ import { EventPropertyUnion, FieldConfig, LinkageData, + SpQueryResult, TimeSettings, } from '@streampipes/platform-services'; import { @@ -76,6 +77,7 @@ import { } from '@angular/material/sidenav'; import { ChartDesignerPanelComponent } from './designer-panel/chart-designer-panel.component'; import { ChartContainerComponent } from '../../../chart-shared/components/chart-container/chart-container.component'; +import { ChartDataPreviewComponent } from './query-result-preview/chart-data-preview.component'; @Component({ selector: 'sp-chart-data-view', @@ -93,6 +95,7 @@ import { ChartContainerComponent } from '../../../chart-shared/components/chart- ChartDesignerPanelComponent, MatDrawerContent, ChartContainerComponent, + ChartDataPreviewComponent, TranslatePipe, ], }) @@ -132,6 +135,7 @@ export class ChartViewComponent currentUser$: Subscription; chartNotFound = false; + latestQueryResults: SpQueryResult[] = []; observableGenerator = this.dataExplorerSharedService.defaultObservableGenerator(); @@ -204,6 +208,7 @@ export class ChartViewComponent loadDataView(dataViewId: string): void { this.dataViewLoaded = false; + this.latestQueryResults = []; this.dataViewService .getChart(dataViewId) .pipe( @@ -242,6 +247,10 @@ export class ChartViewComponent this.routingService.navigateToChart(true, this.dataView.elementId); } + onQueryResultsChanged(results: SpQueryResult[]): void { + this.latestQueryResults = results ?? []; + } + makeDefaultTimeSettings(): TimeSettings { return this.timeSelectionService.getDefaultTimeSettings(); } diff --git a/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.html b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.html new file mode 100644 index 0000000000..9d9a0f965a --- /dev/null +++ b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.html @@ -0,0 +1,100 @@ +<!-- +~ Licensed to the Apache Software Foundation (ASF) under one or more +~ contributor license agreements. See the NOTICE file distributed with +~ this work for additional information regarding copyright ownership. +~ The ASF licenses this file to You under the Apache License, Version 2.0 +~ (the "License"); you may not use this file except in compliance with +~ the License. You may obtain a copy of the License at +~ +~ http://www.apache.org/licenses/LICENSE-2.0 +~ +~ Unless required by applicable law or agreed to in writing, software +~ distributed under the License is distributed on an "AS IS" BASIS, +~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +~ See the License for the specific language governing permissions and +~ limitations under the License. +~ +--> + +<div class="preview-shell" fxLayout="column"> + <div + class="preview-header" + fxLayout="row" + fxLayoutAlign="space-between center" + > + <div class="preview-title" fxFlex> + {{ 'Data preview' | translate }} + </div> + <div + class="preview-header-actions" + fxLayout="row" + fxLayoutAlign="end center" + > + <div class="preview-meta"> + {{ totalRows }} {{ 'rows' | translate }} + @if (!allRowsRendered) { + <span class="preview-meta-note"> + ({{ 'showing first' | translate }} {{ rows.length }}) + </span> + } + </div> + <button + mat-icon-button + class="preview-toggle" + (click)="toggleExpanded()" + [attr.aria-label]=" + (expanded ? 'Collapse preview' : 'Expand preview') + | translate + " + > + <mat-icon>{{ + expanded ? 'expand_more' : 'expand_less' + }}</mat-icon> + </button> + </div> + </div> + + @if (expanded && rows.length > 0 && columns.length > 0) { + <div class="preview-table-scroll" fxFlex> + <table class="preview-table"> + <thead> + <tr> + <th class="index-col">#</th> + @for ( + column of columns; + track trackColumn($index, column) + ) { + <th [title]="displayColumnName(column)"> + {{ displayColumnName(column) }} + </th> + } + </tr> + </thead> + <tbody> + @for (row of rows; track trackRow($index)) { + <tr> + <td class="index-col">{{ $index + 1 }}</td> + @for ( + column of columns; + track trackColumn($index, column) + ) { + <td [title]="stringify(row[column])"> + {{ formatCellValue(column, row[column]) }} + </td> + } + </tr> + } + </tbody> + </table> + </div> + } @else if (expanded) { + <div + class="preview-empty" + fxFlex + fxLayout + fxLayoutAlign="center center" + > + {{ 'Run or adjust the query to see a row preview.' | translate }} + </div> + } +</div> diff --git a/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.scss b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.scss new file mode 100644 index 0000000000..a8449a88b3 --- /dev/null +++ b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.scss @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +:host { + display: block; + min-height: 0; + height: var(--size-data-preview-collapsed); + transition: height 160ms ease; + color: var(--color-default-text); +} + +.preview-shell { + height: 100%; + min-height: 0; + box-sizing: border-box; + border-top: 1px solid var(--color-bg-2); + border-radius: 0; + background: var(--color-bg-0); + overflow: hidden; +} + +.preview-header { + gap: var(--space-sm); + padding: var(--padding-panel-header-y) var(--padding-panel-header-x); + border-bottom: 1px solid var(--color-tab-border); + background: var(--color-panel-header-accent); + min-height: var(--size-panel-header-compact); + box-sizing: border-box; +} + +.preview-title { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-normal); +} + +.preview-header-actions { + gap: var(--space-2xs); +} + +.preview-meta { + font-size: var(--font-size-xs); + color: var(--color-secondary-text); + white-space: nowrap; +} + +.preview-meta-note { + margin-left: var(--space-2xs); +} + +.preview-toggle { + width: var(--size-icon-button-compact); + height: var(--size-icon-button-compact); + line-height: var(--size-icon-button-compact); + color: var(--color-default-text); +} + +.preview-toggle .mat-icon { + font-size: var(--size-icon-compact); + width: var(--size-icon-compact); + height: var(--size-icon-compact); + line-height: var(--size-icon-compact); +} + +.preview-table-scroll { + min-height: 0; + overflow: auto; +} + +.preview-table { + width: max-content; + min-width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: var(--font-size-xs); + line-height: var(--line-height-tight); + background: var(--color-bg-0); +} + +.preview-table th, +.preview-table td { + padding: var(--padding-table-cell-compact-y) + var(--padding-table-cell-compact-x); + text-align: left; + border-bottom: 1px solid var(--color-border-subtle); + border-right: 1px solid var(--color-border-subtle); + white-space: nowrap; + max-width: 16rem; + overflow: hidden; + text-overflow: ellipsis; +} + +.preview-table th { + position: sticky; + top: 0; + z-index: 1; + background: var(--color-bg-0); + color: var(--color-default-text); + font-weight: var(--font-weight-semibold); +} + +.preview-table tbody tr:nth-child(even) td { + background: var(--color-surface-subtle); +} + +.preview-table tbody tr:hover td { + background: var(--color-surface-selected-subtle); +} + +.preview-table th.index-col, +.preview-table td.index-col { + position: sticky; + left: 0; + z-index: 2; + min-width: var(--size-table-index-column); + max-width: var(--size-table-index-column); + text-align: right; + color: var(--color-secondary-text); + background: var(--color-bg-0); +} + +.preview-table tbody tr:nth-child(even) td.index-col { + background: var(--color-surface-subtle); +} + +.preview-table tbody tr:hover td.index-col { + background: var(--color-surface-selected-subtle); +} + +.preview-empty { + min-height: 0; + padding: var(--space-md); + text-align: center; + color: var(--color-secondary-text); + font-size: var(--font-size-xs); +} diff --git a/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.ts b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.ts new file mode 100644 index 0000000000..68c827acc5 --- /dev/null +++ b/ui/src/app/chart/components/chart-view/query-result-preview/chart-data-preview.component.ts @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { DatePipe } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + HostBinding, + Input, + OnChanges, + SimpleChanges, +} from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; +import { MatIconButton } from '@angular/material/button'; +import { + FlexDirective, + LayoutAlignDirective, + LayoutDirective, +} from '@ngbracket/ngx-layout/flex'; +import { SpQueryResult } from '@streampipes/platform-services'; +import { TranslatePipe } from '@ngx-translate/core'; + +type PreviewRow = Record<string, unknown>; + +@Component({ + selector: 'sp-chart-data-preview', + templateUrl: './chart-data-preview.component.html', + styleUrls: ['./chart-data-preview.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + TranslatePipe, + MatIconButton, + MatIcon, + LayoutDirective, + LayoutAlignDirective, + FlexDirective, + ], +}) +export class ChartDataPreviewComponent implements OnChanges { + private static readonly MAX_PREVIEW_ROWS = 2000; + private readonly datePipe = new DatePipe('en-US'); + + @Input() queryResults: SpQueryResult[] = []; + @Input() defaultExpanded = false; + + columns: string[] = []; + rows: PreviewRow[] = []; + totalRows = 0; + allRowsRendered = true; + expanded = false; + + @HostBinding('style.height') + get hostHeight(): string { + return this.expanded + ? 'var(--size-data-preview-expanded)' + : 'var(--size-data-preview-collapsed)'; + } + + @HostBinding('class.expanded') + get hostExpandedClass(): boolean { + return this.expanded; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.defaultExpanded) { + this.expanded = !!this.defaultExpanded; + } + if (changes.queryResults) { + this.rebuildPreview(this.queryResults ?? []); + } + } + + private rebuildPreview(queryResults: SpQueryResult[]): void { + const headerColumns: string[] = []; + const tagColumns: string[] = []; + const flattenedRows: PreviewRow[] = []; + const showSourceColumn = queryResults.length > 1; + + this.totalRows = 0; + + queryResults.forEach(queryResult => { + queryResult.headers?.forEach(header => + this.pushUnique(headerColumns, header), + ); + + queryResult.allDataSeries?.forEach(series => { + const tags = series.tags ?? {}; + Object.keys(tags).forEach(tagKey => + this.pushUnique(tagColumns, tagKey), + ); + + series.rows?.forEach(values => { + this.totalRows += 1; + if ( + flattenedRows.length >= + ChartDataPreviewComponent.MAX_PREVIEW_ROWS + ) { + return; + } + + const row: PreviewRow = {}; + queryResult.headers?.forEach((header, index) => { + row[header] = values?.[index]; + }); + + Object.entries(tags).forEach(([key, value]) => { + row[key] = value; + }); + + if (showSourceColumn) { + row['_source'] = + queryResult.forId || queryResult.sourceIndex; + } + + flattenedRows.push(row); + }); + }); + }); + + const orderedHeaderColumns = this.orderHeaderColumns(headerColumns); + this.columns = showSourceColumn + ? ['_source', ...orderedHeaderColumns, ...tagColumns] + : [...orderedHeaderColumns, ...tagColumns]; + this.rows = flattenedRows; + this.allRowsRendered = + this.totalRows <= ChartDataPreviewComponent.MAX_PREVIEW_ROWS; + } + + private orderHeaderColumns(columns: string[]): string[] { + const timeColumns = columns.filter(column => column === 'time'); + const otherColumns = columns.filter(column => column !== 'time'); + return [...timeColumns, ...otherColumns]; + } + + private pushUnique(collection: string[], value: string): void { + if (value !== undefined && !collection.includes(value)) { + collection.push(value); + } + } + + trackColumn(_index: number, column: string): string { + return column; + } + + trackRow(index: number): number { + return index; + } + + isTimeColumn(column: string): boolean { + return column === 'time'; + } + + displayColumnName(column: string): string { + return column === '_source' ? 'Source' : column; + } + + isValidDateValue(value: unknown): boolean { + if (value === null || value === undefined || value === '') { + return false; + } + + const date = new Date(value as string | number); + return !Number.isNaN(date.getTime()); + } + + stringify(value: unknown): string { + if (value === null || value === undefined) { + return '-'; + } + return String(value); + } + + formatCellValue(column: string, value: unknown): string { + if (this.isTimeColumn(column) && this.isValidDateValue(value)) { + return ( + this.datePipe.transform( + value as string | number | Date, + 'yyyy-MM-dd HH:mm:ss.SSS', + ) ?? this.stringify(value) + ); + } + + return this.stringify(value); + } + + toggleExpanded(): void { + this.expanded = !this.expanded; + } +} diff --git a/ui/src/scss/sp/_variables.scss b/ui/src/scss/sp/_variables.scss index d95fc84247..7900e374dd 100644 --- a/ui/src/scss/sp/_variables.scss +++ b/ui/src/scss/sp/_variables.scss @@ -100,6 +100,39 @@ --mat-sidenav-container-divider-color: var(--color-bg-3); --mat-button-toggle-height: 30px; --mat-table-header-headline-color: var(--mat-sys-on-surface); + + --size-panel-header-compact: calc(var(--space-md) * 3.4); + --size-icon-button-compact: calc(var(--space-md) * 2.2); + --size-icon-compact: calc(var(--space-md) * 1.4); + --size-table-index-column: calc(var(--space-md) * 4); + --size-data-preview-collapsed: calc(var(--size-panel-header-compact) + 2px); + --size-data-preview-expanded: 16.25rem; + + --padding-table-cell-compact-y: var(--space-xs); + --padding-table-cell-compact-x: var(--space-sm); + --padding-panel-header-y: var(--space-sm); + --padding-panel-header-x: var(--space-md); + + --color-surface-subtle: color-mix( + in srgb, + var(--color-bg-2) 55%, + var(--color-bg-0) + ); + --color-surface-selected-subtle: color-mix( + in srgb, + var(--color-primary) 10%, + var(--color-bg-0) + ); + --color-border-subtle: color-mix( + in srgb, + var(--color-bg-3) 72%, + transparent + ); + --color-panel-header-accent: color-mix( + in srgb, + var(--color-primary) 5%, + var(--color-bg-0) + ); } .dark-mode {
