@wpmoo/ui 1.5.0 → 2.1.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,266 @@
1
+ /*!
2
+ * Moo UI password-requirements.js v2.1.0 (https://wpmoo.org/)
3
+ * Copyright 2026 WPMoo Authors
4
+ * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
+ */
6
+
7
+ const instances = /* @__PURE__ */ new WeakMap();
8
+ const MESSAGES = {
9
+ met: "\u2713 %s",
10
+ missing: "\u2014 %s",
11
+ remaining: "Remaining: %s",
12
+ complete: "Shown checks met. Final validation happens on submit.",
13
+ analysis: "Host password analysis must pass."
14
+ };
15
+ const format = (message, value) => message.replace("%s", () => String(value));
16
+ function strip(value, characters) {
17
+ const points = Array.from(value);
18
+ let start = 0;
19
+ let end = points.length;
20
+ while (start < end && characters.includes(points[start])) start++;
21
+ while (end > start && characters.includes(points[end - 1])) end--;
22
+ return points.slice(start, end).join("");
23
+ }
24
+ function passwordRequirementStates(value, policy) {
25
+ const normalized = strip(value.normalize("NFKC"), policy.whitespace);
26
+ const maximum = Math.max(Array.from(value).length, Array.from(value.normalize("NFKC")).length) <= policy.maxlength;
27
+ const checks = {
28
+ length: Math.min(Array.from(strip(value, policy.whitespace)).length, Array.from(normalized).length) >= policy.minlength,
29
+ case: /\p{Uppercase}/u.test(normalized) && /\p{Lowercase}/u.test(normalized),
30
+ number: /\p{Nd}/u.test(normalized),
31
+ symbol: /[\p{P}\p{S}]/u.test(normalized),
32
+ maximum
33
+ };
34
+ return policy.rules.map((rule) => ({ key: rule.key, label: rule.label, warning: Boolean(rule.warning), met: Boolean(checks[rule.key]) }));
35
+ }
36
+ class PasswordRequirements {
37
+ static getInstance(element) {
38
+ return instances.get(element) || null;
39
+ }
40
+ static getOrCreateInstance(element, options) {
41
+ return this.getInstance(element) || new this(element, options);
42
+ }
43
+ constructor(element, options = {}) {
44
+ if (!element?.matches("fieldset[data-password-requirements]") || !element.id) {
45
+ throw new TypeError("PasswordRequirements requires an identified Fieldset.");
46
+ }
47
+ const existing = instances.get(element);
48
+ if (existing) return existing;
49
+ const configuration = JSON.parse(element.dataset.passwordRequirements);
50
+ const document = element.ownerDocument;
51
+ const owner = element.closest(".moo-ui");
52
+ const input = document.getElementById(configuration.input);
53
+ const confirmation = configuration.confirmation && document.getElementById(configuration.confirmation);
54
+ const localControl = (node) => node?.matches('input[type="password"]') && node.closest(".moo-ui") === owner && node.form === element.closest("form");
55
+ const policy = configuration.policy;
56
+ if (!owner || !localControl(input) || configuration.confirmation && (!localControl(confirmation) || confirmation === input) || policy?.version !== 3 || !Number.isInteger(policy.minlength) || policy.minlength < 1 || !Number.isInteger(policy.maxlength) || policy.maxlength < policy.minlength || policy.maxlength > 128 || !Array.isArray(policy.rules) || !policy.rules.length || policy.rules.some((rule) => !["length", "case", "number", "symbol", "maximum"].includes(rule.key) || typeof rule.label !== "string" || rule.warning !== void 0 && typeof rule.warning !== "boolean") || new Set(policy.rules.map((rule) => rule.key)).size !== policy.rules.length || !["length", "maximum"].every((key) => policy.rules.some((rule) => rule.key === key)) || typeof policy.whitespace !== "string" || options.analyze !== void 0 && typeof options.analyze !== "function" || options.messages !== void 0 && (!options.messages || typeof options.messages !== "object" || Object.entries(options.messages).some(([key, value]) => !Object.hasOwn(MESSAGES, key) || typeof value !== "string"))) {
57
+ throw new TypeError("PasswordRequirements requires same-owner controls and a versioned public policy.");
58
+ }
59
+ this._element = element;
60
+ const messages = { ...MESSAGES, ...options.messages };
61
+ this._initialReady = element.getAttribute("data-requirements-ready");
62
+ this._originals = /* @__PURE__ */ new Map();
63
+ this._listeners = [];
64
+ const local = (suffix) => {
65
+ const node = document.getElementById(element.id + "-" + suffix);
66
+ if (!node || !element.contains(node)) throw new TypeError("Missing local requirement description.");
67
+ this._originals.set(node, {
68
+ text: node.textContent,
69
+ className: node.className,
70
+ hidden: node.hidden,
71
+ met: node.getAttribute("data-requirement-met")
72
+ });
73
+ return node;
74
+ };
75
+ const rows = new Map(policy.rules.map((rule) => [rule.key, local(rule.key)]));
76
+ const summary = local("summary");
77
+ const match = confirmation && local("confirmation");
78
+ const hint = configuration.submit && document.getElementById(element.id + "-submit-hint");
79
+ const submit = configuration.submit && document.getElementById(configuration.submit);
80
+ if (configuration.submit && (!input.form || !hint?.matches('[data-bs-toggle="tooltip"]') || !element.contains(hint) || !submit?.matches('button[type="submit"], input[type="submit"]') || submit.closest(".moo-ui") !== owner || submit.form !== input.form)) {
81
+ throw new TypeError("PasswordRequirements requires a same-form submit Tooltip.");
82
+ }
83
+ const initialDisabled = submit?.disabled;
84
+ const initialTabIndex = hint?.getAttribute("tabindex");
85
+ const initialTitle = hint?.getAttribute("data-bs-title");
86
+ const initialRole = hint?.getAttribute("role");
87
+ const initialLabel = hint?.getAttribute("aria-label");
88
+ const initialHidden = hint?.hidden;
89
+ const restoreAttribute = (name, value) => {
90
+ if (value === null) hint.removeAttribute(name);
91
+ else hint.setAttribute(name, value);
92
+ };
93
+ const submitParent = submit?.parentNode;
94
+ const submitNext = submit?.nextSibling;
95
+ const hintParent = hint?.parentNode;
96
+ const hintNext = hint?.nextSibling;
97
+ const hintChildren = hint && [...hint.childNodes];
98
+ const view = document.defaultView;
99
+ const Tooltip = view.bootstrap?.Tooltip || view.Tooltip;
100
+ if (submit && (!Tooltip || typeof Tooltip.getInstance !== "function" || typeof Tooltip.getOrCreateInstance !== "function")) {
101
+ throw new TypeError("PasswordRequirements submit binding requires Bootstrap Tooltip.");
102
+ }
103
+ const previousTooltip = hint && Tooltip?.getInstance(hint);
104
+ const previousTooltipWasEnabled = previousTooltip?._isEnabled;
105
+ const previousTooltipContent = previousTooltip?._newContent;
106
+ let tooltip;
107
+ let gateReady = !submit;
108
+ let lastTitle;
109
+ if (submit) {
110
+ this._listeners.push(() => {
111
+ submit.disabled = initialDisabled;
112
+ if (initialTabIndex === null) hint.removeAttribute("tabindex");
113
+ else hint.setAttribute("tabindex", initialTabIndex);
114
+ restoreAttribute("data-bs-title", initialTitle);
115
+ restoreAttribute("role", initialRole);
116
+ restoreAttribute("aria-label", initialLabel);
117
+ tooltip?.hide();
118
+ if (!previousTooltip) tooltip?.dispose();
119
+ else {
120
+ tooltip.setContent(previousTooltipContent);
121
+ if (previousTooltipWasEnabled) tooltip.enable();
122
+ else tooltip.disable();
123
+ }
124
+ submitParent.insertBefore(submit, submitNext?.parentNode === submitParent ? submitNext : null);
125
+ hintParent.insertBefore(hint, hintNext?.parentNode === hintParent ? hintNext : null);
126
+ hint.replaceChildren(...hintChildren);
127
+ hint.hidden = initialHidden;
128
+ });
129
+ }
130
+ const updateSubmit = (states) => {
131
+ if (!submit) return;
132
+ gateReady = Boolean(input.value) && states.every((state) => state.met);
133
+ submit.disabled = !gateReady;
134
+ hint.tabIndex = gateReady ? -1 : 0;
135
+ restoreAttribute("role", gateReady ? null : initialRole);
136
+ restoreAttribute("aria-label", gateReady ? null : initialLabel);
137
+ const title = states.filter((state) => !state.met && (input.value || !state.warning || state.key === "confirmation")).map((state) => state.label).join(" ");
138
+ hint.setAttribute("data-bs-title", title || initialTitle);
139
+ if (gateReady) {
140
+ tooltip?.hide();
141
+ tooltip?.disable();
142
+ } else {
143
+ tooltip?.enable();
144
+ if (title !== lastTitle) tooltip?.setContent({ ".tooltip-inner": title || initialTitle });
145
+ }
146
+ lastTitle = title;
147
+ };
148
+ const show = (node, label, met, warning = false, active = Boolean(input.value)) => {
149
+ node.textContent = warning ? label : format(met ? messages.met : messages.missing, label);
150
+ node.hidden = warning && (!active || met);
151
+ node.classList.toggle("text-body", !warning && met);
152
+ node.classList.toggle("text-danger", warning);
153
+ node.dataset.requirementMet = String(met);
154
+ };
155
+ const update = () => {
156
+ gateReady = !submit;
157
+ if (submit) submit.disabled = true;
158
+ const states = passwordRequirementStates(input.value, policy);
159
+ const withinMaximum = Math.max(
160
+ Array.from(input.value).length,
161
+ Array.from(input.value.normalize("NFKC")).length
162
+ ) <= policy.maxlength;
163
+ let analysis = null;
164
+ const analysisValue = strip(input.value.normalize("NFKC"), policy.whitespace);
165
+ if (options.analyze && analysisValue && withinMaximum) {
166
+ analysis = options.analyze(analysisValue);
167
+ if (!analysis || typeof analysis.met !== "boolean" || !Number.isInteger(analysis.score) || analysis.score < 0 || analysis.score > 4) {
168
+ throw new TypeError("Password analysis must synchronously return {met: boolean, score: 0..4}.");
169
+ }
170
+ }
171
+ if (options.analyze) states.push({
172
+ key: "analysis",
173
+ label: messages.analysis,
174
+ met: Boolean(analysis?.met),
175
+ warning: true
176
+ });
177
+ if (confirmation) states.push({
178
+ key: "confirmation",
179
+ label: this._originals.get(match).text,
180
+ warning: true,
181
+ met: Boolean(confirmation.value) && confirmation.value === input.value
182
+ });
183
+ for (const state of states.filter((state2) => state2.key !== "analysis")) show(
184
+ state.key === "confirmation" ? match : rows.get(state.key),
185
+ state.label,
186
+ Boolean(input.value) && state.met,
187
+ state.warning,
188
+ state.key === "confirmation" ? Boolean(confirmation.value) : Boolean(input.value)
189
+ );
190
+ const missing = states.filter((state) => !state.met).length;
191
+ const warnings = states.filter((state) => state.warning && !state.met).map((state) => state.label).join(" ");
192
+ const summaryText = !input.value ? this._originals.get(summary).text : missing ? `${format(messages.remaining, missing)} ${warnings}`.trim() : messages.complete;
193
+ if (summary.textContent !== summaryText) summary.textContent = summaryText;
194
+ options.onChange?.(states.map(({ key, met }) => ({ key, met: Boolean(input.value) && met })), Boolean(input.value), analysis?.score ?? null);
195
+ updateSubmit(states);
196
+ };
197
+ instances.set(element, this);
198
+ try {
199
+ if (submit) {
200
+ submitParent.insertBefore(hint, submit);
201
+ hint.replaceChildren(submit);
202
+ hint.hidden = false;
203
+ submit.disabled = true;
204
+ tooltip = Tooltip?.getOrCreateInstance(hint, { container: owner, html: false, trigger: "hover focus" });
205
+ }
206
+ for (const node of [input, confirmation].filter(Boolean)) {
207
+ for (const event of ["input", "change"]) {
208
+ node.addEventListener(event, update);
209
+ this._listeners.push(() => node.removeEventListener(event, update));
210
+ }
211
+ }
212
+ if (input.form) {
213
+ if (submit) {
214
+ const guard = (event) => {
215
+ try {
216
+ update();
217
+ } catch (error) {
218
+ event.preventDefault();
219
+ event.stopImmediatePropagation();
220
+ throw error;
221
+ }
222
+ if (!gateReady) {
223
+ event.preventDefault();
224
+ event.stopImmediatePropagation();
225
+ hint.focus();
226
+ }
227
+ };
228
+ input.form.addEventListener("submit", guard, true);
229
+ this._listeners.push(() => input.form.removeEventListener("submit", guard, true));
230
+ }
231
+ const reset = () => {
232
+ clearTimeout(this._resetTimer);
233
+ this._resetTimer = setTimeout(update, 0);
234
+ };
235
+ input.form.addEventListener("reset", reset);
236
+ this._listeners.push(() => input.form.removeEventListener("reset", reset));
237
+ }
238
+ update();
239
+ element.dataset.requirementsReady = "true";
240
+ this.ready = Promise.resolve(this);
241
+ } catch (error) {
242
+ this.dispose();
243
+ throw error;
244
+ }
245
+ }
246
+ dispose() {
247
+ if (instances.get(this._element) !== this) return;
248
+ clearTimeout(this._resetTimer);
249
+ for (const remove of this._listeners.splice(0)) remove();
250
+ for (const [node, original] of this._originals) {
251
+ node.textContent = original.text;
252
+ node.className = original.className;
253
+ node.hidden = original.hidden;
254
+ if (original.met === null) node.removeAttribute("data-requirement-met");
255
+ else node.setAttribute("data-requirement-met", original.met);
256
+ }
257
+ if (this._initialReady === null) this._element.removeAttribute("data-requirements-ready");
258
+ else this._element.setAttribute("data-requirements-ready", this._initialReady);
259
+ instances.delete(this._element);
260
+ this._originals.clear();
261
+ }
262
+ }
263
+ export {
264
+ PasswordRequirements as default,
265
+ passwordRequirementStates
266
+ };
package/dist/js/sheet.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI sheet.js v1.5.0 (https://wpmoo.org/)
2
+ * Moo UI sheet.js v2.1.0 (https://wpmoo.org/)
3
3
  * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI sidebar.js v1.5.0 (https://wpmoo.org/)
2
+ * Moo UI sidebar.js v2.1.0 (https://wpmoo.org/)
3
3
  * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
package/dist/js/slider.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI slider.js v1.5.0 (https://wpmoo.org/)
2
+ * Moo UI slider.js v2.1.0 (https://wpmoo.org/)
3
3
  * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
package/dist/js/state.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI state.js v1.5.0 (https://wpmoo.org/)
2
+ * Moo UI state.js v2.1.0 (https://wpmoo.org/)
3
3
  * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI table-of-contents.js v1.5.0 (https://wpmoo.org/)
2
+ * Moo UI table-of-contents.js v2.1.0 (https://wpmoo.org/)
3
3
  * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
package/dist/js/toc.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI toc.js v1.5.0 (https://wpmoo.org/)
2
+ * Moo UI toc.js v2.1.0 (https://wpmoo.org/)
3
3
  * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
@@ -3,22 +3,22 @@
3
3
  {
4
4
  "export": "./moo.css",
5
5
  "path": "dist/assets/css/moo.css",
6
- "sha256": "7b1796b32d96ab627d055e7bd7b78cf234f7531ba749a5fc9830da5b75bb8667"
6
+ "sha256": "b59eab8eb8321379f5995c831b17de2984cb90711dbe4fb4cf6ddc879962cb59"
7
7
  },
8
8
  {
9
9
  "export": "./moo-ui.css",
10
10
  "path": "dist/assets/css/moo-ui.css",
11
- "sha256": "ff0f81860d16e6a36d71eae87c3a51882e937a58de4f77d04f84327d68d9f594"
11
+ "sha256": "2a898c1c05759626168d736cd999a9abc26ebc675e47187e70b5e0d84d91b85e"
12
12
  },
13
13
  {
14
14
  "export": "./state.js",
15
15
  "path": "dist/js/state.js",
16
- "sha256": "edddb4704c201edcecc09774457e21a65ac98d7f83be49f791cfb2defbdc2a52"
16
+ "sha256": "d02e78a71a624a0c8d36f33efc4b5a4fa0d2ac49b329e53994c20ff60c386713"
17
17
  }
18
18
  ],
19
19
  "package": {
20
20
  "name": "@wpmoo/ui",
21
- "version": "1.5.0"
21
+ "version": "2.1.0"
22
22
  },
23
23
  "schemaVersion": 1
24
24
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wpmoo/ui",
3
- "version": "1.5.0",
3
+ "version": "2.1.0",
4
4
  "description": "Bootstrap markup. shadcn feel. A frontend-framework-free component system for server-rendered apps.",
5
5
  "license": "MIT",
6
6
  "author": "WPMoo (https://wpmoo.org)",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "style": "dist/assets/css/moo-ui.css",
27
27
  "exports": {
28
+ "./password-requirements.js": "./dist/js/password-requirements.js",
28
29
  "./moo-ui.css": "./dist/assets/css/moo-ui.css",
29
30
  "./moo-ui.min.css": "./dist/assets/css/moo-ui.min.css",
30
31
  "./moo.css": "./dist/assets/css/moo.css",
@@ -56,6 +57,7 @@
56
57
  "./package.json": "./package.json"
57
58
  },
58
59
  "files": [
60
+ "dist/js/password-requirements.js",
59
61
  "dist/assets/css/moo-ui.css",
60
62
  "dist/assets/css/moo-ui.min.css",
61
63
  "dist/assets/css/moo.css",
@@ -83,14 +85,18 @@
83
85
  "certification.json",
84
86
  "README.md",
85
87
  "LICENSE",
86
- "ASSET_LICENSE.md"
88
+ "THIRD_PARTY_NOTICES.md"
87
89
  ],
88
90
  "peerDependencies": {
89
- "bootstrap": ">=5.3.0 <5.4"
91
+ "bootstrap": ">=5.3.0 <5.4",
92
+ "chart.js": ">=4.6.1 <5"
90
93
  },
91
94
  "peerDependenciesMeta": {
92
95
  "bootstrap": {
93
96
  "optional": true
97
+ },
98
+ "chart.js": {
99
+ "optional": true
94
100
  }
95
101
  },
96
102
  "sideEffects": [
@@ -109,7 +115,7 @@
109
115
  },
110
116
  "devDependencies": {
111
117
  "axe-core": "4.14.0",
112
- "chart.js": "4.5.1",
118
+ "chart.js": "4.6.1",
113
119
  "esbuild": "0.28.2"
114
120
  }
115
121
  }
@@ -20,6 +20,12 @@
20
20
  border-radius: var(--bs-border-radius-xl);
21
21
  }
22
22
 
23
+ .wrapper[data-layout="app"][data-shell-mode="document"] {
24
+ height: auto;
25
+ min-height: 100svh;
26
+ overflow: visible;
27
+ }
28
+
23
29
  .wrapper[data-layout="app"] > [data-slot="page"] {
24
30
  display: flex;
25
31
  min-width: 0;
@@ -75,6 +81,10 @@
75
81
  overflow-y: auto;
76
82
  }
77
83
 
84
+ .wrapper[data-layout="app"][data-shell-mode="document"] > [data-slot="page"] > main {
85
+ overflow: visible;
86
+ }
87
+
78
88
  .wrapper[data-layout="app"] > [data-slot="page"] > main > :is(
79
89
  .container,
80
90
  .container-sm,
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI v1.5.0 (https://wpmoo.org/)
2
+ * Moo UI v2.1.0 (https://wpmoo.org/)
3
3
  * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  *
@@ -1,7 +1,8 @@
1
1
  $white: #fff !default;
2
2
  $black: #0a0a0a !default;
3
3
 
4
- $headings-font-weight: 600 !default;
4
+ $moo-heading-font-weight: 600 !default;
5
+ $headings-font-weight: var(--moo-heading-font-weight, #{$moo-heading-font-weight}) !default;
5
6
  $lead-font-size: 1.125rem !default;
6
7
  $lead-font-weight: 400 !default;
7
8
  $display-font-sizes: (
@@ -92,6 +92,7 @@ $moo-theme-colors-border-subtle-dark: (
92
92
  --moo-chart-4: #{$moo-chart-4};
93
93
  --moo-chart-5: #{$moo-chart-5};
94
94
  --moo-heading-font-family: var(--#{$prefix}body-font-family);
95
+ --moo-heading-font-weight: #{$moo-heading-font-weight};
95
96
  --moo-small-font-size: #{$moo-small-font-size};
96
97
  --moo-paragraph-margin-top: #{$moo-paragraph-margin-top};
97
98
  --moo-paragraph-margin-bottom: #{$paragraph-margin-bottom};
package/ASSET_LICENSE.md DELETED
@@ -1,19 +0,0 @@
1
- # WPMoo Visual Asset License
2
-
3
- WPMoo-generated visual assets are copyright (c) 2026 WPMoo
4
- (`wpmoo.org`). All rights reserved.
5
-
6
- These assets are not licensed under the repository's MIT source code license
7
- unless an individual file or directory states otherwise. This includes brand
8
- assets, component preview images, marketing images, screenshots, generated
9
- artwork, and other WPMoo-owned image assets under `site/static/images/`.
10
-
11
- You may view these assets as part of the Moo UI documentation and demo site.
12
- You may not copy, redistribute, modify, sell, sublicense, or use them in another
13
- product, site, template, library, training dataset, or marketing asset without
14
- prior written permission from WPMoo.
15
-
16
- Third-party assets remain under their own licenses. See the
17
- [version-pinned third-party notices](https://github.com/wpmoo-org/ui/blob/v1.5.0/THIRD_PARTY_NOTICES.md).
18
- Those notices are repository-only and are intentionally not shipped in the npm
19
- package.