Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
f11b8f1
UoE/datashare: check 'Inherit policies' by default on the move screen
milanmajchrak Jul 14, 2026
4b21906
UoE/datashare: make clear the Keep-embargo option is about the item's…
milanmajchrak Jul 14, 2026
9b43774
UoE/datashare: add "Keep embargo policies" option to the move-item sc…
milanmajchrak Jul 14, 2026
94ac881
feat: auth gate for admin control of bitstream access conditions
jr-rk Jul 14, 2026
bf362d4
fix: remove redundant error handling
jr-rk Jul 14, 2026
47284d2
fix: patch the JSON patch success handler to clear isSaving before ea…
jr-rk Jul 14, 2026
f7d4b94
feat: Hide the admin sidebar for regular authenticated users while pr…
jr-rk Jul 14, 2026
5d55953
fix(admin-menu): skip authz calls for anonymous, fix lint
jr-rk Jul 14, 2026
953efbf
fix(submission): fail-closed admin check, repair spec teardown crash
jr-rk Jul 14, 2026
0986584
fix(submission): reset isSaving when the save PATCH fails
jr-rk Jul 15, 2026
46707ba
Fix admin sidebar visibility regression and reduce scope
jr-rk Jul 15, 2026
0e97c67
Fix MenuService test stub method signatures
jr-rk Jul 15, 2026
fa49ce5
UoE/datashare: Fix home-page reload flicker (hydration-safe SSR anti-…
MatusBeke Jul 15, 2026
c4bb0ad
test(admin-sidebar): run ngOnInit once per authz test, guard take(1) …
jr-rk Jul 15, 2026
7a8d586
feat: include file name in submission upload notifications
jr-rk Jul 15, 2026
897fab7
fix: make UploaderCompleteEvent fileName optional to match emitter
jr-rk Jul 15, 2026
b193d38
refactor: address Copilot review on upload notification typing
jr-rk Jul 16, 2026
15fe6ef
fix: prevent duplicate values in repeatable submission dropdowns
jr-rk Jul 16, 2026
b4a3147
fix(statistics): clear statistics table pagination params on destroy
jr-rk Jul 16, 2026
9fa95f0
docs(statistics): explain deferred pagination-param cleanup accurately
jr-rk Jul 16, 2026
57eb88c
test(statistics): destroy uninitialised table via the framework path
jr-rk Jul 16, 2026
513c384
feat(submission): disable already-used options in repeatable dropdowns
jr-rk Jul 17, 2026
c9a50fc
style(submission): force disabled-option styling and normal hover cursor
jr-rk Jul 17, 2026
4723ee1
fix(submission): refresh used sibling values on keyboard-open
jr-rk Jul 17, 2026
e16a617
refactor(submission): drop superseded duplicate-value guard, keep dis…
jr-rk Jul 20, 2026
fbb5beb
removed emptyline
jr-rk Jul 20, 2026
384442d
UoE flicker: block pointer-events on the frozen clone (Copilot); keep…
Jul 21, 2026
c356835
UoE flicker: add local Docker reproduction + fix-verification evidence
Jul 21, 2026
239845e
Drop committed flicker screenshots from the PR — keep it source-only
Jul 21, 2026
e248584
UoE flicker: also mask the post-login white flash (unify overlay via …
Jul 21, 2026
4874f28
UoE flicker: render the desktop header on the server (fix reload cont…
Jul 21, 2026
b5d9cc1
UoE flicker: update hostWindowReducer spec for the desktop default width
Jul 21, 2026
265e2df
UoE flicker: mask logged-in reloads with the matching auth state (kil…
Jul 21, 2026
a9b86ee
UoE flicker: drive the admin-sidebar gutter from CSS, not an animation
Jul 22, 2026
721b015
UoE flicker: reserve the scrollbar gutter so the page stops sliding o…
Jul 22, 2026
f6a7a38
UoE flicker: stop the overlay eating clicks, and drop the dead half o…
Jul 22, 2026
407890f
feat(submission): say when a file is being ingested instead of just "…
milanmajchrak Jul 22, 2026
2e60de1
UoE flicker: move the overlay script out of index.html into a source …
Jul 23, 2026
83b38ae
fix(submission): remove obsolete "same file name" upload warning and …
milanmajchrak Jul 23, 2026
81b61b1
fix(submission): remove obsolete "same file name" upload warning and …
milanmajchrak Jul 23, 2026
ecbd755
UoE flicker: minify the overlay when inlining it into index.html
Jul 23, 2026
220b65a
fix(item-page): stop 500 on "show more" for items with many bitstreams
jr-rk Jul 23, 2026
ddacf80
refactor(datashare-submission): address Copilot review on total-size …
milanmajchrak Jul 23, 2026
28adf27
refactor(item-page): revert getNextPage error handling to upstream
jr-rk Jul 23, 2026
e5962dd
UoE flicker: slide the admin sidebar in on login (content stays put)
Jul 23, 2026
670adce
docs(item-page): document base-vs-theme licence rendering; pin spec
jr-rk Jul 23, 2026
f189076
Revert "UoE flicker: slide the admin sidebar in on login (content sta…
Jul 23, 2026
58c0cd6
fix(submission): show an actionable error when a server-path ingest f…
milanmajchrak Jul 23, 2026
1f945a5
fix(submission): shorten the upload-from-path error to a single plain…
milanmajchrak Jul 23, 2026
37cdfcc
test(item-page): isolate ORIGINAL-only assertion to the click
jr-rk Jul 23, 2026
5225331
Merge remote-tracking branch 'origin/datashare-UoEMainLibrary-dspace-…
milanmajchrak Jul 23, 2026
b25717d
test(item-page): assert requested bundle names directly
jr-rk Jul 23, 2026
2507a2f
refactor(datashare): type the tri-state path outcome as boolean | und…
milanmajchrak Jul 23, 2026
0c794b8
docs(item-page): fix misleading bitstream pageSize comment
milanmajchrak Jul 23, 2026
1c6bbb4
UoE flicker: declare terser as an explicit devDependency
Jul 23, 2026
2e5db20
chore(submission): shorten the datashare comments
milanmajchrak Jul 23, 2026
92d66cd
fix(comcol): widen collection/community logo max-width so wide banner…
milanmajchrak Jul 23, 2026
cf527b1
fix(comcol): enlarge result thumbnail width to 175px
milanmajchrak Jul 24, 2026
52a6381
UoE/feat(mydspace): rename submission menu and status labels (LUC070-…
milanmajchrak Jul 24, 2026
2bc0bd6
fix(submission): harden duplicate detection in repeatable scrollable …
milanmajchrak Jul 24, 2026
29ecbb4
style: satisfy lint (indentation + import sort)
milanmajchrak Jul 24, 2026
ae701af
a11y: bind aria-selected to the selected value; clarify 1e spec name …
milanmajchrak Jul 24, 2026
35f669c
fix(i18n): restore keys dropped by the keep-embargo cherry-pick
milanmajchrak Jul 27, 2026
f54e971
chore: keep local-only test Dockerfile and demo harness out of the PR
milanmajchrak Jul 27, 2026
3225237
Fix sync PR UoEMainLibrary#7: restore 3 deleted translations, drop th…
milanmajchrak Jul 27, 2026
a123f12
fix(submission): stop repeatable dropdowns duplicating and clearing v…
milanmajchrak Jul 27, 2026
3ca2011
fix(submission): keep keyboard reorder-cancel and clear-entry consistent
milanmajchrak Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"loaders": "prepend"
}
},
"indexTransform": "webpack/index-html-transform.ts",
"allowedCommonJsDependencies": [
"cerialize",
"core-js",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@
"sass": "~1.94.2",
"sass-loader": "^12.6.0",
"sass-resources-loader": "^2.2.5",
"terser": "5.29.1",
"ts-node": "^8.10.2",
"typescript": "~5.4.5",
"webpack": "5.101.0",
Expand Down
197 changes: 197 additions & 0 deletions src/anti-flicker-overlay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/* eslint-disable */
// This file is inlined verbatim into index.html at build time (see webpack/index-html-transform.ts,
// wired via the build's indexTransform in angular.json). It must stay dependency-free, ES5-safe and
// self-contained: it runs as a synchronous inline <script> before the deferred module bundles, which
// is the only point at which a mask can be installed before the SSR DOM is painted. Editing it here
// keeps index.html clean while the browser still receives it inline.

/*
Anti-flicker overlay: paints a detached clone of the settled page on top while <ds-app> re-renders,
and lifts once the routed page is genuinely painted (150ms fade).

Why it is needed: provideClientHydration() is enabled, but DSpace's theme system defeats it. Every
wrapper is a `ds-themed-*` whose real content is built imperatively via ViewContainerRef
.createComponent() in a client-side ngAfterViewInit, so hydration cannot reuse it and every themed
subtree is destroyed and rebuilt a beat after first paint. Upstream: DSpace/dspace-angular#3867.
The same mask covers the post-login white flash, where root.component hides `.outer-wrapper` behind
`.ds-full-screen-loader` across the `/reload/<ts>` -> `/home` redirect.

It must be inline and synchronous: the CLI injects the bundles as deferred module scripts, so no
Angular hook can run before the SSR DOM has already been painted.

Safety rails: self-disables under Cypress / navigator.webdriver, never absorbs input, lifts on the
first user interaction, and has a 15s hard cap -- it can never trap the user behind a frozen frame.
*/
(function () {
if (typeof window === 'undefined' || typeof document === 'undefined') { return; }
// Cypress drives the real app; a duplicated clone would break element-uniqueness selectors.
if (typeof window.Cypress !== 'undefined') { return; }
// Same reasoning for any WebDriver-based automation (Playwright/Selenium/headless CI).
if (typeof navigator !== 'undefined' && navigator.webdriver) { return; }

var STORE_KEY = '__dspace_ssr_frame';
var OVERLAY_ID = '__dspace_ssr_overlay';
var MIN_CONTENT_HEIGHT = 200; // px: proves #main-content is no longer the d-none'd / empty shell
var MASK_MAX_MS = 15000; // absolute cap per mask -- never trap the user behind a frozen frame

function dsApp() { return document.querySelector('ds-app'); }

// "Real content" = has children and is NOT the fullscreen auth/theme loader (white + spinner).
function isRealContent(el) {
return !!(el && el.firstElementChild && !el.querySelector('.ds-full-screen-loader'));
}

// True if this <ds-app> subtree is rendered in the LOGGED-IN state: the header shows the user-menu
// (avatar / logout), never present when logged out. Used to pick a mask source whose auth state (and
// therefore its admin sidebar / content offset) matches what the reloaded page will settle to.
function loggedInChrome(el) {
return !!(el && el.querySelector && el.querySelector('.fa-user-circle, .dropdownLogout, [data-test="user-menu"]'));
}

// The routed page is actually painted (safe to reveal): no fullscreen loader AND #main-content has
// real layout height. While the loader shows, root.component.html d-none's #main-content's wrapper,
// so its own getBoundingClientRect height is 0 even though it is still in the DOM.
function contentPainted() {
var app = dsApp();
if (!app) { return true; }
if (app.querySelector('.ds-full-screen-loader')) { return false; }
var mc = app.querySelector('#main-content');
var h = (mc && mc.getBoundingClientRect) ? mc.getBoundingClientRect().height : 0;
if (h < MIN_CONTENT_HEIGHT) { return false; }
// Also require the themed navbar to be present with real height. #main-content reports height even
// while the imperative themed re-render (header/navbar) is still mid-flight or the async home data
// hasn't landed -- revealing then flashes an unstyled/half-built page. The themed navbar being laid
// out is a good "the routed page is really rendered" proxy on the datashare theme. If a route has no
// such navbar this stays false and the 15s cap reveals anyway.
var nav = app.querySelector('#main-navbar');
var nh = (nav && nav.getBoundingClientRect) ? nav.getBoundingClientRect().height : 0;
return nh > 0;
}

function currentUrl() { return location.pathname + location.search; }

// Remember the current real view so a later loader/racy-reload can be masked with it. Keyed by URL:
// masking a page with a picture of a DIFFERENT one (or with logged-in chrome after a logout) shows
// the user a page they are not on any more.
function rememberFrame() {
var app = dsApp();
if (isRealContent(app)) {
try {
sessionStorage.setItem(STORE_KEY, JSON.stringify({ url: currentUrl(), html: app.outerHTML }));
} catch (e) { /* quota / disabled */ }
}
}
function savedFrameNode() {
var saved = null;
try { saved = sessionStorage.getItem(STORE_KEY); } catch (e) { /* disabled */ }
if (!saved) { return null; }
var frame = null;
try { frame = JSON.parse(saved); } catch (e) { return null; }
if (!frame || !frame.html || frame.url !== currentUrl()) { return null; }
var holder = document.createElement('div');
holder.innerHTML = frame.html;
return holder.firstElementChild; // the saved <ds-app> element, _ngcontent attrs intact
}

// Install an opaque, inert freeze-frame from a detached <ds-app> node, then watch for reveal.
function installMask(sourceNode) {
if (!sourceNode || document.getElementById(OVERLAY_ID)) { return; }
var overlay = document.createElement('div');
overlay.id = OVERLAY_ID;
// Visual duplicate of content the real app still exposes; hide from a11y tree + make inert so
// keyboard focus can't land on the dead cloned controls.
overlay.setAttribute('aria-hidden', 'true');
overlay.setAttribute('inert', '');
overlay.appendChild(sourceNode);
overlay._dsDeadline = Date.now() + MASK_MAX_MS;
document.body.appendChild(overlay);
revealWhenPainted();
}

// DOM activity clock: the MutationObserver below stamps this on every childList change, so the
// reveal can wait for the app to go QUIET (the themed re-render on reload, and the loader dance on
// login, both churn the DOM) rather than lifting the instant content first appears.
var lastMutationAt = Date.now();
var SETTLE_QUIET_MS = 800;

// Lift the current mask once the routed page is painted AND the DOM has settled (or the 15s cap
// elapses); 150ms fade. Waiting for both is what keeps the reload's themed-wrapper re-render and
// the post-login loader from peeking out: content-painted alone fires a frame or two too early.
function revealWhenPainted() {
var el = document.getElementById(OVERLAY_ID);
if (!el || el._dsFading) { return; }
var settled = contentPainted() && (Date.now() - lastMutationAt) >= SETTLE_QUIET_MS;
if (!settled && Date.now() < el._dsDeadline) {
if (typeof window.requestAnimationFrame === 'function') { window.requestAnimationFrame(revealWhenPainted); }
else { setTimeout(revealWhenPainted, 50); }
return;
}
// The live page is now settled: remember it as the frame to mask the NEXT reload with, so that
// reload's mask matches this exact rendered state (incl. client-only chrome like the admin sidebar).
// Only when genuinely settled -- never on a deadline bail-out over a half-built page.
if (settled) { rememberFrame(); }
el._dsFading = true;
el.style.transition = 'opacity 150ms ease-out';
el.style.opacity = '0';
setTimeout(function () { if (el && el.parentNode) { el.parentNode.removeChild(el); } }, 200);
}

// The moment the user interacts, drop the mask: their click already went through to the live app
// (the overlay is pointer-events: none), so continuing to show a frozen picture of the previous page
// would hide the very navigation they just triggered -- and their own click churns the DOM, which
// pushes the quiet-window reveal further away. Clearing the deadline makes revealWhenPainted lift on
// this frame without remembering a half-built frame.
['pointerdown', 'keydown', 'wheel', 'touchstart'].forEach(function (type) {
window.addEventListener(type, function () {
var el = document.getElementById(OVERLAY_ID);
if (el && !el._dsFading) { el._dsDeadline = 0; revealWhenPainted(); }
}, { capture: true, passive: true });
});

try {
var app = dsApp();
if (!app || !app.firstElementChild) { return; } // SSR-excluded route -> nothing to mask

// Watch the DOM for the whole session: stamp the activity clock (drives the reveal's quiet
// window), keep the page masked whenever the fullscreen auth/theme loader appears unmasked (the
// ~1s white spinner shown on the current page right after a login submit -- before the /reload
// hard-redirect -- and again across /reload -> /home), and keep the saved frame fresh (debounced)
// while idle. Cheap: each mutation runs a couple of querySelectors.
var rememberTimer = null;
function onDomChanged() {
lastMutationAt = Date.now();
if (document.getElementById(OVERLAY_ID)) { return; } // already masked; reveal watcher lifts it
var app2 = dsApp();
if (app2 && app2.querySelector('.ds-full-screen-loader')) {
installMask(savedFrameNode()); // fullscreen loader is up and nothing masks it -> re-mask
} else {
if (rememberTimer) { clearTimeout(rememberTimer); }
rememberTimer = setTimeout(function () {
if (!document.getElementById(OVERLAY_ID) && contentPainted()) { rememberFrame(); }
}, 500);
}
}
if (typeof MutationObserver === 'function') {
new MutationObserver(onDomChanged).observe(document.documentElement, { childList: true, subtree: true });
}

// Install the mask FIRST (before anything slow) so it covers the SSR paint with no gap. Mask with
// whichever source matches the auth state the page will settle to, so client-only chrome (most
// visibly the logged-in admin sidebar, which shifts the body ~55px) is already in the mask and
// nothing moves at reveal. The logged-in header user-menu is the tell:
// - login /reload: the fresh SSR renders authenticated (sidebar present) -> clone the live SSR
// - logged-in F5 reload: the SSR renders anonymous, but the remembered settled frame is logged-in
// - logged-out: neither is logged-in -> the remembered settled frame (or SSR clone on first load)
// We do NOT rememberFrame() here -- the pre-hydration SSR is the unsettled state we must not save;
// rememberFrame() runs at reveal (revealWhenPainted), once the live page has actually settled.
var ssrClone = isRealContent(app) ? app.cloneNode(true) : null;
var savedNode = savedFrameNode();
installMask(loggedInChrome(ssrClone) ? ssrClone
: loggedInChrome(savedNode) ? savedNode
: (savedNode || ssrClone));
} catch (e) {
if (window.console && typeof console.warn === 'function') {
console.warn('[dspace-ssr-overlay] disabled due to error:', e);
}
}
})();
70 changes: 69 additions & 1 deletion src/app/admin/admin-sidebar/admin-sidebar.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ import {
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import {
BehaviorSubject,
of as observableOf,
} from 'rxjs';

import { AuthService } from '../../core/auth/auth.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { ScriptDataService } from '../../core/data/processes/script-data.service';
import { Item } from '../../core/shared/item.model';
import { MenuService } from '../../shared/menu/menu.service';
Expand Down Expand Up @@ -105,6 +109,70 @@ describe('AdminSidebarComponent', () => {
fixture.detectChanges();
});

describe('authorization', () => {
/**
* Re-create the component so that its single ngOnInit runs under the stubs/spies configured by the
* test, instead of the default detectChanges() from the outer beforeEach (which runs before them).
*/
const initFreshComponent = () => {
fixture = TestBed.createComponent(AdminSidebarComponent);
comp = fixture.componentInstance;
comp.sections = observableOf([]);
fixture.detectChanges();
};

beforeEach(() => {
spyOn(menuService, 'showMenu');
spyOn(menuService, 'hideMenu');
});

it('should show the admin menu for a user with an administrative role', () => {
authorizationService.isAuthorized = jasmine.createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.AdministratorOf);
});

initFreshComponent();

expect(menuService.showMenu).toHaveBeenCalledWith(comp.menuID);
expect(menuService.hideMenu).not.toHaveBeenCalled();
});

it('should hide the admin menu for an authenticated non-admin user', () => {
authorizationService.isAuthorized = jasmine.createSpy('isAuthorized').and.returnValue(observableOf(false));

initFreshComponent();

expect(menuService.showMenu).not.toHaveBeenCalled();
expect(menuService.hideMenu).toHaveBeenCalledWith(comp.menuID);
});

it('should hide the admin menu without requesting authorizations for an anonymous user', () => {
spyOn(TestBed.inject(AuthService), 'isAuthenticated').and.returnValue(observableOf(false));
authorizationService.isAuthorized = jasmine.createSpy('isAuthorized').and.returnValue(observableOf(false));

initFreshComponent();

expect(menuService.showMenu).not.toHaveBeenCalled();
expect(menuService.hideMenu).toHaveBeenCalledWith(comp.menuID);
expect(authorizationService.isAuthorized).not.toHaveBeenCalled();
});

it('should reveal the admin menu once authentication resolves (guards against a one-shot take(1) regression)', () => {
const authenticated$ = new BehaviorSubject<boolean>(false);
spyOn(TestBed.inject(AuthService), 'isAuthenticated').and.returnValue(authenticated$);
authorizationService.isAuthorized = jasmine.createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
return observableOf(featureID === FeatureID.AdministratorOf);
});

initFreshComponent();
expect(menuService.hideMenu).toHaveBeenCalledWith(comp.menuID);

authenticated$.next(true);

expect(menuService.showMenu).toHaveBeenCalledWith(comp.menuID);
});
});

