diff --git a/.gitignore b/.gitignore index 0a2fc3d4..1cf47052 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,8 @@ _site .cache .DS_Store +cdn dist docs/assets/images/sprite.svg node_modules src/react -cdn -web-types.json \ No newline at end of file diff --git a/custom-elements-manifest.config.js b/custom-elements-manifest.config.js index e5912cd0..8d3e7fe5 100644 --- a/custom-elements-manifest.config.js +++ b/custom-elements-manifest.config.js @@ -203,6 +203,7 @@ export default { ] }), customElementJetBrainsPlugin({ + outdir: './dist', excludeCss: true, referencesTemplate: (_, tag) => { return { diff --git a/docs/pages/components/alert.md b/docs/pages/components/alert.md index a2451e20..5ea30cbb 100644 --- a/docs/pages/components/alert.md +++ b/docs/pages/components/alert.md @@ -54,7 +54,7 @@ Set the `variant` attribute to change the alert's variant. Your settings have been updated
- Settings will take affect on next login. + Settings will take effect on next login.

@@ -102,7 +102,7 @@ const App = () => ( Your settings have been updated
- Settings will take affect on next login. + Settings will take effect on next login.
@@ -276,7 +276,7 @@ You should always use the `closable` attribute so users can dismiss the notifica Your settings have been updated
- Settings will take affect on next login. + Settings will take effect on next login.
@@ -361,7 +361,7 @@ const App = () => { Your settings have been updated
- Settings will take affect on next login. + Settings will take effect on next login. diff --git a/docs/pages/components/radio-button.md b/docs/pages/components/radio-button.md index c7bea66e..fe161c69 100644 --- a/docs/pages/components/radio-button.md +++ b/docs/pages/components/radio-button.md @@ -87,26 +87,26 @@ const App = () => ( Use the `size` attribute to change a radio button's size. ```html:preview - - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3
- - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3
- - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3 ``` @@ -115,26 +115,26 @@ import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button'; import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group'; const App = () => ( - - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3
- - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3
- - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3 ); ``` @@ -144,26 +144,26 @@ const App = () => ( Use the `pill` attribute to give radio buttons rounded edges. ```html:preview - - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3
- - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3
- - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3 ``` @@ -172,26 +172,26 @@ import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button'; import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group'; const App = () => ( - - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3
- - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3
- - Option 1 - Option 2 - Option 3 + + Option 1 + Option 2 + Option 3 ); ``` diff --git a/docs/pages/frameworks/react.md b/docs/pages/frameworks/react.md index 86f208ef..b87cb9c7 100644 --- a/docs/pages/frameworks/react.md +++ b/docs/pages/frameworks/react.md @@ -32,6 +32,10 @@ If you'd rather not use the CDN for assets, you can create a [build task](https: Now you can start using components! +### Preact + +Preact users facing type errors using components may benefit from setting "paths" in their tsconfig.json so that react types will instead resolve to preact/compat as described in [Preact's typescript documentation](https://preactjs.com/guide/v10/typescript/#typescript-preactcompat-configuration). + ## Usage ### Importing Components diff --git a/docs/pages/getting-started/usage.md b/docs/pages/getting-started/usage.md index 4032588e..6cf1075d 100644 --- a/docs/pages/getting-started/usage.md +++ b/docs/pages/getting-started/usage.md @@ -208,13 +208,13 @@ Shoelace ships with a file called `vscode.html-custom-data.json` that can be use } ``` -If `settings.json` already exists, simply add the above line to the root of the object. Note that you may need to restart VS Code for the changes to take affect. +If `settings.json` already exists, simply add the above line to the root of the object. Note that you may need to restart VS Code for the changes to take effect. -## JetBrains IDEs +### JetBrains IDEs If you are using a [JetBrains IDE](https://www.jetbrains.com/) and you are installing Shoelace from NPM, the editor will automatically detect the `web-types.json` file from the package and you should immediately see component information in your editor. -If you are installing from the CDN, you can [download a local copy](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace/cdn/web-types.json) and add it to the root of your project. +If you are installing from the CDN, you can [download a local copy](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace/dist/web-types.json) and add it to the root of your project. ### Other Editors diff --git a/docs/pages/resources/changelog.md b/docs/pages/resources/changelog.md index c6f73cec..b9f57b57 100644 --- a/docs/pages/resources/changelog.md +++ b/docs/pages/resources/changelog.md @@ -14,8 +14,13 @@ New versions of Shoelace are released as-needed and generally occur when a criti ## Next +- Added the `modal` property to `` and `` to support third-party modals [#1571] +- Fixed a bug in the autoloader causing it to register non-Shoelace elements [#1563] - Fixed a bug in `` that resulted in improper spacing between the label and the required asterisk [#1540] +- Fixed a bug in `` that caused icons to not load when the default library used a sprite [#1572] +- Removed error when a missing popup anchor is provided [#1548] - Updated `@ctrl/tinycolor` to 4.0.1 [#1542] +- Updated Bootstrap Icons to 1.11.0 ## 2.8.0 diff --git a/package-lock.json b/package-lock.json index 9379318a..f5355740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@ctrl/tinycolor": "^4.0.1", "@floating-ui/dom": "^1.2.1", - "@lit-labs/react": "^2.0.1", + "@lit-labs/react": "^2.0.3", "@shoelace-style/animations": "^1.1.0", "@shoelace-style/localize": "^3.1.1", "composed-offset-position": "^0.0.4", @@ -30,7 +30,7 @@ "@web/test-runner": "^0.15.0", "@web/test-runner-commands": "^0.6.5", "@web/test-runner-playwright": "^0.9.0", - "bootstrap-icons": "^1.10.5", + "bootstrap-icons": "^1.11.0", "browser-sync": "^2.29.3", "chalk": "^5.2.0", "change-case": "^4.1.2", @@ -1474,9 +1474,9 @@ } }, "node_modules/@lit-labs/react": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-2.0.1.tgz", - "integrity": "sha512-Nj+XB3HamqaWefN91lpFPJaqjJ78XzGkPWCedB4jyH22GBFEenpE9A/h8B/2dnIGXtNtd9D/RFpUdQ/dBtWFqA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-2.0.3.tgz", + "integrity": "sha512-lSvWbTrbxWqYv/iiOwbAEJfFZrKjO/QjJ4IEXhg43sdD5fNFz4wRXpVsntfVn4DnxpQd+NVRnrsF2USgK0XCTw==", "peerDependencies": { "@types/react": "17 || 18" } @@ -3988,9 +3988,9 @@ } }, "node_modules/bootstrap-icons": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.5.tgz", - "integrity": "sha512-oSX26F37V7QV7NCE53PPEL45d7EGXmBgHG3pDpZvcRaKVzWMqIRL9wcqJUyEha1esFtM3NJzvmxFXDxjJYD0jQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.0.tgz", + "integrity": "sha512-bLTbtACfUqwZf6f/xUYUb7bTRZC68QaQwwy9h1b96NPKfnwqzSatHqDypW6R2CBW7zUE7lP+O93GdZuPY3RIHA==", "dev": true, "funding": [ { @@ -18290,9 +18290,9 @@ } }, "@lit-labs/react": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-2.0.1.tgz", - "integrity": "sha512-Nj+XB3HamqaWefN91lpFPJaqjJ78XzGkPWCedB4jyH22GBFEenpE9A/h8B/2dnIGXtNtd9D/RFpUdQ/dBtWFqA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-2.0.3.tgz", + "integrity": "sha512-lSvWbTrbxWqYv/iiOwbAEJfFZrKjO/QjJ4IEXhg43sdD5fNFz4wRXpVsntfVn4DnxpQd+NVRnrsF2USgK0XCTw==", "requires": {} }, "@lit-labs/ssr-dom-shim": { @@ -20213,9 +20213,9 @@ } }, "bootstrap-icons": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.10.5.tgz", - "integrity": "sha512-oSX26F37V7QV7NCE53PPEL45d7EGXmBgHG3pDpZvcRaKVzWMqIRL9wcqJUyEha1esFtM3NJzvmxFXDxjJYD0jQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.0.tgz", + "integrity": "sha512-bLTbtACfUqwZf6f/xUYUb7bTRZC68QaQwwy9h1b96NPKfnwqzSatHqDypW6R2CBW7zUE7lP+O93GdZuPY3RIHA==", "dev": true }, "boxen": { diff --git a/package.json b/package.json index 5cca9356..a0de7626 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "Cory LaViska", "license": "MIT", "customElements": "dist/custom-elements.json", - "web-types": "./web-types.json", + "web-types": "./dist/web-types.json", "type": "module", "types": "dist/shoelace.d.ts", "jsdelivr": "./cdn/shoelace-autoloader.js", @@ -62,7 +62,7 @@ "dependencies": { "@ctrl/tinycolor": "^4.0.1", "@floating-ui/dom": "^1.2.1", - "@lit-labs/react": "^2.0.1", + "@lit-labs/react": "^2.0.3", "@shoelace-style/animations": "^1.1.0", "@shoelace-style/localize": "^3.1.1", "composed-offset-position": "^0.0.4", @@ -81,7 +81,7 @@ "@web/test-runner": "^0.15.0", "@web/test-runner-commands": "^0.6.5", "@web/test-runner-playwright": "^0.9.0", - "bootstrap-icons": "^1.10.5", + "bootstrap-icons": "^1.11.0", "browser-sync": "^2.29.3", "chalk": "^5.2.0", "change-case": "^4.1.2", diff --git a/scripts/build.js b/scripts/build.js index e46f4ef9..ef6c2846 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -34,6 +34,9 @@ const shoelaceVersion = JSON.stringify(packageData.version.toString()); async function buildTheDocs(watch = false) { return new Promise(async (resolve, reject) => { const afterSignal = '[eleventy.after]'; + + // Totally non-scientific way to handle errors. Perhaps its just better to resolve on stderr? :shrug: + const errorSignal = 'Original error stack trace:'; const args = ['@11ty/eleventy', '--quiet']; const output = []; @@ -65,6 +68,13 @@ async function buildTheDocs(watch = false) { resolve({ child, output }); } }); + + child.stderr.on('data', data => { + if (data.includes(errorSignal)) { + // This closes the dev server, not sure if thats what we want? + reject(output); + } + }); } else { child.on('close', () => { resolve({ child, output }); @@ -205,9 +215,8 @@ await nextTask('Running the TypeScript compiler', () => { }); // Copy the above steps to the CDN directory directly so we don't need to twice the work for nothing. -await nextTask(`Copying Web Types, Themes, Icons, and TS Types to "${cdndir}"`, async () => { +await nextTask(`Themes, Icons, and TS Types to "${cdndir}"`, async () => { await deleteAsync(cdndir); - await copy('./web-types.json', `${outdir}/web-types.json`); await copy(outdir, cdndir); }); diff --git a/src/components/details/details.component.ts b/src/components/details/details.component.ts index b7eab446..d8b83a43 100644 --- a/src/components/details/details.component.ts +++ b/src/components/details/details.component.ts @@ -87,6 +87,7 @@ export default class SlDetails extends ShoelaceElement { } disconnectedCallback() { + super.disconnectedCallback(); this.detailsObserver.disconnect(); } diff --git a/src/components/dialog/dialog.component.ts b/src/components/dialog/dialog.component.ts index 7e708db1..30afa3d5 100644 --- a/src/components/dialog/dialog.component.ts +++ b/src/components/dialog/dialog.component.ts @@ -60,6 +60,10 @@ import type { CSSResultGroup } from 'lit'; * @animation dialog.denyClose - The animation to use when a request to close the dialog is denied. * @animation dialog.overlay.show - The animation to use when showing the dialog's overlay. * @animation dialog.overlay.hide - The animation to use when hiding the dialog's overlay. + * + * @property modal - Exposes the internal modal utility that controls focus trapping. To temporarily disable focus + * trapping and allow third-party modals spawned from an active Shoelace modal, call `modal.activateExternal()` when + * the third-party modal opens. Upon closing, call `modal.deactivateExternal()` to restore Shoelace's focus trapping. */ export default class SlDialog extends ShoelaceElement { static styles: CSSResultGroup = styles; @@ -69,8 +73,8 @@ export default class SlDialog extends ShoelaceElement { private readonly hasSlotController = new HasSlotController(this, 'footer'); private readonly localize = new LocalizeController(this); - private modal = new Modal(this); private originalTrigger: HTMLElement | null; + public modal = new Modal(this); @query('.dialog') dialog: HTMLElement; @query('.dialog__panel') panel: HTMLElement; diff --git a/src/components/drawer/drawer.component.ts b/src/components/drawer/drawer.component.ts index 6ce9e885..d3a84171 100644 --- a/src/components/drawer/drawer.component.ts +++ b/src/components/drawer/drawer.component.ts @@ -68,6 +68,10 @@ import type { CSSResultGroup } from 'lit'; * @animation drawer.denyClose - The animation to use when a request to close the drawer is denied. * @animation drawer.overlay.show - The animation to use when showing the drawer's overlay. * @animation drawer.overlay.hide - The animation to use when hiding the drawer's overlay. + * + * @property modal - Exposes the internal modal utility that controls focus trapping. To temporarily disable focus + * trapping and allow third-party modals spawned from an active Shoelace modal, call `modal.activateExternal()` when + * the third-party modal opens. Upon closing, call `modal.deactivateExternal()` to restore Shoelace's focus trapping. */ export default class SlDrawer extends ShoelaceElement { static styles: CSSResultGroup = styles; @@ -75,8 +79,8 @@ export default class SlDrawer extends ShoelaceElement { private readonly hasSlotController = new HasSlotController(this, 'footer'); private readonly localize = new LocalizeController(this); - private modal = new Modal(this); private originalTrigger: HTMLElement | null; + public modal = new Modal(this); @query('.drawer') drawer: HTMLElement; @query('.drawer__panel') panel: HTMLElement; diff --git a/src/components/icon/icon.component.ts b/src/components/icon/icon.component.ts index 4f379de9..542ede6e 100644 --- a/src/components/icon/icon.component.ts +++ b/src/components/icon/icon.component.ts @@ -15,6 +15,11 @@ type SVGResult = HTMLTemplateResult | SVGSVGElement | typeof RETRYABLE_ERROR | t let parser: DOMParser; const iconCache = new Map>(); +interface IconSource { + url?: string; + fromLibrary: boolean; +} + /** * @summary Icons are symbols that can be used to represent various options within an application. * @documentation https://shoelace.style/components/icon @@ -104,12 +109,19 @@ export default class SlIcon extends ShoelaceElement { unwatchIcon(this); } - private getUrl() { + private getIconSource(): IconSource { const library = getIconLibrary(this.library); if (this.name && library) { - return library.resolver(this.name); + return { + url: library.resolver(this.name), + fromLibrary: true + }; } - return this.src; + + return { + url: this.src, + fromLibrary: false + }; } @watch('label') @@ -129,8 +141,8 @@ export default class SlIcon extends ShoelaceElement { @watch(['name', 'src', 'library']) async setIcon() { - const library = getIconLibrary(this.library); - const url = this.getUrl(); + const { url, fromLibrary } = this.getIconSource(); + const library = fromLibrary ? getIconLibrary(this.library) : undefined; if (!url) { this.svg = null; @@ -154,7 +166,7 @@ export default class SlIcon extends ShoelaceElement { iconCache.delete(url); } - if (url !== this.getUrl()) { + if (url !== this.getIconSource().url) { // If the url has changed while fetching the icon, ignore this request return; } diff --git a/src/components/mutation-observer/mutation-observer.component.ts b/src/components/mutation-observer/mutation-observer.component.ts index 61b05ce4..85ed39e7 100644 --- a/src/components/mutation-observer/mutation-observer.component.ts +++ b/src/components/mutation-observer/mutation-observer.component.ts @@ -52,6 +52,7 @@ export default class SlMutationObserver extends ShoelaceElement { } disconnectedCallback() { + super.disconnectedCallback(); this.stopObserver(); } diff --git a/src/components/popup/popup.component.ts b/src/components/popup/popup.component.ts index 3ab4c0d1..93bda74c 100644 --- a/src/components/popup/popup.component.ts +++ b/src/components/popup/popup.component.ts @@ -198,6 +198,7 @@ export default class SlPopup extends ShoelaceElement { } disconnectedCallback() { + super.disconnectedCallback(); this.stop(); } @@ -246,13 +247,10 @@ export default class SlPopup extends ShoelaceElement { this.anchorEl = this.anchorEl.assignedElements({ flatten: true })[0] as HTMLElement; } - if (!this.anchorEl) { - throw new Error( - 'Invalid anchor element: no anchor could be found using the anchor slot or the anchor attribute.' - ); + // If the anchor is valid, start it up + if (this.anchorEl) { + this.start(); } - - this.start(); } private start() { diff --git a/src/components/tab-group/tab-group.component.ts b/src/components/tab-group/tab-group.component.ts index f1753807..27ecf18a 100644 --- a/src/components/tab-group/tab-group.component.ts +++ b/src/components/tab-group/tab-group.component.ts @@ -117,6 +117,7 @@ export default class SlTabGroup extends ShoelaceElement { } disconnectedCallback() { + super.disconnectedCallback(); this.mutationObserver.disconnect(); this.resizeObserver.unobserve(this.nav); } diff --git a/src/internal/modal.ts b/src/internal/modal.ts index 680a0e97..40128393 100644 --- a/src/internal/modal.ts +++ b/src/internal/modal.ts @@ -5,6 +5,7 @@ let activeModals: HTMLElement[] = []; export default class Modal { element: HTMLElement; + isExternalActivated: boolean; tabDirection: 'forward' | 'backward' = 'forward'; currentFocus: HTMLElement | null; @@ -12,6 +13,7 @@ export default class Modal { this.element = element; } + /** Activates focus trapping. */ activate() { activeModals.push(this.element); document.addEventListener('focusin', this.handleFocusIn); @@ -19,6 +21,7 @@ export default class Modal { document.addEventListener('keyup', this.handleKeyUp); } + /** Deactivates focus trapping. */ deactivate() { activeModals = activeModals.filter(modal => modal !== this.element); this.currentFocus = null; @@ -27,13 +30,24 @@ export default class Modal { document.removeEventListener('keyup', this.handleKeyUp); } + /** Determines if this modal element is currently active or not. */ isActive() { // The "active" modal is always the most recent one shown return activeModals[activeModals.length - 1] === this.element; } - checkFocus() { - if (this.isActive()) { + /** Activates external modal behavior and temporarily disables focus trapping. */ + activateExternal() { + this.isExternalActivated = true; + } + + /** Deactivates external modal behavior and re-enables focus trapping. */ + deactivateExternal() { + this.isExternalActivated = false; + } + + private checkFocus() { + if (this.isActive() && !this.isExternalActivated) { const tabbableElements = getTabbableElements(this.element); if (!this.element.matches(':focus-within')) { const start = tabbableElements[0]; @@ -56,11 +70,9 @@ export default class Modal { return getTabbableElements(this.element).findIndex(el => el === this.currentFocus); } - /** - * Checks if the `startElement` is already focused. This is important if the modal already - * has an existing focus prior to the first tab key. - */ - startElementAlreadyFocused(startElement: HTMLElement) { + // Checks if the `startElement` is already focused. This is important if the modal already has an existing focus prior + // to the first tab key. + private startElementAlreadyFocused(startElement: HTMLElement) { for (const activeElement of activeElements()) { if (startElement === activeElement) { return true; @@ -70,8 +82,8 @@ export default class Modal { return false; } - handleKeyDown = (event: KeyboardEvent) => { - if (event.key !== 'Tab') return; + private handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Tab' || this.isExternalActivated) return; if (event.shiftKey) { this.tabDirection = 'backward'; diff --git a/src/shoelace-autoloader.ts b/src/shoelace-autoloader.ts index a0d66c00..4b42c452 100644 --- a/src/shoelace-autoloader.ts +++ b/src/shoelace-autoloader.ts @@ -15,13 +15,13 @@ const observer = new MutationObserver(mutations => { */ export async function discover(root: Element | ShadowRoot) { const rootTagName = root instanceof Element ? root.tagName.toLowerCase() : ''; - const rootIsCustomElement = rootTagName?.includes('-'); + const rootIsShoelaceElement = rootTagName?.startsWith('sl-'); const tags = [...root.querySelectorAll(':not(:defined)')] .map(el => el.tagName.toLowerCase()) .filter(tag => tag.startsWith('sl-')); - // If the root element is an undefined custom element, add it to the list - if (rootIsCustomElement && !customElements.get(rootTagName)) { + // If the root element is an undefined Shoelace component, add it to the list + if (rootIsShoelaceElement && !customElements.get(rootTagName)) { tags.push(rootTagName); } @@ -35,14 +35,14 @@ export async function discover(root: Element | ShadowRoot) { * Registers an element by tag name. */ function register(tagName: string): Promise { - const tagWithoutPrefix = tagName.replace(/^sl-/i, ''); - const path = getBasePath(`components/${tagWithoutPrefix}/${tagWithoutPrefix}.js`); - // If the element is already defined, there's nothing more to do if (customElements.get(tagName)) { return Promise.resolve(); } + const tagWithoutPrefix = tagName.replace(/^sl-/i, ''); + const path = getBasePath(`components/${tagWithoutPrefix}/${tagWithoutPrefix}.js`); + // Register it return new Promise((resolve, reject) => { import(path).then(() => resolve()).catch(() => reject(new Error(`Unable to autoload <${tagName}> from ${path}`))); diff --git a/src/translations/de.ts b/src/translations/de.ts index 1894e600..21b1e069 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -13,7 +13,7 @@ const translation: Translation = { copy: 'Kopieren', currentValue: 'Aktueller Wert', error: 'Fehler', - goToSlide: (slide, count) => `Gehen Sie zu Folie ${slide} von ${count}`, + goToSlide: (slide, count) => `Zu Folie ${slide} von ${count} gehen`, hidePassword: 'Passwort verbergen', loading: 'Wird geladen', nextSlide: 'Nächste Folie', @@ -28,7 +28,7 @@ const translation: Translation = { resize: 'Größe ändern', scrollToEnd: 'Zum Ende scrollen', scrollToStart: 'Zum Anfang scrollen', - selectAColorFromTheScreen: 'Wähle eine Farbe vom Bildschirm', + selectAColorFromTheScreen: 'Farbe vom Bildschirm auswählen', showPassword: 'Passwort anzeigen', slideNum: slide => `Folie ${slide}`, toggleColorFormat: 'Farbformat umschalten'