+ `);
+ });
+});
diff --git a/packages/helpers/sb-action-logger.js b/packages/helpers/sb-action-logger.js
new file mode 100644
index 000000000..872ef5678
--- /dev/null
+++ b/packages/helpers/sb-action-logger.js
@@ -0,0 +1,3 @@
+import { SbActionLogger } from './sb-action-logger/src/SbActionLogger.js';
+
+window.customElements.define('sb-action-logger', SbActionLogger);
diff --git a/packages/helpers/sb-action-logger/README.md b/packages/helpers/sb-action-logger/README.md
new file mode 100644
index 000000000..a5ced2026
--- /dev/null
+++ b/packages/helpers/sb-action-logger/README.md
@@ -0,0 +1,35 @@
+# Storybook Action Logger
+
+[//]: # 'AUTO INSERT HEADER PREPUBLISH'
+
+A visual element to show action logs in Storybook demos `sb-action-logger`.
+
+**This is a demonstrative tool**, not a debugging tool (although it may help initially).
+If you try logging complex values such as arrays, objects or promises,
+you should expect to get only the string interpretation as the output in this logger.
+
+## Live Demo/Documentation
+
+> See our [storybook](http://lion-web-components.netlify.com/?path=/docs/helpers-storybook-action-logger) for a live demo and API documentation
+
+## Installation
+
+```bash
+npm i @lion/helpers
+```
+
+## Usage
+
+```html
+
+
+
+```
+
+Then, with the sb-action-logger instance selected, call the `log` method on it.
+
+```js
+myActionLoggerInstance.log('Hello, World!');
+```
diff --git a/packages/helpers/sb-action-logger/custom-elements.json b/packages/helpers/sb-action-logger/custom-elements.json
new file mode 100644
index 000000000..22cb809c4
--- /dev/null
+++ b/packages/helpers/sb-action-logger/custom-elements.json
@@ -0,0 +1,46 @@
+{
+ "version": 0,
+ "tags": [
+ {
+ "name": "sb-action-logger",
+ "description": "A visual element to show action logs in Storybook demos",
+ "properties": [
+ {
+ "name": "title",
+ "type": "String",
+ "description": "The title of action logger",
+ "default": "Action Logger"
+ }
+ ],
+ "events": [],
+ "slots": [],
+ "cssProperties": [
+ {
+ "name": "--sb-action-logger-title-color",
+ "description": "Color of the title",
+ "type": "Color"
+ },
+ {
+ "name": "--sb-action-logger-text-color",
+ "description": "Color of the logs' text",
+ "type": "Color"
+ },
+ {
+ "name": "--sb-action-logger-cue-color-primary",
+ "description": "Primary color of the visual cue",
+ "type": "Color"
+ },
+ {
+ "name": "--sb-action-logger-cue-color-secondary",
+ "description": "Secondary color of the visual cue",
+ "type": "Color"
+ },
+ {
+ "name": "--sb-action-logger-cue-duration",
+ "description": "Duration of the visual cue",
+ "type": "Number"
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/helpers/sb-action-logger/src/SbActionLogger.js b/packages/helpers/sb-action-logger/src/SbActionLogger.js
new file mode 100644
index 000000000..80595a9d7
--- /dev/null
+++ b/packages/helpers/sb-action-logger/src/SbActionLogger.js
@@ -0,0 +1,178 @@
+import { css, html, LitElement, render } from '@lion/core';
+
+/** @typedef {import('lit-html').TemplateResult} TemplateResult */
+
+export class SbActionLogger extends LitElement {
+ static get properties() {
+ return {
+ title: { type: String, reflect: true },
+ __logCounter: { type: Number },
+ };
+ }
+
+ static get styles() {
+ return css`
+ :host {
+ --sb-action-logger-title-color: black;
+ --sb-action-logger-text-color: black;
+ --sb-action-logger-cue-color-primary: #3f51b5;
+ --sb-action-logger-cue-color-secondary: #c5cae9;
+ --sb-action-logger-cue-duration: 1000ms;
+
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+ display: block;
+ }
+
+ .header__info {
+ color: var(--sb-action-logger-title-color);
+ display: flex;
+ align-items: center;
+ padding: 16px;
+ font-size: 16px;
+ }
+
+ .header__clear {
+ margin-left: 16px;
+ border-radius: 0px;
+ background-color: rgba(0, 0, 0, 0.05);
+ border: none;
+ cursor: pointer;
+ padding: 8px;
+ }
+
+ .header__clear:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+
+ .header__title {
+ margin: 0;
+ font-weight: bold;
+ flex-grow: 1;
+ }
+
+ .header__log-cue {
+ position: relative;
+ height: 3px;
+ background-color: var(--sb-action-logger-cue-color-secondary);
+ overflow: hidden;
+ }
+
+ .header__log-cue-overlay {
+ position: absolute;
+ height: 3px;
+ width: 50px;
+ left: -50px;
+ background-color: var(--sb-action-logger-cue-color-primary);
+ }
+
+ .header__log-cue-overlay--slide {
+ animation: slidethrough var(--sb-action-logger-cue-duration) ease-in;
+ }
+
+ @keyframes slidethrough {
+ from {
+ left: -50px;
+ width: 50px;
+ }
+
+ to {
+ left: 100%;
+ width: 500px;
+ }
+ }
+
+ .logger {
+ overflow-y: auto;
+ max-height: 110px;
+ }
+
+ .logger__log {
+ padding: 16px;
+ }
+
+ .logger__log:not(:last-child) {
+ border-bottom: 1px solid lightgrey;
+ }
+
+ .logger__log code {
+ color: var(--sb-action-logger-text-color);
+ white-space: pre-wrap; /* css-3 */
+ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ word-wrap: break-word; /* Internet Explorer 5.5+ */
+ }
+ `;
+ }
+
+ constructor() {
+ super();
+ this.title = 'Action Logger';
+ this.__logCounter = 0;
+ }
+
+ /**
+ * Renders the passed content as a node, and appends it to the logger
+ * Only supports simple values, will be interpreted to a String
+ * E.g. an Object will become '[object Object]'
+ *
+ * @param {} content Content to be logged to the action logger
+ */
+ log(content) {
+ const loggerEl = this.shadowRoot.querySelector('.logger');
+ const offlineRenderContainer = document.createElement('div');
+ render(this._logTemplate(content), offlineRenderContainer);
+ // TODO: Feature, combine duplicate consecutive logs as 1 dom element and add a counter for dupes
+ loggerEl.appendChild(offlineRenderContainer.firstElementChild);
+ this.__logCounter += 1;
+ this.__animateCue();
+ loggerEl.scrollTo({ top: loggerEl.scrollHeight, behavior: 'smooth' });
+ }
+
+ /**
+ * Protected getter that returns the template of a single log
+ *
+ * @return {TemplateResult} TemplateResult that uses the content passed to create a log
+ */
+ // eslint-disable-next-line class-methods-use-this
+ _logTemplate(content) {
+ return html`
+
+ ${content}
+
+ `;
+ }
+
+ __animateCue() {
+ const cueEl = this.shadowRoot.querySelector('.header__log-cue-overlay');
+ cueEl.classList.remove('header__log-cue-overlay--slide');
+ // This triggers browser to stop batching changes because it has to evaluate something.
+ // eslint-disable-next-line no-void
+ void this.offsetWidth;
+ // So that when we arrive here, the browser sees this adding as an actual 'change'
+ // and this means the animation gets refired.
+ cueEl.classList.add('header__log-cue-overlay--slide');
+ }
+
+ __clearLogs() {
+ const loggerEl = this.shadowRoot.querySelector('.logger');
+ loggerEl.innerHTML = '';
+ this.__logCounter = 0;
+ }
+
+ render() {
+ return html`
+
+
+
${this.title}
+
${this.__logCounter}
+
+
+
+
+
+
+
+ `;
+ }
+}
diff --git a/packages/helpers/sb-action-logger/stories/index.stories.mdx b/packages/helpers/sb-action-logger/stories/index.stories.mdx
new file mode 100644
index 000000000..d1beeddfb
--- /dev/null
+++ b/packages/helpers/sb-action-logger/stories/index.stories.mdx
@@ -0,0 +1,116 @@
+import {
+ Story,
+ Preview,
+ Meta,
+ Props,
+ html,
+} from '@open-wc/demoing-storybook';
+
+import '../../sb-action-logger.js';
+
+
+
+# Storybook Action Logger
+
+A visual element to show action logs in Storybook demos `sb-action-logger`
+
+
+ {html`
+
+
To log: Hello, World!
+
+
+ `}
+
+
+```html
+
To log: Hello, World!
+
+
+```
+
+Note that you need some reference to your logger. Above example shows this by using a unique ID.
+
+## Features:
+
+- A public method `log` to log things to the action logger.
+- Overridable `title` property.
+- Clear button to clear logs
+- A counter to count the total amount of logs
+
+## How to use
+
+### Installation
+
+```bash
+npm i sb-action-logger
+```
+
+## API
+
+
+
+## Variations
+
+### Custom Title
+
+
+ {html`
+
+
+ `}
+
+
+
+## Rationale
+
+This component was created due to a need for a nice visual action logger in Storybook Docs mode.
+
+In docs mode, you do not have access to the action logger addon component, and it is nice to be able to show actions inline in your demos and documentation.
+
+### Opinionated
+
+I added quite a bit of styling on this component to make it look decent.
+There are a bunch of styles that you can easily override to make it fit your design system which I believe should be enough in most cases.
+If you need more control you can always:
+
+- Override the host styles, there's a few custom CSS props that you can override as well.
+- Extend the component and apply your overrides
+- Open an issue if you believe it would be good to make something more flexible / configurable
+
+Maybe in the future I will abstract this component to a more generic (ugly) one with no styles, so it's more friendly as a shared component.
+
+### Plugin
+
+If you use an action logger inside your Story in Storybook, you will also see it in your canvas, and this may not be your intention.
+
+One idea I have is that we can simplify the usage further by making this a Storybook (docs-)plugin or decorator or whatever.
+I am not too familiar with them at the moment, but it would be cool if someone can simply enable an action logger option on a particular Story inside their .mdx,
+and then actions are automatically logged to the visual logger below it. Would need to figure out how to catch the action and pass it to the visual logger element.
+
+I have not investigated yet on the how, but that is the rough idea. Feel free to help out here :)
+
+## Future
+
+I plan on adding more features.
+They can always be found in the test folder where I specify new features as tests first, and then I skip them until I implement them. Easy to find them that way.
+If the feature you'd like is not in the tests, I probably did not think about it yet or did not plan to do it yet, so in that case feel free to make an issue so we can add it.
+
+I'm happy to accept pull requests for skipped tests (features to be added), see the CONTRIBUTING.md on GitHub for more details on how to contribute to this codebase.
diff --git a/packages/helpers/sb-action-logger/test/sb-action-logger.test.js b/packages/helpers/sb-action-logger/test/sb-action-logger.test.js
new file mode 100644
index 000000000..80a7a45e8
--- /dev/null
+++ b/packages/helpers/sb-action-logger/test/sb-action-logger.test.js
@@ -0,0 +1,125 @@
+import { expect, fixture, html } from '@open-wc/testing';
+import '../../sb-action-logger.js';
+
+// Note: skips are left out of first iteration
+
+describe('sb-action-logger', () => {
+ it('has a default title "Action Logger"', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ expect(el.shadowRoot.querySelector('.header__title').innerText).to.equal('Action Logger');
+ });
+
+ it('has a title property / attribute that can be overridden', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ const titleEl = el.shadowRoot.querySelector('.header__title');
+
+ expect(titleEl.innerText).to.equal('Logging your favorite fruit');
+ });
+
+ describe('Logger behavior', () => {
+ it('is possible to send something to the logger using the log method', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ el.log('Hello, World!');
+
+ const loggerEl = el.shadowRoot.querySelector('.logger');
+
+ expect(loggerEl.children.length).to.equal(1);
+ expect(loggerEl.firstElementChild.innerText).to.equal('Hello, World!');
+ });
+
+ it('appends new logs to the logger', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ el.log('Hello, World!');
+ el.log('Hello, Planet!');
+ el.log('Hello, Earth!');
+ el.log('Hello, World!');
+ el.log('Hello, Planet!');
+
+ const loggerEl = el.shadowRoot.querySelector('.logger');
+
+ expect(loggerEl.children.length).to.equal(5);
+ });
+
+ it('shows a visual cue whenever something is logged to the logger', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ const cueEl = el.shadowRoot.querySelector('.header__log-cue-overlay');
+ expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.false;
+
+ el.log('Hello, World!');
+ expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.true;
+ });
+
+ it('has a visual counter that counts the amount of total logs', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ const cueEl = el.shadowRoot.querySelector('.header__log-cue-overlay');
+
+ expect(cueEl.classList.contains('.header__log-cue-overlay--slide')).to.be.false;
+
+ el.log('Hello, World!');
+ expect(cueEl.classList.contains('header__log-cue-overlay--slide')).to.be.true;
+ });
+
+ it('has a clear button that clears the logs and resets the counter', async () => {
+ const el = await fixture(html`
+
+ `);
+
+ el.log('Hello, World!');
+ el.log('Hello, Planet!');
+
+ const clearBtn = el.shadowRoot.querySelector('.header__clear');
+ clearBtn.click();
+
+ expect(el.shadowRoot.querySelector('.logger').children.length).to.equal(0);
+ });
+
+ it('duplicate consecutive logs are kept as one', async () => {
+ const el = await fixture(html`
+
+ `);
+ expect(el).to.be.true;
+ });
+ });
+
+ describe('Potential Additional Features', () => {
+ it.skip('duplicate consecutive adds a visual counter to count per duplicate', async () => {
+ const el = await fixture(html`
+
+ `);
+ expect(el).to.be.true;
+ });
+
+ // This is handy if you don't want to keep track of updates
+ it.skip('can be set to mode=simple for only showing a single log statement', async () => {
+ const el = await fixture(html`
+
+ `);
+ expect(el).to.be.true;
+ });
+
+ it.skip('fires a sb-action-logged event when something is logged to the logger', async () => {
+ const el = await fixture(html`
+
+ `);
+ expect(el).to.be.true;
+ });
+ });
+});
diff --git a/packages/helpers/sb-locale-switcher.js b/packages/helpers/sb-locale-switcher.js
new file mode 100644
index 000000000..b69598f0c
--- /dev/null
+++ b/packages/helpers/sb-locale-switcher.js
@@ -0,0 +1,3 @@
+import { SbLocaleSwitcher } from './sb-locale-switcher/src/SbLocaleSwitcher.js';
+
+window.customElements.define('sb-locale-switcher', SbLocaleSwitcher);
diff --git a/packages/helpers/sb-locale-switcher/src/SbLocaleSwitcher.js b/packages/helpers/sb-locale-switcher/src/SbLocaleSwitcher.js
new file mode 100644
index 000000000..09857fc87
--- /dev/null
+++ b/packages/helpers/sb-locale-switcher/src/SbLocaleSwitcher.js
@@ -0,0 +1,31 @@
+import { LitElement, html } from '@lion/core';
+
+export class SbLocaleSwitcher extends LitElement {
+ static get properties() {
+ return {
+ showLocales: { type: Array, attribute: 'show-locales' },
+ };
+ }
+
+ constructor() {
+ super();
+ this.showLocales = ['en-GB', 'en-US', 'en-AU', 'nl-NL', 'nl-BE'];
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ callback(locale) {
+ document.documentElement.lang = locale;
+ }
+
+ render() {
+ return html`
+ ${this.showLocales.map(
+ showLocale => html`
+
+ `,
+ )}
+ `;
+ }
+}