diff --git a/src/__tests__/electron.js b/src/__tests__/electron.js index 747c6af3..b42fd813 100644 --- a/src/__tests__/electron.js +++ b/src/__tests__/electron.js @@ -33,6 +33,7 @@ const mockBrowserWindowInstance = () => { setFullScreen: jest.fn(), webContents: { loadedUrl: '', + browserWindowInstance: () => instance, copy: jest.fn(), copyImageAt: jest.fn(), cut: jest.fn(), @@ -68,6 +69,9 @@ const mockElectronInstance = ({...overriddenProps} = {}) => { getPath: jest.fn(), setPath: jest.fn() }, + contextBridge: { + exposeInMainWorld: jest.fn() + }, globalShortcut: { listeners: {}, register: jest.fn((accelerator, callback) => { @@ -87,6 +91,9 @@ const mockElectronInstance = ({...overriddenProps} = {}) => { delete instance.ipcMain.listeners[eventName]; }) }, + ipcRenderer: { + send: jest.fn() + }, nativeTheme: {}, session: { fromPartition: jest.fn(() => ({ @@ -96,6 +103,7 @@ const mockElectronInstance = ({...overriddenProps} = {}) => { }, ...overriddenProps }; + instance.BrowserWindow.fromWebContents = jest.fn(webContents => webContents.browserWindowInstance()); return instance; }; diff --git a/src/about/__tests__/about.browser.test.mjs b/src/about/__tests__/about.browser.test.mjs new file mode 100644 index 00000000..2cd29847 --- /dev/null +++ b/src/about/__tests__/about.browser.test.mjs @@ -0,0 +1,49 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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 {jest} from '@jest/globals'; +import {loadDOM} from '../../__tests__/index.mjs'; +import {fireEvent} from '@testing-library/dom'; + +describe('About in Browser test suite', () => { + beforeEach(async () => { + jest.resetModules(); + window.electron = { + close: jest.fn(), + versions: {electron: '1.33.7', chrome: '1337', node: '42', v8: '13.37'} + }; + await loadDOM({meta: import.meta, path: ['..', 'index.html']}); + }); + test('close, click should close dialog', () => { + // When + fireEvent.click(document.querySelector('.top-app-bar .leading-navigation-icon')); + // Then + expect(window.electron.close).toHaveBeenCalledTimes(1); + }); + test.each([ + {label: 'Electron', expectedVersion: '1.33.7'}, + {label: 'Chromium', expectedVersion: '1337'}, + {label: 'Node', expectedVersion: '42'}, + {label: 'V8', expectedVersion: '13.37'} + ])('Should display $label with version $expectedVersion', ({label, expectedVersion}) => { + const versions = Array.from(document.querySelectorAll('.about-content__version')) + .map(v => ({ + label: v.querySelector('.version__component').textContent, + version: v.querySelector('.version__value').textContent + })); + expect(versions).toContainEqual({label, version: expectedVersion}); + }); +}); + diff --git a/src/about/__tests__/index.html.test.mjs b/src/about/__tests__/index.html.test.mjs new file mode 100644 index 00000000..7559a8e4 --- /dev/null +++ b/src/about/__tests__/index.html.test.mjs @@ -0,0 +1,33 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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 {jest} from '@jest/globals'; +import {loadDOM} from '../../__tests__/index.mjs'; + +describe('About index.html test suite', () => { + beforeEach(async () => { + jest.resetModules(); + window.electron = { + close: jest.fn(), + versions: {} + }; + await loadDOM({meta: import.meta, path: ['..', 'index.html']}); + }); + test('loads required styles', () => { + expect(Array.from(document.querySelectorAll('link[rel=stylesheet]')) + .map(link => link.getAttribute('href'))) + .toEqual(['./about.browser.css']); + }); +}); diff --git a/src/about/__tests__/index.test.js b/src/about/__tests__/index.test.js new file mode 100644 index 00000000..ec4401c2 --- /dev/null +++ b/src/about/__tests__/index.test.js @@ -0,0 +1,67 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ +describe('About module test suite', () => { + let electron; + let sender; + let about; + beforeEach(() => { + jest.resetModules(); + jest.mock('electron', () => require('../../__tests__').mockElectronInstance()); + electron = require('electron'); + sender = electron.browserWindowInstance.webContents; + about = require('../'); + }); + describe('openAboutDialog', () => { + describe('webPreferences', () => { + test('is sandboxed', () => { + // When + about.openAboutDialog({sender}); + // Then + const BrowserView = electron.BrowserView; + expect(BrowserView).toHaveBeenCalledTimes(1); + expect(BrowserView).toHaveBeenCalledWith({ + webPreferences: expect.objectContaining({sandbox: true, nodeIntegration: false}) + }); + }); + test('has no node integration', () => { + // When + about.openAboutDialog({sender}); + // Then + expect(electron.BrowserView).toHaveBeenCalledWith({ + webPreferences: expect.objectContaining({nodeIntegration: false}) + }); + }); + test('has context isolation', () => { + // When + about.openAboutDialog({sender}); + // Then + expect(electron.BrowserView).toHaveBeenCalledWith({ + webPreferences: expect.objectContaining({contextIsolation: true}) + }); + }); + }); + test('should open dialog and add event listeners', () => { + // When + about.openAboutDialog({sender: electron.browserWindowInstance.webContents}); + // Then + expect(electron.browserViewInstance.webContents.loadURL).toHaveBeenCalledTimes(1); + expect(electron.browserViewInstance.webContents.loadURL) + .toHaveBeenCalledWith(expect.stringMatching(/.+?\/index.html$/)); // NOSONAR + expect(electron.browserViewInstance.webContents.on).toHaveBeenCalledWith('will-navigate', expect.any(Function)); + }); + }); +}); + diff --git a/src/about/__tests__/preload.test.js b/src/about/__tests__/preload.test.js new file mode 100644 index 00000000..6a501582 --- /dev/null +++ b/src/about/__tests__/preload.test.js @@ -0,0 +1,53 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ +describe('About Module preload test suite', () => { + let electron; + beforeEach(() => { + jest.resetModules(); + jest.mock('electron', () => require('../../__tests__').mockElectronInstance()); + electron = require('electron'); + }); + describe('preload (just for coverage and sanity, see bundle tests)', () => { + beforeEach(() => { + global.APP_EVENTS = require('../../constants').APP_EVENTS; + require('../preload'); + }); + describe('creates an API', () => { + test('with entries', () => { + expect(electron.contextBridge.exposeInMainWorld).toHaveBeenCalledWith('electron', { + close: expect.toBeFunction(), + versions: expect.toBeObject() + }); + }); + test('with close function', () => { + const electronApi = electron.contextBridge.exposeInMainWorld.mock.calls[0][1]; + electronApi.close(); + expect(electron.ipcRenderer.send).toHaveBeenCalledWith('closeDialog'); + }); + }); + }); + describe('preload.bundle', () => { + beforeEach(() => { + require('../../../bundles/about.preload'); + }); + test('creates an API', () => { + expect(electron.contextBridge.exposeInMainWorld).toHaveBeenCalledWith('electron', { + close: expect.toBeFunction(), + versions: expect.toBeObject() + }); + }); + }); +}); diff --git a/src/about/about.browser.css b/src/about/about.browser.css new file mode 100644 index 00000000..42f3a90a --- /dev/null +++ b/src/about/about.browser.css @@ -0,0 +1,40 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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 '../styles/main.css'; + +:root { + overflow: auto; +} + +body { + margin: 0; + padding: 0; +} + +.about-content { + padding: 16px; +} +.about-content .about-content__release { + margin: 8px 0; +} +.about-content .version__component { + font-weight: var(--md-ref-typeface-weight-medium); + margin-right: 8px; +} +.about-content .version__value { + font-family: monospace; + color: var(--md-sys-color-on-surface-variant); +} diff --git a/src/about/about.browser.mjs b/src/about/about.browser.mjs new file mode 100644 index 00000000..03ed1aa0 --- /dev/null +++ b/src/about/about.browser.mjs @@ -0,0 +1,63 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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 {ELECTRONIM_VERSION, html, render, Card, TopAppBar} from '../components/index.mjs'; + +const {close, versions} = window.electron; + +const TWITTER_LINK = 'https://twitter.com/share?url=https://github.com/manusa/electronim&text=I%27m%20using%20ElectronIM%20as%20my%20communications%20center%20and%20I%20love%20it%2C%20you%20should%20try%20it%20out%20too%21'; + +const getAppMenu = () => document.querySelector('.about-root'); + +const Version = ({component, value}) => html` +
+ ${component} + ${value} +
+`; + +const About = () => html` + <${TopAppBar} icon='\uE5C4' iconClick=${close} headline='About ElectronIM'/> +
+ <${Card} headline=${`ElectronIM ${ELECTRONIM_VERSION}`}> +
+ + Release Notes + - + Apache License, Version 2.0 + +
+
+ <${Version} component='Electron' value=${versions.electron} /> + <${Version} component='Chromium' value=${versions.chrome} /> + <${Version} component='Node' value=${versions.node} /> + <${Version} component='V8' value=${versions.v8} /> +
+ <${Card.Divider}/> +
+

+ Do you enjoy using ElectronIM? Help us spread the word by + sharing it with your friends! +

+

+ A GitHub star also goes a long way to help us grow the + project! +

+
+ +
+`; + +render(html`<${About} />`, getAppMenu()); diff --git a/src/about/index.html b/src/about/index.html new file mode 100644 index 00000000..f75e7eb6 --- /dev/null +++ b/src/about/index.html @@ -0,0 +1,29 @@ + + + + + + + + ElectronIM About + + + +
+ + + diff --git a/src/about/index.js b/src/about/index.js new file mode 100644 index 00000000..ecae9495 --- /dev/null +++ b/src/about/index.js @@ -0,0 +1,37 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ +const {BrowserView, BrowserWindow} = require('electron'); +const path = require('path'); +const {handleRedirect} = require('../tab-manager/redirect'); +const {showDialog} = require('../browser-window'); + +const webPreferences = { + contextIsolation: true, + nodeIntegration: false, + sandbox: true, + preload: path.resolve(__dirname, '..', '..', 'bundles', 'about.preload.js') +}; + +const openAboutDialog = event => { + const aboutView = new BrowserView({webPreferences}); + aboutView.webContents.loadURL(`file://${__dirname}/index.html`); + const handleRedirectForCurrentUrl = handleRedirect(aboutView); + aboutView.webContents.on('will-navigate', handleRedirectForCurrentUrl); + aboutView.webContents.on('new-window', handleRedirectForCurrentUrl); + showDialog(BrowserWindow.fromWebContents(event.sender), aboutView); +}; + +module.exports = {openAboutDialog}; diff --git a/src/about/preload.js b/src/about/preload.js new file mode 100644 index 00000000..acedec8e --- /dev/null +++ b/src/about/preload.js @@ -0,0 +1,22 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ +/* eslint-disable no-undef */ +const {contextBridge, ipcRenderer} = require('electron'); + +contextBridge.exposeInMainWorld('electron', { + close: () => ipcRenderer.send(APP_EVENTS.closeDialog), + versions: {...process.versions} +}); diff --git a/src/app-menu/__tests__/app-menu.browser.test.mjs b/src/app-menu/__tests__/app-menu.browser.test.mjs index d6812a96..f043cc2d 100644 --- a/src/app-menu/__tests__/app-menu.browser.test.mjs +++ b/src/app-menu/__tests__/app-menu.browser.test.mjs @@ -21,6 +21,7 @@ describe('App Menu in Browser test suite', () => { beforeEach(async () => { jest.resetModules(); window.electron = { + aboutOpenDialog: jest.fn(), close: jest.fn(), helpOpenDialog: jest.fn(), settingsOpenDialog: jest.fn() @@ -33,46 +34,36 @@ describe('App Menu in Browser test suite', () => { // Then expect(window.electron.close).toHaveBeenCalledTimes(1); }); - describe('Settings entry', () => { - let settings; + describe.each([ + {testId: 'about', icon: 'fas fa-info-circle', label: 'About', expectedFunction: 'aboutOpenDialog'}, + {testId: 'help', icon: 'fas fa-question-circle', label: 'Help', expectedFunction: 'helpOpenDialog'}, + {testId: 'settings', icon: 'fas fa-cog', label: 'Settings', expectedFunction: 'settingsOpenDialog'} + ])('$label entry', ({testId, icon, label, expectedFunction}) => { + let entry; beforeEach(() => { - settings = getByTestId(document, 'settings-menu-entry'); + entry = getByTestId(document, `${testId}-menu-entry`); }); test('should be visible', () => { - expect(settings.getAttribute('class')) + expect(entry.getAttribute('class')) .toMatch(/^dropdown-item*/); - expect(settings.textContent).toBe('Settings'); + expect(entry.textContent).toBe(label); }); - test('click, should open settings dialog', () => { + test(`should have icon ${icon}`, () => { + expect(entry.querySelector('i').getAttribute('class')).toBe(icon); + }); + test('click, should invoke function', () => { // When - fireEvent.click(settings); + fireEvent.click(entry); // Then - expect(window.electron.settingsOpenDialog).toHaveBeenCalledTimes(1); + expect(window.electron[expectedFunction]).toHaveBeenCalledTimes(1); }); test('hover, should activate entry', async () => { // When - fireEvent.mouseOver(settings); + fireEvent.mouseOver(entry); // Then - await waitFor(() => expect(settings.getAttribute('class')) + await waitFor(() => expect(entry.getAttribute('class')) .toContain('is-active')); }); }); - describe('Help entry', () => { - let help; - beforeEach(() => { - help = getByTestId(document, 'help-menu-entry'); - }); - test('should be visible', () => { - expect(help.getAttribute('class')) - .toMatch(/^dropdown-item*/); - expect(help.textContent).toBe('Help'); - }); - test('click, should open settings dialog', () => { - // When - fireEvent.click(help); - // Then - expect(window.electron.helpOpenDialog).toHaveBeenCalledTimes(1); - }); - }); }); diff --git a/src/app-menu/__tests__/preload.test.js b/src/app-menu/__tests__/preload.test.js index 66b57b9a..9722081e 100644 --- a/src/app-menu/__tests__/preload.test.js +++ b/src/app-menu/__tests__/preload.test.js @@ -17,10 +17,7 @@ describe('App Menu Module preload test suite', () => { let electron; beforeEach(() => { jest.resetModules(); - jest.mock('electron', () => ({ - contextBridge: {exposeInMainWorld: jest.fn()}, - ipcRenderer: {send: jest.fn()} - })); + jest.mock('electron', () => require('../../__tests__').mockElectronInstance()); electron = require('electron'); }); describe('preload (just for coverage and sanity, see bundle tests)', () => { @@ -30,6 +27,7 @@ describe('App Menu Module preload test suite', () => { }); test('creates an API', () => { expect(electron.contextBridge.exposeInMainWorld).toHaveBeenCalledWith('electron', { + aboutOpenDialog: expect.toBeFunction(), close: expect.toBeFunction(), helpOpenDialog: expect.toBeFunction(), settingsOpenDialog: expect.toBeFunction() @@ -40,17 +38,14 @@ describe('App Menu Module preload test suite', () => { beforeEach(() => { api = electron.contextBridge.exposeInMainWorld.mock.calls[0][1]; }); - test('close, invokes appMenuClose', () => { - api.close(); - expect(electron.ipcRenderer.send).toHaveBeenCalledWith('appMenuClose'); - }); - test('helpOpenDialog, invokes helpOpenDialog', () => { - api.helpOpenDialog(); - expect(electron.ipcRenderer.send).toHaveBeenCalledWith('helpOpenDialog'); - }); - test('settingsOpenDialog, invokes settingsOpenDialog', () => { - api.settingsOpenDialog(); - expect(electron.ipcRenderer.send).toHaveBeenCalledWith('settingsOpenDialog'); + test.each([ + ['aboutOpenDialog', 'aboutOpenDialog'], + ['close', 'appMenuClose'], + ['helpOpenDialog', 'helpOpenDialog'], + ['settingsOpenDialog', 'settingsOpenDialog'] + ])('%s invokes %s', (apiMethod, event) => { + api[apiMethod](); + expect(electron.ipcRenderer.send).toHaveBeenCalledWith(event); }); }); }); @@ -60,6 +55,7 @@ describe('App Menu Module preload test suite', () => { }); test('creates an API', () => { expect(electron.contextBridge.exposeInMainWorld).toHaveBeenCalledWith('electron', { + aboutOpenDialog: expect.toBeFunction(), close: expect.toBeFunction(), helpOpenDialog: expect.toBeFunction(), settingsOpenDialog: expect.toBeFunction() diff --git a/src/app-menu/app-menu.browser.mjs b/src/app-menu/app-menu.browser.mjs index edd3c446..dbf822f5 100644 --- a/src/app-menu/app-menu.browser.mjs +++ b/src/app-menu/app-menu.browser.mjs @@ -15,10 +15,16 @@ */ import {html, render, DropDown, Icon} from '../components/index.mjs'; -const {close, helpOpenDialog, settingsOpenDialog} = window.electron; +const {aboutOpenDialog, close, helpOpenDialog, settingsOpenDialog} = window.electron; const getAppMenu = () => document.querySelector('.app-menu'); +const MenuItem = ({icon, label, onClick, testId}) => html` + <${DropDown.Item} data-testid=${testId} onClick=${onClick}> + <${Icon} icon=${icon}>${label} + +`; + const AppMenu = () => { const noBubbling = func => e => { e.preventDefault(); @@ -30,12 +36,12 @@ const AppMenu = () => {
<${DropDown} active=${true}> <${DropDown.Menu}> - <${DropDown.Item} data-testid='help-menu-entry' onClick=${noBubbling(helpOpenDialog)}> - <${Icon} icon='fas fa-question-circle'>Help - - <${DropDown.Item} data-testid='settings-menu-entry' onClick=${noBubbling(settingsOpenDialog)}> - <${Icon} icon='fas fa-cog'>Settings - + <${MenuItem} icon='fas fa-question-circle' label='Help' testId='help-menu-entry' + onClick=${noBubbling(helpOpenDialog)} /> + <${MenuItem} icon='fas fa-cog' label='Settings' testId='settings-menu-entry' + onClick=${noBubbling(settingsOpenDialog)} /> + <${MenuItem} icon='fas fa-info-circle' label='About' testId='about-menu-entry' + onClick=${noBubbling(aboutOpenDialog)} />
diff --git a/src/app-menu/preload.js b/src/app-menu/preload.js index 764db475..f7f2467a 100644 --- a/src/app-menu/preload.js +++ b/src/app-menu/preload.js @@ -18,6 +18,7 @@ const {contextBridge, ipcRenderer} = require('electron'); contextBridge.exposeInMainWorld('electron', { close: () => ipcRenderer.send(APP_EVENTS.appMenuClose), + aboutOpenDialog: () => ipcRenderer.send(APP_EVENTS.aboutOpenDialog), helpOpenDialog: () => ipcRenderer.send(APP_EVENTS.helpOpenDialog), settingsOpenDialog: () => ipcRenderer.send(APP_EVENTS.settingsOpenDialog) }); diff --git a/src/components/card.mjs b/src/components/card.mjs new file mode 100644 index 00000000..33107944 --- /dev/null +++ b/src/components/card.mjs @@ -0,0 +1,37 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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 {html} from './index.mjs'; + +/** + A card based on Material design (3) guidelines. + */ +export const Card = ({ + headline, + subHeadline, + children +}) => html` +
+
+
${headline}
+ ${subHeadline && html`
${subHeadline}
`} +
${children}
+
+
+`; + +Card.Divider = () => html` +
+`; diff --git a/src/components/index.mjs b/src/components/index.mjs index e6a3508f..3d3649ea 100644 --- a/src/components/index.mjs +++ b/src/components/index.mjs @@ -16,8 +16,9 @@ export {APP_EVENTS, ELECTRONIM_VERSION} from '../../bundles/constants.mjs'; export {html, render, useLayoutEffect, useReducer, useState} from '../../bundles/preact.mjs'; +export {Card} from './card.mjs'; export {Checkbox, Field, HorizontalField, Select, sizes} from './form/index.mjs'; export {DropDown} from './drop-down.mjs'; export {Icon} from './icon.mjs'; export {Panel} from './panel.mjs'; -export {TopBar} from './top-bar.mjs'; +export {TopBar, TopAppBar} from './top-bar.mjs'; diff --git a/src/components/top-bar.mjs b/src/components/top-bar.mjs index af5f6ea8..1cfc16b4 100644 --- a/src/components/top-bar.mjs +++ b/src/components/top-bar.mjs @@ -38,3 +38,23 @@ export const TopBar = ({ `; }; + +/** + * A top app bar based on Material design (3) guidelines. + * + * @param icon represented using a Material Icon codepoint. + * @param iconClick callback function to be executed when the icon is clicked. + * @param headline the headline of the app bar. + * @see https://m3.material.io/components/top-app-bar + */ +export const TopAppBar = ({ + icon, + iconClick = () => {}, + headline +}) => html` +
+
${icon}
+
${headline}
+
+
+`; diff --git a/src/constants/index.js b/src/constants/index.js index 4b7428ca..906f95e3 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -25,6 +25,7 @@ const findRootDir = () => { }; const APP_EVENTS = { + aboutOpenDialog: 'aboutOpenDialog', activateTab: 'activateTab', activateTabInContainer: 'activateTabInContainer', addTabs: 'addTabs', diff --git a/src/help/__tests__/index.test.js b/src/help/__tests__/index.test.js index f3a83f03..1fc549a8 100644 --- a/src/help/__tests__/index.test.js +++ b/src/help/__tests__/index.test.js @@ -14,30 +14,23 @@ limitations under the License. */ describe('Help module test suite', () => { - let mockBrowserView; + let electron; + let sender; let help; beforeEach(() => { jest.resetModules(); - mockBrowserView = require('../../__tests__').mockBrowserWindowInstance(); - jest.mock('electron', () => ({ - BrowserView: jest.fn(() => mockBrowserView) - })); + jest.mock('electron', () => require('../../__tests__').mockElectronInstance()); + electron = require('electron'); + sender = electron.browserWindowInstance.webContents; help = require('../'); }); describe('openHelpDialog', () => { - let mainWindow; - beforeEach(() => { - mainWindow = { - getContentBounds: jest.fn(() => ({width: 13, height: 37})), - setBrowserView: jest.fn() - }; - }); describe('webPreferences', () => { test('is sandboxed', () => { // When - help.openHelpDialog(mainWindow)(); + help.openHelpDialog({sender}); // Then - const BrowserView = require('electron').BrowserView; + const BrowserView = electron.BrowserView; expect(BrowserView).toHaveBeenCalledTimes(1); expect(BrowserView).toHaveBeenCalledWith({ webPreferences: expect.objectContaining({sandbox: true, nodeIntegration: false}) @@ -45,28 +38,29 @@ describe('Help module test suite', () => { }); test('has no node integration', () => { // When - help.openHelpDialog(mainWindow)(); + help.openHelpDialog({sender}); // Then - expect(require('electron').BrowserView).toHaveBeenCalledWith({ + expect(electron.BrowserView).toHaveBeenCalledWith({ webPreferences: expect.objectContaining({nodeIntegration: false}) }); }); test('has context isolation', () => { // When - help.openHelpDialog(mainWindow)(); + help.openHelpDialog({sender}); // Then - expect(require('electron').BrowserView).toHaveBeenCalledWith({ + expect(electron.BrowserView).toHaveBeenCalledWith({ webPreferences: expect.objectContaining({contextIsolation: true}) }); }); }); test('should open dialog and add event listeners', () => { // When - help.openHelpDialog(mainWindow)(); + help.openHelpDialog({sender: electron.browserWindowInstance.webContents}); // Then - expect(mockBrowserView.webContents.loadURL).toHaveBeenCalledTimes(1); - expect(mockBrowserView.webContents.loadURL).toHaveBeenCalledWith(expect.stringMatching(/.+?\/index.html$/)); - expect(mockBrowserView.webContents.on).toHaveBeenCalledWith('will-navigate', expect.any(Function)); + expect(electron.browserViewInstance.webContents.loadURL).toHaveBeenCalledTimes(1); + expect(electron.browserViewInstance.webContents.loadURL) + .toHaveBeenCalledWith(expect.stringMatching(/.+?\/index.html$/)); // NOSONAR + expect(electron.browserViewInstance.webContents.on).toHaveBeenCalledWith('will-navigate', expect.any(Function)); }); }); }); diff --git a/src/help/__tests__/preload.test.js b/src/help/__tests__/preload.test.js index 9df51158..14d2b66d 100644 --- a/src/help/__tests__/preload.test.js +++ b/src/help/__tests__/preload.test.js @@ -17,10 +17,7 @@ describe('Help Module preload test suite', () => { let electron; beforeEach(() => { jest.resetModules(); - jest.mock('electron', () => ({ - contextBridge: {exposeInMainWorld: jest.fn()}, - ipcRenderer: {send: jest.fn()} - })); + jest.mock('electron', () => require('../../__tests__').mockElectronInstance()); electron = require('electron'); window.APP_EVENTS = require('../../constants').APP_EVENTS; }); diff --git a/src/help/index.js b/src/help/index.js index 43fe7c1f..b37e22c1 100644 --- a/src/help/index.js +++ b/src/help/index.js @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -const {BrowserView} = require('electron'); +const {BrowserView, BrowserWindow} = require('electron'); const path = require('path'); const {handleRedirect} = require('../tab-manager/redirect'); const {showDialog} = require('../browser-window'); @@ -25,13 +25,13 @@ const webPreferences = { preload: path.resolve(__dirname, '..', '..', 'bundles', 'help.preload.js') }; -const openHelpDialog = mainWindow => () => { +const openHelpDialog = event => { const helpView = new BrowserView({webPreferences}); helpView.webContents.loadURL(`file://${__dirname}/index.html`); const handleRedirectForCurrentUrl = handleRedirect(helpView); helpView.webContents.on('will-navigate', handleRedirectForCurrentUrl); helpView.webContents.on('new-window', handleRedirectForCurrentUrl); - showDialog(mainWindow, helpView); + showDialog(BrowserWindow.fromWebContents(event.sender), helpView); }; module.exports = {openHelpDialog}; diff --git a/src/main/__tests__/global-listeners.test.js b/src/main/__tests__/global-listeners.test.js index 546edc99..e35cbd93 100644 --- a/src/main/__tests__/global-listeners.test.js +++ b/src/main/__tests__/global-listeners.test.js @@ -50,13 +50,25 @@ describe('Main :: Global listeners module test suite', () => { x: 0, y: 0 })); }); - test('appMenuClose, should hide app-menu', () => { - // When - eventBus.listeners.appMenuClose(); - // Then - expect(browserWindow.removeBrowserView).toHaveBeenCalledWith( - expect.objectContaining({isAppMenu: true}) - ); + describe('appMenuClose', () => { + test('with menu hidden, should return', () => { + // Given + browserWindow.getBrowserViews = jest.fn(() => []); + // When + eventBus.listeners.appMenuClose(); + // Then + expect(browserWindow.removeBrowserView).not.toHaveBeenCalled(); + }); + test('with menu visible, should hide app-menu', () => { + // Given + browserWindow.getBrowserViews = jest.fn(() => [{isAppMenu: true}]); + // When + eventBus.listeners.appMenuClose(); + // Then + expect(browserWindow.removeBrowserView).toHaveBeenCalledWith( + expect.objectContaining({isAppMenu: true}) + ); + }); }); describe('closeDialog', () => { describe('with dialog visible (<= 1 view)', () => { @@ -121,7 +133,7 @@ describe('Main :: Global listeners module test suite', () => { }); test('helpOpenDialog, should open help dialog', () => { // When - eventBus.listeners.helpOpenDialog(); + eventBus.listeners.helpOpenDialog({sender: browserWindow.webContents}); // Then const browserView = electron.BrowserView.mock.results .map(r => r.value).filter(bv => bv.webContents.loadedUrl.endsWith('/help/index.html'))[0]; diff --git a/src/main/index.js b/src/main/index.js index 4ca520f0..43a1e59d 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -16,6 +16,7 @@ const {BrowserWindow, Notification, app, desktopCapturer, ipcMain: eventBus, nativeTheme} = require('electron'); const {registerGlobalShortcuts} = require('./keyboard-shortcuts'); const {APP_EVENTS} = require('../constants'); +const {openAboutDialog} = require('../about'); const {newAppMenu, isNotAppMenu} = require('../app-menu'); const {TABS_CONTAINER_HEIGHT, newTabContainer, isNotTabContainer} = require('../chrome-tabs'); const {openHelpDialog} = require('../help'); @@ -164,6 +165,9 @@ const appMenuOpen = () => { }; const appMenuClose = () => { + if (!mainWindow.getBrowserViews().find(bv => bv.isAppMenu)) { + return; + } mainWindow.removeBrowserView(appMenu); activateTab(tabManager.getActiveTab()); }; @@ -194,6 +198,7 @@ const saveSettings = (_event, settings) => { }; const initGlobalListeners = () => { + eventBus.on(APP_EVENTS.aboutOpenDialog, openAboutDialog); eventBus.on(APP_EVENTS.appMenuOpen, appMenuOpen); eventBus.on(APP_EVENTS.appMenuClose, appMenuClose); eventBus.on(APP_EVENTS.closeDialog, closeDialog); @@ -201,7 +206,7 @@ const initGlobalListeners = () => { eventBus.handle(APP_EVENTS.dictionaryGetAvailableNative, getAvailableNativeDictionaries); eventBus.handle(APP_EVENTS.dictionaryGetEnabled, getEnabledDictionaries); eventBus.on(APP_EVENTS.fullscreenToggle, fullscreenToggle); - eventBus.on(APP_EVENTS.helpOpenDialog, openHelpDialog(mainWindow)); + eventBus.on(APP_EVENTS.helpOpenDialog, openHelpDialog); eventBus.handle(APP_EVENTS.settingsLoad, loadSettings); eventBus.on(APP_EVENTS.settingsOpenDialog, openSettingsDialog(mainWindow)); eventBus.on(APP_EVENTS.settingsSave, saveSettings); diff --git a/src/settings/__tests__/index.html.test.mjs b/src/settings/__tests__/index.html.test.mjs new file mode 100644 index 00000000..0c26585e --- /dev/null +++ b/src/settings/__tests__/index.html.test.mjs @@ -0,0 +1,31 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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 {jest} from '@jest/globals'; +import {loadDOM} from '../../__tests__/index.mjs'; +import {ipcRenderer} from './settings.browser.mjs'; + +describe('Settings index.html test suite', () => { + beforeEach(async () => { + jest.resetModules(); + window.ipcRenderer = ipcRenderer(); + await loadDOM({meta: import.meta, path: ['..', 'index.html']}); + }); + test('loads required styles', () => { + expect(Array.from(document.querySelectorAll('link[rel=stylesheet]')) + .map(link => link.getAttribute('href'))) + .toEqual(['./settings.browser.css']); + }); +}); diff --git a/src/settings/__tests__/preload.test.js b/src/settings/__tests__/preload.test.js index 75848635..28e981e8 100644 --- a/src/settings/__tests__/preload.test.js +++ b/src/settings/__tests__/preload.test.js @@ -13,17 +13,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -const {waitFor} = require('@testing-library/dom'); describe('Settings Module preload test suite', () => { beforeEach(() => { jest.resetModules(); + jest.mock('electron', () => require('../../__tests__').mockElectronInstance({ + ipcRenderer: 'the-ipc-renderer' + })); }); describe('preload (just for coverage and sanity, see bundle tests)', () => { beforeEach(() => { - jest.mock('../settings.browser.css', () => {}); - jest.mock('electron', () => ({ - ipcRenderer: 'the-ipc-renderer' - })); require('../preload'); }); test('adds required variables', () => { @@ -34,19 +32,8 @@ describe('Settings Module preload test suite', () => { beforeEach(() => { require('../../../bundles/settings.preload'); }); - test('loads styles in order', async () => { - // When - document.body.append(document.createElement('div')); - // Then - await waitFor(() => expect(document.head.children.length).toBeGreaterThan(0)); - const styles = Array.from(document.querySelectorAll('style')); - expect(styles).toHaveLength(10); - expect(styles[1].innerHTML).toMatch(/:root \{.+--color-accent-fg:/s); // Variables - expect(styles[2].innerHTML).toContain('html.electronim,'); // Base - expect(styles[3].innerHTML).toContain('.electronim h1,'); // Typography - expect(styles[5].innerHTML).toContain('.electronim .control .checkbox {'); // CheckBox - expect(styles[6].innerHTML).toContain('.electronim .top-bar.navbar {'); // NavBar - expect(styles[9].innerHTML).toContain('.settings.container {'); // Settings-specific + test('adds required variables', () => { + expect(window.ipcRenderer).toEqual('the-ipc-renderer'); }); }); }); diff --git a/src/settings/index.html b/src/settings/index.html index 93bea7f1..b42e572e 100644 --- a/src/settings/index.html +++ b/src/settings/index.html @@ -20,12 +20,13 @@ ElectronIM Settings +
+ - diff --git a/src/settings/preload.js b/src/settings/preload.js index e0da3531..87f33c56 100644 --- a/src/settings/preload.js +++ b/src/settings/preload.js @@ -16,6 +16,4 @@ /* eslint-disable no-undef */ const {ipcRenderer} = require('electron'); -require('./settings.browser.css'); - window.ipcRenderer = ipcRenderer; diff --git a/src/styles/base/fonts.css b/src/styles/base/fonts.css new file mode 100644 index 00000000..8ee03600 --- /dev/null +++ b/src/styles/base/fonts.css @@ -0,0 +1,45 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ +@font-face { + font-family: 'Material Symbols Outlined'; + font-style: normal; + font-weight: 400; + src: url('fonts/material-symbols-outlined-48pt.woff2') format('woff2'); +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('fonts/roboto-regular.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('fonts/roboto-medium.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('fonts/roboto-bold.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/src/styles/base/fonts/material-symbols-outlined-48pt.woff2 b/src/styles/base/fonts/material-symbols-outlined-48pt.woff2 new file mode 100644 index 00000000..462c2164 Binary files /dev/null and b/src/styles/base/fonts/material-symbols-outlined-48pt.woff2 differ diff --git a/src/styles/base/fonts/roboto-bold.woff2 b/src/styles/base/fonts/roboto-bold.woff2 new file mode 100644 index 00000000..771fbecc Binary files /dev/null and b/src/styles/base/fonts/roboto-bold.woff2 differ diff --git a/src/styles/base/fonts/roboto-medium.woff2 b/src/styles/base/fonts/roboto-medium.woff2 new file mode 100644 index 00000000..29342a8d Binary files /dev/null and b/src/styles/base/fonts/roboto-medium.woff2 differ diff --git a/src/styles/base/fonts/roboto-regular.woff2 b/src/styles/base/fonts/roboto-regular.woff2 new file mode 100644 index 00000000..020729ef Binary files /dev/null and b/src/styles/base/fonts/roboto-regular.woff2 differ diff --git a/src/styles/base/index.css b/src/styles/base/index.css index 4bc6d8ac..574c11c6 100644 --- a/src/styles/base/index.css +++ b/src/styles/base/index.css @@ -15,5 +15,7 @@ */ @import './variables.css'; @import './colors.css'; +@import './material3/index.css'; @import './base.css'; +@import './fonts.css'; @import './typography.css'; diff --git a/src/styles/base/material3/colors.css b/src/styles/base/material3/colors.css new file mode 100644 index 00000000..cc3a3e22 --- /dev/null +++ b/src/styles/base/material3/colors.css @@ -0,0 +1,182 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ + +:root { + /* https://github.com/material-foundation/material-tokens/blob/main/css/theme/light.css */ + --md-sys-color-primary: var(--md-ref-palette-primary40); + --md-sys-color-surface-tint: var(--md-sys-color-primary); + --md-sys-color-surface-tint-color: var(--md-sys-color-primary); + --md-sys-color-on-error-container: var(--md-ref-palette-error10); + --md-sys-color-on-error: var(--md-ref-palette-error100); + --md-sys-color-error-container: var(--md-ref-palette-error90); + --md-sys-color-on-tertiary-container: var(--md-ref-palette-tertiary10); + --md-sys-color-on-tertiary: var(--md-ref-palette-tertiary100); + --md-sys-color-tertiary-container: var(--md-ref-palette-tertiary90); + --md-sys-color-tertiary: var(--md-ref-palette-tertiary40); + --md-sys-color-shadow: var(--md-ref-palette-neutral0-rgb); + --md-sys-color-error: var(--md-ref-palette-error40); + --md-sys-color-outline: var(--md-ref-palette-neutral-variant50); + --md-sys-color-outline-variant: var(--md-ref-palette-neutral-variant80); + --md-sys-color-on-background: var(--md-ref-palette-neutral10); + --md-sys-color-background: var(--md-ref-palette-neutral99); + --md-sys-color-inverse-on-surface: var(--md-ref-palette-neutral95); + --md-sys-color-inverse-surface: var(--md-ref-palette-neutral20); + --md-sys-color-on-surface-variant: var(--md-ref-palette-neutral-variant30); + --md-sys-color-on-surface: var(--md-ref-palette-neutral10); + --md-sys-color-surface-variant: var(--md-ref-palette-neutral-variant90); + --md-sys-color-surface: var(--md-ref-palette-neutral99); + --md-sys-color-on-secondary-container: var(--md-ref-palette-secondary10); + --md-sys-color-on-secondary: var(--md-ref-palette-secondary100); + --md-sys-color-secondary-container: var(--md-ref-palette-secondary90); + --md-sys-color-secondary: var(--md-ref-palette-secondary40); + --md-sys-color-inverse-primary: var(--md-ref-palette-primary80); + --md-sys-color-on-primary-container: var(--md-ref-palette-primary10); + --md-sys-color-on-primary: var(--md-ref-palette-primary100); + --md-sys-color-primary-container: var(--md-ref-palette-primary90); +} + +@media (prefers-color-scheme: dark) { + :root { + /* https://github.com/material-foundation/material-tokens/blob/main/css/theme/dark.css */ + --md-sys-color-surface-tint: var(--md-sys-color-primary); + --md-sys-color-surface-tint-color: var(--md-sys-color-primary); + --md-sys-color-on-error-container: var(--md-ref-palette-error80); + --md-sys-color-on-error: var(--md-ref-palette-error20); + --md-sys-color-error-container: var(--md-ref-palette-error30); + --md-sys-color-on-tertiary-container: var(--md-ref-palette-tertiary90); + --md-sys-color-on-tertiary: var(--md-ref-palette-tertiary20); + --md-sys-color-tertiary-container: var(--md-ref-palette-tertiary30); + --md-sys-color-tertiary: var(--md-ref-palette-tertiary80); + --md-sys-color-shadow: var(--md-ref-palette-neutral0-rgb); + --md-sys-color-error: var(--md-ref-palette-error80); + --md-sys-color-outline: var(--md-ref-palette-neutral-variant60); + --md-sys-color-outline-variant: var(--md-ref-palette-neutral-variant30); + --md-sys-color-on-background: var(--md-ref-palette-neutral90); + --md-sys-color-background: var(--md-ref-palette-neutral10); + --md-sys-color-inverse-on-surface: var(--md-ref-palette-neutral20); + --md-sys-color-inverse-surface: var(--md-ref-palette-neutral90); + --md-sys-color-on-surface-variant: var(--md-ref-palette-neutral-variant80); + --md-sys-color-on-surface: var(--md-ref-palette-neutral90); + --md-sys-color-surface-variant: var(--md-ref-palette-neutral-variant30); + --md-sys-color-surface: var(--md-ref-palette-neutral10); + --md-sys-color-on-secondary-container: var(--md-ref-palette-secondary90); + --md-sys-color-on-secondary: var(--md-ref-palette-secondary20); + --md-sys-color-secondary-container: var(--md-ref-palette-secondary30); + --md-sys-color-secondary: var(--md-ref-palette-secondary80); + --md-sys-color-inverse-primary: var(--md-ref-palette-primary40); + --md-sys-color-on-primary-container: var(--md-ref-palette-primary90); + --md-sys-color-on-primary: var(--md-ref-palette-primary20); + --md-sys-color-primary-container: var(--md-ref-palette-primary30); + --md-sys-color-primary: var(--md-ref-palette-primary80); + } +} + +/* https://github.com/material-foundation/material-tokens/blob/main/css/colors.css */ +.primary { + color: var(--md-sys-color-on-primary); + background-color: var(--md-sys-color-primary); +} +.on-primary { + color: var(--md-sys-color-primary); + background-color: var(--md-sys-color-on-primary); +} +.primary-container { + color: var(--md-sys-color-on-primary-container); + background-color: var(--md-sys-color-primary-container); +} +.on-primary-container { + color: var(--md-sys-color-primary-container); + background-color: var(--md-sys-color-on-primary-container); +} +.secondary { + color: var(--md-sys-color-on-secondary); + background-color: var(--md-sys-color-secondary); +} +.on-secondary { + color: var(--md-sys-color-secondary); + background-color: var(--md-sys-color-on-secondary); +} +.secondary-container { + color: var(--md-sys-color-on-secondary-container); + background-color: var(--md-sys-color-secondary-container); +} +.on-secondary-container { + color: var(--md-sys-color-secondary-container); + background-color: var(--md-sys-color-on-secondary-container); +} +.tertiary { + color: var(--md-sys-color-on-tertiary); + background-color: var(--md-sys-color-tertiary); +} +.on-tertiary { + color: var(--md-sys-color-tertiary); + background-color: var(--md-sys-color-on-tertiary); +} +.tertiary-container { + color: var(--md-sys-color-on-tertiary-container); + background-color: var(--md-sys-color-tertiary-container); +} +.on-tertiary-container { + color: var(--md-sys-color-tertiary-container); + background-color: var(--md-sys-color-on-tertiary-container); +} +.background { + color: var(--md-sys-color-on-background); + background-color: var(--md-sys-color-background); +} +.surface { + color: var(--md-sys-color-on-surface); + background-color: var(--md-sys-color-surface); +} +.surface-variant { + color: var(--md-sys-color-on-surface-variant); + background-color: var(--md-sys-color-surface-variant); +} +.on-surface-variant { + color: var(--md-sys-color-surface-variant); + background-color: var(--md-sys-color-on-surface-variant); +} +.outline { + border: 1px solid var(--md-sys-color-outline); +} +.error { + color: var(--md-sys-color-on-error); + background-color: var(--md-sys-color-error); +} +.on-error { + color: var(--md-sys-color-error); + background-color: var(--md-sys-color-on-error); +} +.error-container { + color: var(--md-sys-color-on-error-container); + background-color: var(--md-sys-color-error-container); +} +.on-error-container { + color: var(--md-sys-color-error-container); + background-color: var(--md-sys-color-on-error-container); +} +.black { + background-color: var(--md-ref-palette-black); +} +.black-text { + color: var(--md-ref-palette-black); +} +.white { + background-color: var(--md-ref-palette-white); +} +.white-text { + color: var(--md-ref-palette-white); +} diff --git a/src/styles/base/material3/elevation.css b/src/styles/base/material3/elevation.css new file mode 100644 index 00000000..4f64bf24 --- /dev/null +++ b/src/styles/base/material3/elevation.css @@ -0,0 +1,67 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ + +/* https://github.com/material-foundation/material-tokens/blob/main/css/elevation.css */ + +:root { + /* Surface tint color */ + --md-sys-elevation-surface-tint-color: var(--md-sys-color-primary); + /* +5 */ + --md-sys-elevation-level5-value: 12px; + --md-sys-elevation-level5-unit: 1px; + --md-sys-elevation-level5: 12px; + /* +4 */ + --md-sys-elevation-level4-value: 8px; + --md-sys-elevation-level4-unit: 1px; + --md-sys-elevation-level4: 8px; + /* +3 */ + --md-sys-elevation-level3-value: 6px; + --md-sys-elevation-level3-unit: 1px; + --md-sys-elevation-level3: 6px; + /* +2 */ + --md-sys-elevation-level2-value: 3px; + --md-sys-elevation-level2-unit: 1px; + --md-sys-elevation-level2: 3px; + /* +1 */ + --md-sys-elevation-level1-value: 1px; + --md-sys-elevation-level1-unit: 1px; + --md-sys-elevation-level1: 1px; + /* 0 */ + --md-sys-elevation-level0-value: 0px; + --md-sys-elevation-level0-unit: 1px; + --md-sys-elevation-level0: 0px; +} +.elevation-0 { + box-shadow: var(--md-sys-elevation-level0); +} +.elevation-1 { + box-shadow: + 0 2px 1px -1px rgba(var(--md-sys-color-shadow), 0.2), + 0 1px 1px 0 rgba(var(--md-sys-color-shadow), 0.14), + 0 1px 3px 0 rgba(var(--md-sys-color-shadow), 0.12); +} +.elevation-2 { + box-shadow: var(--md-sys-elevation-level2); +} +.elevation-3 { + box-shadow: var(--md-sys-elevation-level3); +} +.elevation-4 { + box-shadow: var(--md-sys-elevation-level4); +} +.elevation-5 { + box-shadow: var(--md-sys-elevation-level5); +} diff --git a/src/styles/base/material3/index.css b/src/styles/base/material3/index.css new file mode 100644 index 00000000..2fbd5c5d --- /dev/null +++ b/src/styles/base/material3/index.css @@ -0,0 +1,20 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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 './palette.css'; +@import './colors.css'; +@import './elevation.css'; +@import './shape.css'; +@import './typography.css'; diff --git a/src/styles/base/material3/palette.css b/src/styles/base/material3/palette.css new file mode 100644 index 00000000..00b3ed9e --- /dev/null +++ b/src/styles/base/material3/palette.css @@ -0,0 +1,101 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ + +/* https://github.com/material-foundation/material-tokens/blob/main/css/palette.css */ + +:root { + --md-ref-palette-error0: #000000ff; + --md-ref-palette-error10: #410e0bff; + --md-ref-palette-error20: #601410ff; + --md-ref-palette-error30: #8c1d18ff; + --md-ref-palette-error40: #b3261eff; + --md-ref-palette-error50: #dc362eff; + --md-ref-palette-error60: #e46962ff; + --md-ref-palette-error70: #ec928eff; + --md-ref-palette-error80: #f2b8b5ff; + --md-ref-palette-error90: #f9dedcff; + --md-ref-palette-error95: #fceeeeff; + --md-ref-palette-error99: #fffbf9ff; + --md-ref-palette-error100: #ffffffff; + --md-ref-palette-tertiary0: #000000ff; + --md-ref-palette-tertiary10: #31111dff; + --md-ref-palette-tertiary20: #492532ff; + --md-ref-palette-tertiary30: #633b48ff; + --md-ref-palette-tertiary40: #7d5260ff; + --md-ref-palette-tertiary50: #986977ff; + --md-ref-palette-tertiary60: #b58392ff; + --md-ref-palette-tertiary70: #d29dacff; + --md-ref-palette-tertiary80: #efb8c8ff; + --md-ref-palette-tertiary90: #ffd8e4ff; + --md-ref-palette-tertiary95: #ffecf1ff; + --md-ref-palette-tertiary99: #fffbfaff; + --md-ref-palette-tertiary100: #ffffffff; + --md-ref-palette-secondary0: #000000ff; + --md-ref-palette-secondary10: #1d192bff; + --md-ref-palette-secondary20: #332d41ff; + --md-ref-palette-secondary30: #4a4458ff; + --md-ref-palette-secondary40: #625b71ff; + --md-ref-palette-secondary50: #7a7289ff; + --md-ref-palette-secondary60: #958da5ff; + --md-ref-palette-secondary70: #b0a7c0ff; + --md-ref-palette-secondary80: #ccc2dcff; + --md-ref-palette-secondary90: #e8def8ff; + --md-ref-palette-secondary95: #f6edffff; + --md-ref-palette-secondary99: #fffbfeff; + --md-ref-palette-secondary100: #ffffffff; + --md-ref-palette-primary0: #000000ff; + --md-ref-palette-primary10: #21005dff; + --md-ref-palette-primary20: #381e72ff; + --md-ref-palette-primary30: #4f378bff; + --md-ref-palette-primary40: #6750a4ff; + --md-ref-palette-primary50: #7f67beff; + --md-ref-palette-primary60: #9a82dbff; + --md-ref-palette-primary70: #b69df8ff; + --md-ref-palette-primary80: #d0bcffff; + --md-ref-palette-primary90: #eaddffff; + --md-ref-palette-primary95: #f6edffff; + --md-ref-palette-primary99: #fffbfeff; + --md-ref-palette-primary100: #ffffffff; + --md-ref-palette-neutral-variant0: #000000ff; + --md-ref-palette-neutral-variant10: #1d1a22ff; + --md-ref-palette-neutral-variant20: #322f37ff; + --md-ref-palette-neutral-variant30: #49454fff; + --md-ref-palette-neutral-variant40: #605d66ff; + --md-ref-palette-neutral-variant50: #79747eff; + --md-ref-palette-neutral-variant60: #938f99ff; + --md-ref-palette-neutral-variant70: #aea9b4ff; + --md-ref-palette-neutral-variant80: #cac4d0ff; + --md-ref-palette-neutral-variant90: #e7e0ecff; + --md-ref-palette-neutral-variant95: #f5eefaff; + --md-ref-palette-neutral-variant99: #fffbfeff; + --md-ref-palette-neutral-variant100: #ffffffff; + --md-ref-palette-neutral0: #000000ff; + --md-ref-palette-neutral0-rgb: 0, 0, 0; + --md-ref-palette-neutral10: #1c1b1fff; + --md-ref-palette-neutral20: #313033ff; + --md-ref-palette-neutral30: #484649ff; + --md-ref-palette-neutral40: #605d62ff; + --md-ref-palette-neutral50: #787579ff; + --md-ref-palette-neutral60: #939094ff; + --md-ref-palette-neutral70: #aeaaaeff; + --md-ref-palette-neutral80: #c9c5caff; + --md-ref-palette-neutral90: #e6e1e5ff; + --md-ref-palette-neutral95: #f4eff4ff; + --md-ref-palette-neutral99: #fffbfeff; + --md-ref-palette-neutral100: #ffffffff; + --md-ref-palette-black: #000000ff; + --md-ref-palette-white: #ffffffff; +} diff --git a/src/styles/base/material3/shape.css b/src/styles/base/material3/shape.css new file mode 100644 index 00000000..862d9bd4 --- /dev/null +++ b/src/styles/base/material3/shape.css @@ -0,0 +1,115 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ + +/* https://github.com/material-foundation/material-tokens/blob/main/css/shape.css */ + +:root { + /* Fully rounded */ + --md-sys-shape-corner-full-family: 3px; + /* Extra large top rounding */ + --md-sys-shape-corner-extra-large-top-family: 1px; + --md-sys-shape-corner-extra-large-top-default-size: 0px; + --md-sys-shape-corner-extra-large-top-top-left: 28px; + --md-sys-shape-corner-extra-large-top-top-right-unit: 1px; + --md-sys-shape-corner-extra-large-top-top-right: 28px; + /* Extra large rounding */ + --md-sys-shape-corner-extra-large-family: 1px; + --md-sys-shape-corner-extra-large-default-size-unit: 1px; + --md-sys-shape-corner-extra-large-default-size: 28px; + /* Large top rounding */ + --md-sys-shape-corner-large-top-family: 1px; + --md-sys-shape-corner-large-top-default-size-unit: 1px; + --md-sys-shape-corner-large-top-default-size: 0px; + --md-sys-shape-corner-large-top-top-left-unit: 1px; + --md-sys-shape-corner-large-top-top-left: 16px; + --md-sys-shape-corner-large-top-top-right-unit: 1px; + --md-sys-shape-corner-large-top-top-right: 16px; + /* Large end rounding */ + --md-sys-shape-corner-large-end-family: 1px; + --md-sys-shape-corner-large-end-default-size-unit: 1px; + --md-sys-shape-corner-large-end-default-size: 0px; + --md-sys-shape-corner-large-end-top-right-unit: 1px; + --md-sys-shape-corner-large-end-top-right: 16px; + --md-sys-shape-corner-large-end-bottom-right-unit: 1px; + --md-sys-shape-corner-large-end-bottom-right: 16px; + /* Large rounding */ + --md-sys-shape-corner-large-family: 1px; + --md-sys-shape-corner-large-default-size-unit: 1px; + --md-sys-shape-corner-large-default-size: 16px; + /* Medium rounding */ + --md-sys-shape-corner-medium-family: 1px; + --md-sys-shape-corner-medium-default-size-unit: 1px; + --md-sys-shape-corner-medium-default-size: 12px; + /* Small rounding */ + --md-sys-shape-corner-small-family: 1px; + --md-sys-shape-corner-small-default-size-unit: 1px; + --md-sys-shape-corner-small-default-size: 8px; + /* Extra small top rounding */ + --md-sys-shape-corner-extra-small-top-family: 1px; + --md-sys-shape-corner-extra-small-top-default-size-unit: 1px; + --md-sys-shape-corner-extra-small-top-default-size: 0px; + --md-sys-shape-corner-extra-small-top-top-left-unit: 1px; + --md-sys-shape-corner-extra-small-top-top-left: 4px; + --md-sys-shape-corner-extra-small-top-top-right-unit: 1px; + --md-sys-shape-corner-extra-small-top-top-right: 4px; + /* Extra small rounding */ + --md-sys-shape-corner-extra-small-family: 1px; + --md-sys-shape-corner-extra-small-default-size-unit: 1px; + --md-sys-shape-corner-extra-small-default-size: 4px; + /* No rounding */ + --md-sys-shape-corner-none-family: 1px; + --md-sys-shape-corner-none-default-size-unit: 1px; + --md-sys-shape-corner-none-default-size: 0px; + + --md-sys-shape-small: var(--md-sys-shape-corner-small-default-size); + --md-sys-shape-medium: var(--md-sys-shape-corner-medium-default-size); + --md-sys-shape-large: var(--md-sys-shape-corner-large-default-size); +} + +.shape-none { + border-radius: var(--md-sys-shape-corner-none-default-size); +} +.shape-extra-small { + border-radius: var(--md-sys-shape-corner-extra-small-default-size); +} +.shape-small { + border-radius: var(--md-sys-shape-corner-small-default-size); +} +.shape-medium { + border-radius: var(--md-sys-shape-corner-medium-default-size); +} +.shape-large { + border-radius: var(--md-sys-shape-corner-large-default-size); +} +.shape-extra-large { + border-radius: var(--md-sys-shape-corner-extra-large-default-size); +} +.extra-small-top { + border-top-left-radius: var(--md-sys-shape-corner-extra-small-top-top-left); + border-top-right-radius: var(--md-sys-shape-corner-extra-small-top-top-right); +} +.large-end { + border-top-right-radius: var(--md-sys-shape-corner-large-end-top-right); + border-bottom-right-radius: var(--md-sys-shape-corner-large-end-bottom-right); +} +.large-top { + border-top-left-radius: var(--md-sys-shape-corner-large-top-top-left); + border-top-right-radius: var(--md-sys-shape-corner-large-top-top-right); +} +.extra-large-top { + border-top-left-radius: var(--md-sys-shape-corner-extra-large-top-top-left); + border-top-right-radius: var(--md-sys-shape-corner-extra-large-top-top-right); +} diff --git a/src/styles/base/material3/typography.css b/src/styles/base/material3/typography.css new file mode 100644 index 00000000..867bf6d0 --- /dev/null +++ b/src/styles/base/material3/typography.css @@ -0,0 +1,524 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ + +/* https://github.com/material-foundation/material-tokens/blob/main/css/typography.css */ + +:root { + /* Label Small */ + --md-sys-typescale-label-small-text-transform: unset; + --md-sys-typescale-label-small-axis-value: unset; + --md-sys-typescale-label-small-font-style: unset; + --md-sys-typescale-label-small-text-decoration: unset; + /* Label Small line height */ + --md-sys-typescale-label-small-line-height-value: 16px; + --md-sys-typescale-label-small-line-height-unit: 2px; + --md-sys-typescale-label-small-line-height: 16px; + /* Label Small font tracking */ + --md-sys-typescale-label-small-tracking-value: 0.5px; + --md-sys-typescale-label-small-tracking-unit: 2px; + --md-sys-typescale-label-small-tracking: 0.5px; + /* Label Small font size */ + --md-sys-typescale-label-small-size-value: 11px; + --md-sys-typescale-label-small-size-unit: 2px; + --md-sys-typescale-label-small-size: 11px; + /* Label Small font weight */ + --md-sys-typescale-label-small-weight: var(--md-ref-typeface-weight-medium); + /* Label Small font name */ + --md-sys-typescale-label-small-font: var(--md-ref-typeface-plain); + /* Label Medium */ + --md-sys-typescale-label-medium-axis-value: unset; + --md-sys-typescale-label-medium-font-style: unset; + --md-sys-typescale-label-medium-text-decoration: unset; + /* Label Medium text transform */ + --md-sys-typescale-label-medium-text-transform: 1; + /* Label Medium line height */ + --md-sys-typescale-label-medium-line-height-value: 16px; + --md-sys-typescale-label-medium-line-height-unit: 2px; + --md-sys-typescale-label-medium-line-height: 16px; + /* Label Medium font tracking */ + --md-sys-typescale-label-medium-tracking-value: 0.5px; + --md-sys-typescale-label-medium-tracking-unit: 2px; + --md-sys-typescale-label-medium-tracking: 0.5px; + /* Label Medium font size */ + --md-sys-typescale-label-medium-size-value: 12px; + --md-sys-typescale-label-medium-size-unit: 2px; + --md-sys-typescale-label-medium-size: 12px; + /* Label Medium font weight */ + --md-sys-typescale-label-medium-weight: var(--md-ref-typeface-weight-medium); + /* Label Medium font name */ + --md-sys-typescale-label-medium-font: var(--md-ref-typeface-plain); + /* Label Large */ + --md-sys-typescale-label-large-text-transform: unset; + --md-sys-typescale-label-large-axis-value: unset; + --md-sys-typescale-label-large-font-style: unset; + --md-sys-typescale-label-large-text-decoration: unset; + /* Label Large line height */ + --md-sys-typescale-label-large-line-height-value: 20px; + --md-sys-typescale-label-large-line-height-unit: 2px; + --md-sys-typescale-label-large-line-height: 20px; + /* Label Large font tracking */ + --md-sys-typescale-label-large-tracking-value: 0.10000000149011612px; + --md-sys-typescale-label-large-tracking-unit: 2px; + --md-sys-typescale-label-large-tracking: 0.10000000149011612px; + /* Label Large font size */ + --md-sys-typescale-label-large-size-value: 14px; + --md-sys-typescale-label-large-size-unit: 2px; + --md-sys-typescale-label-large-size: 14px; + /* Label Large font weight */ + --md-sys-typescale-label-large-weight: var(--md-ref-typeface-weight-medium); + /* Label Large font name */ + --md-sys-typescale-label-large-font: var(--md-ref-typeface-plain); + /* Body Small */ + --md-sys-typescale-body-small-text-transform: unset; + --md-sys-typescale-body-small-axis-value: unset; + --md-sys-typescale-body-small-font-style: unset; + --md-sys-typescale-body-small-text-decoration: unset; + /* Body Small line height */ + --md-sys-typescale-body-small-line-height-value: 16px; + --md-sys-typescale-body-small-line-height-unit: 2px; + --md-sys-typescale-body-small-line-height: 16px; + /* Body Small font tracking */ + --md-sys-typescale-body-small-tracking-value: 0.4000000059604645px; + --md-sys-typescale-body-small-tracking-unit: 2px; + --md-sys-typescale-body-small-tracking: 0.4000000059604645px; + /* Body Small font size */ + --md-sys-typescale-body-small-size-value: 12px; + --md-sys-typescale-body-small-size-unit: 2px; + --md-sys-typescale-body-small-size: 12px; + /* Body Small font weight */ + --md-sys-typescale-body-small-weight: var(--md-ref-typeface-weight-regular); + /* Body Small font name */ + --md-sys-typescale-body-small-font: var(--md-ref-typeface-plain); + /* Body Medium */ + --md-sys-typescale-body-medium-text-transform: unset; + --md-sys-typescale-body-medium-axis-value: unset; + --md-sys-typescale-body-medium-font-style: unset; + --md-sys-typescale-body-medium-text-decoration: unset; + /* Body Medium line height */ + --md-sys-typescale-body-medium-line-height-value: 20px; + --md-sys-typescale-body-medium-line-height-unit: 2px; + --md-sys-typescale-body-medium-line-height: 20px; + /* Body Medium font tracking */ + --md-sys-typescale-body-medium-tracking-value: 0.25px; + --md-sys-typescale-body-medium-tracking-unit: 2px; + --md-sys-typescale-body-medium-tracking: 0.25px; + /* Body Medium font size */ + --md-sys-typescale-body-medium-size-value: 14px; + --md-sys-typescale-body-medium-size-unit: 2px; + --md-sys-typescale-body-medium-size: 14px; + /* Body Medium font weight */ + --md-sys-typescale-body-medium-weight: var(--md-ref-typeface-weight-regular); + /* Body Medium font name */ + --md-sys-typescale-body-medium-font: var(--md-ref-typeface-plain); + /* Body Large */ + --md-sys-typescale-body-large-text-transform: unset; + --md-sys-typescale-body-large-axis-value: unset; + --md-sys-typescale-body-large-font-style: unset; + --md-sys-typescale-body-large-text-decoration: unset; + /* Body Large line height */ + --md-sys-typescale-body-large-line-height-value: 24px; + --md-sys-typescale-body-large-line-height-unit: 2px; + --md-sys-typescale-body-large-line-height: 24px; + /* Body Large font tracking */ + --md-sys-typescale-body-large-tracking-value: 0.5px; + --md-sys-typescale-body-large-tracking-unit: 2px; + --md-sys-typescale-body-large-tracking: 0.5px; + /* Body Large font size */ + --md-sys-typescale-body-large-size-value: 16px; + --md-sys-typescale-body-large-size-unit: 2px; + --md-sys-typescale-body-large-size: 16px; + /* Body Large font weight */ + --md-sys-typescale-body-large-weight: var(--md-ref-typeface-weight-regular); + /* Body Large font name */ + --md-sys-typescale-body-large-font: var(--md-ref-typeface-plain); + /* Title Small */ + --md-sys-typescale-title-small-text-transform: unset; + --md-sys-typescale-title-small-axis-value: unset; + --md-sys-typescale-title-small-font-style: unset; + --md-sys-typescale-title-small-text-decoration: unset; + /* Title Small line height */ + --md-sys-typescale-title-small-line-height-value: 20px; + --md-sys-typescale-title-small-line-height-unit: 2px; + --md-sys-typescale-title-small-line-height: 20px; + /* Title Small font tracking */ + --md-sys-typescale-title-small-tracking-value: 0.10000000149011612px; + --md-sys-typescale-title-small-tracking-unit: 2px; + --md-sys-typescale-title-small-tracking: 0.10000000149011612px; + /* Title Small font size */ + --md-sys-typescale-title-small-size-value: 14px; + --md-sys-typescale-title-small-size-unit: 2px; + --md-sys-typescale-title-small-size: 14px; + /* Title Small font weight */ + --md-sys-typescale-title-small-weight: var(--md-ref-typeface-weight-medium); + /* Title Small font name */ + --md-sys-typescale-title-small-font: var(--md-ref-typeface-plain); + /* Title Medium */ + --md-sys-typescale-title-medium-text-transform: unset; + --md-sys-typescale-title-medium-axis-value: unset; + --md-sys-typescale-title-medium-font-style: unset; + --md-sys-typescale-title-medium-text-decoration: unset; + /* Title Medium line height */ + --md-sys-typescale-title-medium-line-height-value: 24px; + --md-sys-typescale-title-medium-line-height-unit: 2px; + --md-sys-typescale-title-medium-line-height: 24px; + /* Title Medium font tracking */ + --md-sys-typescale-title-medium-tracking-value: 0.15000000596046448px; + --md-sys-typescale-title-medium-tracking-unit: 2px; + --md-sys-typescale-title-medium-tracking: 0.15000000596046448px; + /* Title Medium font size */ + --md-sys-typescale-title-medium-size-value: 16px; + --md-sys-typescale-title-medium-size-unit: 2px; + --md-sys-typescale-title-medium-size: 16px; + /* Title Medium font weight */ + --md-sys-typescale-title-medium-weight: var(--md-ref-typeface-weight-medium); + /* Title Medium font name */ + --md-sys-typescale-title-medium-font: var(--md-ref-typeface-plain); + /* Title Large */ + --md-sys-typescale-title-large-text-transform: unset; + --md-sys-typescale-title-large-axis-value: unset; + --md-sys-typescale-title-large-font-style: unset; + --md-sys-typescale-title-large-text-decoration: unset; + /* Title Large line height */ + --md-sys-typescale-title-large-line-height-value: 28px; + --md-sys-typescale-title-large-line-height-unit: 2px; + --md-sys-typescale-title-large-line-height: 28px; + /* Title Large font tracking */ + --md-sys-typescale-title-large-tracking-value: 0px; + --md-sys-typescale-title-large-tracking-unit: 2px; + --md-sys-typescale-title-large-tracking: 0px; + /* Title Large font size */ + --md-sys-typescale-title-large-size-value: 22px; + --md-sys-typescale-title-large-size-unit: 2px; + --md-sys-typescale-title-large-size: 22px; + /* Title Large font weight */ + --md-sys-typescale-title-large-weight: var(--md-ref-typeface-weight-regular); + /* Title Large font name */ + --md-sys-typescale-title-large-font: var(--md-ref-typeface-brand); + /* Headline Small */ + --md-sys-typescale-headline-small-text-transform: unset; + --md-sys-typescale-headline-small-axis-value: unset; + --md-sys-typescale-headline-small-font-style: unset; + --md-sys-typescale-headline-small-text-decoration: unset; + /* Headline Small line height */ + --md-sys-typescale-headline-small-line-height-value: 32px; + --md-sys-typescale-headline-small-line-height-unit: 2px; + --md-sys-typescale-headline-small-line-height: 32px; + /* Headline Small font tracking */ + --md-sys-typescale-headline-small-tracking-value: 0px; + --md-sys-typescale-headline-small-tracking-unit: 2px; + --md-sys-typescale-headline-small-tracking: 0px; + /* Headline Small font size */ + --md-sys-typescale-headline-small-size-value: 24px; + --md-sys-typescale-headline-small-size-unit: 2px; + --md-sys-typescale-headline-small-size: 24px; + /* Headline Small font weight */ + --md-sys-typescale-headline-small-weight: var( + --md-ref-typeface-weight-regular + ); + /* Headline Small font name */ + --md-sys-typescale-headline-small-font: var(--md-ref-typeface-brand); + /* Headline Medium */ + --md-sys-typescale-headline-medium-text-transform: unset; + --md-sys-typescale-headline-medium-axis-value: unset; + --md-sys-typescale-headline-medium-font-style: unset; + --md-sys-typescale-headline-medium-text-decoration: unset; + /* Headline Medium line height */ + --md-sys-typescale-headline-medium-line-height-value: 36px; + --md-sys-typescale-headline-medium-line-height-unit: 2px; + --md-sys-typescale-headline-medium-line-height: 36px; + /* Headline Medium font tracking */ + --md-sys-typescale-headline-medium-tracking-value: 0px; + --md-sys-typescale-headline-medium-tracking-unit: 2px; + --md-sys-typescale-headline-medium-tracking: 0px; + /* Headline Medium font size */ + --md-sys-typescale-headline-medium-size-value: 28px; + --md-sys-typescale-headline-medium-size-unit: 2px; + --md-sys-typescale-headline-medium-size: 28px; + /* Headline Medium font weight */ + --md-sys-typescale-headline-medium-weight: var( + --md-ref-typeface-weight-regular + ); + /* Headline Medium font name */ + --md-sys-typescale-headline-medium-font: var(--md-ref-typeface-brand); + /* Headline Large */ + --md-sys-typescale-headline-large-text-transform: unset; + --md-sys-typescale-headline-large-axis-value: unset; + --md-sys-typescale-headline-large-font-style: unset; + --md-sys-typescale-headline-large-text-decoration: unset; + /* Headline Large line height */ + --md-sys-typescale-headline-large-line-height-value: 40px; + --md-sys-typescale-headline-large-line-height-unit: 2px; + --md-sys-typescale-headline-large-line-height: 40px; + /* Headline Large font tracking */ + --md-sys-typescale-headline-large-tracking-value: 0px; + --md-sys-typescale-headline-large-tracking-unit: 2px; + --md-sys-typescale-headline-large-tracking: 0px; + /* Headline Large font size */ + --md-sys-typescale-headline-large-size-value: 32px; + --md-sys-typescale-headline-large-size-unit: 2px; + --md-sys-typescale-headline-large-size: 32px; + /* Headline Large font name */ + --md-sys-typescale-headline-large-font: var(--md-ref-typeface-brand); + /* Headline Large font weight */ + --md-sys-typescale-headline-large-weight: var( + --md-ref-typeface-weight-regular + ); + /* Display Small */ + --md-sys-typescale-display-small-text-transform: unset; + --md-sys-typescale-display-small-axis-value: unset; + --md-sys-typescale-display-small-font-style: unset; + --md-sys-typescale-display-small-text-decoration: unset; + /* Display Small line height */ + --md-sys-typescale-display-small-line-height-value: 44px; + --md-sys-typescale-display-small-line-height-unit: 2px; + --md-sys-typescale-display-small-line-height: 44px; + /* Display Small font tracking */ + --md-sys-typescale-display-small-tracking-value: 0px; + --md-sys-typescale-display-small-tracking-unit: 2px; + --md-sys-typescale-display-small-tracking: 0px; + /* Display Small font size */ + --md-sys-typescale-display-small-size-value: 36px; + --md-sys-typescale-display-small-size-unit: 2px; + --md-sys-typescale-display-small-size: 36px; + /* Display Small font weight */ + --md-sys-typescale-display-small-weight: var( + --md-ref-typeface-weight-regular + ); + /* Display Small font name */ + --md-sys-typescale-display-small-font: var(--md-ref-typeface-brand); + /* Display Medium */ + --md-sys-typescale-display-medium-text-transform: unset; + --md-sys-typescale-display-medium-axis-value: unset; + --md-sys-typescale-display-medium-font-style: unset; + --md-sys-typescale-display-medium-text-decoration: unset; + /* Display Medium line height */ + --md-sys-typescale-display-medium-line-height-value: 52px; + --md-sys-typescale-display-medium-line-height-unit: 2px; + --md-sys-typescale-display-medium-line-height: 52px; + /* Display Medium font tracking */ + --md-sys-typescale-display-medium-tracking-value: 0px; + --md-sys-typescale-display-medium-tracking-unit: 2px; + --md-sys-typescale-display-medium-tracking: 0px; + /* Display Medium font size */ + --md-sys-typescale-display-medium-size-value: 45px; + --md-sys-typescale-display-medium-size-unit: 2px; + --md-sys-typescale-display-medium-size: 45px; + /* Display Medium font weight */ + --md-sys-typescale-display-medium-weight: var( + --md-ref-typeface-weight-regular + ); + /* Display Medium font name */ + --md-sys-typescale-display-medium-font: var(--md-ref-typeface-brand); + /* Display Large */ + --md-sys-typescale-display-large-text-transform: unset; + --md-sys-typescale-display-large-axis-value: unset; + --md-sys-typescale-display-large-font-style: unset; + --md-sys-typescale-display-large-text-decoration: unset; + /* Display Large line height */ + --md-sys-typescale-display-large-line-height-value: 64px; + --md-sys-typescale-display-large-line-height-unit: 2px; + --md-sys-typescale-display-large-line-height: 64px; + /* Display Large font tracking */ + --md-sys-typescale-display-large-tracking-value: -0.25px; + --md-sys-typescale-display-large-tracking-unit: 2px; + --md-sys-typescale-display-large-tracking: -0.25px; + /* Display Large font size */ + --md-sys-typescale-display-large-size-value: 57px; + --md-sys-typescale-display-large-size-unit: 2px; + --md-sys-typescale-display-large-size: 57px; + /* Display Large font weight */ + --md-sys-typescale-display-large-weight: var( + --md-ref-typeface-weight-regular + ); + /* Display Large font name */ + --md-sys-typescale-display-large-font: var(--md-ref-typeface-brand); + /* Plain typeface */ + --md-ref-typeface-plain: Roboto; + /* Brand typeface */ + --md-ref-typeface-brand: Roboto; + /* Bold weight */ + --md-ref-typeface-weight-bold: 700; + /* Medium weight */ + --md-ref-typeface-weight-medium: 500; + /* Regular weight */ + --md-ref-typeface-weight-regular: 400; +} + +/* Label Small */ +.label-small { + font-family: var(--md-sys-typescale-label-small-font); + font-weight: var(--md-sys-typescale-label-small-weight); + font-size: var(--md-sys-typescale-label-small-size); + font-style: var(--md-sys-typescale-label-small-font-style); + letter-spacing: var(--md-sys-typescale-label-small-tracking); + line-height: var(--md-sys-typescale-label-small-line-height); + text-transform: var(--md-sys-typescale-label-small-text-transform); + text-decoration: var(--md-sys-typescale-label-small-text-decoration); +} +/* Label Medium */ +.label-medium { + font-family: var(--md-sys-typescale-label-medium-font); + font-weight: var(--md-sys-typescale-label-medium-weight); + font-size: var(--md-sys-typescale-label-medium-size); + font-style: var(--md-sys-typescale-label-medium-font-style); + letter-spacing: var(--md-sys-typescale-label-medium-tracking); + line-height: var(--md-sys-typescale-label-medium-line-height); + text-transform: var(--md-sys-typescale-label-medium-text-transform); + text-decoration: var(--md-sys-typescale-label-medium-text-decoration); +} +/* Label Large */ +.label-large { + font-family: var(--md-sys-typescale-label-large-font); + font-weight: var(--md-sys-typescale-label-large-weight); + font-size: var(--md-sys-typescale-label-large-size); + font-style: var(--md-sys-typescale-label-large-font-style); + letter-spacing: var(--md-sys-typescale-label-large-tracking); + line-height: var(--md-sys-typescale-label-large-line-height); + text-transform: var(--md-sys-typescale-label-large-text-transform); + text-decoration: var(--md-sys-typescale-label-large-text-decoration); +} +/* Body Small */ +.body-small { + font-family: var(--md-sys-typescale-body-small-font); + font-weight: var(--md-sys-typescale-body-small-weight); + font-size: var(--md-sys-typescale-body-small-size); + font-style: var(--md-sys-typescale-body-small-font-style); + letter-spacing: var(--md-sys-typescale-body-small-tracking); + line-height: var(--md-sys-typescale-body-small-line-height); + text-transform: var(--md-sys-typescale-body-small-text-transform); + text-decoration: var(--md-sys-typescale-body-small-text-decoration); +} +/* Body Medium */ +.body-medium { + font-family: var(--md-sys-typescale-body-medium-font); + font-weight: var(--md-sys-typescale-body-medium-weight); + font-size: var(--md-sys-typescale-body-medium-size); + font-style: var(--md-sys-typescale-body-medium-font-style); + letter-spacing: var(--md-sys-typescale-body-medium-tracking); + line-height: var(--md-sys-typescale-body-medium-line-height); + text-transform: var(--md-sys-typescale-body-medium-text-transform); + text-decoration: var(--md-sys-typescale-body-medium-text-decoration); +} +/* Body Large */ +.body-large { + font-family: var(--md-sys-typescale-body-large-font); + font-weight: var(--md-sys-typescale-body-large-weight); + font-size: var(--md-sys-typescale-body-large-size); + font-style: var(--md-sys-typescale-body-large-font-style); + letter-spacing: var(--md-sys-typescale-body-large-tracking); + line-height: var(--md-sys-typescale-body-large-line-height); + text-transform: var(--md-sys-typescale-body-large-text-transform); + text-decoration: var(--md-sys-typescale-body-large-text-decoration); +} +/* Title Small */ +.title-small { + font-family: var(--md-sys-typescale-title-small-font); + font-weight: var(--md-sys-typescale-title-small-weight); + font-size: var(--md-sys-typescale-title-small-size); + font-style: var(--md-sys-typescale-title-small-font-style); + letter-spacing: var(--md-sys-typescale-title-small-tracking); + line-height: var(--md-sys-typescale-title-small-line-height); + text-transform: var(--md-sys-typescale-title-small-text-transform); + text-decoration: var(--md-sys-typescale-title-small-text-decoration); +} +/* Title Medium */ +.title-medium { + font-family: var(--md-sys-typescale-title-medium-font); + font-weight: var(--md-sys-typescale-title-medium-weight); + font-size: var(--md-sys-typescale-title-medium-size); + font-style: var(--md-sys-typescale-title-medium-font-style); + letter-spacing: var(--md-sys-typescale-title-medium-tracking); + line-height: var(--md-sys-typescale-title-medium-line-height); + text-transform: var(--md-sys-typescale-title-medium-text-transform); + text-decoration: var(--md-sys-typescale-title-medium-text-decoration); +} +/* Title Large */ +.title-large { + font-family: var(--md-sys-typescale-title-large-font); + font-weight: var(--md-sys-typescale-title-large-weight); + font-size: var(--md-sys-typescale-title-large-size); + font-style: var(--md-sys-typescale-title-large-font-style); + letter-spacing: var(--md-sys-typescale-title-large-tracking); + line-height: var(--md-sys-typescale-title-large-line-height); + text-transform: var(--md-sys-typescale-title-large-text-transform); + text-decoration: var(--md-sys-typescale-title-large-text-decoration); +} +/* Headline Small */ +.headline-small { + font-family: var(--md-sys-typescale-headline-small-font); + font-weight: var(--md-sys-typescale-headline-small-weight); + font-size: var(--md-sys-typescale-headline-small-size); + font-style: var(--md-sys-typescale-headline-small-font-style); + letter-spacing: var(--md-sys-typescale-headline-small-tracking); + line-height: var(--md-sys-typescale-headline-small-line-height); + text-transform: var(--md-sys-typescale-headline-small-text-transform); + text-decoration: var(--md-sys-typescale-headline-small-text-decoration); +} +/* Headline Medium */ +.headline-medium { + font-family: var(--md-sys-typescale-headline-medium-font); + font-weight: var(--md-sys-typescale-headline-medium-weight); + font-size: var(--md-sys-typescale-headline-medium-size); + font-style: var(--md-sys-typescale-headline-medium-font-style); + letter-spacing: var(--md-sys-typescale-headline-medium-tracking); + line-height: var(--md-sys-typescale-headline-medium-line-height); + text-transform: var(--md-sys-typescale-headline-medium-text-transform); + text-decoration: var(--md-sys-typescale-headline-medium-text-decoration); +} +/* Headline Large */ +.headline-large { + font-family: var(--md-sys-typescale-headline-large-font); + font-weight: var(--md-sys-typescale-headline-large-weight); + font-size: var(--md-sys-typescale-headline-large-size); + font-style: var(--md-sys-typescale-headline-large-font-style); + letter-spacing: var(--md-sys-typescale-headline-large-tracking); + line-height: var(--md-sys-typescale-headline-large-line-height); + text-transform: var(--md-sys-typescale-headline-large-text-transform); + text-decoration: var(--md-sys-typescale-headline-large-text-decoration); +} +/* Display Small */ +.display-small { + font-family: var(--md-sys-typescale-display-small-font); + font-weight: var(--md-sys-typescale-display-small-weight); + font-size: var(--md-sys-typescale-display-small-size); + font-style: var(--md-sys-typescale-display-small-font-style); + letter-spacing: var(--md-sys-typescale-display-small-tracking); + line-height: var(--md-sys-typescale-display-small-line-height); + text-transform: var(--md-sys-typescale-display-small-text-transform); + text-decoration: var(--md-sys-typescale-display-small-text-decoration); +} +/* Display Medium */ +.display-medium { + font-family: var(--md-sys-typescale-display-medium-font); + font-weight: var(--md-sys-typescale-display-medium-weight); + font-size: var(--md-sys-typescale-display-medium-size); + font-style: var(--md-sys-typescale-display-medium-font-style); + letter-spacing: var(--md-sys-typescale-display-medium-tracking); + line-height: var(--md-sys-typescale-display-medium-line-height); + text-transform: var(--md-sys-typescale-display-medium-text-transform); + text-decoration: var(--md-sys-typescale-display-medium-text-decoration); +} +/* Display Large */ +.display-large { + font-family: var(--md-sys-typescale-display-large-font); + font-weight: var(--md-sys-typescale-display-large-weight); + font-size: var(--md-sys-typescale-display-large-size); + font-style: var(--md-sys-typescale-display-large-font-style); + letter-spacing: var(--md-sys-typescale-display-large-tracking); + line-height: var(--md-sys-typescale-display-large-line-height); + text-transform: var(--md-sys-typescale-display-large-text-transform); + text-decoration: var(--md-sys-typescale-display-large-text-decoration); +} diff --git a/src/styles/components/card.css b/src/styles/components/card.css new file mode 100644 index 00000000..16534213 --- /dev/null +++ b/src/styles/components/card.css @@ -0,0 +1,31 @@ +/* + Copyright 2022 Marc Nuri San Felix + + Licensed 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. + */ +:root { + --material3-card-padding: 16px; +} + +.electronim .material3.card:hover { + background-color: var(--md-sys-color-secondary-container); +} + +.electronim .material3.card .card__content { + padding: var(--material3-card-padding); +} + +.electronim .material3.card .card__divider { + margin: 8px calc(-1 * var(--material3-card-padding)); + border-bottom: 1px solid var(--md-sys-color-outline-variant); +} diff --git a/src/styles/components/index.css b/src/styles/components/index.css index 3ab628e4..8de4a4a6 100644 --- a/src/styles/components/index.css +++ b/src/styles/components/index.css @@ -13,5 +13,6 @@ See the License for the specific language governing permissions and limitations under the License. */ +@import './card.css'; @import './checkbox.css'; @import './top-bar.css'; diff --git a/src/styles/components/top-bar.css b/src/styles/components/top-bar.css index 4a5775f4..e7dea9cf 100644 --- a/src/styles/components/top-bar.css +++ b/src/styles/components/top-bar.css @@ -16,3 +16,21 @@ .electronim .top-bar.navbar { background: var(--color-canvas-default); } + +.electronim .material3.top-app-bar.small { + padding: 0 16px; + height: 64px; + display: flex; + gap: 16px; + align-items: center; +} + +.electronim .material3.top-app-bar .leading-navigation-icon { + font-family: 'Material Symbols Outlined', sans-serif; + cursor: pointer; + user-select: none; +} +.electronim .material3.top-app-bar.small .leading-navigation-icon { + width: 24px; + text-align: center; +} diff --git a/webpack.js b/webpack.js index 7bdd49f4..4635b1a5 100755 --- a/webpack.js +++ b/webpack.js @@ -25,6 +25,7 @@ const {APP_EVENTS, ELECTRONIM_VERSION} = require('./src/constants'); const BUNDLES_DIR = 'bundles'; const PRELOAD_ENTRIES = [ + 'about', 'app-menu', 'chrome-tabs', 'help',