@jungherz-de/notionkit-elements 1.19.0 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +8 -4
- package/SKILL.md +31 -29
- package/dist/components/base.js +2 -0
- package/dist/components/nk-theme-toggle.js +163 -22
- package/dist/components/nk-tooltip.js +61 -5
- package/dist/notionkit-elements.esm.js +228 -29
- package/dist/notionkit-elements.esm.min.js +1 -1
- package/dist/notionkit-elements.esm.min.js.map +1 -1
- package/dist/notionkit-elements.js +228 -29
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/components/overlays/nk-tooltip.js +61 -5
- package/src/components/shell/nk-theme-toggle.js +163 -22
- package/src/util/strings.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/notionkit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.1",
|
|
4
4
|
"description": "Vanilla JavaScript Web Components wrapping the NotionKit CSS foundation – <nk-*> custom elements with Shadow DOM, automatic theme sync and native form participation.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jungherz GmbH <info@jungherz.com> (https://www.jungherz.com)",
|
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
"dark-mode"
|
|
64
64
|
],
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"@jungherz-de/notionkit": ">=1.
|
|
66
|
+
"@jungherz-de/notionkit": ">=1.20.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@jungherz-de/notionkit": "^1.
|
|
69
|
+
"@jungherz-de/notionkit": "^1.20.0",
|
|
70
70
|
"@playwright/test": "^1.50.0",
|
|
71
71
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
72
72
|
"@rollup/plugin-terser": "^0.4.0",
|
|
@@ -23,6 +23,17 @@ import { deepActiveElement } from '../../util/focus.js';
|
|
|
23
23
|
// [data-tooltip], a press or Escape. A line break in the text is kept.
|
|
24
24
|
// While it shows, the target's aria-describedby names it (in the same
|
|
25
25
|
// document or shadow root).
|
|
26
|
+
//
|
|
27
|
+
// Following the pointer and the focus inside a shadow root (1.20.1):
|
|
28
|
+
// pointerover, focusin and focusout carry a relatedTarget, and when target
|
|
29
|
+
// and relatedTarget share a shadow tree the DOM standard ends the event path
|
|
30
|
+
// at its host – the document never hears of a move from one [data-tooltip]
|
|
31
|
+
// to the next in the same root. So the pointer is also read from
|
|
32
|
+
// pointermove, which has no relatedTarget, and acted on only when the
|
|
33
|
+
// element under it changes; and a key that moved the focus – Tab, the arrow
|
|
34
|
+
// keys of a group – is caught on keyup, comparing the focused element with
|
|
35
|
+
// the one before the key went down. A move by script alone inside one
|
|
36
|
+
// shadow root stays unseen.
|
|
26
37
|
const hostSheet = new CSSStyleSheet();
|
|
27
38
|
hostSheet.replaceSync(`:host { display: block; position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none; }`);
|
|
28
39
|
let count = 0;
|
|
@@ -100,14 +111,50 @@ class NkTooltip extends NkElement {
|
|
|
100
111
|
this.show(el, own ? null : el.getAttribute('data-tooltip'), own ? this.getAttribute('shortcut') : el.getAttribute('data-tooltip-key'));
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
/**
|
|
115
|
+
* The pointer is over `el` now (null: no [data-tooltip]). pointerover acts
|
|
116
|
+
* on every boundary it crosses, as it always has; pointermove – the only
|
|
117
|
+
* one that reaches the document from inside a shadow root – only when the
|
|
118
|
+
* element under the pointer changed, so a hand resting on the page leaves
|
|
119
|
+
* a tooltip the keyboard or show(rect) opened alone.
|
|
120
|
+
*/
|
|
121
|
+
_pointer(e, move) {
|
|
122
|
+
const el = e.pointerType === 'touch' ? null : this._targetOf(e);
|
|
123
|
+
if (move && el === this._hover) return;
|
|
124
|
+
this._hover = el;
|
|
125
|
+
if (el === this._target) return;
|
|
126
|
+
this.hide();
|
|
127
|
+
if (!el) return;
|
|
128
|
+
this._target = el;
|
|
129
|
+
this._timer = setTimeout(() => this._showFor(el), this._delay);
|
|
130
|
+
}
|
|
131
|
+
|
|
103
132
|
setupEvents() {
|
|
104
|
-
this._onOver = (e) =>
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
this._onOver = (e) => this._pointer(e, false);
|
|
134
|
+
this._onMove = (e) => this._pointer(e, true);
|
|
135
|
+
// The pointer left the window (no relatedTarget): a tooltip – shown or
|
|
136
|
+
// still waiting – for what it was on goes with it. One the keyboard or
|
|
137
|
+
// show(rect) opened is not the pointer's and stays.
|
|
138
|
+
this._onOut = (e) => {
|
|
139
|
+
if (e.relatedTarget || e.pointerType === 'touch') return;
|
|
140
|
+
if (this._target && this._target === this._hover) this.hide();
|
|
141
|
+
this._hover = null;
|
|
142
|
+
};
|
|
143
|
+
// Capture, before an element's own keydown moves the focus (a roving tab
|
|
144
|
+
// stop does); not for a held key's repeats, so keyup compares with where
|
|
145
|
+
// the focus was before the key went down.
|
|
146
|
+
this._onKeyDown = (e) => { if (!e.repeat) this._focusBefore = deepActiveElement(); };
|
|
147
|
+
this._onKeyUp = (e) => {
|
|
148
|
+
const active = deepActiveElement();
|
|
149
|
+
if (active === this._focusBefore) return;
|
|
150
|
+
this._focusBefore = active;
|
|
151
|
+
// keyup goes to the focused element, so its path is the focus's path.
|
|
152
|
+
const el = this._targetOf(e);
|
|
153
|
+
if (el && el === this._target) return; // focusin got there first
|
|
107
154
|
this.hide();
|
|
108
|
-
if (!el) return;
|
|
155
|
+
if (!el || !active?.matches(':focus-visible')) return;
|
|
109
156
|
this._target = el;
|
|
110
|
-
this.
|
|
157
|
+
this._showFor(el);
|
|
111
158
|
};
|
|
112
159
|
this._onFocus = (e) => {
|
|
113
160
|
const el = this._targetOf(e);
|
|
@@ -119,7 +166,11 @@ class NkTooltip extends NkElement {
|
|
|
119
166
|
this._onHide = () => this.hide();
|
|
120
167
|
this._onKey = (e) => { if (e.key === 'Escape' && this.open) this.hide(); };
|
|
121
168
|
document.addEventListener('pointerover', this._onOver);
|
|
169
|
+
document.addEventListener('pointermove', this._onMove, { capture: true, passive: true });
|
|
170
|
+
document.addEventListener('pointerout', this._onOut);
|
|
122
171
|
document.addEventListener('focusin', this._onFocus);
|
|
172
|
+
document.addEventListener('keydown', this._onKeyDown, true);
|
|
173
|
+
document.addEventListener('keyup', this._onKeyUp, true);
|
|
123
174
|
document.addEventListener('focusout', this._onHide);
|
|
124
175
|
document.addEventListener('pointerdown', this._onHide, true);
|
|
125
176
|
document.addEventListener('scroll', this._onHide, true);
|
|
@@ -130,13 +181,18 @@ class NkTooltip extends NkElement {
|
|
|
130
181
|
|
|
131
182
|
teardownEvents() {
|
|
132
183
|
document.removeEventListener('pointerover', this._onOver);
|
|
184
|
+
document.removeEventListener('pointermove', this._onMove, { capture: true });
|
|
185
|
+
document.removeEventListener('pointerout', this._onOut);
|
|
133
186
|
document.removeEventListener('focusin', this._onFocus);
|
|
187
|
+
document.removeEventListener('keydown', this._onKeyDown, true);
|
|
188
|
+
document.removeEventListener('keyup', this._onKeyUp, true);
|
|
134
189
|
document.removeEventListener('focusout', this._onHide);
|
|
135
190
|
document.removeEventListener('pointerdown', this._onHide, true);
|
|
136
191
|
document.removeEventListener('scroll', this._onHide, true);
|
|
137
192
|
document.removeEventListener('wheel', this._onHide, { capture: true });
|
|
138
193
|
document.removeEventListener('keydown', this._onKey);
|
|
139
194
|
this.hide();
|
|
195
|
+
this._hover = this._focusBefore = null;
|
|
140
196
|
}
|
|
141
197
|
|
|
142
198
|
onAttributeChanged(name) { if (name === 'shortcut' && !this._text.data) this._fill(null); if (name === 'for') this.hide(); }
|
|
@@ -7,62 +7,203 @@ import { NkElement, getCurrentTheme } from '../../base.js';
|
|
|
7
7
|
// system preference on first connect when <html> carries no theme yet, and
|
|
8
8
|
// accepts postMessage({ nkTheme }) from a parent page (the landing page's
|
|
9
9
|
// embedded demo uses that).
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
//
|
|
11
|
+
// <nk-theme-toggle modes="auto light dark"> adds a third state (1.20.0):
|
|
12
|
+
// `auto` follows prefers-color-scheme, live, and is what no stored value
|
|
13
|
+
// means – so the documented inline script before first paint keeps working
|
|
14
|
+
// and a choice made without `modes` still counts. The glyph then shows the
|
|
15
|
+
// mode (☀️, 🌙, both for auto) instead of the next theme, the title names
|
|
16
|
+
// the mode and the next one, other tabs follow through the storage event,
|
|
17
|
+
// and `theme-color` keeps <meta name="theme-color"> in step. Two states stay
|
|
18
|
+
// the default, where nothing about this element changes.
|
|
19
|
+
const GLYPH = { light: '☀️', dark: '🌙' };
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
class NkThemeToggle extends NkElement {
|
|
22
|
+
static get observedAttributes() { return ['storage-key', 'title', 'modes', 'theme-color']; }
|
|
14
23
|
|
|
15
24
|
render() {
|
|
16
25
|
this._btn = this.createElement('button', ['nk-topbar-btn', 'nk-theme-toggle'], { type: 'button' });
|
|
17
|
-
this._syncTitle();
|
|
18
26
|
this._wrapper.appendChild(this._btn);
|
|
19
|
-
|
|
27
|
+
this._readThemeColors();
|
|
28
|
+
if (this.auto) {
|
|
29
|
+
// With `auto` this element owns the theme: a stored choice, else the
|
|
30
|
+
// system. The inline script before first paint has set the same value.
|
|
31
|
+
this._applyMode();
|
|
32
|
+
} else {
|
|
33
|
+
const root = document.documentElement;
|
|
34
|
+
let stored = null;
|
|
35
|
+
try { stored = localStorage.getItem(this.storageKey); } catch {}
|
|
36
|
+
if (stored === 'light' || stored === 'dark') root.setAttribute('data-theme', stored);
|
|
37
|
+
else if (!root.hasAttribute('data-theme')) root.setAttribute('data-theme', this.systemTheme);
|
|
38
|
+
this._syncThemeColor();
|
|
39
|
+
this._syncGlyph();
|
|
40
|
+
this._syncTitle();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Modes ──
|
|
45
|
+
|
|
46
|
+
/** The states offered, in the author's order; without a theme among them the default two. */
|
|
47
|
+
get modes() {
|
|
48
|
+
const list = [...new Set((this.getAttribute('modes') || '').toLowerCase().split(/\s+/))].filter(m => m === 'auto' || m === 'light' || m === 'dark');
|
|
49
|
+
return list.some(m => m !== 'auto') ? list : ['light', 'dark'];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Whether `auto` is offered – the only thing that changes this element's behaviour. */
|
|
53
|
+
get auto() { return this.modes.includes('auto'); }
|
|
54
|
+
|
|
55
|
+
/** One query object per element: a listener on a throwaway one is neither removable nor safe from collection. */
|
|
56
|
+
get systemQuery() { return (this._query ??= matchMedia('(prefers-color-scheme: dark)')); }
|
|
57
|
+
|
|
58
|
+
get systemTheme() { return this.systemQuery.matches ? 'dark' : 'light'; }
|
|
59
|
+
|
|
60
|
+
/** The mode in force: 'auto' (nothing stored), 'light' or 'dark'. Without `auto` the theme on screen. */
|
|
61
|
+
get mode() {
|
|
62
|
+
if (!this.auto) return getCurrentTheme();
|
|
20
63
|
let stored = null;
|
|
21
|
-
try { stored = localStorage.getItem(this.storageKey); } catch {}
|
|
22
|
-
if (stored === 'light' || stored === 'dark')
|
|
23
|
-
|
|
64
|
+
try { stored = localStorage.getItem(this.storageKey); } catch { return this._unsaved || 'auto'; }
|
|
65
|
+
if (stored === 'light' || stored === 'dark') return stored;
|
|
66
|
+
return this._unsaved || 'auto';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The theme on screen for a mode: `auto` is whatever the system says. */
|
|
70
|
+
themeFor(mode = this.mode) { return mode === 'auto' ? this.systemTheme : mode; }
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The mode a click moves to. From `auto` to the theme that is *not* on
|
|
74
|
+
* screen, then to the other one, then back to `auto` – so every click but
|
|
75
|
+
* the last changes something visibly. Without `auto` it cycles the themes.
|
|
76
|
+
*/
|
|
77
|
+
nextMode(mode = this.mode) {
|
|
78
|
+
const modes = this.modes, themes = modes.filter(m => m !== 'auto'), system = this.systemTheme;
|
|
79
|
+
if (!modes.includes('auto')) return themes[(themes.indexOf(mode) + 1) % themes.length];
|
|
80
|
+
if (mode === 'auto') return themes.find(m => m !== system) ?? themes[0];
|
|
81
|
+
if (mode !== system) return themes.includes(system) ? system : 'auto';
|
|
82
|
+
return 'auto';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Applying ──
|
|
86
|
+
|
|
87
|
+
_applyMode(mode = this.mode) {
|
|
88
|
+
const theme = this.themeFor(mode);
|
|
89
|
+
const root = document.documentElement;
|
|
90
|
+
// Only on a change: the observer in base.js mirrors data-theme into every
|
|
91
|
+
// element, and auto re-applies the same value on every system change.
|
|
92
|
+
if (root.getAttribute('data-theme') !== theme) root.setAttribute('data-theme', theme);
|
|
93
|
+
this._syncThemeColor(mode);
|
|
24
94
|
this._syncGlyph();
|
|
95
|
+
this._syncTitle();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The page's own theme-color values, per medium, before this element touched them. */
|
|
99
|
+
_readThemeColors() {
|
|
100
|
+
this._bars = [...document.querySelectorAll('meta[name="theme-color"]')].map(meta => ({
|
|
101
|
+
meta, dark: (meta.getAttribute('media') || '').includes('dark'), content: meta.getAttribute('content'),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `theme-color`: while a theme is fixed every meta carries the colour of
|
|
107
|
+
* that theme, so the browser bar follows the choice; back on `auto` each
|
|
108
|
+
* one carries its own again. The colours are the page's – never invented.
|
|
109
|
+
*/
|
|
110
|
+
_syncThemeColor(mode = this.mode) {
|
|
111
|
+
if (!this.hasAttribute('theme-color') || !this._bars?.length) return;
|
|
112
|
+
const auto = mode === 'auto', dark = this.themeFor(mode) === 'dark';
|
|
113
|
+
const fixed = (this._bars.find(bar => bar.dark === dark) ?? this._bars[0]).content;
|
|
114
|
+
for (const bar of this._bars) if (bar.content !== null) bar.meta.setAttribute('content', auto ? bar.content : fixed);
|
|
25
115
|
}
|
|
26
116
|
|
|
27
117
|
_syncGlyph() {
|
|
28
|
-
if (this._btn)
|
|
118
|
+
if (!this._btn) return;
|
|
119
|
+
if (!this.auto) {
|
|
120
|
+
// Two states: the glyph is the next theme, as it has always been.
|
|
121
|
+
this._btn.replaceChildren(getCurrentTheme() === 'dark' ? GLYPH.light : GLYPH.dark);
|
|
122
|
+
this._btn.removeAttribute('aria-label');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const mode = this.mode;
|
|
126
|
+
const glyph = this.createElement('span', ['nk-theme-glyph'], { 'aria-hidden': 'true' });
|
|
127
|
+
if (mode === 'auto') glyph.classList.add('auto');
|
|
128
|
+
for (const text of mode === 'auto' ? [GLYPH.light, GLYPH.dark] : [GLYPH[mode]]) {
|
|
129
|
+
glyph.appendChild(Object.assign(document.createElement('span'), { textContent: text }));
|
|
130
|
+
}
|
|
131
|
+
this._btn.replaceChildren(glyph);
|
|
132
|
+
// Two emojis are no accessible name; the title's text is.
|
|
133
|
+
this._btn.setAttribute('aria-label', this._titleText());
|
|
29
134
|
}
|
|
30
135
|
|
|
31
136
|
_syncTheme(theme) {
|
|
32
137
|
super._syncTheme(theme);
|
|
33
138
|
this._syncGlyph();
|
|
139
|
+
this._syncTitle();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_titleText() {
|
|
143
|
+
const own = this.getAttribute('title');
|
|
144
|
+
if (own) return own;
|
|
145
|
+
if (!this.auto) return this.str('toggleTheme');
|
|
146
|
+
const name = (mode) => this.str(mode === 'auto' ? 'themeAuto' : mode === 'dark' ? 'themeDark' : 'themeLight');
|
|
147
|
+
return this.str('appearance').replace('{mode}', name(this.mode)).replace('{next}', name(this.nextMode()));
|
|
34
148
|
}
|
|
35
149
|
|
|
150
|
+
_syncTitle() { if (this._btn) this._btn.title = this._titleText(); }
|
|
151
|
+
onStringsChanged() { this._syncTitle(); if (this.auto) this._syncGlyph(); }
|
|
152
|
+
|
|
36
153
|
setupEvents() {
|
|
37
|
-
this._onClick = () => this.apply(
|
|
38
|
-
this._onMessage = (e) => { const t = e.data?.nkTheme; if (t === 'light' || t === 'dark') this.apply(t); };
|
|
154
|
+
this._onClick = () => this.apply(this.nextMode());
|
|
155
|
+
this._onMessage = (e) => { const t = e.data?.nkTheme; if (t === 'light' || t === 'dark' || (t === 'auto' && this.auto)) this.apply(t); };
|
|
156
|
+
// `auto` has to notice a system switch while the page is open, and a
|
|
157
|
+
// choice made in another tab. Both only matter where `auto` is offered.
|
|
158
|
+
this._onSystem = () => { if (this.mode === 'auto') this._applyMode(); };
|
|
159
|
+
this._onStorage = (e) => { if (e.key === this.storageKey || e.key === null) this._applyMode(); };
|
|
39
160
|
this._btn.addEventListener('click', this._onClick);
|
|
40
161
|
window.addEventListener('message', this._onMessage);
|
|
162
|
+
if (this.auto) {
|
|
163
|
+
this.systemQuery.addEventListener('change', this._onSystem);
|
|
164
|
+
window.addEventListener('storage', this._onStorage);
|
|
165
|
+
}
|
|
41
166
|
}
|
|
42
167
|
|
|
43
168
|
teardownEvents() {
|
|
44
169
|
this._btn?.removeEventListener('click', this._onClick);
|
|
45
170
|
window.removeEventListener('message', this._onMessage);
|
|
171
|
+
if (this._onSystem) this.systemQuery.removeEventListener('change', this._onSystem);
|
|
172
|
+
window.removeEventListener('storage', this._onStorage);
|
|
46
173
|
}
|
|
47
174
|
|
|
48
|
-
_syncTitle() { this._btn.title = this.getAttribute('title') || this.str('toggleTheme'); }
|
|
49
|
-
onStringsChanged() { this._syncTitle(); }
|
|
50
|
-
|
|
51
175
|
onAttributeChanged(name) {
|
|
52
|
-
if (
|
|
176
|
+
if (!this._btn) return;
|
|
177
|
+
if (name === 'modes') {
|
|
178
|
+
// Re-arm: the listeners above hang on whether `auto` is offered.
|
|
179
|
+
if (this.isConnected) { this.teardownEvents(); this.setupEvents(); }
|
|
180
|
+
this.auto ? this._applyMode() : this._syncGlyph();
|
|
181
|
+
}
|
|
182
|
+
this._syncTitle();
|
|
53
183
|
}
|
|
54
184
|
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
185
|
+
/**
|
|
186
|
+
* Sets the mode on <html>, persists it and reports it. `light` / `dark`
|
|
187
|
+
* are stored, `auto` (only where it is offered) clears the entry – that is
|
|
188
|
+
* what the inline script before first paint reads.
|
|
189
|
+
*/
|
|
190
|
+
apply(mode) {
|
|
191
|
+
const wanted = mode === 'auto' && this.auto ? 'auto' : mode === 'light' || mode === 'dark' ? mode : this.themeFor(mode);
|
|
192
|
+
try {
|
|
193
|
+
if (wanted === 'auto') localStorage.removeItem(this.storageKey);
|
|
194
|
+
else localStorage.setItem(this.storageKey, wanted);
|
|
195
|
+
this._unsaved = null;
|
|
196
|
+
} catch { this._unsaved = wanted; }
|
|
197
|
+
this._applyMode(wanted);
|
|
198
|
+
this.emit('nk-change', { value: this.auto ? wanted : this.themeFor(wanted), theme: this.themeFor(wanted) });
|
|
61
199
|
}
|
|
62
200
|
|
|
63
201
|
get storageKey() { return this.getAttribute('storage-key') || 'nk-theme'; }
|
|
64
|
-
|
|
202
|
+
/** The mode: 'auto' | 'light' | 'dark'. Without `auto` the theme, as before. */
|
|
203
|
+
get value() { return this.auto ? this.mode : getCurrentTheme(); }
|
|
65
204
|
set value(v) { this.apply(v); }
|
|
205
|
+
/** The theme on screen – 'light' or 'dark', whatever the mode. */
|
|
206
|
+
get theme() { return this.themeFor(); }
|
|
66
207
|
}
|
|
67
208
|
|
|
68
209
|
customElements.define('nk-theme-toggle', NkThemeToggle);
|
package/src/util/strings.js
CHANGED
|
@@ -16,6 +16,7 @@ const BUILT_IN = {
|
|
|
16
16
|
commentPlaceholder: 'Comment …', send: 'Send', askPlaceholder: 'Ask something …',
|
|
17
17
|
commandPlaceholder: 'Search or type a command …', emojiPlaceholder: 'Search…',
|
|
18
18
|
toggleTheme: 'Toggle light / dark', synced: '⟳ synced',
|
|
19
|
+
appearance: 'Appearance: {mode} – click: {next}', themeAuto: 'automatic (follows the system)', themeLight: 'light', themeDark: 'dark',
|
|
19
20
|
message: 'Message', breadcrumb: 'Breadcrumb', commandPalette: 'Command palette', comment: 'Comment',
|
|
20
21
|
searchEmoji: 'Search emoji', settings: 'Settings', title: 'Title', changeIcon: 'Change icon',
|
|
21
22
|
add: 'Add', more: 'More',
|
|
@@ -30,6 +31,7 @@ const BUILT_IN = {
|
|
|
30
31
|
commentPlaceholder: 'Kommentieren …', send: 'Senden', askPlaceholder: 'Frag etwas …',
|
|
31
32
|
commandPlaceholder: 'Suchen oder Befehl eingeben …', emojiPlaceholder: 'Suchen …',
|
|
32
33
|
toggleTheme: 'Hell / Dunkel', synced: '⟳ synchronisiert',
|
|
34
|
+
appearance: 'Darstellung: {mode} – Klick: {next}', themeAuto: 'automatisch (wie das System)', themeLight: 'hell', themeDark: 'dunkel',
|
|
33
35
|
message: 'Nachricht', breadcrumb: 'Pfad', commandPalette: 'Befehlspalette', comment: 'Kommentar',
|
|
34
36
|
searchEmoji: 'Emoji suchen', settings: 'Einstellungen', title: 'Titel', changeIcon: 'Symbol ändern',
|
|
35
37
|
add: 'Hinzufügen', more: 'Mehr',
|