describe('startSlide', () => {
describe('when expanding', () => {
beforeEach(() => {
Expand Down
20 changes: 14 additions & 6 deletions src/app/admin/admin-sidebar/admin-sidebar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ import {
BehaviorSubject,
combineLatest,
Observable,
of as observableOf,
} from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
first,
map,
switchMap,
withLatestFrom,
} from 'rxjs/operators';

import { AuthService } from '../../core/auth/auth.service';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
import { slideSidebar } from '../../shared/animations/slide';
import { MenuComponent } from '../../shared/menu/menu.component';
import { MenuService } from '../../shared/menu/menu.service';
Expand Down Expand Up @@ -108,12 +111,17 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
*/
ngOnInit(): void {
super.ngOnInit();
this.authService.isAuthenticated()
.subscribe((loggedIn: boolean) => {
if (loggedIn) {
this.menuService.showMenu(this.menuID);
}
});
this.authService.isAuthenticated().pipe(
switchMap((isAuthenticated: boolean) => isAuthenticated
? this.authorizationService.isAuthorized(FeatureID.AdministratorOf)
: observableOf(false)),
).subscribe((isSiteAdmin: boolean) => {
if (isSiteAdmin) {
this.menuService.showMenu(this.menuID);
} else {
this.menuService.hideMenu(this.menuID);
}
});
this.menuCollapsed.pipe(first())
.subscribe((collapsed: boolean) => {
this.sidebarOpen = !collapsed;
Expand Down
1 change: 1 addition & 0 deletions src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,5 @@ describe('App component', () => {
});

});

});
9 changes: 7 additions & 2 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,13 @@ export class AppComponent implements OnInit, AfterViewInit {
distinctUntilChanged(),
);

this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
// Window size only has meaning in the browser. On the server `nativeWindow` is unavailable, so
// dispatching here would overwrite the desktop default in hostWindow.reducer with an undefined
// size — keep the default (which is what SSR renders responsive components with). See the note on
// the reducer's initialState.
if (isPlatformBrowser(this.platformId)) {
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
}
}

private storeCSSVariables() {
Expand Down Expand Up @@ -181,5 +187,4 @@ export class AppComponent implements OnInit, AfterViewInit {
}
});
}

}
Loading
Loading