Skip to content

Commit

Permalink
Configurable logo and title (#160)
Browse files Browse the repository at this point in the history
* added asynchronous config for settings.header to specify logo, title and favicon
* refactored navigation stuff to separate folder, excluded logo and title as separate component
* added Promise testcase
* hide logo container if there is no logo
* fixed tests and added assertions for logo and title
* adjusted left header spacing for title only
* added docu for configurable logo and title
* added support for data:image favicons
  • Loading branch information
maxmarkus authored Nov 5, 2018
1 parent 6a288e9 commit b3c41cb
Show file tree
Hide file tree
Showing 16 changed files with 251 additions and 38 deletions.
1 change: 0 additions & 1 deletion core/examples/luigi-sample-angular/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"polyfills": "src/polyfills.ts",
"assets": [
"src/assets",
"src/favicon.ico",
"src/index.html",
"src/logout.html",
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ <h1 class="fd-section__title">Child node 2</h1>
<p>Navigation node config for this <strong>defaultChildNode</strong> demo:</p>
<app-code-snippet data="{
pathSegment: 'dps',
label: 'Default Child Node Example',
label: 'Default Child node Example',
defaultChildNode: 'dps2',
children: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,13 @@ Luigi.setConfig({
useHashRouting: true
},
settings: {
header: () => ({
logo:
'data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MTIuMzggMjA0Ij48ZGVmcz48c3R5bGU+LmNscy0xLC5jbHMtMntmaWxsLXJ1bGU6ZXZlbm9kZH0uY2xzLTF7ZmlsbDp1cmwoI2xpbmVhci1ncmFkaWVudCl9LmNscy0ye2ZpbGw6I2ZmZn08L3N0eWxlPjxsaW5lYXJHcmFkaWVudCBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iMjA2LjE5IiB4Mj0iMjA2LjE5IiB5Mj0iMjA0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjMDBiOGYxIi8+PHN0b3Agb2Zmc2V0PSIuMDIiIHN0b3AtY29sb3I9IiMwMWI2ZjAiLz48c3RvcCBvZmZzZXQ9Ii4zMSIgc3RvcC1jb2xvcj0iIzBkOTBkOSIvPjxzdG9wIG9mZnNldD0iLjU4IiBzdG9wLWNvbG9yPSIjMTc3NWM4Ii8+PHN0b3Agb2Zmc2V0PSIuODIiIHN0b3AtY29sb3I9IiMxYzY1YmYiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMxZTVmYmIiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48dGl0bGU+U0FQX2dyYWRfUl9zY3JuX1plaWNoZW5mbMOkY2hlIDE8L3RpdGxlPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTAgMjA0aDIwOC40MUw0MTIuMzggMEgwdjIwNCIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTI0NC43MyAzOC4zNmgtNDAuNnY5Ni41MmwtMzUuNDYtOTYuNTVoLTM1LjE2bC0zMC4yNyA4MC43MkMxMDAgOTguNyA3OSA5MS42NyA2Mi40IDg2LjQgNTEuNDYgODIuODkgMzkuODUgNzcuNzIgNDAgNzJjLjA5LTQuNjggNi4yMy05IDE4LjM4LTguMzggOC4xNy40MyAxNS4zNyAxLjA5IDI5LjcxIDhsMTQuMS0yNC41NUM4OS4wNiA0MC40MiA3MSAzNi4yMSA1Ni4xNyAzNi4xOWgtLjA5Yy0xNy4yOCAwLTMxLjY4IDUuNi00MC42IDE0LjgzQTM0LjIzIDM0LjIzIDAgMCAwIDUuNzcgNzQuN0M1LjU0IDg3LjE1IDEwLjExIDk2IDE5LjcxIDEwM2M4LjEgNS45NCAxOC40NiA5Ljc5IDI3LjYgMTIuNjIgMTEuMjcgMy40OSAyMC40NyA2LjUzIDIwLjM2IDEzQTkuNTcgOS41NyAwIDAgMSA2NSAxMzVjLTIuODEgMi45LTcuMTMgNC0xMy4wOSA0LjEtMTEuNDkuMjQtMjAtMS41Ni0zMy42MS05LjU5TDUuNzcgMTU0LjQyYTkzLjc3IDkzLjc3IDAgMCAwIDQ2IDEyLjIyaDIuMTFjMTQuMjQtLjI1IDI1Ljc0LTQuMzEgMzQuOTItMTEuNzEuNTMtLjQxIDEtLjg0IDEuNDktMS4yOGwtNC4xMiAxMC44NUgxMjNsNi4xOS0xOC44MmE2Ny40NiA2Ny40NiAwIDAgMCAyMS42OCAzLjQzIDY4LjMzIDY4LjMzIDAgMCAwIDIxLjE2LTMuMjVsNiAxOC42NGg2MC4xNHYtMzloMTMuMTFjMzEuNzEgMCA1MC40Ni0xNi4xNSA1MC40Ni00My4yIDAtMzAuMTEtMTguMjItNDMuOTQtNTcuMDEtNDMuOTR6TTE1MC45MSAxMjFhMzYuOTMgMzYuOTMgMCAwIDEtMTMtMi4yOGwxMi44Ny00MC41OWguMjJsMTIuNjUgNDAuNzFhMzguNSAzOC41IDAgMCAxLTEyLjc0IDIuMTZ6bTk2LjItMjMuMzNoLTguOTRWNjQuOTFoOC45NGMxMS45MyAwIDIxLjQ0IDQgMjEuNDQgMTYuMTQgMCAxMi42LTkuNTEgMTYuNTctMjEuNDQgMTYuNTciLz48L3N2Zz4=',
title: 'Luigi Demo',
favicon: '/assets/favicon-sap.ico'
// favicon: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0QjU1QkE5OENDODQxMUU4QURDRUZCOEE3ODIzNTRFQyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo0QjU1QkE5OUNDODQxMUU4QURDRUZCOEE3ODIzNTRFQyI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjRCNTVCQTk2Q0M4NDExRThBRENFRkI4QTc4MjM1NEVDIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjRCNTVCQTk3Q0M4NDExRThBRENFRkI4QTc4MjM1NEVDIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+YoKa3gAABKVJREFUeNrsVltMHFUY/mZ2lp1dFsICu8Cy0BZpuAkoYGkhoFa0kLSC1bY22tgHbdLE2HDzgRp9MRotxYYGH0wkpNUYYqKYGm9pCGiV2EaEamm0IdysdLkusAvMXmb8zwxdwIq2L/Zl/+Rkz57zn/N9+1++s5yiKLibxoUIhAiECNxtAkJR1+SLPdP+fRDwPzHhAEXhIckJlQn6WgE+fyEkqRQyDwQUbXCMGq993jRGT17hyHPa3tq1lbvBcavnlFuxIdNYCqAiyfB5baLwjaD3B6awIAHLfsAowBAuQPLRyVkf4CNvi0Ej4/GtXqjjVF/4aX85sIYkOfjZj6D1iDDAoFtPkE3JvzgurPFYkqHeSZDCFAN3S6gvisP+1EhkWUS4fQH0Ti3jR+cSGgdmsTC6gJcetOMg7TN74+cpnLs0CUdKBD4uT9Y4rQRwzO3DhfFFNPdNE2laMAkaMQbulZFnDWs5GK+vnySiMkVLmCACTWV2VOfGBokaBQG7kszqONF9nZUq3nogFqJep+6/khuDc51jEJVwbI8zrovydpsR+1IiUbXJjJ0fXdNiztJC4FkW3Tt7rULNDAVNlLUA8TvjjUHwD67OgHu1B/aWy2j4YRyPdgzCPTyHh9Iig+DMtsWbYEw0YWxqKbjGzpae7kPbwIz6/WGHGXsyooAJD1iKcwi8LDasZorS61VWy4TnZTl4SU6sEftzYnHDtYQ3237F+W/HADOPI2kWdf+624vJJb86P5xlgeRaJfDTuBvfnR3A0a+GgmtbqZ4wvYhsa1jzrnixZp5y5FPW1zbfOejCqYvjGgGrEe1PbYV8vBAd1XmIiNADeg5PZ0Sr+w1dY3iv16nOD6VFI8osUL1pRVZiN6PquQx0VN4TvLxndB5RDlNTuSP82Cwhs5rm/q4DNgOH6rbLGHJ6UFfiQFKUqG5UZsSg82guPrs6TSnk1Do68+UgDAlmHC9OxA5HBCo2R2JkTkIKFe7e9Gh13LSz/RO48ud8y7P322pnKf8+Isr9kxDZRAqT14/m1j40d/yGLXRx04FMVGXGoiDerA6tgxS8fSAdZua/YodzbZD8WgqdlJ5hqolpaudPfpnA+dH5k09kxNTJLOwbgKsE+v+Yw+5iB/Zk2/Bu1wiGh1xwery3OPIUhfrS5HVrj6Va1MuZtV4aR0PzRWo7PXSbo1peKHLU8QTr8QbUJthQimNEHT48lI1IEpsjBQkbOp7oHkEMCdDioh8JlO8n77Wp63peu93K+t0jIbMk+dTu++KrZygiPhKkfwNXCVgMBPx+Lx7Pt2PblihYTGG4Rq3zaf8NpFO+U6JNmKSIvNzaS+1EkaEuCM+Ng+X5vLUvGr6+4oS9MPFkeU5c3YyHgSv/Ca4S4CmC7V/8jvauYYAVIFMHBsTSYNRrXizMTFqjRVVUPHPLeOT1bk1gmOySkibnO04/U5Za55qX6Hm5PXCVQCCgWGExqrlT+8TLBJpIRBvX6zibS4HVR0VckdjFALJ2bGqsyE+sZ+Bev3zb4CoBk57/XhQFOy8KAYh3+LIu+fiU7IQL5QWO11ws53cIHvpHFCIQIhAiwOwvAQYAgVjy36N7Pc4AAAAASUVORK5CYII='
})
// hideNavigation: true
// backdropDisabled: true
}
Expand Down
Binary file not shown.
Binary file removed core/examples/luigi-sample-angular/src/favicon.ico
Binary file not shown.
2 changes: 1 addition & 1 deletion core/examples/luigi-sample-angular/src/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Luigi</title>
<title></title>
<link rel='stylesheet' href='/luigi-core/luigi.css'>
</head>
<body>
Expand Down
5 changes: 2 additions & 3 deletions core/src/App.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,11 @@
<LeftNav pathData={navigationPath} />
</div>


<script type="text/javascript">
import Backdrop from './Backdrop.html';
import { fade } from 'svelte-transitions';
import TopNav from './TopNav.html';
import LeftNav from './LeftNav.html';
import TopNav from './navigation/TopNav.html';
import LeftNav from './navigation/LeftNav.html';
import * as Routing from './services/routing.js';
import { getConfigValue, getConfigValueFromObject } from './services/config.js';
import { isFunction } from './utilities/helpers.js';
Expand Down
5 changes: 2 additions & 3 deletions core/src/LeftNav.html → core/src/navigation/LeftNav.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,9 @@ <h1 class="fd-side-nav__title">{key}</h1>
</div>

<script type="text/javascript">
import Backdrop from './Backdrop.html';
import { getChildren, getLeftNavData } from './services/navigation.js';
import { handleRouteClick } from './services/routing.js';
import { getConfigValue, getConfigBooleanValue } from './services/config.js';
import { handleRouteClick } from '../services/routing.js';
import { getConfigValue, getConfigBooleanValue } from '../services/config.js';

const setLeftNavData = async (current, component) => {
const componentData = component.get();
Expand Down
46 changes: 46 additions & 0 deletions core/src/navigation/LogoTitle.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@


{#if hasLogo}
<div on:click="goTo('/')" class="fd-global-nav__logo fd-has-margin-left-none" ref:logo></div>
{/if}
<div class="fd-global-nav__product-name">
{#if title}
<a on:click="goTo('/')">{title}</a>
{/if}
</div>

<script type="text/javascript">
import { processHeaderSettings } from './services/header.js';
import { getConfigBooleanValue } from '../services/config.js';

export default {
oncreate() {
processHeaderSettings(this);
},
methods: {
goTo(path) {
if (getConfigBooleanValue('routing.useHashRouting')) {
window.location.hash = path;
} else {
window.location.pathname = path;
}
}
}
};
</script>

<style type="text/scss">
@import 'node_modules/fundamental-ui/scss/components/global-nav';
.fd-global-nav__logo {
cursor: pointer;
background-image: none;
margin-left: 0px;
}
.fd-global-nav__product-name {
color: #0a6ed1;
height: 50px;
font-size: 24px;
line-height: 1em;
padding: 13px 0;
}
</style>
34 changes: 11 additions & 23 deletions core/src/TopNav.html → core/src/navigation/TopNav.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
<div class="fd-ui__header {hideNavComponent ? 'hideNavComponent' : ''}">
<nav class="fd-global-nav">
<div class="fd-global-nav__group fd-global-nav__group--left">
<div class="fd-global-nav__logo fd-has-margin-left-none"></div>
<div class="fd-global-nav__product-name">
<a on:click="goTo('/overview')">LUIGI</a>
</div>
<LogoTitle />
</div>

<div class="fd-global-nav__group fd-global-nav__group--right">
<div class="fd-global-nav__actions">
{#if children && pathData.length > 0}
Expand Down Expand Up @@ -45,10 +43,10 @@
</div>

<script type="text/javascript">
import Backdrop from './Backdrop.html';
import Authorization from './Authorization.html';
import { handleRouteClick } from './services/routing.js';
import { getConfigValue, getConfigBooleanValue } from './services/config.js';
import LogoTitle from './LogoTitle.html';
import Authorization from '../Authorization.html';
import { handleRouteClick } from '../services/routing.js';
import { getConfigValue, getConfigValueAsync, getConfigBooleanValue } from '../services/config.js';

const getNegatedBoolString = str => {
return str === 'true' ? 'false' : 'true';
Expand Down Expand Up @@ -107,13 +105,6 @@
},
methods: {
handleClick: node => handleRouteClick(node),
goTo(path) {
if (getConfigBooleanValue('routing.useHashRouting')) {
window.location.hash = path;
} else {
window.location.pathname = path;
}
},
toggleDropdownState(name) {
const dropDownStates = this.get().dropDownStates || {};
dropDownStates[name] = getNegatedBoolString(dropDownStates[name]);
Expand All @@ -130,7 +121,8 @@
}
},
components: {
Authorization
Authorization,
LogoTitle
}
};
</script>
Expand Down Expand Up @@ -158,18 +150,14 @@
border-bottom: solid 1px #dededf;
padding: 0 10px;
}
.fd-global-nav__group--left {
padding-left: 20px;
}

.fd-global-nav {
height: 100%;
border: none;
}
.fd-global-nav__product-name {
color: #0a6ed1;
height: 50px;
font-size: 24px;
line-height: 1em;
padding: 13px 0;
}

.hideNavComponent {
display: none;
Expand Down
42 changes: 42 additions & 0 deletions core/src/navigation/services/header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
getConfigValueAsync,
getConfigBooleanValue
} from '../../services/config.js';

export const processHeaderSettings = component => {
return getConfigValueAsync('settings.header').then(header => {
if (!header) {
return;
}

// Set Title and Logo
if (header.title) {
component.set({ title: header.title });
document.title = header.title;
}

const hasLogo = Boolean(header.logo);
component.set({ hasLogo });
if (hasLogo) {
component.refs.logo.style.backgroundImage = 'url(' + header.logo + ')';
}

// Set Favicon
if (header.favicon) {
const isInvalidFaviconFormat =
!header.favicon.split('?')[0].endsWith('.ico') &&
!header.favicon.startsWith('data:image');
if (isInvalidFaviconFormat) {
console.warn(
'Favicon is not an .ico filetype and might get displayed wrong.'
);
}
const link = Object.assign(document.createElement('link'), {
type: 'image/x-icon',
rel: 'shortcut icon',
href: header.favicon
});
document.getElementsByTagName('head')[0].appendChild(link);
}
});
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getConfigValue, getConfigValueFromObjectAsync } from './config';
import { getConfigValue, getConfigValueFromObjectAsync } from '../../services/config';

const isNodeAccessPermitted = (
nodeToCheckPermissionFor,
Expand Down
2 changes: 1 addition & 1 deletion core/src/services/routing.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getNavigationPath } from './navigation';
import { getNavigationPath } from '../navigation/services/navigation';
import {
getConfigValue,
getConfigValueAsync,
Expand Down
126 changes: 126 additions & 0 deletions core/test/header.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
const headerService = require('../src/navigation/services/header');
const assert = require('chai').assert;
const sinon = require('sinon');

describe('LogoTitle', function() {
afterEach(() => {
sinon.restore();
});

describe('processHeaderSettings()', function() {
let component;
const setHeaderSettings = headerSettings => {
window.Luigi.config = {
settings: {
header: Object.assign({}, headerSettings)
}
};
};

beforeEach(() => {
component = {
set: sinon.spy()
};
});

it('should not fail for undefined arguments', async () => {
window.Luigi.config = {};
await headerService.processHeaderSettings();
});

it('should resolve title', async () => {
// given
const headerSettings = {
title: 'Luigi Demo'
};
setHeaderSettings(headerSettings);

document.title = '';

// when
await headerService.processHeaderSettings(component);

// then
assert.equal(document.title, headerSettings.title, 'document title');
assert(
component.set.calledWith({ title: headerSettings.title }),
'component set() title'
);
});

it('should hide logo if not defined', async () => {
setHeaderSettings({});

// when
await headerService.processHeaderSettings(component);

// then
assert(
component.set.calledWith({ hasLogo: false }),
'component set() hasLogo false'
);
});

it('should resolve logo and set hasLogo', async () => {
// given
const headerSettings = {
logo: 'data:image/svg+xml;base64,XXX='
};
setHeaderSettings(headerSettings);

component.refs = {
logo: {
style: {
backgroundImage: null
}
}
};

// when
await headerService.processHeaderSettings(component);

// then
assert.equal(
component.refs.logo.style.backgroundImage,
'url(' + headerSettings.logo + ')',
'backgroundImage logo'
);
assert(
component.set.calledOnceWith({ hasLogo: true }),
'component set() hasLogo'
);
});

it('should resolve favicon', async () => {
// given
const headerSettings = {
favicon: '/assets/favicon.ico'
};
setHeaderSettings(headerSettings);

sinon.stub(document, 'createElement').returns({});
const appendChild = sinon.spy();
sinon.stub(document, 'getElementsByTagName').returns([{ appendChild }]);

const expectedLink = {
type: 'image/x-icon',
rel: 'shortcut icon',
href: headerSettings.favicon
};

// when
await headerService.processHeaderSettings(component);

// then
assert(
document.createElement.calledOnce,
'document.createElement() call'
);
assert(
document.getElementsByTagName.calledOnceWith('head'),
'document.getElementsByTagName() call'
);
assert(appendChild.calledOnceWith(expectedLink), 'appendChild() call');
});
});
});
2 changes: 1 addition & 1 deletion core/test/navigation.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const navigation = require('../src/services/navigation');
const navigation = require('../src/navigation/services/navigation');
const chai = require('chai');
const expect = chai.expect;
const assert = chai.assert;
Expand Down
13 changes: 10 additions & 3 deletions docs/general-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ The configuration file also contains a section called **Settings** in which you
````
settings: {
hideNavigation: false
backdropDisabled : false
backdropDisabled : false,
header: { object / function / Promise
logo: 'path/to/image.png',
title: 'Luigi Demo',
favicon: 'path/to/favicon.ico'
}
}
````
**hideNavigation** disables Luigi's default out-of-the-box navigation when set to `true`. Subsequently, neither the left nor top navigation are visible in your application. You are then free to provide your own navigation UI. By default, the navigation is enabled. If you want to keep it, set the value to `false` or leave this option unconfigured.

**backdropDisabled** prevents the backdrop layer from covering the top and left navigation when showing modal windows, when set to `true`. By default, the backdrop is enabled and covers the top and left navigation when showing modal windows. To keep the default configuration, set the value to `false` or leave this option unconfigured.
**backdropDisabled** When set to `true`, prevents the backdrop layer from covering the top and left navigation when showing modal windows. By default, the backdrop is enabled and covers the top and left navigation when showing modal windows. To keep the default configuration, set the value to `false` or leave this option unconfigured.
**header.logo** defines the top left navigation logo. It has a fixed height of 28px.
**header.title** defines the top left navigation title.
**header.favicon** defines the favicon. Requires a standard favicon file with the `.ico` extension, and 16x16px or 32x32px dimensions.

0 comments on commit b3c41cb

Please sign in to comment.