@schukai/monster 4.109.0 → 4.110.0

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.
@@ -0,0 +1,178 @@
1
+ /* monster-sheet – Excel-like grid control */
2
+
3
+ :host {
4
+ --sheet-border-color: var(--monster-theme-control-border-color);
5
+ --sheet-border-strong: var(--monster-color-primary-4);
6
+ --sheet-border-width: var(--monster-theme-control-border-width, 1px);
7
+ --sheet-border-style: var(--monster-theme-control-border-style, solid);
8
+ --sheet-header-bg: var(--monster-bg-color-primary-2);
9
+ --sheet-header-fg: var(--monster-color-primary-1);
10
+ --sheet-cell-bg: var(--monster-bg-color-primary-1);
11
+ --sheet-cell-fg: var(--monster-color-primary-1);
12
+ --sheet-grid-gap: 0;
13
+ --sheet-row-header-width: 56px;
14
+ --sheet-header-height: 32px;
15
+ --sheet-cell-padding-inline: 8px;
16
+ --sheet-cell-padding-block: 6px;
17
+ --sheet-resize-handle: 6px;
18
+ color: var(--sheet-cell-fg);
19
+ display: block;
20
+ font-family: var(--monster-font-family);
21
+ }
22
+
23
+ [part="control"] {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: var(--monster-space-4);
27
+ height: 100%;
28
+ }
29
+
30
+ [part="toolbar"] {
31
+ display: flex;
32
+ gap: var(--monster-space-4);
33
+ }
34
+
35
+ [part="grid-wrapper"] {
36
+ border: var(--sheet-border-width) var(--sheet-border-style)
37
+ var(--sheet-border-color);
38
+ overflow: auto;
39
+ position: relative;
40
+ flex: 1;
41
+ min-height: 0;
42
+ }
43
+
44
+ [part="grid"] {
45
+ display: grid;
46
+ gap: var(--sheet-grid-gap);
47
+ min-width: max-content;
48
+ background: var(--sheet-cell-bg);
49
+ }
50
+
51
+ :host([data-virtualized]) [part="grid"] {
52
+ position: absolute;
53
+ top: 0;
54
+ left: 0;
55
+ will-change: transform;
56
+ }
57
+
58
+ :host([data-virtualized]) [part="corner"],
59
+ :host([data-virtualized]) [part="column-header"],
60
+ :host([data-virtualized]) [part="row-header"] {
61
+ position: relative;
62
+ }
63
+
64
+ :host([data-resizing]) [part="grid"] {
65
+ user-select: none;
66
+ }
67
+
68
+ [part="corner"],
69
+ [part="column-header"],
70
+ [part="row-header"] {
71
+ background: var(--sheet-header-bg);
72
+ color: var(--sheet-header-fg);
73
+ display: flex;
74
+ align-items: center;
75
+ font-weight: 600;
76
+ position: sticky;
77
+ z-index: 2;
78
+ white-space: nowrap;
79
+ word-break: keep-all;
80
+ }
81
+
82
+ [part="corner"] {
83
+ left: 0;
84
+ top: 0;
85
+ z-index: 3;
86
+ }
87
+
88
+ [part="column-header"] {
89
+ top: 0;
90
+ justify-content: center;
91
+ }
92
+
93
+ [part="row-header"] {
94
+ left: 0;
95
+ justify-content: flex-end;
96
+ padding-inline: var(--sheet-cell-padding-inline);
97
+ }
98
+
99
+ [part="cell"] {
100
+ border-right: var(--sheet-border-width) var(--sheet-border-style)
101
+ var(--sheet-border-color);
102
+ border-bottom: var(--sheet-border-width) var(--sheet-border-style)
103
+ var(--sheet-border-color);
104
+ background: var(--sheet-cell-bg);
105
+ display: flex;
106
+ align-items: stretch;
107
+ }
108
+
109
+ [part="column-header"],
110
+ [part="row-header"],
111
+ [part="corner"] {
112
+ border-bottom: var(--sheet-border-width) var(--sheet-border-style)
113
+ var(--sheet-border-strong);
114
+ border-right: var(--sheet-border-width) var(--sheet-border-style)
115
+ var(--sheet-border-strong);
116
+ }
117
+
118
+ [part="column-header"],
119
+ [part="corner"] {
120
+ border-bottom: 0;
121
+ }
122
+
123
+ [part="row-header"],
124
+ [part="corner"] {
125
+ border-right: 0;
126
+ }
127
+
128
+ [part="cell-input"] {
129
+ border: 0;
130
+ background: transparent;
131
+ color: inherit;
132
+ font: inherit;
133
+ padding: var(--sheet-cell-padding-block) var(--sheet-cell-padding-inline);
134
+ width: 100%;
135
+ outline: none;
136
+ }
137
+
138
+ [part="cell-input"]:focus {
139
+ background: var(--monster-bg-color-primary-2);
140
+ }
141
+
142
+ [part="column-resize"] {
143
+ position: absolute;
144
+ right: -1px;
145
+ top: 0;
146
+ height: 100%;
147
+ width: var(--sheet-resize-handle);
148
+ cursor: col-resize;
149
+ pointer-events: auto;
150
+ touch-action: none;
151
+ z-index: 4;
152
+ background: linear-gradient(
153
+ to right,
154
+ transparent 0,
155
+ transparent calc(100% - 1px),
156
+ var(--sheet-border-strong) calc(100% - 1px),
157
+ var(--sheet-border-strong) 100%
158
+ );
159
+ }
160
+
161
+ [part="row-resize"] {
162
+ position: absolute;
163
+ bottom: -1px;
164
+ left: 0;
165
+ width: 100%;
166
+ height: var(--sheet-resize-handle);
167
+ cursor: row-resize;
168
+ pointer-events: auto;
169
+ touch-action: none;
170
+ z-index: 4;
171
+ background: linear-gradient(
172
+ to bottom,
173
+ transparent 0,
174
+ transparent calc(100% - 1px),
175
+ var(--sheet-border-strong) calc(100% - 1px),
176
+ var(--sheet-border-strong) 100%
177
+ );
178
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Copyright © Volker Schukai and all contributing authors, 2026. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact Volker Schukai.
11
+ */
12
+
13
+ import { addAttributeToken } from "../../../dom/attributes.mjs";
14
+ import { ATTRIBUTE_ERRORMESSAGE } from "../../../dom/constants.mjs";
15
+
16
+ export { SheetStyleSheet };
17
+
18
+ /**
19
+ * @private
20
+ * @type {CSSStyleSheet}
21
+ */
22
+ const SheetStyleSheet = new CSSStyleSheet();
23
+
24
+ try {
25
+ SheetStyleSheet.insertRule(
26
+ `
27
+ @layer sheet {
28
+ :host{--sheet-border-color:var(--monster-theme-control-border-color);--sheet-border-strong:var(--monster-color-primary-4);--sheet-border-width:var(--monster-theme-control-border-width,1px);--sheet-border-style:var(--monster-theme-control-border-style,solid);--sheet-header-bg:var(--monster-bg-color-primary-2);--sheet-header-fg:var(--monster-color-primary-1);--sheet-cell-bg:var(--monster-bg-color-primary-1);--sheet-cell-fg:var(--monster-color-primary-1);--sheet-grid-gap:0;--sheet-row-header-width:56px;--sheet-header-height:32px;--sheet-cell-padding-inline:8px;--sheet-cell-padding-block:6px;--sheet-resize-handle:6px;color:var(--sheet-cell-fg);display:block;font-family:var(--monster-font-family)}[part=control]{flex-direction:column;height:100%}[part=control],[part=toolbar]{display:flex;gap:var(--monster-space-4)}[part=grid-wrapper]{border:var(--sheet-border-width) var(--sheet-border-style) var(--sheet-border-color);flex:1;min-height:0;overflow:auto;position:relative}[part=grid]{background:var(--sheet-cell-bg);display:grid;gap:var(--sheet-grid-gap);min-width:-moz-max-content;min-width:max-content}:host([data-virtualized]) [part=grid]{left:0;position:absolute;top:0;will-change:transform}:host([data-virtualized]) [part=column-header],:host([data-virtualized]) [part=corner],:host([data-virtualized]) [part=row-header]{position:relative}:host([data-resizing]) [part=grid]{-webkit-user-select:none;-moz-user-select:none;user-select:none}[part=column-header],[part=corner],[part=row-header]{align-items:center;background:var(--sheet-header-bg);color:var(--sheet-header-fg);display:flex;font-weight:600;position:sticky;white-space:nowrap;word-break:keep-all;z-index:2}[part=corner]{left:0;top:0;z-index:3}[part=column-header]{justify-content:center;top:0}[part=row-header]{justify-content:flex-end;left:0;padding-inline:var(--sheet-cell-padding-inline)}[part=cell]{align-items:stretch;background:var(--sheet-cell-bg);border-bottom:var(--sheet-border-width) var(--sheet-border-style) var(--sheet-border-color);border-right:var(--sheet-border-width) var(--sheet-border-style) var(--sheet-border-color);display:flex}[part=column-header],[part=corner],[part=row-header]{border-bottom:var(--sheet-border-width) var(--sheet-border-style) var(--sheet-border-strong);border-right:var(--sheet-border-width) var(--sheet-border-style) var(--sheet-border-strong)}[part=column-header],[part=corner]{border-bottom:0}[part=corner],[part=row-header]{border-right:0}[part=cell-input]{background:transparent;border:0;color:inherit;font:inherit;outline:none;padding:var(--sheet-cell-padding-block) var(--sheet-cell-padding-inline);width:100%}[part=cell-input]:focus{background:var(--monster-bg-color-primary-2)}[part=column-resize]{background:linear-gradient(to right,transparent 0,transparent calc(100% - 1px),var(--sheet-border-strong) calc(100% - 1px),var(--sheet-border-strong) 100%);cursor:col-resize;height:100%;right:-1px;top:0;width:var(--sheet-resize-handle)}[part=column-resize],[part=row-resize]{pointer-events:auto;position:absolute;touch-action:none;z-index:4}[part=row-resize]{background:linear-gradient(to bottom,transparent 0,transparent calc(100% - 1px),var(--sheet-border-strong) calc(100% - 1px),var(--sheet-border-strong) 100%);bottom:-1px;cursor:row-resize;height:var(--sheet-resize-handle);left:0;width:100%}
29
+ }`,
30
+ 0,
31
+ );
32
+ } catch (e) {
33
+ addAttributeToken(
34
+ document.getRootNode().querySelector("html"),
35
+ ATTRIBUTE_ERRORMESSAGE,
36
+ e + "",
37
+ );
38
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact Volker Schukai.
11
+ *
12
+ * SPDX-License-Identifier: AGPL-3.0
13
+ */
14
+
15
+ import { registerCustomElement } from "../dom/customelement.mjs";
16
+ import { sanitizeHtml } from "../dom/sanitize-html.mjs";
17
+ import { addErrorAttribute } from "../dom/error.mjs";
18
+ import { getDocument } from "../dom/util.mjs";
19
+ import { getDocumentTranslations } from "./translations.mjs";
20
+ import { isObject, isString } from "../types/is.mjs";
21
+ import { validateObject, validateString } from "../types/validate.mjs";
22
+
23
+ export {
24
+ I18nReplaceMarker,
25
+ setI18nReplaceSanitizePolicy,
26
+ setI18nReplaceDefaultSanitizePolicy,
27
+ getI18nReplaceSanitizePolicy,
28
+ };
29
+
30
+ /**
31
+ * @private
32
+ * @type {string}
33
+ */
34
+ const TAG_PREFIX = "monster-";
35
+
36
+ /**
37
+ * @private
38
+ * @type {string}
39
+ */
40
+ const TAG_NAME = `${TAG_PREFIX}i18n-replace`;
41
+
42
+ /**
43
+ * @private
44
+ * @type {Map<string, object>}
45
+ */
46
+ const sanitizePolicies = new Map();
47
+
48
+ /**
49
+ * @private
50
+ * @type {string}
51
+ */
52
+ let defaultSanitizePolicy = "default";
53
+
54
+ sanitizePolicies.set("default", {});
55
+ sanitizePolicies.set("strict", {
56
+ blockedTags: ["script", "iframe", "object", "embed", "link", "meta"],
57
+ });
58
+
59
+ /**
60
+ * Register or update a sanitize policy used by <monster-i18n-replace>.
61
+ *
62
+ * @param {string} name
63
+ * @param {object} options
64
+ * @return {void}
65
+ */
66
+ function setI18nReplaceSanitizePolicy(name, options = {}) {
67
+ name = validateString(name);
68
+ if (!isObject(options)) {
69
+ options = {};
70
+ }
71
+ sanitizePolicies.set(name, validateObject(options));
72
+ }
73
+
74
+ /**
75
+ * Set the default sanitize policy name.
76
+ *
77
+ * @param {string} name
78
+ * @return {void}
79
+ */
80
+ function setI18nReplaceDefaultSanitizePolicy(name) {
81
+ defaultSanitizePolicy = validateString(name);
82
+ }
83
+
84
+ /**
85
+ * Get a sanitize policy by name.
86
+ *
87
+ * @param {string} name
88
+ * @return {object|undefined}
89
+ */
90
+ function getI18nReplaceSanitizePolicy(name) {
91
+ if (!isString(name)) {
92
+ return sanitizePolicies.get(defaultSanitizePolicy);
93
+ }
94
+ return sanitizePolicies.get(name);
95
+ }
96
+
97
+ /**
98
+ * Replace marker element with translated HTML.
99
+ */
100
+ class I18nReplaceMarker extends HTMLElement {
101
+ /**
102
+ * @return {string}
103
+ */
104
+ static getTag() {
105
+ return TAG_NAME;
106
+ }
107
+
108
+ connectedCallback() {
109
+ try {
110
+ replaceWithTranslation.call(this);
111
+ } catch (e) {
112
+ addErrorAttribute(this, e);
113
+ }
114
+ }
115
+ }
116
+
117
+ registerCustomElement(I18nReplaceMarker);
118
+
119
+ /**
120
+ * @private
121
+ * @return {void}
122
+ */
123
+ function replaceWithTranslation() {
124
+ const key = this.getAttribute("key");
125
+ if (!isString(key) || key.trim() === "") {
126
+ return;
127
+ }
128
+
129
+ let translations = null;
130
+ try {
131
+ translations = getDocumentTranslations();
132
+ } catch (e) {}
133
+
134
+ const { countValue, countKeyword } = parseCount.call(this);
135
+ const fallback = resolveFallback.call(
136
+ this,
137
+ countValue,
138
+ countKeyword,
139
+ translations,
140
+ );
141
+
142
+ let value = fallback;
143
+ if (translations) {
144
+ try {
145
+ if (countValue !== undefined || countKeyword !== undefined) {
146
+ const keyOrCount = countKeyword ?? countValue;
147
+ value = translations.getPluralRuleText(key, keyOrCount, fallback);
148
+ } else {
149
+ value = translations.getText(key, fallback);
150
+ }
151
+ } catch (e) {
152
+ value = fallback;
153
+ }
154
+ }
155
+
156
+ if (!isString(value)) {
157
+ value = fallback;
158
+ }
159
+
160
+ if (countValue !== undefined) {
161
+ value = replaceCountPlaceholder(value, countValue, translations);
162
+ }
163
+
164
+ const policyName = this.getAttribute("sanitize") || defaultSanitizePolicy;
165
+ const policy = getI18nReplaceSanitizePolicy(policyName) || {};
166
+ const sanitized = sanitizeHtml(value, policy);
167
+
168
+ replaceElementWithHtml(this, sanitized);
169
+ }
170
+
171
+ /**
172
+ * @private
173
+ * @return {{countValue: number|undefined, countKeyword: string|undefined}}
174
+ */
175
+ function parseCount() {
176
+ const raw = this.getAttribute("count");
177
+ if (!isString(raw) || raw.trim() === "") {
178
+ return { countValue: undefined, countKeyword: undefined };
179
+ }
180
+
181
+ const trimmed = raw.trim();
182
+ const parsed = Number.parseInt(trimmed, 10);
183
+ if (!Number.isNaN(parsed)) {
184
+ return { countValue: parsed, countKeyword: undefined };
185
+ }
186
+
187
+ const keyword = trimmed.toLowerCase();
188
+ return { countValue: undefined, countKeyword: keyword };
189
+ }
190
+
191
+ /**
192
+ * @private
193
+ * @param {number|undefined} countValue
194
+ * @param {string|undefined} countKeyword
195
+ * @param {object|null} translations
196
+ * @return {string}
197
+ */
198
+ function resolveFallback(countValue, countKeyword, translations) {
199
+ if (countValue !== undefined || countKeyword !== undefined) {
200
+ const keyword =
201
+ countKeyword ?? resolvePluralKeyword(countValue, translations);
202
+ const specific = this.getAttribute(`fallback-${keyword}`);
203
+ if (isString(specific) && specific.length > 0) {
204
+ return specific;
205
+ }
206
+ }
207
+
208
+ const generic = this.getAttribute("fallback");
209
+ if (isString(generic)) {
210
+ return generic;
211
+ }
212
+
213
+ return this.innerHTML || "";
214
+ }
215
+
216
+ /**
217
+ * @private
218
+ * @param {number|undefined} countValue
219
+ * @param {object|null} translations
220
+ * @return {string}
221
+ */
222
+ function resolvePluralKeyword(countValue, translations) {
223
+ if (countValue === undefined) {
224
+ return "other";
225
+ }
226
+
227
+ let locale = undefined;
228
+ try {
229
+ locale = translations?.locale?.toString();
230
+ } catch (e) {}
231
+
232
+ try {
233
+ const pr = new Intl.PluralRules(locale);
234
+ return pr.select(countValue);
235
+ } catch (e) {
236
+ return "other";
237
+ }
238
+ }
239
+
240
+ /**
241
+ * @private
242
+ * @param {string} html
243
+ * @param {number} countValue
244
+ * @param {object|null} translations
245
+ * @return {string}
246
+ */
247
+ function replaceCountPlaceholder(html, countValue, translations) {
248
+ let locale = undefined;
249
+ try {
250
+ locale = translations?.locale?.toString();
251
+ } catch (e) {}
252
+
253
+ let formatted = `${countValue}`;
254
+ try {
255
+ formatted = new Intl.NumberFormat(locale).format(countValue);
256
+ } catch (e) {}
257
+
258
+ return html.replaceAll("{count}", formatted);
259
+ }
260
+
261
+ /**
262
+ * @private
263
+ * @param {HTMLElement} element
264
+ * @param {string} html
265
+ * @return {void}
266
+ */
267
+ function replaceElementWithHtml(element, html) {
268
+ const document = element.ownerDocument || getDocument();
269
+ const template = document.createElement("template");
270
+ template.innerHTML = html;
271
+
272
+ const parent = element.parentNode;
273
+ if (!parent) {
274
+ return;
275
+ }
276
+
277
+ const fragment = template.content;
278
+ parent.insertBefore(fragment, element);
279
+ parent.removeChild(element);
280
+ }
@@ -63,6 +63,7 @@ export * from "./components/form/login.mjs";
63
63
  export * from "./components/form/confirm-button.mjs";
64
64
  export * from "./components/form/context-info.mjs";
65
65
  export * from "./components/form/context-base.mjs";
66
+ export * from "./components/form/sheet.mjs";
66
67
  export * from "./components/form/context-warning.mjs";
67
68
  export * from "./components/form/context-note.mjs";
68
69
  export * from "./components/form/context-error.mjs";
@@ -249,6 +250,7 @@ export * from "./i18n/time-ago.mjs";
249
250
  export * from "./i18n/formatter.mjs";
250
251
  export * from "./i18n/internal.mjs";
251
252
  export * from "./i18n/locale.mjs";
253
+ export * from "./i18n/replace-marker.mjs";
252
254
  export * from "./i18n/provider.mjs";
253
255
  export * from "./i18n/providers/fetch.mjs";
254
256
  export * from "./i18n/providers/embed.mjs";
@@ -156,7 +156,7 @@ function getMonsterVersion() {
156
156
  }
157
157
 
158
158
  /** don't touch, replaced by make with package.json version */
159
- monsterVersion = new Version("4.108.0");
159
+ monsterVersion = new Version("4.109.0");
160
160
 
161
161
  return monsterVersion;
162
162
  }
@@ -7,7 +7,7 @@ describe('Monster', function () {
7
7
  let monsterVersion
8
8
 
9
9
  /** don´t touch, replaced by make with package.json version */
10
- monsterVersion = new Version("4.108.0")
10
+ monsterVersion = new Version("4.109.0")
11
11
 
12
12
  let m = getMonsterVersion();
13
13
 
@@ -9,8 +9,8 @@
9
9
  </head>
10
10
  <body>
11
11
  <div id="headline" style="display: flex;align-items: center;justify-content: center;flex-direction: column;">
12
- <h1 style='margin-bottom: 0.1em;'>Monster 4.108.0</h1>
13
- <div id="lastupdate" style='font-size:0.7em'>last update Mi 28. Jan 02:10:31 CET 2026</div>
12
+ <h1 style='margin-bottom: 0.1em;'>Monster 4.109.0</h1>
13
+ <div id="lastupdate" style='font-size:0.7em'>last update Mi 28. Jan 19:17:34 CET 2026</div>
14
14
  </div>
15
15
  <div id="mocha-errors"
16
16
  style="color: red;font-weight: bold;display: flex;align-items: center;justify-content: center;flex-direction: column;margin:20px;"></div>