diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 03d9549ea..52ff3626e 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -1,6 +1,7 @@
+
\ No newline at end of file
diff --git a/ui/locales/app.de.json b/ui/locales/app.de.json
index 6d2be36a6..8976af170 100644
--- a/ui/locales/app.de.json
+++ b/ui/locales/app.de.json
@@ -62,7 +62,6 @@
"dtd_cant_upload_kml_error": "Fehler beim Hochladen von KML",
"dtd_change_order_label": "Anordnen",
"dtd_clamp_to_ground": "Auf Terrain legen",
- "dtd_configure_data_btn": "Angezeigte Daten konfigurieren",
"dtd_displayed_data_label": "Angezeigte Daten",
"dtd_download_hint": "Herunterladen / Legende",
"dtd_empty_map_label": "Kein Hintergrund",
diff --git a/ui/locales/app.en.json b/ui/locales/app.en.json
index 5ad4cc796..e7a72090d 100644
--- a/ui/locales/app.en.json
+++ b/ui/locales/app.en.json
@@ -62,7 +62,6 @@
"dtd_cant_upload_kml_error": "Error while uploading KML",
"dtd_change_order_label": "Change order",
"dtd_clamp_to_ground": "Clamp to terrain",
- "dtd_configure_data_btn": "Configure data displayed",
"dtd_displayed_data_label": "Data displayed",
"dtd_download_hint": "Download / Legend",
"dtd_empty_map_label": "No background",
diff --git a/ui/locales/app.fr.json b/ui/locales/app.fr.json
index acaf7fae2..182704e46 100644
--- a/ui/locales/app.fr.json
+++ b/ui/locales/app.fr.json
@@ -62,7 +62,6 @@
"dtd_cant_upload_kml_error": "Erreur lors du téléchargement de KML",
"dtd_change_order_label": "Réarranger",
"dtd_clamp_to_ground": "Fixation au terrain",
- "dtd_configure_data_btn": "Configurer les données affichées",
"dtd_displayed_data_label": "Données affichées",
"dtd_download_hint": "Télécharger / Légende",
"dtd_empty_map_label": "Sans arrière-plan",
diff --git a/ui/locales/app.it.json b/ui/locales/app.it.json
index 85c97cb3c..5f6ef7bfc 100644
--- a/ui/locales/app.it.json
+++ b/ui/locales/app.it.json
@@ -62,7 +62,6 @@
"dtd_cant_upload_kml_error": "Errore nel download di KML",
"dtd_change_order_label": "Riordinare",
"dtd_clamp_to_ground": "Morsetto al terreno",
- "dtd_configure_data_btn": "Configurare i dati visualizzati ",
"dtd_displayed_data_label": "Dati mostrati",
"dtd_download_hint": "Scaricare / Scala",
"dtd_empty_map_label": "Senza sfondo",
diff --git a/ui/src/components/core/core-button.ts b/ui/src/components/core/core-button.ts
new file mode 100644
index 000000000..5c6a0f365
--- /dev/null
+++ b/ui/src/components/core/core-button.ts
@@ -0,0 +1,44 @@
+import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+
+@customElement('ngm-core-button')
+export class CoreButton extends LitElement {
+ @property({reflect: true})
+ accessor variant: Variant = 'default';
+
+ @property({type: Boolean, attribute: 'active', reflect: true})
+ accessor isActive: boolean = false;
+
+ readonly render = () => html`
+
+ `;
+
+ static readonly styles = css`
+ button {
+ font-family: var(--font);
+ font-size: 14px;
+ }
+
+ :host([variant='text']) button {
+ color: var(--color-highlight--darker);
+
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
+ }
+
+ :host([variant='text'][active]) button {
+ color: var(--color-action);
+ }
+
+ :host([variant='text']) button:hover {
+ color: var(--color-action--light);
+ }
+ `;
+}
+
+export type Variant =
+ | 'default'
+ | 'text'
diff --git a/ui/src/components/core/core-icon.ts b/ui/src/components/core/core-icon.ts
new file mode 100644
index 000000000..840407d6d
--- /dev/null
+++ b/ui/src/components/core/core-icon.ts
@@ -0,0 +1,49 @@
+import {css, html, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+
+@customElement('ngm-core-icon')
+export class CoreIcon extends LitElement {
+ @property()
+ accessor icon: IconName | null = null;
+
+ @property({type: Boolean, attribute: 'interactive'})
+ accessor isInteractive: boolean = false;
+
+ readonly render = () => html``;
+
+ static readonly styles = css`
+ :host {
+ display: inline-block;
+
+ --size: var(--icon-size--normal);
+ width: var(--size);
+ height: var(--size);
+ background-color: var(--color-bg-contrast);
+
+ mask: var(--mask, none) no-repeat center;
+ -webkit-mask: var(--mask, none) no-repeat center;
+
+ /* Hide element if no valid icon has been specified. */
+ visibility: hidden;
+ }
+
+ :host([interactive]:hover) {
+ cursor: pointer;
+ background-color: var(--color-action);
+ }
+
+ :host([icon='close']) {
+ visibility: initial;
+ --mask: url('images/i_close.svg');
+ }
+
+ :host([icon='dropdown']) {
+ visibility: initial;
+ --mask: url('images/i_menu-1.svg')
+ }
+ `;
+}
+
+export type IconName =
+ | 'close'
+ | 'dropdown'
diff --git a/ui/src/components/core/index.ts b/ui/src/components/core/index.ts
new file mode 100644
index 000000000..5446187a9
--- /dev/null
+++ b/ui/src/components/core/index.ts
@@ -0,0 +1,2 @@
+import './core-button';
+import './core-icon';
diff --git a/ui/src/components/layers/layers-catalog.ts b/ui/src/components/layers/layers-catalog.ts
new file mode 100644
index 000000000..732b450de
--- /dev/null
+++ b/ui/src/components/layers/layers-catalog.ts
@@ -0,0 +1,217 @@
+import {css, html, TemplateResult, unsafeCSS} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+import {LitElementI18n} from '../../i18n.js';
+import i18next from 'i18next';
+import auth from '../../store/auth';
+import type {LayerTreeNode} from '../../layertree';
+import $ from 'jquery';
+
+
+import fomanticTransitionCss from 'fomantic-ui-css/components/transition.css';
+import fomanticAccordionCss from 'fomantic-ui-css/components/accordion.css';
+import 'fomantic-ui-css/components/transition.js';
+import {LayerEvent} from './layers-display';
+
+
+@customElement('ngm-layers-catalog')
+export class NgmLayersCatalog extends LitElementI18n {
+ @property({type: Array})
+ accessor layers: LayerTreeNode[] = [];
+
+ @state()
+ accessor userGroups: string[] = [];
+
+ constructor() {
+ super();
+ auth.user.subscribe((user) => {
+ this.userGroups = user?.['cognito:groups'] ?? [];
+ });
+ }
+
+ firstUpdated(): void {
+ const {shadowRoot} = this;
+ if (shadowRoot == null) {
+ return;
+ }
+ $(shadowRoot.querySelectorAll(':host > .ui.accordion')).accordion({
+ duration: 150,
+ });
+ }
+
+ getCategoryOrLayerTemplate(node: LayerTreeNode, level: string): TemplateResult {
+ if (node.children) {
+ return this.getCategoryTemplate(node, level);
+ }
+ return this.getLayerTemplate(node);
+ }
+
+ getCategoryTemplate(category: LayerTreeNode, level: string): TemplateResult {
+ // if it is a restricted layer, the user must be logged in to see it
+ const content = category.children?.filter(
+ node => !(node.restricted && (!node.restricted.some(g => this.userGroups.includes(g))))
+ ).map(node => this.getCategoryOrLayerTemplate(node, 'second-level'));
+
+ if (!content?.length) return html``;
+
+ return html`
+
+
+
+
+
+
+ ${content}
+
+
+ `;
+ }
+
+ getLayerTemplate(layer: LayerTreeNode): TemplateResult {
+ return html`
+ {
+ this.dispatchEvent(new CustomEvent('layer-click', {
+ composed: true,
+ bubbles: true,
+ detail: {
+ layer
+ }
+ }) satisfies LayerEvent);
+ }}>
+
+
+
+
+ `;
+ }
+
+ render() {
+ return html`${this.layers.map(node => this.getCategoryOrLayerTemplate(node, 'first-level'))}`;
+ }
+
+ static readonly styles = css`
+ ${unsafeCSS(fomanticTransitionCss)}
+ ${unsafeCSS(fomanticAccordionCss)}
+
+ :host, :host * {
+ box-sizing: border-box;
+ }
+
+ .category.ui.accordion {
+ margin-top: 0;
+ }
+
+ .category.ui.accordion > .title ~ .content,
+ .category.ui.accordion .category.ui.accordion > .title ~ .content
+ {
+ padding-top: 0;
+ }
+
+ .category > .title {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ }
+
+ .category > .title.active > label {
+ color: var(--color-action);
+ }
+
+ .category > .title:hover > label {
+ color: var(--color-action--light);
+ }
+
+ .category > .title > ngm-core-icon {
+ background-color: var(--color-highlight--darker);
+ }
+
+ .category > .title:hover > ngm-core-icon {
+ background-color: var(--color-action--light);
+ }
+
+ .category > .title.first-level {
+ font-weight: 700;
+ }
+
+ .category > .title > label {
+ cursor: pointer;
+ font-size: 14px;
+ margin-left: 10px;
+ }
+
+ .category > .title.active > ngm-core-icon {
+ transform: rotate(90deg);
+ }
+
+ .ngm-checkbox {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+ cursor: pointer;
+ }
+
+ .ngm-checkbox:hover {
+ color: var(--color-action--light);
+ }
+
+ .ngm-checkbox.active {
+ color: var(--color-action);
+ }
+
+ .ngm-checkbox > input {
+ display: none;
+ }
+
+ .ngm-checkbox:hover > .ngm-checkbox-icon {
+ border-color: var(--color-action--light);
+ }
+
+ .ngm-checkbox > .ngm-checkbox-icon {
+ display: inline-block;
+ position: relative;
+ width: 19px;
+ height: 18px;
+ border-radius: 2px;
+ border: 2px solid var(--ngm-interaction);
+ transition: all 0.5s ease;
+ }
+
+ .ngm-checkbox > label {
+ margin-left: 10px;
+ cursor: pointer;
+ }
+
+ .ngm-checkbox.active > .ngm-checkbox-icon {
+ border-color: var(--color-action);
+ }
+
+ .ngm-checkbox input:checked + .ngm-checkbox-icon {
+ background-color: var(--color-action);
+ }
+
+ .ngm-checkbox.active:hover > .ngm-checkbox-icon {
+ background-color: var(--color-action--light);
+ }
+
+ .ngm-checkbox > .ngm-checkbox-icon::before {
+ box-sizing: content-box;
+ content: "";
+ top: -2px;
+ left: 3px;
+ width: 6px;
+ height: 12px;
+ display: none;
+ position: absolute;
+ transform: rotate(45deg);
+ transition: all 0.5s ease;
+ border-right: 2px solid #fff;
+ border-bottom: 2px solid #fff;
+ }
+
+ .ngm-checkbox input:checked + .ngm-checkbox-icon::before {
+ display: block;
+ }
+ `;
+}
diff --git a/ui/src/components/layers/layers-display.ts b/ui/src/components/layers/layers-display.ts
new file mode 100644
index 000000000..a281a54da
--- /dev/null
+++ b/ui/src/components/layers/layers-display.ts
@@ -0,0 +1,274 @@
+import {customElement, property, state} from 'lit/decorators.js';
+import {LitElementI18n} from '../../i18n.js';
+import {css, html} from 'lit';
+import i18next from 'i18next';
+import {CustomDataSource, Viewer} from 'cesium';
+import {PropertyValues} from '@lit/reactive-element';
+import LayersActions from '../../layers/LayersActions';
+import {DEFAULT_LAYER_OPACITY, LayerConfig, LayerTreeNode} from '../../layertree';
+import '../../layers/ngm-layers';
+import '../../layers/ngm-layers-sort';
+import MainStore from '../../store/main';
+import {Subscription} from 'rxjs';
+import {classMap} from 'lit/directives/class-map.js';
+import {query} from 'lit/decorators.js';
+import {parseKml, renderWithDelay} from '../../cesiumutils';
+
+@customElement('ngm-layers-display')
+export class NgmLayersDisplay extends LitElementI18n {
+ @property({type: Array})
+ accessor layers: LayerTreeNode[] = []
+
+ @state()
+ private accessor isReordering = false
+
+ @state()
+ private accessor viewer: Viewer | null = null
+
+ @state()
+ private accessor actions: LayersActions | null = null;
+
+ private readonly subscription = new Subscription();
+
+ @query('.ngm-side-bar-panel > .ngm-toast-placeholder')
+ accessor toastPlaceholder;
+
+ @state()
+ private accessor globeQueueLength = 0
+
+ constructor() {
+ super();
+
+ this.subscription.add(MainStore.viewer.subscribe((viewer) => {
+ this.viewer = viewer;
+ this.initializeViewer();
+ }));
+
+ this.handleLayerRemoval = this.handleLayerRemoval.bind(this);
+ this.handleReordering = this.handleReordering.bind(this);
+ this.toggleReordering = this.toggleReordering.bind(this);
+ this.handleLayerUpdate = this.handleLayerUpdate.bind(this);
+ this.handleKmlUpload = this.handleKmlUpload.bind(this);
+ }
+
+ private initializeViewer(): void {
+ if (this.viewer == null) {
+ return;
+ }
+ this.subscription.add(this.viewer.scene.globe.tileLoadProgressEvent.addEventListener((queueLength) => {
+ this.globeQueueLength = queueLength;
+ }));
+ }
+
+ readonly render2 = () => html`
+
+
+ ${this.isReordering ? i18next.t('dtd_finish_ordering_label') : i18next.t('dtd_change_order_label')}
+
+
+
+ ${this.isReordering ? this.renderSortableLayers() : this.renderLayers()}
+
+ `;
+
+ readonly render = () => html`
+
+
+
+
+ ${this.isReordering ? i18next.t('dtd_finish_ordering_label') : i18next.t('dtd_change_order_label')}
+
+ ${this.isReordering
+ ? this.renderSortableLayers()
+ : this.renderLayers()}
+
+
+
+
+
+
+
+
+
+ `;
+
+ private readonly renderLayers = () => html`
+
+
+ `;
+
+ private readonly renderSortableLayers = () => html`
+
+
+ `;
+
+ updated(changedProperties: PropertyValues): void {
+ if (changedProperties.has('viewer' as keyof NgmLayersDisplay)) {
+ this.actions = this.viewer == null ? null : new LayersActions(this.viewer);
+ }
+ }
+
+ disconnectedCallback(): void {
+ this.subscription.unsubscribe();
+ }
+
+ private toggleReordering(): void {
+ this.isReordering = !this.isReordering;
+ }
+
+ private async handleReordering(e: LayersReorderEvent): Promise {
+ const {actions} = this;
+ if (actions == null) {
+ return;
+ }
+ await actions.reorderLayers(e.detail);
+ if (!this.isReordering) {
+ this.updateLayers(e.detail);
+ }
+ }
+
+ private async handleLayerRemoval(e: LayerRemovalEvent): Promise {
+ const newLayers = [...this.layers];
+ newLayers.splice(e.detail.idx, 1);
+ this.updateLayers(newLayers);
+ this.removeLayer(e.detail.config);
+ }
+
+ private async handleLayerUpdate(e: LayerChangeEvent): Promise {
+ this.updateLayer(e.detail);
+ }
+
+ // TODO Cleanup/Refactor this function.
+ // As of now, this function remains unchanged to before the navigation-catalog refactoring.
+ private async handleKmlUpload(file: File, clampToGround: boolean): Promise {
+ if (this.viewer == null) {
+ return;
+ }
+
+ const dataSource = new CustomDataSource();
+ const name = await parseKml(this.viewer, file, dataSource, clampToGround);
+ const layer = `${name.replace(' ', '_')}_${Date.now()}`;
+
+ // name used as id for datasource
+ dataSource.name = layer;
+ MainStore.addUploadedKmlName(dataSource.name);
+ await this.viewer.dataSources.add(dataSource);
+ await renderWithDelay(this.viewer);
+
+ // done like this to have correct rerender of component
+ const dataSourcePromise = Promise.resolve(dataSource);
+ const config: LayerConfig = {
+ load() { return dataSourcePromise; },
+ label: name,
+ layer,
+ promise: dataSourcePromise,
+ opacity: DEFAULT_LAYER_OPACITY,
+ notSaveToPermalink: true,
+ ownKml: true,
+ opacityDisabled: true
+ };
+ this.clickLayer(config);
+ await this.viewer.zoomTo(dataSource);
+ this.requestUpdate();
+ }
+
+ private updateLayers(layers: LayerTreeNode[]): void {
+ this.dispatchEvent(new CustomEvent('layers-update', {
+ detail: {
+ layers,
+ },
+ }) satisfies LayersUpdateEvent);
+ }
+
+ private updateLayer(layer: LayerConfig): void {
+ this.dispatchEvent(new CustomEvent('layer-update', {
+ detail: {
+ layer,
+ },
+ }) satisfies LayerEvent);
+ }
+
+ private removeLayer(layer: LayerConfig): void {
+ this.dispatchEvent(new CustomEvent('layer-removal', {
+ detail: {
+ layer,
+ },
+ }) satisfies LayerEvent);
+ }
+
+ private clickLayer(layer: LayerConfig): void {
+ this.dispatchEvent(new CustomEvent('layer-click', {
+ bubbles: true,
+ composed: true,
+ detail: {
+ layer,
+ },
+ }) satisfies LayerEvent);
+ }
+
+ private openIonModal(): void {
+ this.dispatchEvent(new CustomEvent('openIonModal', {
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
+ // TODO Make all children of this component use the Shadow DOM so we can remove this.
+ createRenderRoot() {
+ return this;
+ }
+
+ static readonly styles = css`
+ ngm-layers-display .actions {
+ display: flex;
+ justify-content: flex-end;
+ padding-top: 9px;
+ }
+
+ ngm-layers-display * {
+ box-sizing: border-box;
+ }
+ `;
+}
+
+export type LayersUpdateEvent = CustomEvent<{
+ layers: LayerTreeNode[]
+}>
+
+export type LayerEvent = CustomEvent<{
+ layer: LayerConfig | LayerTreeNode
+}>
+
+type LayerRemovalEvent = CustomEvent<{
+ idx: number
+ config: LayerConfig
+}>
+
+type LayersReorderEvent = CustomEvent
+type LayerChangeEvent = CustomEvent
diff --git a/ui/src/components/navigation/navigation-layer-panel.ts b/ui/src/components/navigation/navigation-layer-panel.ts
new file mode 100644
index 000000000..315b634ba
--- /dev/null
+++ b/ui/src/components/navigation/navigation-layer-panel.ts
@@ -0,0 +1,151 @@
+import {LitElementI18n} from '../../i18n';
+import {css, html, unsafeCSS} from 'lit';
+import i18next from 'i18next';
+import {customElement, property} from 'lit/decorators.js';
+import {LayerConfig} from '../../layertree';
+import './navigation-panel';
+import './navigation-panel-header';
+import '../layers/layers-catalog';
+import '../layers/layers-display';
+import {LayerEvent, LayersUpdateEvent} from '../layers/layers-display';
+
+
+@customElement('ngm-navigation-layer-panel')
+export class NavigationLayerPanel extends LitElementI18n {
+ @property()
+ public accessor layers: LayerConfig[] | null = null
+
+ @property()
+ public accessor displayLayers: LayerConfig[] | null = null
+
+ // @state()
+ // private accessor activeTab = Tab.Catalog
+
+ constructor() {
+ super();
+
+ this.close = this.close.bind(this);
+ this.handleDisplayLayersUpdate = this.handleDisplayLayersUpdate.bind(this);
+ this.handleDisplayLayerUpdate = this.handleDisplayLayerUpdate.bind(this);
+ this.handleDisplayLayerRemoval = this.handleDisplayLayerRemoval.bind(this);
+ }
+
+ readonly render = () => html`
+
+
+
+
+ ${i18next.t('lyr_geocatalog_label')}
+
+ ${this.renderCatalog()}
+
+
+
+ ${i18next.t('dtd_displayed_data_label')}
+
+ ${this.renderDisplay()}
+
+
+ `;
+
+ private readonly renderCatalog = () => html`
+
+ `;
+
+ private readonly renderDisplay = () => html`
+
+ `;
+
+ connectedCallback(): void {
+ super.connectedCallback();
+ this.setAttribute('role', 'complementary');
+ }
+
+ private close(): void {
+ this.dispatchEvent(new CustomEvent('close'));
+ }
+
+ private handleDisplayLayersUpdate(e: LayersUpdateEvent): void {
+ this.dispatchEvent(new CustomEvent('display-layers-update', {
+ detail: e.detail,
+ }) satisfies LayersUpdateEvent);
+ }
+
+ private handleDisplayLayerUpdate(e: LayerEvent): void {
+ this.dispatchEvent(new CustomEvent('display-layer-update', {
+ detail: e.detail,
+ }) satisfies LayerEvent);
+ }
+
+ private handleDisplayLayerRemoval(e: LayerEvent): void {
+ this.dispatchEvent(new CustomEvent('display-layer-removal', {
+ detail: e.detail,
+ }) satisfies LayerEvent);
+ }
+
+ private handleDisplayLayerClick(e: LayerEvent): void {
+ this.dispatchEvent(new CustomEvent('display-layer-click', {
+ detail: e.detail,
+ }) satisfies LayerEvent);
+ }
+
+ // TODO Make all children of this component use the Shadow DOM so we can remove this.
+ createRenderRoot() {
+ return this;
+ }
+
+ static readonly styles = css`
+ ngm-navigation-layer-panel * {
+ box-sizing: border-box;
+ }
+
+ ngm-navigation-layer-panel ngm-navigation-panel {
+ --display-height: 500px;
+ --catalog-height: calc(var(--panel-height) - var(--display-height) - 34px);
+
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-template-rows: repeat(2, 1fr);
+ max-height: calc(100vh - var(--ngm-header-height));
+ padding: 0;
+ }
+
+ ngm-navigation-layer-panel ngm-navigation-panel > section {
+ --section-height: calc(var(--panel-height) / 2);
+
+ position: relative;
+ max-height: var(--section-height);
+ background-color: var(--color-bg--dark);
+ }
+
+
+ ngm-navigation-layer-panel ngm-navigation-panel > section > * {
+ max-width: calc(100vw);
+ padding: 0 var(--panel-padding);
+ }
+
+ ngm-navigation-layer-panel ngm-layers-catalog,
+ ngm-navigation-layer-panel ngm-layers-display {
+ display: block;
+ height: calc(var(--section-height) - 34px);
+ max-height: calc(var(--section-height) - 34px);
+ overflow-y: auto;
+ }
+ `;
+}
+
+// enum Tab {
+// Catalog,
+// Data,
+// }
diff --git a/ui/src/components/navigation/navigation-panel-header.ts b/ui/src/components/navigation/navigation-panel-header.ts
new file mode 100644
index 000000000..b53cdcdca
--- /dev/null
+++ b/ui/src/components/navigation/navigation-panel-header.ts
@@ -0,0 +1,51 @@
+import {LitElementI18n} from '../../i18n';
+import {css, html, nothing} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+import '../core';
+
+@customElement('ngm-navigation-panel-header')
+export class NavigationPanelHeader extends LitElementI18n {
+ @property({type: Boolean, attribute: 'closeable'})
+ accessor isCloseable: boolean = false;
+
+ constructor() {
+ super();
+
+ this.close = this.close.bind(this);
+ }
+
+ readonly render = () => html`
+
+ ${this.isCloseable ? html`
+
+ ` : nothing}
+ `;
+
+ connectedCallback(): void {
+ super.connectedCallback();
+ this.setAttribute('role', 'heading');
+ }
+
+ private close(): void {
+ this.dispatchEvent(new CustomEvent('close'));
+ }
+
+ static readonly styles = css`
+ :host {
+ box-sizing: border-box;
+ border-bottom: 2px solid #DFE2E6;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 700;
+ height: 34px;
+ padding: 4px 0 2px 0;
+ color: var(--color-bg-contrast--light);
+ }
+ `;
+}
+
diff --git a/ui/src/components/navigation/navigation-panel.ts b/ui/src/components/navigation/navigation-panel.ts
new file mode 100644
index 000000000..54aa3027a
--- /dev/null
+++ b/ui/src/components/navigation/navigation-panel.ts
@@ -0,0 +1,32 @@
+import {LitElementI18n} from '../../i18n';
+import {css, html} from 'lit';
+import {customElement} from 'lit/decorators.js';
+import '../core';
+
+@customElement('ngm-navigation-panel')
+export class NavigationPanel extends LitElementI18n {
+ readonly render = () => html``;
+
+ static readonly styles = css`
+ :host {
+ --panel-height: calc(100vh - var(--ngm-header-height));
+
+ box-sizing: border-box;
+ width: 530px;
+ max-width: 100vw;
+ height: var(--panel-height);
+ max-height: var(--panel-height);
+ max-width: calc(100vw);
+ padding: 0 var(--panel-padding);
+
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ box-shadow: 4px 0 4px #00000029;
+ z-index: 5;
+
+ background-color: var(--color-bg--dark);
+ }
+ `;
+}
+
diff --git a/ui/src/elements/ngm-side-bar.ts b/ui/src/elements/ngm-side-bar.ts
index 2fc3952a9..dc93b9d93 100644
--- a/ui/src/elements/ngm-side-bar.ts
+++ b/ui/src/elements/ngm-side-bar.ts
@@ -3,8 +3,8 @@ import {LitElementI18n} from '../i18n.js';
import '../toolbox/ngm-toolbox';
import '../layers/ngm-layers';
import '../layers/ngm-layers-sort';
-import '../layers/ngm-catalog';
import './dashboard/ngm-dashboard';
+import '../components/navigation/navigation-layer-panel';
import LayersActions from '../layers/LayersActions';
import {DEFAULT_LAYER_OPACITY, LayerType} from '../constants';
import defaultLayerTree, {LayerConfig} from '../layertree';
@@ -17,7 +17,7 @@ import {
getSliceParam,
getZoomToPosition,
setCesiumToolbarParam,
- syncLayersParam
+ syncLayersParam,
} from '../permalink';
import {createCesiumObject} from '../layers/helpers';
import i18next from 'i18next';
@@ -46,7 +46,7 @@ import type QueryManager from '../query/QueryManager';
import DashboardStore from '../store/dashboard';
import {getAssets} from '../api-ion';
-import {parseKml, renderWithDelay} from '../cesiumutils';
+import {LayerEvent, LayersUpdateEvent} from '../components/layers/layers-display';
type SearchLayer = {
layer: string
@@ -56,6 +56,12 @@ type SearchLayer = {
dataSourceName?: string
}
+interface LayerClickEvent {
+ detail: {
+ layer: string
+ }
+}
+
@customElement('ngm-side-bar')
export class SideBar extends LitElementI18n {
@property({type: Object})
@@ -68,25 +74,20 @@ export class SideBar extends LitElementI18n {
accessor catalogLayers: LayerConfig[] | undefined;
@state()
accessor activeLayers: LayerConfig[] = [];
+
+ // TODO change this back to `null`
@state()
accessor activePanel: string | null = null;
@state()
accessor showHeader = false;
@state()
- accessor globeQueueLength_ = 0;
- @state()
accessor mobileShowAll = false;
@state()
accessor hideDataDisplayed = false;
@state()
- accessor layerOrderChangeActive = false;
- @state()
accessor debugToolsActive = getCesiumToolbarParam();
@query('.ngm-side-bar-panel > .ngm-toast-placeholder')
accessor toastPlaceholder;
- @query('ngm-catalog')
- accessor catalogElement;
- private viewer: Viewer | null = null;
private layerActions: LayersActions | undefined;
private zoomedToPosition = false;
private accordionInited = false;
@@ -94,10 +95,18 @@ export class SideBar extends LitElementI18n {
private shareDownListener = evt => {
if (!evt.composedPath().includes(this)) this.activePanel = null;
};
+ private viewer: Viewer | null = null;
constructor() {
super();
- MainStore.viewer.subscribe(viewer => this.viewer = viewer);
+
+ this.handleDisplayLayersUpdate = this.handleDisplayLayersUpdate.bind(this);
+ this.handleDisplayLayerUpdate = this.handleDisplayLayerUpdate.bind(this);
+ this.handleDisplayLayerRemoval = this.handleDisplayLayerRemoval.bind(this);
+
+ MainStore.viewer.subscribe((viewer) => {
+ this.viewer = viewer;
+ });
auth.user.subscribe((user) => {
if (!user && this.activeLayers) {
@@ -115,7 +124,7 @@ export class SideBar extends LitElementI18n {
this.activeLayers.forEach(layer => this.removeLayerWithoutSync(layer));
}
await this.syncActiveLayers();
- this.catalogElement.requestUpdate();
+ this.requestUpdate();
MainStore.nextLayersRemove();
});
@@ -184,15 +193,6 @@ export class SideBar extends LitElementI18n {
@click=${() => this.togglePanel('settings')}>
`;
- const dataMobileHeader = html`
- this.hideDataDisplayed = true}
- class="ngm-data-catalog-label ${classMap({active: this.hideDataDisplayed})}">
- ${i18next.t('lyr_geocatalog_label')}
-
- this.hideDataDisplayed = false}
- class="ngm-data-catalog-label ${classMap({active: !this.hideDataDisplayed})}">
- ${i18next.t('dtd_displayed_data_label')}
-
`;
return html`
this.activePanel = ''}
- @layerclick=${evt => this.onCatalogLayerClicked(evt.detail.layer)}
+ @close=${() => this.activePanel = null}
+ @layerclick=${(e: LayerClickEvent) => this.onCatalogLayerClicked(e.detail.layer)}
>
-
-
-
this.hideDataDisplayed = !this.hideDataDisplayed}>
- ${i18next.t('dtd_configure_data_btn')}
-
-
this.onCatalogLayerClicked(evt.detail.layer)}>
-
-
+ this.activePanel = null}"
+ @layer-click=${(e: LayerClickEvent) => this.onCatalogLayerClicked(e.detail.layer)}
+ @display-layers-update="${this.handleDisplayLayersUpdate}"
+ @display-layer-update="${this.handleDisplayLayerUpdate}"
+ @display-layer-removal="${this.handleDisplayLayerRemoval}"
+ >
this.activePanel = 'tools'}
- @close=${() => this.activePanel = ''}>
+ @close=${() => this.activePanel = null}>
${this.activePanel !== 'share' ? '' : html`
`}
-
-
-
-
-
- ${this.layerOrderChangeActive ? i18next.t('dtd_finish_ordering_label') : i18next.t('dtd_change_order_label')}
-
- ${this.layerOrderChangeActive ?
- html`
-
this.onLayersOrderChange(evt.detail)}>
- ` :
- html`
-
this.onRemoveDisplayedLayer(evt)}
- @layerChanged=${evt => this.onLayerChanged(evt)}>
- `
- }
-
-
this.onKmlUpload(file, clampToGround)}>
-
-
-
-
-
-
-
`;
}
@@ -341,7 +290,7 @@ export class SideBar extends LitElementI18n {
return;
}
this.activePanel = panelName;
- if (this.activePanel === 'data' && !this.mobileView) this.hideDataDisplayed = false;
+ // if (this.activePanel === 'data' && !this.mobileView) this.hideDataDisplayed = false;
}
async syncActiveLayers() {
@@ -434,9 +383,6 @@ export class SideBar extends LitElementI18n {
this.catalogLayers = [...defaultLayerTree];
await this.syncActiveLayers();
}
- this.viewer.scene.globe.tileLoadProgressEvent.addEventListener(queueLength => {
- this.globeQueueLength_ = queueLength;
- });
}
// hide share panel on any action outside side bar
if (!this.shareListenerAdded && this.activePanel === 'share') {
@@ -505,18 +451,6 @@ export class SideBar extends LitElementI18n {
this.viewer!.scene.requestRender();
}
- onLayerChanged(evt) {
- this.queryManager!.hideObjectInformation();
- const catalogLayers = this.catalogLayers ? this.catalogLayers : [];
- this.catalogLayers = [...catalogLayers];
- this.activeLayers = [...this.activeLayers];
- syncLayersParam(this.activeLayers);
- if (evt.detail) {
- this.maybeShowVisibilityHint(evt.detail);
- }
- this.requestUpdate();
- }
-
maybeShowVisibilityHint(config: LayerConfig) {
if (this.displayUndergroundHint
&& config.visible
@@ -527,28 +461,6 @@ export class SideBar extends LitElementI18n {
}
}
- async onRemoveDisplayedLayer(evt) {
- const {config, idx} = evt.detail;
- this.activeLayers.splice(idx, 1);
- await this.removeLayer(config);
- }
-
- async removeLayerWithoutSync(config: LayerConfig) {
- if (config.setVisibility) {
- config.setVisibility(false);
- } else {
- const c = await config.promise;
- if (c instanceof CustomDataSource || c instanceof GeoJsonDataSource) {
- this.viewer!.dataSources.getByName(c.name)[0].show = false;
- }
- }
- config.visible = false;
- config.displayed = false;
- if (config.remove) {
- config.remove();
- }
- }
-
async removeLayer(config: LayerConfig) {
await this.removeLayerWithoutSync(config);
this.viewer!.scene.requestRender();
@@ -694,44 +606,40 @@ export class SideBar extends LitElementI18n {
return layer.promise;
}
- toggleLayerOrderChange() {
- this.layerOrderChangeActive = !this.layerOrderChangeActive;
+ private handleDisplayLayersUpdate(e: LayersUpdateEvent): void {
+ this.activeLayers = e.detail.layers;
}
- async onLayersOrderChange(layers: LayerConfig[]) {
- await this.layerActions!.reorderLayers(layers);
- // update activeLayers only when ordering finished
- if (!this.layerOrderChangeActive) {
- this.activeLayers = [...layers];
+ private handleDisplayLayerUpdate(e: LayerEvent): void {
+ this.queryManager!.hideObjectInformation();
+ const catalogLayers = this.catalogLayers ? this.catalogLayers : [];
+ this.catalogLayers = [...catalogLayers];
+ this.activeLayers = [...this.activeLayers];
+ syncLayersParam(this.activeLayers);
+ if (e.detail) {
+ this.maybeShowVisibilityHint(e.detail.layer);
}
- this.dispatchEvent(new CustomEvent('layerChanged'));
+ this.requestUpdate();
}
- async onKmlUpload(file: File, clampToGround: boolean) {
- if (!this.viewer) return;
- const dataSource = new CustomDataSource();
- const name = await parseKml(this.viewer, file, dataSource, clampToGround);
- const layer = `${name.replace(' ', '_')}_${Date.now()}`;
- // name used as id for datasource
- dataSource.name = layer;
- MainStore.addUploadedKmlName(dataSource.name);
- await this.viewer.dataSources.add(dataSource);
- await renderWithDelay(this.viewer);
- // done like this to have correct rerender of component
- const promise = Promise.resolve(dataSource);
- const config: LayerConfig = {
- load() { return promise; },
- label: name,
- layer,
- promise: promise,
- opacity: DEFAULT_LAYER_OPACITY,
- notSaveToPermalink: true,
- ownKml: true,
- opacityDisabled: true
- };
- await this.onCatalogLayerClicked(config);
- this.viewer.zoomTo(dataSource);
- this.requestUpdate();
+ private async handleDisplayLayerRemoval(e: LayerEvent): Promise {
+ await this.removeLayer(e.detail.layer);
+ }
+
+ private async removeLayerWithoutSync(layer: LayerConfig): Promise {
+ if (layer.setVisibility) {
+ layer.setVisibility(false);
+ } else {
+ const c = await layer.promise;
+ if (c instanceof CustomDataSource || c instanceof GeoJsonDataSource) {
+ this.viewer!.dataSources.getByName(c.name)[0].show = false;
+ }
+ }
+ layer.visible = false;
+ layer.displayed = false;
+ if (layer.remove) {
+ layer.remove();
+ }
}
toggleDebugTools(event) {
diff --git a/ui/src/index.ts b/ui/src/index.ts
index 636ff825b..ae2307bc9 100644
--- a/ui/src/index.ts
+++ b/ui/src/index.ts
@@ -2,6 +2,7 @@ import './jquery.polyfill';
import './style/index.css';
import {ReactiveElement} from 'lit';
+
// Detect issues following lit2 migration
ReactiveElement.enableWarning?.('migration');
diff --git a/ui/src/layers/ngm-catalog.ts b/ui/src/layers/ngm-catalog.ts
deleted file mode 100644
index 94d126eb4..000000000
--- a/ui/src/layers/ngm-catalog.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import type {TemplateResult} from 'lit';
-import {html} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
-import {LitElementI18n} from '../i18n.js';
-import i18next from 'i18next';
-import auth from '../store/auth';
-import type {LayerTreeNode} from '../layertree';
-
-@customElement('ngm-catalog')
-export class Catalog extends LitElementI18n {
- @property({type: Array})
- accessor layers: LayerTreeNode[] = [];
- @state()
- accessor userGroups: string[] = [];
-
- constructor() {
- super();
- auth.user.subscribe((user) => {
- this.userGroups = user?.['cognito:groups'] ?? [];
- });
- }
-
- getCategoryOrLayerTemplate(node: LayerTreeNode, level: string): TemplateResult {
- if (node.children) {
- return this.getCategoryTemplate(node, level);
- }
- return this.getLayerTemplate(node);
- }
-
- getCategoryTemplate(category: LayerTreeNode, level: string): TemplateResult {
- // if it is a restricted layer, the user must be logged in to see it
- const content = category.children?.filter(
- node => !(node.restricted && (!node.restricted.some(g => this.userGroups.includes(g))))
- ).map(node => this.getCategoryOrLayerTemplate(node, 'second-level'));
-
- if (!content?.length) return html``;
-
- return html`
-
-
-
-
-
-
- ${content}
-
-
- `;
- }
-
- getLayerTemplate(layer: LayerTreeNode): TemplateResult {
- return html`
- {
- this.dispatchEvent(new CustomEvent('layerclick', {
- detail: {
- layer
- }
- }));
- }}>
-
-
-
-
- `;
- }
-
- render() {
- return html`${this.layers.map(node => this.getCategoryOrLayerTemplate(node, 'first-level'))}`;
- }
-
- createRenderRoot() {
- return this;
- }
-}
diff --git a/ui/src/layers/ngm-layers-item.ts b/ui/src/layers/ngm-layers-item.ts
index e23fe9cbd..02c423b05 100644
--- a/ui/src/layers/ngm-layers-item.ts
+++ b/ui/src/layers/ngm-layers-item.ts
@@ -1,5 +1,5 @@
import i18next from 'i18next';
-import {html} from 'lit';
+import {css, html, unsafeCSS} from 'lit';
import {customElement, property, query, state} from 'lit/decorators.js';
import {LitElementI18n} from '../i18n.js';
import {classMap} from 'lit-html/directives/class-map.js';
@@ -10,6 +10,15 @@ import {styleMap} from 'lit/directives/style-map.js';
import {Sortable} from 'sortablejs';
import type LayersAction from './LayersActions';
import {debounce} from '../utils';
+import {PropertyValues} from '@lit/reactive-element';
+
+import iconsCss from '../style/icons.css';
+import layersCss from '../style/layers.css';
+import sliderCss from '../style/ngm-slider.css';
+
+import fomanticTransitionCss from 'fomantic-ui-css/components/transition.css';
+import fomanticDropdownCss from 'fomantic-ui-css/components/dropdown.css';
+import 'fomantic-ui-css/components/transition.js';
const GEOCAT_LANG_CODE = {
'de': 'ger',
@@ -41,11 +50,17 @@ export class NgmLayersItem extends LitElementI18n {
private toggleItemSelection = () => this.movable ? Sortable.utils.select(this) : Sortable.utils.deselect(this);
private debouncedOpacityChange = debounce(() => this.changeOpacity(), 250, true);
- firstUpdated() {
- $(this.querySelector('.ui.dropdown')!).dropdown();
+ firstUpdated(): void {
+ if (this.shadowRoot != null) {
+ $(this.shadowRoot.querySelectorAll('.ui.dropdown')).dropdown({
+ on: 'mouseup',
+ collapseOnActionable: false,
+ });
+ }
}
- updated(changedProps) {
+ updated(changedProps: PropertyValues): void {
+ super.updated(changedProps);
if (changedProps.has('changeOrderActive')) {
this.updateMovableState();
}
@@ -54,7 +69,6 @@ export class NgmLayersItem extends LitElementI18n {
this.requestUpdate();
});
}
- super.updated(changedProps);
}
connectedCallback() {
@@ -127,6 +141,7 @@ export class NgmLayersItem extends LitElementI18n {
showLayerLegend(config: LayerConfig) {
this.dispatchEvent(new CustomEvent('showLayerLegend', {
+ composed: true,
bubbles: true,
detail: {
config
@@ -136,6 +151,7 @@ export class NgmLayersItem extends LitElementI18n {
showWmtsDatePicker(config: LayerConfig) {
this.dispatchEvent(new CustomEvent('showWmtsDatePicker', {
+ composed: true,
bubbles: true,
detail: {
config
@@ -145,6 +161,7 @@ export class NgmLayersItem extends LitElementI18n {
showVoxelFilter(config: LayerConfig) {
this.dispatchEvent(new CustomEvent('showVoxelFilter', {
+ composed: true,
bubbles: true,
detail: {
config
@@ -166,9 +183,9 @@ export class NgmLayersItem extends LitElementI18n {
${i18next.t('dtd_legend')}
` : ''}
${this.config?.geocatId ? html`
-
Geocat.ch
` : ''}
@@ -273,10 +290,6 @@ export class NgmLayersItem extends LitElementI18n {
`;
}
- createRenderRoot() {
- return this;
- }
-
cloneNode(deep) {
const node = super.cloneNode(deep) as NgmLayersItem;
node.config = this.config;
@@ -285,4 +298,19 @@ export class NgmLayersItem extends LitElementI18n {
node.clone = true;
return node;
}
+
+ static readonly styles = css`
+ ${unsafeCSS(fomanticTransitionCss)}
+ ${unsafeCSS(fomanticDropdownCss)}
+ ${unsafeCSS(iconsCss)}
+ ${unsafeCSS(layersCss.replaceAll('ngm-layers-item', ':host'))}
+ ${unsafeCSS(sliderCss)}
+
+ .ui.dropdown .menu > .item {
+ font-size: 14px;
+ text-decoration: none;
+ padding: 10px 16px;
+ min-height: unset;
+ }
+ `;
}
diff --git a/ui/src/layers/ngm-layers.ts b/ui/src/layers/ngm-layers.ts
index daa3e811b..3de0a48fa 100644
--- a/ui/src/layers/ngm-layers.ts
+++ b/ui/src/layers/ngm-layers.ts
@@ -4,13 +4,14 @@ import {LitElementI18n} from '../i18n.js';
import './ngm-layers-item';
import DashboardStore from '../store/dashboard';
import {LayerConfig} from '../layertree';
+import type LayersAction from './LayersActions';
@customElement('ngm-layers')
export default class NgmLayers extends LitElementI18n {
@property({type: Array})
accessor layers: LayerConfig[] = [];
@property({type: Object})
- accessor actions: any;
+ accessor actions: LayersAction | null = null;
updated(changedProperties: PropertyValues) {
if (changedProperties.has('layers')) {
@@ -52,8 +53,4 @@ export default class NgmLayers extends LitElementI18n {
const reverse = [...this.layers].reverse();
return html`${reverse.map((c, idx) => this.createLayerTemplate(c, idx, len))}`;
}
-
- createRenderRoot() {
- return this;
- }
}
diff --git a/ui/src/style/index.css b/ui/src/style/index.css
index bfeb7e982..eb4baf4b7 100644
--- a/ui/src/style/index.css
+++ b/ui/src/style/index.css
@@ -64,6 +64,8 @@
@import 'ngm-ion-modal.css';
@import 'ngm-info-table.css';
+@import '../styles/index.css';
+
* {
scrollbar-color: light;
scrollbar-width: thin;
diff --git a/ui/src/style/layers.css b/ui/src/style/layers.css
index 8ab73406d..1f5481a02 100644
--- a/ui/src/style/layers.css
+++ b/ui/src/style/layers.css
@@ -50,7 +50,7 @@ ngm-layers-item, .ngm-base-layer {
flex-direction: row;
}
-ngm-layers-item, .ngm-base-layer > div:first-child {
+.ngm-base-layer > div:first-child {
padding: 0 11px;
}
@@ -141,11 +141,11 @@ ngm-layers-item .ui.dropdown .menu .item:hover {
background-color: var(--ngm-hover);
}
-.ngm-extension-panel .ui.header {
+.ngm-extension-panel .ui.header, ngm-navigation-panel .ui.header {
margin-top: 7px;
}
-.ngm-extension-panel .ui.divider {
+.ngm-extension-panel .ui.divider, ngm-navigation-panel .ui.divider {
margin-top: 15px;
margin-bottom: 8px;
border-top: 1px solid #DFE2E6 !important;
diff --git a/ui/src/style/ngm-side-bar.css b/ui/src/style/ngm-side-bar.css
index c048e708d..c360956b5 100644
--- a/ui/src/style/ngm-side-bar.css
+++ b/ui/src/style/ngm-side-bar.css
@@ -1,5 +1,7 @@
ngm-side-bar {
display: flex;
+ z-index: 5;
+ max-width: 100vw;
}
.ngm-menu,
@@ -65,9 +67,9 @@ ngm-side-bar {
width: 250px;
height: calc(100vh - var(--ngm-header-height));
max-width: 1028px;
- background-color: #F1F3F5;
+ background-color: var(--color-bg--dark);
box-shadow: 4px 0 4px #00000029;
- padding: 10px;
+ padding: var(--panel-padding);
position: absolute;
margin-left: var(--ngm-left-side-bar-width);
z-index: 5;
@@ -76,6 +78,10 @@ ngm-side-bar {
overflow-y: auto;
}
+ngm-navigation-catalog .ngm-side-bar-panel {
+ margin-left: 0;
+}
+
.inner-toolbar-settings {
display: flex;
flex-direction: column;
diff --git a/ui/src/styles/index.css b/ui/src/styles/index.css
new file mode 100644
index 000000000..75bc137a3
--- /dev/null
+++ b/ui/src/styles/index.css
@@ -0,0 +1,24 @@
+
+:root {
+ --color-bg: #FFF;
+ --color-bg--dark: #F1F3F5;
+
+ --color-bg-contrast: #000;
+ --color-bg-contrast--light: #212529;
+ --color-bg-contrast--lighter: #656363;
+
+ --color-highlight: #C5F6FA;
+ --color-highlight--dark: #66D9E8;
+ --color-highlight--darker: #0B7285;
+
+ --color-action: #a83526;
+ --color-action--light: #FF0000;
+
+ --font: 'Inter', sans-serif;
+ font-family: var(--font);
+ font-size: 16px;
+
+ --panel-padding: 10px;
+
+ --icon-size--normal: 24px;
+}
diff --git a/ui/src/styles/theme.ts b/ui/src/styles/theme.ts
new file mode 100644
index 000000000..8748caf81
--- /dev/null
+++ b/ui/src/styles/theme.ts
@@ -0,0 +1,26 @@
+import {css, CSSResult} from 'lit';
+
+type Breakpoint =
+ | 'xs'
+ | 'sm'
+ | 'md'
+ | 'lg'
+ | 'xl'
+ | 'xxl'
+
+const breakpoints: Record = {
+ xs: 0,
+ sm: 576,
+ md: 768,
+ lg: 992,
+ xl: 1200,
+ xxl: 1400,
+};
+
+export const upFrom = (breakpoint: Breakpoint): CSSResult => {
+ const min = breakpoints[breakpoint];
+ if (min === 0) {
+ throw new Error(`can't target breakpoint '${breakpoint}' with media query as it starts at zero pixels`);
+ }
+ return css`@media (min-width: ${min}px)`;
+};
diff --git a/ui/src/typings/styles.d.ts b/ui/src/typings/styles.d.ts
new file mode 100644
index 000000000..5bf18d724
--- /dev/null
+++ b/ui/src/typings/styles.d.ts
@@ -0,0 +1,4 @@
+declare module '*.css' {
+ const styles: string;
+ export default styles;
+}
diff --git a/ui/webpack.config.js b/ui/webpack.config.js
index ff483f9a6..e5b97cbcc 100644
--- a/ui/webpack.config.js
+++ b/ui/webpack.config.js
@@ -73,15 +73,19 @@ const config = {
type: 'asset',
},
{
- test: /\.css$/i,
- issuer: /\.(?:js|ts)$/,
- use: ['style-loader', 'css-loader'],
+ test: /\/style\/index.css$/i,
+ //use: [isDev ? "style-loader" : MiniCssExtractPlugin.loader, 'css-loader'],
+ use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
{
test: /\.css$/i,
- issuer: {not: [/\.(?:js|ts)$/]},
- //use: [isDev ? "style-loader" : MiniCssExtractPlugin.loader, 'css-loader'],
- use: [MiniCssExtractPlugin.loader, 'css-loader'],
+ exclude: /\/style\/index.css$/i,
+ use: [{
+ loader: 'css-loader',
+ options: {
+ exportType: 'string',
+ }
+ }],
},
{
test: /\.ts$/,