@xihan-ui/web-components 1.0.0 → 2.0.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +4919 -6
  2. package/README.md +10 -3
  3. package/custom-elements.json +38698 -19900
  4. package/dist/backgrounds.d.ts +7 -7
  5. package/dist/backgrounds.d.ts.map +1 -1
  6. package/dist/backgrounds.js +8 -8
  7. package/dist/backgrounds.js.map +1 -1
  8. package/dist/{config-Cz16-P4i.d.ts → config-etfwIorI.d.ts} +17 -8
  9. package/dist/config-etfwIorI.d.ts.map +1 -0
  10. package/dist/define.d.ts +8641 -4081
  11. package/dist/define.d.ts.map +1 -1
  12. package/dist/define.js +11420 -5706
  13. package/dist/define.js.map +1 -1
  14. package/dist/{element-ib25-l85.js → element-BKQR3ce3.js} +4 -4
  15. package/dist/element-BKQR3ce3.js.map +1 -0
  16. package/dist/{element-DRoFjrYO.d.ts → element-DFbmhZLP.d.ts} +3 -3
  17. package/dist/{element-DRoFjrYO.d.ts.map → element-DFbmhZLP.d.ts.map} +1 -1
  18. package/dist/index.d.ts +11 -12
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/{machine-controller-DE6edQbK.js → machine-controller-C1ffftux.js} +35 -29
  22. package/dist/machine-controller-C1ffftux.js.map +1 -0
  23. package/dist/registry-DR6I2_zF.js.map +1 -1
  24. package/dist/services.d.ts +184 -0
  25. package/dist/services.d.ts.map +1 -0
  26. package/dist/services.js +701 -0
  27. package/dist/services.js.map +1 -0
  28. package/dist/toast-BTlM12YF.js +1405 -0
  29. package/dist/toast-BTlM12YF.js.map +1 -0
  30. package/dist/{types-BvWWqMd_.d.ts → types-C1Bybm6P.d.ts} +2 -2
  31. package/dist/{types-BvWWqMd_.d.ts.map → types-C1Bybm6P.d.ts.map} +1 -1
  32. package/package.json +23 -16
  33. package/dist/config-Cz16-P4i.d.ts.map +0 -1
  34. package/dist/element-ib25-l85.js.map +0 -1
  35. package/dist/machine-controller-DE6edQbK.js.map +0 -1
@@ -0,0 +1,1405 @@
1
+ import { a as containsPart, d as onXhConfigChange, f as resolveXhConfig, m as withXhConfig, o as discoverParts, r as createSpreader, s as wcNormalize, t as MachineController } from "./machine-controller-C1ffftux.js";
2
+ import { t as XhReactiveElement } from "./element-BKQR3ce3.js";
3
+ import { buttonAnatomy, buttonMachine, buttonMeta, connectButton, connectDialog, connectLoadingBar, connectNotification, connectNotificationItem, connectToast, dialogAnatomy, dialogMachine, dialogMeta, loadingBarAnatomy, loadingBarMachine, loadingBarMeta, notificationAnatomy, notificationMachine, notificationMeta, toastAnatomy, toastMachine, toastMeta } from "@xihan-ui/headless";
4
+ import { DIAGNOSTIC_CODES, createCounterIdGenerator, createPortalLease, createRuntimeConfig, createScope, getDiagnostics, reportDiagnostic } from "@xihan-ui/core";
5
+ import { attachCssExit, createPresence } from "@xihan-ui/core/presence";
6
+ //#region src/dom/part-contract.ts
7
+ /** `name` 是否已注册为 `xh-<name>` 元素、且该元素的解剖名就是它。 */
8
+ function isRegisteredScope(name) {
9
+ if (typeof customElements === "undefined") return false;
10
+ return customElements.get(`xh-${name}`)?.partContract?.anatomy.name === name;
11
+ }
12
+ /**
13
+ * 委派登记里真正归内嵌部件管的作者名 → 允许的 scope 集合。
14
+ * 与宿主解剖重名的角色名由宿主自己接线(date-picker 的 root / control 等),不进这张表。
15
+ */
16
+ function delegatedScopesOf(contract) {
17
+ const own = new Set(contract.anatomy.parts);
18
+ const out = /* @__PURE__ */ new Map();
19
+ for (const delegate of contract.delegates ?? []) for (const part of delegate.parts) {
20
+ if (own.has(part)) continue;
21
+ const scopes = out.get(part);
22
+ if (scopes) scopes.add(delegate.name);
23
+ else out.set(part, /* @__PURE__ */ new Set([delegate.name]));
24
+ }
25
+ return out;
26
+ }
27
+ /**
28
+ * 比对已发现的角色节点与契约:缺必需 part 报 error,出现解剖外的 part 名报 warn,
29
+ * 委派目标不是已注册元素的解剖名报 error。
30
+ * 委派节点的接线结果排到微任务里核:宿主在本函数返回后同步接线,微任务里读到的已是接线后的 data-scope。
31
+ * 通道静默时直接返回,不做扫描。
32
+ */
33
+ function validatePartContract(contract, parts, host, instanceId) {
34
+ if (getDiagnostics().getLevel() === "silent") return;
35
+ const scope = contract.anatomy.name;
36
+ for (const part of contract.meta.requiredParts) {
37
+ if (parts.has(part)) continue;
38
+ reportDiagnostic({
39
+ code: DIAGNOSTIC_CODES.wcMissingPart,
40
+ level: "error",
41
+ message: `缺少必需的角色节点 data-xh-part="${part}",该部件不会被接线`,
42
+ scope,
43
+ instanceId,
44
+ part,
45
+ node: host,
46
+ detail: {
47
+ tag: host.tagName.toLowerCase(),
48
+ requiredParts: contract.meta.requiredParts
49
+ }
50
+ });
51
+ }
52
+ const known = new Set(contract.anatomy.parts);
53
+ for (const delegate of contract.delegates ?? []) for (const part of delegate.parts) known.add(part);
54
+ for (const part of parts.keys()) {
55
+ if (known.has(part)) continue;
56
+ reportDiagnostic({
57
+ code: DIAGNOSTIC_CODES.wcUnknownPart,
58
+ level: "warn",
59
+ message: `角色节点 data-xh-part="${part}" 不在 ${scope} 的解剖内,不会被接线`,
60
+ scope,
61
+ instanceId,
62
+ part,
63
+ node: parts.get(part)?.[0] ?? host,
64
+ detail: {
65
+ tag: host.tagName.toLowerCase(),
66
+ knownParts: contract.anatomy.parts
67
+ }
68
+ });
69
+ }
70
+ for (const [part, allowed] of Object.entries(contract.tags ?? {})) for (const el of parts.get(part) ?? []) {
71
+ const tag = el.tagName.toLowerCase();
72
+ if (allowed.includes(tag)) continue;
73
+ reportDiagnostic({
74
+ code: DIAGNOSTIC_CODES.wcWrongPartTag,
75
+ level: "warn",
76
+ message: `角色节点 data-xh-part="${part}" 是 <${tag}>,须是 ${allowed.map((t) => `<${t}>`).join(" 或 ")},否则原生语义静默失效`,
77
+ scope,
78
+ instanceId,
79
+ part,
80
+ node: el,
81
+ detail: {
82
+ tag,
83
+ allowed
84
+ }
85
+ });
86
+ }
87
+ if (!contract.delegates?.length) return;
88
+ const unregistered = /* @__PURE__ */ new Set();
89
+ for (const delegate of contract.delegates) {
90
+ if (isRegisteredScope(delegate.name)) continue;
91
+ unregistered.add(delegate.name);
92
+ reportDiagnostic({
93
+ code: DIAGNOSTIC_CODES.invariant,
94
+ level: "error",
95
+ message: `partContract.delegates 登记的 "${delegate.name}" 不是已注册元素 <xh-${delegate.name}> 的解剖名,委派给它的角色节点无从核对`,
96
+ scope,
97
+ instanceId,
98
+ node: host,
99
+ detail: {
100
+ tag: host.tagName.toLowerCase(),
101
+ delegate: delegate.name,
102
+ parts: delegate.parts
103
+ }
104
+ });
105
+ }
106
+ queueMicrotask(() => validateDelegatedParts(contract, parts, host, instanceId, unregistered));
107
+ }
108
+ /**
109
+ * 核对委派节点的接线结果:登记为归内嵌部件的角色节点,接线后 data-scope 必须是登记的 scope。
110
+ * 没拿到 data-scope 即没被接线(多半是写错了位置),拿到别的 scope 即登记与接线对不上。
111
+ * 已从宿主离场的节点与登记目标未注册的委派跳过。
112
+ */
113
+ function validateDelegatedParts(contract, parts, host, instanceId, skipScopes = /* @__PURE__ */ new Set()) {
114
+ if (!host.isConnected || getDiagnostics().getLevel() === "silent") return;
115
+ const scope = contract.anatomy.name;
116
+ for (const [part, scopes] of delegatedScopesOf(contract)) {
117
+ const expected = [...scopes].filter((s) => !skipScopes.has(s));
118
+ if (expected.length === 0) continue;
119
+ for (const el of parts.get(part) ?? []) {
120
+ if (!host.contains(el)) continue;
121
+ const wired = el.getAttribute("data-scope");
122
+ if (wired !== null && expected.includes(wired)) continue;
123
+ const target = expected.map((s) => `"${s}"`).join(" 或 ");
124
+ reportDiagnostic({
125
+ code: wired === null ? DIAGNOSTIC_CODES.wcUnknownPart : DIAGNOSTIC_CODES.invariant,
126
+ level: wired === null ? "warn" : "error",
127
+ message: wired === null ? `角色节点 data-xh-part="${part}" 登记为委派给 ${target},接线后没有 data-scope:没被接线,检查它是否放在了该放的部件里` : `角色节点 data-xh-part="${part}" 登记为委派给 ${target},接线后 data-scope 却是 "${wired}",契约登记与接线对不上`,
128
+ scope,
129
+ instanceId,
130
+ part,
131
+ node: el,
132
+ detail: {
133
+ tag: host.tagName.toLowerCase(),
134
+ expectedScopes: expected,
135
+ wiredScope: wired
136
+ }
137
+ });
138
+ }
139
+ }
140
+ }
141
+ //#endregion
142
+ //#region src/dom/stacking-context.ts
143
+ /** 初始值是 none 的一批:取任何别的值都建层叠上下文。 */
144
+ const NONE_INITIAL = [
145
+ "transform",
146
+ "translate",
147
+ "rotate",
148
+ "scale",
149
+ "perspective",
150
+ "filter",
151
+ "backdrop-filter",
152
+ "clip-path",
153
+ "mask-image",
154
+ "view-transition-name"
155
+ ];
156
+ /** 建层叠上下文的那几档 contain;单独的 size 与 style 不算。 */
157
+ const CONTAIN = /\b(?:layout|paint|strict|content)\b/;
158
+ /** container-type 只有带 size 的两档施加布局限制;normal 与 scroll-state 不算。 */
159
+ const CONTAINER_TYPE = /\bsize\b/;
160
+ /** will-change 点到这些属性时,元素照样建层叠上下文。前缀能覆盖 transform-style、backdrop-filter、mask-image 等派生名。 */
161
+ const WILL_CHANGE = /\b(?:transform|translate|rotate|scale|perspective|filter|opacity|mix-blend-mode|isolation|clip-path|mask|contain|content-visibility|container-type|view-transition-name)\b/;
162
+ const FLEX_OR_GRID = /^(?:inline-)?(?:flex|grid)$/;
163
+ /** 未设置的属性在部分无头 DOM 实现里回空串而不是初始值,两者一并当没设。 */
164
+ function unset(value, initial) {
165
+ return value === "" || value === initial;
166
+ }
167
+ /** 元素标识:标签名 + id + 前几个类名,够在页面里认出是哪一个。 */
168
+ function describeElement(el) {
169
+ const id = el.id ? `#${el.id}` : "";
170
+ const names = Array.from(el.classList);
171
+ const cls = names.length ? `.${names.slice(0, 3).join(".")}${names.length > 3 ? "…" : ""}` : "";
172
+ return `${el.tagName.toLowerCase()}${id}${cls}`;
173
+ }
174
+ /**
175
+ * 这个元素建不建层叠上下文。命中即回那条属性与取值。
176
+ * 判据与「抢走包含块」不同:position 本身与 z-index 在这里算数,而 container-type 收窄到带 size 的两档。
177
+ */
178
+ function stackingCauseOf(el) {
179
+ const view = el.ownerDocument.defaultView;
180
+ if (!view) return null;
181
+ const style = view.getComputedStyle(el);
182
+ const read = (property) => style.getPropertyValue(property);
183
+ const cause = (property) => ({
184
+ property,
185
+ value: read(property)
186
+ });
187
+ for (const property of NONE_INITIAL) if (!unset(read(property), "none")) return cause(property);
188
+ if (read("transform-style") === "preserve-3d") return cause("transform-style");
189
+ const opacity = Number.parseFloat(read("opacity"));
190
+ if (Number.isFinite(opacity) && opacity < 1) return cause("opacity");
191
+ if (!unset(read("mix-blend-mode"), "normal")) return cause("mix-blend-mode");
192
+ if (read("isolation") === "isolate") return cause("isolation");
193
+ if (CONTAIN.test(read("contain"))) return cause("contain");
194
+ if (/^(?:auto|hidden)$/.test(read("content-visibility"))) return cause("content-visibility");
195
+ if (CONTAINER_TYPE.test(read("container-type"))) return cause("container-type");
196
+ if (WILL_CHANGE.test(read("will-change"))) return cause("will-change");
197
+ const position = read("position");
198
+ if (position === "fixed" || position === "sticky") return cause("position");
199
+ if (unset(read("z-index"), "auto")) return null;
200
+ if (position === "relative" || position === "absolute") return cause("z-index");
201
+ const parent = el.parentElement;
202
+ if (parent && FLEX_OR_GRID.test(view.getComputedStyle(parent).display)) return cause("z-index");
203
+ return null;
204
+ }
205
+ /**
206
+ * 从浮层往上找第一个建层叠上下文的祖先,走到 body 为止。
207
+ * 撞上外层浮层的 positioner 即止步:那一层由它自己的宿主去查。
208
+ * body 与 documentElement 不算:页面内容与浮层同在其中,相对次序不受影响,
209
+ * 而滚动锁本来就会给 body 写上 position: fixed。
210
+ */
211
+ function findStackingTrap(positioner) {
212
+ const doc = positioner.ownerDocument;
213
+ const stop = doc.body ?? doc.documentElement;
214
+ for (let node = positioner.parentElement; node && node !== stop; node = node.parentElement) {
215
+ if (node.dataset.part === "positioner") return null;
216
+ const cause = stackingCauseOf(node);
217
+ if (cause) return {
218
+ ancestor: node,
219
+ cause
220
+ };
221
+ }
222
+ return null;
223
+ }
224
+ /**
225
+ * 浮层展开后查一次祖先链,被层叠上下文困住就投诊断。
226
+ * 通道静默时直接返回,不做扫描。
227
+ */
228
+ function reportStackingTrap(positioner, scope, instanceId) {
229
+ if (getDiagnostics().getLevel() === "silent") return;
230
+ const trap = findStackingTrap(positioner);
231
+ if (!trap) return;
232
+ const where = describeElement(trap.ancestor);
233
+ const { property, value } = trap.cause;
234
+ reportDiagnostic({
235
+ code: DIAGNOSTIC_CODES.overlayStackingTrap,
236
+ level: "warn",
237
+ message: `浮层的祖先 <${where}> 用 ${property}: ${value} 建了层叠上下文,浮层的层号只在它内部有效,会被上层兄弟盖住;去掉该祖先这条属性,或别把浮层放进这类容器`,
238
+ scope,
239
+ instanceId,
240
+ part: "positioner",
241
+ node: trap.ancestor,
242
+ detail: {
243
+ ancestor: where,
244
+ property,
245
+ value
246
+ }
247
+ });
248
+ }
249
+ //#endregion
250
+ //#region src/element-base.ts
251
+ let instanceSeq = 0;
252
+ /**
253
+ * 变动中是否真有角色节点进出。只认进出的元素自身是角色节点、或其子树中有角色节点:
254
+ * 业务内容(图表、虚拟列表、面板中的业务 DOM)的增删与 part 集合无关,不应引发重新接线;
255
+ * 更重要的是切断一条死循环:角色节点若本身是会在属性变化时改写自身子节点的自定义元素,
256
+ * 宿主重新接线、写属性、该节点改子节点、再次命中观察器会形成环。
257
+ */
258
+ function touchesParts(record) {
259
+ for (const list of [record.addedNodes, record.removedNodes]) for (const node of Array.from(list)) if (node.nodeType === 1 && containsPart(node)) return true;
260
+ return false;
261
+ }
262
+ /**
263
+ * 角色节点上属于作者的声明(与状态机写入的状态分属两侧),改变即等于更换了一个条目,必须重新接线。
264
+ * 只观察这几个而不是所有属性:wire() 每帧都向角色节点写 aria- 与 data-,全量观察等于自己触发自己。
265
+ */
266
+ const AUTHORED_ATTRS = [
267
+ "value",
268
+ "name",
269
+ "data-path",
270
+ "disabled",
271
+ "aria-disabled"
272
+ ];
273
+ function rewritesDeclaration(record) {
274
+ if (record.type !== "attributes" || !record.attributeName) return false;
275
+ const el = record.target;
276
+ return containsPart(el) && AUTHORED_ATTRS.includes(record.attributeName);
277
+ }
278
+ var XhElement = class extends XhReactiveElement {
279
+ /** 子类声明所属组件的解剖与元数据,接线后据它校验作者写的角色节点。 */
280
+ static partContract;
281
+ spreader = createSpreader();
282
+ partMap = /* @__PURE__ */ new Map();
283
+ partObserver;
284
+ instanceId = `${++instanceSeq}`;
285
+ createRenderRoot() {
286
+ return this;
287
+ }
288
+ getPart(name, index = 0) {
289
+ return this.partMap.get(name)?.[index] ?? null;
290
+ }
291
+ getParts(name) {
292
+ return this.partMap.get(name) ?? [];
293
+ }
294
+ /**
295
+ * 仍归本实例所有、但为脱离祖先层叠上下文而迁移到 Portal 的角色根。
296
+ * 默认没有;返回的根必须与宿主属于同一 Document。
297
+ */
298
+ externalPartRoots() {
299
+ return [];
300
+ }
301
+ /** 接管前记下角色节点上作者写的内联 display。 */
302
+ authorDisplay = /* @__PURE__ */ new WeakMap();
303
+ /**
304
+ * 用内联 display 保证收起态:作者层若为该 part 声明了 display,会覆盖 UA 的 `[hidden]{display:none}`,
305
+ * 仅靠 hidden 属性无法收起。展开时恢复作者原本的内联值而不是清为 '':后者会把作者写在该节点上的
306
+ * `style="display:grid"` 一并清除,且无法恢复。
307
+ */
308
+ setPartHidden(el, hidden) {
309
+ if (!el) return;
310
+ if (!this.authorDisplay.has(el)) this.authorDisplay.set(el, el.style.display);
311
+ el.style.display = hidden ? "none" : this.authorDisplay.get(el) ?? "";
312
+ }
313
+ refreshParts() {
314
+ const externalRoots = this.externalPartRoots();
315
+ const ownerDocument = this.ownerDocument;
316
+ for (const root of externalRoots) if (root.ownerDocument !== ownerDocument) throw new Error("[xh] Web Component 的 Portal 角色根必须与宿主属于同一 Document");
317
+ const next = discoverParts(this, externalRoots);
318
+ const live = /* @__PURE__ */ new Set();
319
+ for (const els of next.values()) for (const el of els) live.add(el);
320
+ const gone = [];
321
+ for (const els of this.partMap.values()) for (const el of els) if (!live.has(el)) gone.push(el);
322
+ if (gone.length) this.onPartsReleased(gone);
323
+ for (const el of gone) this.spreader.release(el);
324
+ this.partMap = next;
325
+ this.syncPartObservers();
326
+ }
327
+ /**
328
+ * 角色节点即将离开本宿主时的回调,交还前调用(节点上仍带着本状态机写入的标记)。
329
+ * 承载焦点的节点被移除时浏览器不派发 focusout(Chrome 如此),状态机无法得知,需要焦点语义的子类在此如实上报。
330
+ */
331
+ onPartsReleased(_nodes) {}
332
+ connectedCallback() {
333
+ super.connectedCallback();
334
+ this.observeParts();
335
+ this.stopConfigWatch ??= onXhConfigChange(() => this.requestUpdate());
336
+ this.requestUpdate();
337
+ }
338
+ stopConfigWatch;
339
+ /**
340
+ * 把沿祖先链解析到的全局配置合入这份 props:translations 按组件名分桶合并,
341
+ * locale 与 size 在元素上未提供时取配置中的值。不运行状态机的元素在 wire() 中用它包一层。
342
+ */
343
+ configured(component, props) {
344
+ return withXhConfig(component, props, this);
345
+ }
346
+ /** 已排了断开后的交还,还没跑到。 */
347
+ releaseScheduled = false;
348
+ disconnectedCallback() {
349
+ super.disconnectedCallback();
350
+ this.stopConfigWatch?.();
351
+ this.stopConfigWatch = void 0;
352
+ this.partObserver?.disconnect();
353
+ this.partObserver = void 0;
354
+ this.scheduleRelease();
355
+ }
356
+ /**
357
+ * 排一次断开后的交还,到微任务里再按 isConnected 决定跑不跑。
358
+ * 元素被移动(remove 后同步 append 到别处)走的也是 disconnect,延到微任务才分得清移动与真离场。
359
+ */
360
+ scheduleRelease() {
361
+ if (this.releaseScheduled) return;
362
+ this.releaseScheduled = true;
363
+ queueMicrotask(() => {
364
+ this.releaseScheduled = false;
365
+ if (!this.isConnected) this.releaseAllParts();
366
+ });
367
+ }
368
+ /**
369
+ * 交还全部角色节点并清空 partMap,重连时由 refreshParts 重新发现。
370
+ * 宿主离场后没有观察器再触发更新,refreshParts 那条交还路径够不着,没摘的监听器会经节点钉住整台宿主。
371
+ */
372
+ releaseAllParts() {
373
+ const gone = [];
374
+ for (const els of this.partMap.values()) gone.push(...els);
375
+ if (!gone.length) return;
376
+ this.onPartsReleased(gone);
377
+ for (const el of gone) this.spreader.release(el);
378
+ this.partMap = /* @__PURE__ */ new Map();
379
+ }
380
+ /** 层叠上下文那条诊断已经查过。 */
381
+ stackingChecked = false;
382
+ /**
383
+ * 浮层首次展开后查一次祖先链有没有建层叠上下文,命中即投诊断。
384
+ * 每个实例只查一次,重定位与后续展开不再重复。
385
+ */
386
+ checkStackingTrap(scope) {
387
+ if (this.stackingChecked) return;
388
+ const positioner = this.getParts("positioner").find((el) => el.dataset.state === "open");
389
+ if (!positioner) return;
390
+ this.stackingChecked = true;
391
+ reportStackingTrap(positioner, scope, this.instanceId);
392
+ }
393
+ /** 标记 wire() 正在写属性。 */
394
+ wiring = false;
395
+ /**
396
+ * 作者在运行期改动 Light DOM 后重新接线,两类改动都要接住:增删角色节点(漏掉会留下没有
397
+ * data-scope/data-part 与处理器的"死条目");原地改写节点上的声明(列表换数据复用节点时就是这形状,
398
+ * 漏掉会留着上一轮写的 aria-selected="true",而它自称的 value 已经变了,读屏念出的选中项就不是真的选中项)。
399
+ */
400
+ observeParts() {
401
+ if (this.partObserver) return;
402
+ this.partObserver = new MutationObserver((records) => {
403
+ if (this.wiring) return;
404
+ if (records.some((r) => (touchesParts(r) || rewritesDeclaration(r)) && this.ownsSubtree(r.target))) this.requestUpdate();
405
+ });
406
+ this.syncPartObservers();
407
+ }
408
+ syncPartObservers() {
409
+ const observer = this.partObserver;
410
+ if (!observer) return;
411
+ observer.disconnect();
412
+ const observe = (root) => observer.observe(root, {
413
+ childList: true,
414
+ subtree: true,
415
+ attributes: true,
416
+ attributeFilter: [...AUTHORED_ATTRS]
417
+ });
418
+ observe(this);
419
+ for (const root of this.externalPartRoots()) observe(root);
420
+ }
421
+ /** 目标是否归本宿主管:嵌套 xh-* 子树归内层元素自己管,外层不替它重跑 wire(discoverParts 本来也跳过它们)。 */
422
+ ownsSubtree(target) {
423
+ for (const root of this.externalPartRoots()) {
424
+ if (target !== root && !root.contains(target)) continue;
425
+ for (let node = target; node; node = node.parentNode) {
426
+ if (node === root) return true;
427
+ const tag = node.tagName;
428
+ if (typeof tag === "string" && tag.toLowerCase().startsWith("xh-")) break;
429
+ }
430
+ }
431
+ for (let node = target; node; node = node.parentNode) {
432
+ if (node === this) return true;
433
+ const tag = node.tagName;
434
+ if (typeof tag === "string" && tag.toLowerCase().startsWith("xh-")) return false;
435
+ }
436
+ return false;
437
+ }
438
+ updated(changed) {
439
+ super.updated(changed);
440
+ if (!this.isConnected) return;
441
+ this.refreshParts();
442
+ const contract = this.constructor.partContract;
443
+ if (contract) validatePartContract(contract, this.partMap, this, this.instanceId);
444
+ this.wiring = true;
445
+ try {
446
+ this.wire();
447
+ } finally {
448
+ queueMicrotask(() => {
449
+ this.wiring = false;
450
+ });
451
+ }
452
+ this.checkStackingTrap(contract?.anatomy.name);
453
+ }
454
+ };
455
+ //#endregion
456
+ //#region src/overlay-exit.ts
457
+ function createOverlayExit(options) {
458
+ let disposed = false;
459
+ const tracked = /* @__PURE__ */ new Map();
460
+ const presence = createPresence({
461
+ config: options.config,
462
+ open: options.open,
463
+ onRenderedChange: (rendered) => {
464
+ if (!rendered && !disposed) options.onExitComplete();
465
+ }
466
+ });
467
+ return {
468
+ presence,
469
+ get visible() {
470
+ return !disposed && presence.rendered;
471
+ },
472
+ track(...nodes) {
473
+ if (disposed) return;
474
+ const next = new Set(nodes.filter((node) => node !== null));
475
+ for (const node of next) if (!tracked.has(node)) tracked.set(node, attachCssExit(node, presence));
476
+ for (const [node, detach] of tracked) if (!next.has(node)) {
477
+ tracked.delete(node);
478
+ detach();
479
+ }
480
+ },
481
+ update(open) {
482
+ if (!disposed) presence.update(open);
483
+ },
484
+ dispose() {
485
+ if (disposed) return;
486
+ disposed = true;
487
+ presence.dispose();
488
+ for (const detach of tracked.values()) detach();
489
+ tracked.clear();
490
+ }
491
+ };
492
+ }
493
+ //#endregion
494
+ //#region src/elements/button.ts
495
+ /**
496
+ * `<xh-button>`:按钮行为宿主,宿主属性即 button props;机器只承载按压通道(data-pressed)。
497
+ *
498
+ * @customElement xh-button
499
+ * @attr {'button'|'submit'|'reset'} type - 原生按钮类型,默认 button
500
+ * @attr {boolean} disabled - 禁用(原生 disabled,失去焦点)
501
+ * @attr {boolean} icon-only - 仅图标:左右内边距清零、宽高相等;作者须自行提供可及名
502
+ * @attr {boolean} full-width - 撑满行宽
503
+ * @attr {boolean} loading - 加载中(aria-disabled,保留焦点并拦截点击)
504
+ * @attr {'solid'|'subtle'|'outline'|'ghost'} variant - 视觉变体,默认 solid(只有 Button 缺省品牌实心,其余触发器缺省中性)
505
+ * @attr {'brand'|'neutral'|'success'|'warning'|'danger'|'info'} tone - 颜色
506
+ * @attr {'sm'|'md'|'lg'} size - 尺寸
507
+ * @attr {'button'|'a'} as - 根节点渲染的标签,默认 button;写 a 时不再产出 type 与原生 disabled
508
+ * @csspart root - 承载 data-scope/data-part/data-* 的原生 button(as="a" 时是 a)
509
+ */
510
+ var XhButtonElement = class extends XhElement {
511
+ static partContract = {
512
+ anatomy: buttonAnatomy,
513
+ meta: buttonMeta
514
+ };
515
+ static properties = {
516
+ type: {},
517
+ disabled: { type: Boolean },
518
+ iconOnly: {
519
+ type: Boolean,
520
+ attribute: "icon-only"
521
+ },
522
+ fullWidth: {
523
+ type: Boolean,
524
+ attribute: "full-width"
525
+ },
526
+ loading: { type: Boolean },
527
+ variant: {},
528
+ tone: {},
529
+ size: {},
530
+ as: {}
531
+ };
532
+ ctrl = new MachineController(this, buttonMachine, () => this.machineProps());
533
+ machineProps() {
534
+ const root = this.getPart("root");
535
+ return {
536
+ type: this.type,
537
+ disabled: this.disabled,
538
+ loading: this.loading,
539
+ variant: this.variant,
540
+ tone: this.tone,
541
+ size: this.size,
542
+ as: this.as,
543
+ iconOnly: this.iconOnly,
544
+ fullWidth: this.fullWidth,
545
+ ariaLabel: root?.getAttribute("aria-label") ?? void 0,
546
+ ariaLabelledby: root?.getAttribute("aria-labelledby") ?? void 0
547
+ };
548
+ }
549
+ wire() {
550
+ const root = this.getPart("root");
551
+ if (!root) return;
552
+ const api = connectButton(this.ctrl.service, wcNormalize);
553
+ this.spreader.spread(root, api.getRootProps());
554
+ const put = (name, props) => {
555
+ const el = this.getPart(name);
556
+ if (el) this.spreader.spread(el, props);
557
+ };
558
+ put("label", api.getLabelProps());
559
+ put("prefix", api.getPrefixProps());
560
+ put("suffix", api.getSuffixProps());
561
+ put("indicator", api.getIndicatorProps());
562
+ }
563
+ };
564
+ //#endregion
565
+ //#region src/runtime/portal-lease-controller.ts
566
+ /**
567
+ * Web Components 对 Core PortalLease 的共享生命周期接线。
568
+ * placeholder、独占壳、视觉桥、事务回滚与精确归位仍全部由 Core 持有。
569
+ */
570
+ var PortalLeaseController = class {
571
+ options;
572
+ lease = null;
573
+ constructor(options) {
574
+ this.options = options;
575
+ }
576
+ /** XhElement 用它继续发现已经迁移到宿主外的全部角色节点。 */
577
+ get roots() {
578
+ return this.lease?.roots ?? [];
579
+ }
580
+ /**
581
+ * 某个根在作者结构里的落点:未搬迁时就是它自己,搬迁中是租约留在原位的占位节点。
582
+ * 宿主自己生成、要排在浮层之前的节点(表单出口)按它定位,浮层归位后顺序不变。
583
+ */
584
+ homeOf(root) {
585
+ const index = this.lease?.roots.indexOf(root) ?? -1;
586
+ return index >= 0 ? this.lease.placeholders[index] : root;
587
+ }
588
+ /** Presence 仍可见时持有租约;退场完成或必要节点缺席时精确归位。 */
589
+ sync(active) {
590
+ const source = this.options.source();
591
+ const roots = this.options.roots();
592
+ if (!active || !source || !roots.length) {
593
+ if (this.release()) this.options.onChange();
594
+ return;
595
+ }
596
+ const target = this.resolveTarget();
597
+ if (this.lease?.source === source && this.lease.target === target && roots.length === this.lease.roots.length && roots.every((root, index) => root === this.lease.roots[index])) return;
598
+ this.release();
599
+ this.lease = createPortalLease({
600
+ source,
601
+ target,
602
+ roots,
603
+ onShellReady: this.options.onShellReady
604
+ });
605
+ this.options.onChange();
606
+ }
607
+ /** 宿主断开或 realm 换代时立即归位;Core 的 release 自身幂等。 */
608
+ dispose() {
609
+ this.release();
610
+ }
611
+ release() {
612
+ const lease = this.lease;
613
+ if (!lease) return false;
614
+ this.lease = null;
615
+ lease.release();
616
+ return true;
617
+ }
618
+ resolveTarget() {
619
+ const resolver = this.options.portalContainer?.() ?? this.options.config()?.portalContainer;
620
+ let target;
621
+ try {
622
+ target = resolver?.();
623
+ } catch (error) {
624
+ if (this.release()) this.options.onChange();
625
+ throw error;
626
+ }
627
+ if (target) return target;
628
+ if (this.release()) this.options.onChange();
629
+ throw new Error(`[xh] ${this.options.name} 的 portalContainer 必须返回已连接的同 Document Element`);
630
+ }
631
+ };
632
+ //#endregion
633
+ //#region src/runtime/anchored-portal-controller.ts
634
+ /**
635
+ * 锚定浮层的 Web Components 共享 Portal 接线。
636
+ * placeholder、独占壳、视觉桥、事务回滚与精确归位全部由 Core PortalLease 持有。
637
+ */
638
+ var AnchoredPortalController = class {
639
+ options;
640
+ portal;
641
+ constructor(options) {
642
+ this.options = options;
643
+ this.portal = new PortalLeaseController({
644
+ ...options,
645
+ roots: () => {
646
+ const root = options.root();
647
+ return root ? [root] : [];
648
+ }
649
+ });
650
+ }
651
+ /** XhElement 用它继续发现已经迁移到宿主外的全部角色节点。 */
652
+ get roots() {
653
+ return this.portal.roots;
654
+ }
655
+ /** positioner 在作者结构里的落点:未搬迁时是它自己,搬迁中是占位节点;缺席时为 null。 */
656
+ home() {
657
+ const root = this.options.root();
658
+ return root ? this.portal.homeOf(root) : null;
659
+ }
660
+ /** Presence 仍可见时持有租约;退场完成或必要节点缺席时精确归位。 */
661
+ sync(active) {
662
+ this.portal.sync(active);
663
+ }
664
+ /** 宿主断开或 realm 换代时立即归位;Core 的 release 自身幂等。 */
665
+ dispose() {
666
+ this.portal.dispose();
667
+ }
668
+ };
669
+ //#endregion
670
+ //#region src/runtime/portal-host.ts
671
+ /**
672
+ * 只供真正物理 Portal 宿主继承;普通 XhElement(包括 NavigationMenu)不公开此能力。
673
+ * property 响应式由基类统一声明,实例变化会自然触发各宿主既有 wire/sync。
674
+ */
675
+ var XhPortalHostElement = class extends XhElement {
676
+ static properties = { portalContainer: { attribute: false } };
677
+ createPortalLeaseController(options) {
678
+ return new PortalLeaseController({
679
+ ...options,
680
+ portalContainer: () => this.portalContainer ?? resolveXhConfig(this).portalContainer
681
+ });
682
+ }
683
+ createAnchoredPortalController(options) {
684
+ return new AnchoredPortalController({
685
+ ...options,
686
+ portalContainer: () => this.portalContainer ?? resolveXhConfig(this).portalContainer
687
+ });
688
+ }
689
+ };
690
+ //#endregion
691
+ //#region src/elements/dialog.ts
692
+ const BOOLEAN_CONVERTER$3 = { fromAttribute: (v) => v === null ? void 0 : v !== "false" };
693
+ const STRING_CONVERTER$3 = { fromAttribute: (v) => v ?? void 0 };
694
+ /**
695
+ * `<xh-dialog>`:Light-DOM 行为宿主,运行 dialog 状态机并把 connect 产出接到角色节点上,
696
+ * 关闭时用内联 style.display 隐藏浮层子树。
697
+ *
698
+ * @customElement xh-dialog
699
+ * @attr {boolean} open - 受控开合;未提供该属性即非受控
700
+ * @attr {boolean} default-open - 非受控初始为打开
701
+ * @attr {boolean} modal - 模态(陷入焦点、锁定滚动、遮罩交互外关闭),默认 true
702
+ * @attr {'dialog'|'alertdialog'} role - 语义角色,默认 dialog
703
+ * @attr {boolean} close-on-escape - Esc 关闭,默认 true
704
+ * @attr {boolean} restore-focus - 关闭后把焦点归还触发元素,默认 true
705
+ * @attr {string} initial-focus - 展开后先聚焦到 content 内匹配此选择器的元素
706
+ * @attr {'sm'|'md'|'lg'} size - 尺寸:只影响 content 的最大宽度,写在 content 上
707
+ * @attr {'opaque'|'blur'|'transparent'} variant - 遮罩形态:只影响 backdrop 的底色与模糊
708
+ * @fires open-change - open 状态变化;detail 为 `{ open: boolean }`
709
+ * @fires exit-complete - 退出完成且本层资源已释放
710
+ * @csspart trigger - 触发按钮
711
+ * @csspart backdrop - 遮罩层
712
+ * @csspart positioner - 浮层定位容器
713
+ * @csspart content - 对话框内容(role / aria-modal / 焦点陷阱所在)
714
+ * @csspart header - 面板头:标题与说明所在的段,不随正文滚动
715
+ * @csspart indicator - 语气徽记:未提供内容时由皮肤按节点上的 data-tone 绘制兜底字形
716
+ * @csspart title - 标题(aria-labelledby 目标)
717
+ * @csspart description - 描述(aria-describedby 目标)
718
+ * @csspart body - 正文:面板中唯一会滚动的段
719
+ * @csspart footer - 面板尾:动作按钮所在的段,不随正文滚动
720
+ * @csspart close-trigger - 关闭按钮
721
+ */
722
+ var XhDialogElement = class extends XhPortalHostElement {
723
+ static partContract = {
724
+ anatomy: dialogAnatomy,
725
+ meta: dialogMeta
726
+ };
727
+ static properties = {
728
+ open: { converter: BOOLEAN_CONVERTER$3 },
729
+ defaultOpen: {
730
+ type: Boolean,
731
+ attribute: "default-open"
732
+ },
733
+ modal: { converter: BOOLEAN_CONVERTER$3 },
734
+ closeOnEscape: {
735
+ converter: BOOLEAN_CONVERTER$3,
736
+ attribute: "close-on-escape"
737
+ },
738
+ restoreFocus: {
739
+ converter: BOOLEAN_CONVERTER$3,
740
+ attribute: "restore-focus"
741
+ },
742
+ initialFocus: {
743
+ converter: STRING_CONVERTER$3,
744
+ attribute: "initial-focus"
745
+ },
746
+ size: { converter: STRING_CONVERTER$3 },
747
+ variant: { converter: STRING_CONVERTER$3 },
748
+ translations: { attribute: false }
749
+ };
750
+ idGen = createCounterIdGenerator();
751
+ dialogScope = createScope(() => this, this.idGen);
752
+ config = null;
753
+ contentNode = null;
754
+ exit = null;
755
+ backdropNode = null;
756
+ portal = this.createPortalLeaseController({
757
+ name: "Dialog 视口模态",
758
+ config: () => this.config,
759
+ source: () => this,
760
+ roots: () => {
761
+ const positioner = this.getPart("positioner");
762
+ return this.backdropNode && positioner ? [this.backdropNode, positioner] : [];
763
+ },
764
+ onChange: () => this.requestUpdate()
765
+ });
766
+ notify = (details) => {
767
+ this.dispatchEvent(new CustomEvent("open-change", {
768
+ detail: details,
769
+ bubbles: true,
770
+ composed: true
771
+ }));
772
+ };
773
+ ctrl = new MachineController(this, dialogMachine, () => this.machineProps(), {
774
+ scope: this.dialogScope,
775
+ onBuilt: (svc) => this.injectRefs(svc)
776
+ });
777
+ machineProps() {
778
+ return {
779
+ open: this.open,
780
+ defaultOpen: this.defaultOpen ?? false,
781
+ modal: this.modal,
782
+ role: this.getAttribute("role") ?? void 0,
783
+ closeOnEscape: this.closeOnEscape,
784
+ restoreFocus: this.restoreFocus,
785
+ initialFocus: this.initialFocus,
786
+ size: this.size,
787
+ variant: this.variant,
788
+ translations: this.translations,
789
+ onOpenChange: this.notify,
790
+ onExitComplete: () => this.dispatchEvent(new CustomEvent("exit-complete", {
791
+ bubbles: true,
792
+ composed: true
793
+ }))
794
+ };
795
+ }
796
+ ensureConfig() {
797
+ if (this.config) return;
798
+ this.config = createRuntimeConfig({
799
+ scope: this.dialogScope,
800
+ idGenerator: this.idGen,
801
+ scrollRoot: () => resolveXhConfig(this).scrollRoot?.() ?? null
802
+ });
803
+ }
804
+ /** 退场闸门建一次;presence 不是响应式 cell,退场结束要显式排一次更新才轮得到收起。 */
805
+ ensureExit(open) {
806
+ this.ensureConfig();
807
+ this.exit ??= createOverlayExit({
808
+ config: this.config,
809
+ open,
810
+ onExitComplete: () => this.requestUpdate()
811
+ });
812
+ return this.exit;
813
+ }
814
+ externalPartRoots() {
815
+ return this.portal.roots;
816
+ }
817
+ registerLayer = () => {
818
+ this.ensureConfig();
819
+ return this.config.layerRegistry.register({
820
+ kind: "modal",
821
+ node: () => this.contentNode,
822
+ branches: () => [],
823
+ isModal: () => this.machineProps().modal ?? true,
824
+ surfaces: () => [this.backdropNode].filter(Boolean)
825
+ });
826
+ };
827
+ injectRefs(svc) {
828
+ this.ensureConfig();
829
+ svc.refs.set("config", this.config);
830
+ svc.refs.set("registerLayer", this.registerLayer);
831
+ svc.refs.set("presence", this.ensureExit(svc.state.get() === "open").presence);
832
+ svc.refs.set("getContentEl", () => this.contentNode);
833
+ svc.refs.set("getTriggerEl", () => this.getPart("trigger"));
834
+ svc.refs.set("branches", () => []);
835
+ }
836
+ wire() {
837
+ const svc = this.ctrl.service;
838
+ const api = connectDialog(svc, wcNormalize);
839
+ const open = svc.state.get() === "open";
840
+ this.contentNode = this.getPart("content");
841
+ this.backdropNode = this.getPart("backdrop");
842
+ const put = (name, props) => {
843
+ const el = this.getPart(name);
844
+ if (el) this.spreader.spread(el, props);
845
+ };
846
+ put("trigger", api.getTriggerProps());
847
+ put("backdrop", api.getBackdropProps());
848
+ put("positioner", api.getPositionerProps());
849
+ put("content", api.getContentProps());
850
+ put("header", api.getHeaderProps());
851
+ put("indicator", api.getIndicatorProps());
852
+ put("title", api.getTitleProps());
853
+ put("description", api.getDescriptionProps());
854
+ put("body", api.getBodyProps());
855
+ put("footer", api.getFooterProps());
856
+ put("close-trigger", api.getCloseTriggerProps());
857
+ const exit = this.ensureExit(open);
858
+ const modal = this.machineProps().modal ?? true;
859
+ exit.track(this.contentNode, modal ? this.backdropNode : null);
860
+ exit.update(open);
861
+ const visible = exit.visible;
862
+ const positioner = this.getPart("positioner");
863
+ this.portal.sync(modal && (open || visible));
864
+ if (positioner) this.setPartHidden(positioner, !visible);
865
+ if (this.backdropNode) this.setPartHidden(this.backdropNode, !visible || !modal);
866
+ this.setPartHidden(this.contentNode, !visible);
867
+ }
868
+ disconnectedCallback() {
869
+ super.disconnectedCallback();
870
+ this.exit?.dispose();
871
+ this.exit = null;
872
+ this.portal.dispose();
873
+ if (this.ctrl.service.state.get() !== "open") this.setPartHidden(this.contentNode, true);
874
+ this.config = null;
875
+ }
876
+ };
877
+ //#endregion
878
+ //#region src/elements/loading-bar.ts
879
+ const STRING_CONVERTER$2 = { fromAttribute: (v) => v ?? void 0 };
880
+ const NUMBER_CONVERTER$2 = { fromAttribute: (v) => v == null || v === "" ? void 0 : Number(v) };
881
+ const BOOLEAN_CONVERTER$2 = { fromAttribute: (v) => v === null ? void 0 : v !== "false" };
882
+ const HEIGHT_CONVERTER = { fromAttribute: (v) => {
883
+ if (v == null || v === "") return void 0;
884
+ const px = Number(v);
885
+ return Number.isFinite(px) ? px : v;
886
+ } };
887
+ /**
888
+ * `<xh-loading-bar>`:Light-DOM 行为宿主:作者写 root / track / range 角色节点,
889
+ * 元素运行 loading-bar 状态机并把 connect 产出接上。
890
+ *
891
+ * 两种模式互不相干:提供 `value` 即为确定进度,宽度按它显示、读屏可以获取 aria-valuenow;
892
+ * 未提供时使用模拟进度:`loading` 置真时先跳到 `minimum`,随后按 `trickle-speed` 逐拍爬升,
893
+ * 步长按剩余量取,因此越接近满格越慢且永远到不了 100;此时按规范不报告 aria-valuenow,
894
+ * 属性缺席本身就是进度未知的表达。`loading` 转为假时先到达 100,走完淡出窗口才归零收起。
895
+ *
896
+ * 收起态只给 root 加 hidden 并写一条内联 display,作者编写的结构一个都不卸载。
897
+ *
898
+ * @customElement xh-loading-bar
899
+ * @attr {number} value - 受控进度值(0-100);提供后即为确定进度,内部爬升整体让位
900
+ * @attr {number} default-value - 非受控初值,默认 0
901
+ * @attr {boolean} loading - 加载开关:属性存在即开始,`loading="false"` 或移除即结束
902
+ * @attr {string|number} height - 进度条厚度:纯数字按像素,其余按 CSS 长度;默认 2px
903
+ * @attr {'brand'|'neutral'|'success'|'warning'|'danger'|'info'} tone - 语气,决定进度段使用哪族颜色;提供 color 时以 color 为准
904
+ * @attr {boolean} trickle - 不确定进度时自行向前爬升,默认开启;`trickle="false"` 关闭
905
+ * @attr {number} trickle-speed - 爬升节拍毫秒,默认 200;<=0 等同于关闭爬升
906
+ * @attr {number} minimum - 起步值,默认 8
907
+ * @attr {number} fade-duration - 到达 100 之后的淡出窗口毫秒,默认 200
908
+ * @fires value-change - 进度值变化;detail 为 `{ value: number }`
909
+ * @csspart root - progressbar 本身(承载名字、值域、data-state 与收起态)
910
+ * @csspart track - 背景槽
911
+ * @csspart range - 进度段;宽度由元素写入内联样式,样式层不应修改该轴
912
+ * @csspart peg - 进度段末端的亮边(对读屏隐藏)
913
+ */
914
+ var XhLoadingBarElement = class extends XhElement {
915
+ static partContract = {
916
+ anatomy: loadingBarAnatomy,
917
+ meta: loadingBarMeta
918
+ };
919
+ static properties = {
920
+ value: { converter: NUMBER_CONVERTER$2 },
921
+ defaultValue: {
922
+ converter: NUMBER_CONVERTER$2,
923
+ attribute: "default-value"
924
+ },
925
+ loading: { converter: BOOLEAN_CONVERTER$2 },
926
+ height: { converter: HEIGHT_CONVERTER },
927
+ tone: { converter: STRING_CONVERTER$2 },
928
+ trickle: { converter: BOOLEAN_CONVERTER$2 },
929
+ trickleSpeed: {
930
+ converter: NUMBER_CONVERTER$2,
931
+ attribute: "trickle-speed"
932
+ },
933
+ minimum: { converter: NUMBER_CONVERTER$2 },
934
+ fadeDuration: {
935
+ converter: NUMBER_CONVERTER$2,
936
+ attribute: "fade-duration"
937
+ },
938
+ translations: { attribute: false }
939
+ };
940
+ notify = (details) => {
941
+ this.dispatchEvent(new CustomEvent("value-change", {
942
+ detail: details,
943
+ bubbles: true,
944
+ composed: true
945
+ }));
946
+ };
947
+ ctrl = new MachineController(this, loadingBarMachine, () => this.machineProps());
948
+ machineProps() {
949
+ return {
950
+ value: this.value,
951
+ defaultValue: this.defaultValue,
952
+ loading: this.loading,
953
+ height: this.height,
954
+ tone: this.tone,
955
+ trickle: this.trickle,
956
+ trickleSpeed: this.trickleSpeed,
957
+ minimum: this.minimum,
958
+ fadeDuration: this.fadeDuration,
959
+ translations: this.translations,
960
+ onValueChange: this.notify
961
+ };
962
+ }
963
+ wire() {
964
+ const api = connectLoadingBar(this.ctrl.service, wcNormalize);
965
+ const put = (name, props) => {
966
+ const el = this.getPart(name);
967
+ if (el) this.spreader.spread(el, props);
968
+ };
969
+ put("root", api.getRootProps());
970
+ put("track", api.getTrackProps());
971
+ put("range", api.getRangeProps());
972
+ put("peg", api.getPegProps());
973
+ this.setPartHidden(this.getPart("root"), !api.visible);
974
+ }
975
+ };
976
+ //#endregion
977
+ //#region src/elements/notification.ts
978
+ const STRING_CONVERTER$1 = { fromAttribute: (v) => v ?? void 0 };
979
+ const NUMBER_CONVERTER$1 = { fromAttribute: (v) => v == null || v === "" ? void 0 : Number(v) };
980
+ const BOOLEAN_CONVERTER$1 = { fromAttribute: (v) => v === null ? void 0 : v !== "false" };
981
+ /**
982
+ * 该堆叠对应哪个位置。属性缺席时交 undefined,由 connect 回落到队列的 placement。
983
+ * 写错时照原样交出:它与九个位置没有一个匹配,该堆叠就一直为空:
984
+ * 明显的空白好过静默冒充另一个位置,把通知放到作者未预期的角落。
985
+ */
986
+ function groupPlacement(el) {
987
+ const raw = el.getAttribute("placement");
988
+ return raw == null || raw === "" ? void 0 : raw;
989
+ }
990
+ /**
991
+ * `<xh-notification>`:Light-DOM 行为宿主:作者写 root 与若干 group 角色节点,
992
+ * 元素运行 notification 状态机并把 connect 产出接上。每个 group 用 `placement` 属性声明所在位置,
993
+ * 未写时落在队列的 placement 上。
994
+ *
995
+ * 单条通知的节点由作者按队列渲染(读取 `items` 或监听 `items-change`),元素不替作者生成:
996
+ * 生成节点等于收走模板控制权,图标、进度条、i18n 文案都无法再加入。
997
+ * 每条渲染为一个 `<xh-notification-item>`,它走完退场会冒泡一条 status-change,本元素据此删除记录。
998
+ *
999
+ * @customElement xh-notification
1000
+ * @attr {'top-start'|'top'|'top-end'|'middle-start'|'middle'|'middle-end'|'bottom-start'|'bottom'|'bottom-end'} placement - 默认落位,默认 bottom-end
1001
+ * @attr {number} max - 每个位置最多同时保留几条,超出时先移除低优先级、同级中移除最旧的;默认 5,提供 Infinity 即不限
1002
+ * @attr {'id'|'content'} dedupe - 重复的处理方式,默认 id;content 则同一内容合并为一条并计数
1003
+ * @attr {number} gap - 同一组内的间距(px),默认 16
1004
+ * @attr {number} duration - 单条未写 duration 时的默认停留毫秒
1005
+ * @attr {number} remove-delay - 单条未写 remove-delay 时的默认退场窗口毫秒
1006
+ * @attr {boolean} pause-on-page-idle - 页面切到后台时暂停计时,逐条下发
1007
+ * @fires items-change - 队列变化;detail 为 `{ items: NotificationRecord[] }`
1008
+ * @csspart root - 队列的作用域包装(display: contents,不占布局),承载 data-count / data-empty
1009
+ * @csspart group - role=region 的地标,某一个位置上的一组;可自带 placement 属性,承载 data-placement / data-count / data-empty 与间距
1010
+ */
1011
+ var XhNotificationElement = class extends XhElement {
1012
+ static partContract = {
1013
+ anatomy: notificationAnatomy,
1014
+ meta: notificationMeta
1015
+ };
1016
+ static properties = {
1017
+ items: { attribute: false },
1018
+ defaultItems: { attribute: false },
1019
+ placement: { converter: STRING_CONVERTER$1 },
1020
+ max: { converter: NUMBER_CONVERTER$1 },
1021
+ dedupe: { converter: STRING_CONVERTER$1 },
1022
+ gap: { converter: NUMBER_CONVERTER$1 },
1023
+ duration: { converter: NUMBER_CONVERTER$1 },
1024
+ removeDelay: {
1025
+ converter: NUMBER_CONVERTER$1,
1026
+ attribute: "remove-delay"
1027
+ },
1028
+ pauseOnPageIdle: {
1029
+ converter: BOOLEAN_CONVERTER$1,
1030
+ attribute: "pause-on-page-idle"
1031
+ },
1032
+ translations: { attribute: false }
1033
+ };
1034
+ notify = (details) => {
1035
+ this.dispatchEvent(new CustomEvent("items-change", {
1036
+ detail: details,
1037
+ bubbles: true,
1038
+ composed: true
1039
+ }));
1040
+ };
1041
+ ctrl = new MachineController(this, notificationMachine, () => this.machineProps());
1042
+ machineProps() {
1043
+ return {
1044
+ items: this.items,
1045
+ defaultItems: this.defaultItems,
1046
+ placement: this.placement,
1047
+ max: this.max,
1048
+ dedupe: this.dedupe,
1049
+ gap: this.gap,
1050
+ duration: this.duration,
1051
+ removeDelay: this.removeDelay,
1052
+ pauseOnPageIdle: this.pauseOnPageIdle,
1053
+ translations: this.translations,
1054
+ onItemsChange: this.notify
1055
+ };
1056
+ }
1057
+ /**
1058
+ * 命令式入口共用的取法。状态机在进入文档(hostConnected)后才建立:
1059
+ * 尚未进入文档就发通知是调用方的时序问题,明确报错好过把通知静默丢弃。
1060
+ */
1061
+ commands() {
1062
+ if (!this.ctrl.service) throw new Error("[xh] <xh-notification> 还没进文档,命令式接口此时不可用");
1063
+ return connectNotification(this.ctrl.service, wcNormalize);
1064
+ }
1065
+ /** 入队并返回 id;同 id 已存在则就地改写,位置不变(处理中转为已完成即使用此路径)。 */
1066
+ create(options = {}) {
1067
+ return this.commands().create(options);
1068
+ }
1069
+ /**
1070
+ * 刻意不命名为 `update`:那是 Lit 自身的渲染生命周期钩子,占用它会使宿主每次重渲
1071
+ * 都进入此处(并把 changedProperties 当作 id),组件将无法工作。
1072
+ * Vue 侧没有这层基类,composable 上仍命名为 `update`。
1073
+ */
1074
+ updateItem(id, options) {
1075
+ this.commands().update(id, options);
1076
+ }
1077
+ dismiss(id) {
1078
+ this.commands().dismiss(id);
1079
+ }
1080
+ dismissAll() {
1081
+ this.commands().dismissAll();
1082
+ }
1083
+ /** max 之内、按加入先后排列的可见条目,已补齐默认值,可直接展开给 `<xh-notification-item>`。 */
1084
+ get visibleNotifications() {
1085
+ return this.commands().visibleNotifications;
1086
+ }
1087
+ /** 当前有条目的位置,按九宫格固定顺序。 */
1088
+ get placements() {
1089
+ return this.commands().placements;
1090
+ }
1091
+ get count() {
1092
+ return this.commands().count;
1093
+ }
1094
+ getItemsByPlacement(placement) {
1095
+ return this.commands().getItemsByPlacement(placement);
1096
+ }
1097
+ /**
1098
+ * 单条通知退场完成后会冒泡上来:记录应从队列中删除。
1099
+ * 识别元素名而不是只识别事件名:avatar 之类的部件也派发 status-change,且完全可能位于通知内部。
1100
+ */
1101
+ onItemStatus = (event) => {
1102
+ if (event.target?.tagName.toLowerCase() !== "xh-notification-item") return;
1103
+ const detail = event.detail;
1104
+ if (detail?.status !== "unmounted") return;
1105
+ if (this.ctrl.service?.getStatus() !== "Started") return;
1106
+ this.dismiss(detail.id);
1107
+ };
1108
+ connectedCallback() {
1109
+ super.connectedCallback();
1110
+ this.addEventListener("status-change", this.onItemStatus);
1111
+ }
1112
+ disconnectedCallback() {
1113
+ this.removeEventListener("status-change", this.onItemStatus);
1114
+ super.disconnectedCallback();
1115
+ }
1116
+ wire() {
1117
+ const api = connectNotification(this.ctrl.service, wcNormalize);
1118
+ const root = this.getPart("root");
1119
+ if (root) this.spreader.spread(root, api.getRootProps());
1120
+ for (const el of this.getParts("group")) this.spreader.spread(el, api.getGroupProps({ placement: groupPlacement(el) }));
1121
+ }
1122
+ };
1123
+ const ITEM_CONTRACT = {
1124
+ anatomy: notificationAnatomy,
1125
+ meta: {
1126
+ component: "notification",
1127
+ requiredParts: ["item"]
1128
+ }
1129
+ };
1130
+ /**
1131
+ * `<xh-notification-item>`:单条通知卡片:作者写 item / item-indicator / item-title /
1132
+ * item-description / item-action-trigger / item-close-trigger 角色节点,
1133
+ * 元素运行生命周期状态机并把 connect 产出接上。
1134
+ *
1135
+ * item 承载 role 与 aria-live:默认 status + polite(排队等待读屏的空隙),
1136
+ * tone="danger" 换为 alert + assertive(打断当前朗读)。指针停在卡片上、
1137
+ * 或焦点落进卡片内部都会暂停倒计时,离开后继续剩余部分。
1138
+ *
1139
+ * 退场窗口结束时只把卡片收起、不删除节点:作者写在其中的内容归作者,
1140
+ * 何时把该条从队列中删除是 `<xh-notification>` 的职责(它接收本元素冒泡的 status-change)。
1141
+ *
1142
+ * @customElement xh-notification-item
1143
+ * @attr {string} id - 队列身份,`<xh-notification>` 按它寻址;未提供时使用实例自身的 scope id
1144
+ * @attr {string} title - 标题文案;作者未在 item-title 部件中写内容时由元素填入
1145
+ * @attr {string} description - 补充说明;作者未在 item-description 部件中写内容时由元素填入
1146
+ * @attr {'info'|'success'|'warning'|'danger'} tone - 语气,默认 info;danger 使用 alert + assertive
1147
+ * @attr {boolean} loading - 事情尚未完成:图标换为转圈,且不自动消失
1148
+ * @attr {number} duration - 停留毫秒,默认 5000;<=0 即关闭自动消失
1149
+ * @attr {number} remove-delay - 退场窗口毫秒,默认 200,留给退场动画
1150
+ * @attr {boolean} closable - 是否提供可用的关闭按钮,默认 true;写 closable="false" 关闭
1151
+ * @attr {boolean} pause-on-page-idle - 页面切到后台时暂停计时,默认关闭
1152
+ * @attr {boolean} paused - 由宿主整组一起暂停计时,默认关闭;与指针、焦点等来源并存
1153
+ * @fires status-change - 生命周期落定;detail 为 `{ id: string, status: 'dismissing'|'unmounted' }`
1154
+ * @fires action - 操作按钮被按下;detail 为 `{ id: string }`
1155
+ * @csspart item - role=status(danger 时 alert)的卡片,承载 data-tone / data-loading / data-state / data-paused
1156
+ * @csspart item-indicator - 语气指示符;留空即由皮肤按卡片上的语气绘制兜底字形,卡片加载中则换为转圈
1157
+ * @csspart item-title - 标题,aria-labelledby 的目标
1158
+ * @csspart item-description - 补充说明,aria-describedby 的目标
1159
+ * @csspart item-action-trigger - 操作按钮:先发出 action 再进入退场
1160
+ * @csspart item-progress - 倒计时条;不自动消失时收起
1161
+ * @csspart item-close-trigger - 关闭按钮;closable=false 时为原生 disabled 并收起
1162
+ */
1163
+ var XhNotificationItemElement = class extends XhElement {
1164
+ static partContract = ITEM_CONTRACT;
1165
+ static properties = {
1166
+ itemId: {
1167
+ converter: STRING_CONVERTER$1,
1168
+ attribute: "id"
1169
+ },
1170
+ titleText: {
1171
+ converter: STRING_CONVERTER$1,
1172
+ attribute: "title"
1173
+ },
1174
+ description: { converter: STRING_CONVERTER$1 },
1175
+ tone: { converter: STRING_CONVERTER$1 },
1176
+ loading: { converter: BOOLEAN_CONVERTER$1 },
1177
+ duration: { converter: NUMBER_CONVERTER$1 },
1178
+ removeDelay: {
1179
+ converter: NUMBER_CONVERTER$1,
1180
+ attribute: "remove-delay"
1181
+ },
1182
+ closable: { converter: BOOLEAN_CONVERTER$1 },
1183
+ pauseOnPageIdle: {
1184
+ converter: BOOLEAN_CONVERTER$1,
1185
+ attribute: "pause-on-page-idle"
1186
+ },
1187
+ paused: { converter: BOOLEAN_CONVERTER$1 },
1188
+ translations: { attribute: false }
1189
+ };
1190
+ notifyStatus = (details) => {
1191
+ this.dispatchEvent(new CustomEvent("status-change", {
1192
+ detail: details,
1193
+ bubbles: true,
1194
+ composed: true
1195
+ }));
1196
+ };
1197
+ notifyAction = (details) => {
1198
+ this.dispatchEvent(new CustomEvent("action", {
1199
+ detail: details,
1200
+ bubbles: true,
1201
+ composed: true
1202
+ }));
1203
+ };
1204
+ ctrl = new MachineController(this, toastMachine, () => this.machineProps(), { configName: "notification" });
1205
+ machineProps() {
1206
+ return {
1207
+ id: this.itemId,
1208
+ title: this.titleText,
1209
+ description: this.description,
1210
+ tone: this.tone,
1211
+ loading: this.loading,
1212
+ duration: this.duration,
1213
+ removeDelay: this.removeDelay,
1214
+ closable: this.closable,
1215
+ pauseOnPageIdle: this.pauseOnPageIdle,
1216
+ paused: this.paused,
1217
+ translations: this.translations,
1218
+ onStatusChange: this.notifyStatus,
1219
+ onAction: this.notifyAction
1220
+ };
1221
+ }
1222
+ /** 这个部件的文字归不归作者:头一回见到它时定案,之后不再改口。 */
1223
+ authorOwnsText = /* @__PURE__ */ new WeakMap();
1224
+ /**
1225
+ * 作者没往部件里写内容时,用 props 上的文案填进去。
1226
+ *
1227
+ * 归属只判一次:判完本轮就会把文字写进去,再回读分不清是作者写的还是自己写的,
1228
+ * 从那以后 title 属性改了也再刷不动(Vue 侧是响应式渲染,会跟着改,两侧就此分叉)。
1229
+ */
1230
+ fillText(el, text) {
1231
+ if (!el) return;
1232
+ let authored = this.authorOwnsText.get(el);
1233
+ if (authored === void 0) {
1234
+ authored = el.childElementCount > 0 || (el.textContent ?? "").trim() !== "";
1235
+ this.authorOwnsText.set(el, authored);
1236
+ }
1237
+ if (authored) return;
1238
+ const next = text ?? "";
1239
+ if (el.textContent !== next) el.textContent = next;
1240
+ }
1241
+ wire() {
1242
+ const api = connectNotificationItem(this.ctrl.service, wcNormalize);
1243
+ const put = (name, props) => {
1244
+ const el = this.getPart(name);
1245
+ if (el) this.spreader.spread(el, props);
1246
+ };
1247
+ put("item", api.getItemProps());
1248
+ put("item-indicator", api.getItemIndicatorProps());
1249
+ put("item-title", api.getItemTitleProps());
1250
+ put("item-description", api.getItemDescriptionProps());
1251
+ put("item-action-trigger", api.getItemActionTriggerProps());
1252
+ put("item-progress", api.getItemProgressProps());
1253
+ put("item-close-trigger", api.getItemCloseTriggerProps());
1254
+ this.fillText(this.getPart("item-title"), api.title);
1255
+ this.fillText(this.getPart("item-description"), api.description);
1256
+ this.setPartHidden(this.getPart("item"), api.status === "unmounted");
1257
+ this.setPartHidden(this.getPart("item-close-trigger"), !api.closable);
1258
+ }
1259
+ };
1260
+ //#endregion
1261
+ //#region src/elements/toast.ts
1262
+ const STRING_CONVERTER = { fromAttribute: (v) => v ?? void 0 };
1263
+ const NUMBER_CONVERTER = { fromAttribute: (v) => v == null || v === "" ? void 0 : Number(v) };
1264
+ const BOOLEAN_CONVERTER = { fromAttribute: (v) => v === null ? void 0 : v !== "false" };
1265
+ /**
1266
+ * `<xh-toast>`:Light-DOM 行为宿主:作者写 root / title / action-trigger / close-trigger
1267
+ * 角色节点,元素运行 toast 状态机并把 connect 产出接上。
1268
+ *
1269
+ * root 承载 role 与 aria-live:默认 status + polite(排队等待读屏的空隙),
1270
+ * tone="danger" 换为 alert + assertive(打断当前朗读)。指针停在提示上、
1271
+ * 或焦点落进提示内部都会暂停倒计时,离开后继续剩余部分。
1272
+ *
1273
+ * 退场窗口结束时只把 root 收起、不删除节点:作者写在其中的内容归作者,
1274
+ * 何时把该条从队列中删除是全局服务的职责(它接收本元素冒泡的 status-change)。
1275
+ *
1276
+ * @customElement xh-toast
1277
+ * @attr {string} id - 队列身份,全局服务按它寻址;未提供时使用实例自身的 scope id
1278
+ * @attr {string} title - 标题文案;作者未在 title 部件中写内容时由元素填入
1279
+ * @attr {string} description - 简短补充说明;作者未在 description 部件中写内容时由元素填入
1280
+ * @attr {'info'|'success'|'warning'|'danger'} tone - 语气,默认 info;danger 使用 alert + assertive
1281
+ * @attr {boolean} loading - 事情尚未完成:行首换为转圈,且不自动消失
1282
+ * @attr {number} duration - 停留毫秒,默认 4000;<=0 即关闭自动消失
1283
+ * @attr {number} remove-delay - 退场窗口毫秒,默认 300,留给退场动画
1284
+ * @attr {boolean} closable - 是否提供可用的关闭按钮,默认 true;写 closable="false" 关闭
1285
+ * @attr {boolean} pause-on-page-idle - 页面切到后台时暂停计时,单组件默认关闭;全局服务默认开启
1286
+ * @attr {boolean} paused - 由宿主整组一起暂停计时,默认关闭;与指针、焦点等来源并存
1287
+ * @fires status-change - 生命周期落定;detail 为 `{ id: string, status: 'dismissing'|'unmounted' }`
1288
+ * @fires action - 操作按钮被按下;detail 为 `{ id: string }`
1289
+ * @csspart root - role=status(danger 时 alert)的容器,承载 data-tone / data-loading / data-state / data-paused
1290
+ * @csspart indicator - 语气指示符(对读屏隐藏),同样承载 data-loading;不渲染它时字形由 root 的伪元素兜底
1291
+ * @csspart content - 标题与说明的文本列
1292
+ * @csspart title - 标题,aria-labelledby 的目标
1293
+ * @csspart description - 可选的简短补充说明
1294
+ * @csspart action-trigger - 操作按钮:先发出 action 再进入退场
1295
+ * @csspart progress - 倒计时条;不自动消失时收起
1296
+ * @csspart close-trigger - 关闭按钮;closable=false 时为原生 disabled 并收起
1297
+ */
1298
+ var XhToastElement = class extends XhElement {
1299
+ static partContract = {
1300
+ anatomy: toastAnatomy,
1301
+ meta: toastMeta
1302
+ };
1303
+ static properties = {
1304
+ toastId: {
1305
+ converter: STRING_CONVERTER,
1306
+ attribute: "id"
1307
+ },
1308
+ titleText: {
1309
+ converter: STRING_CONVERTER,
1310
+ attribute: "title"
1311
+ },
1312
+ descriptionText: {
1313
+ converter: STRING_CONVERTER,
1314
+ attribute: "description"
1315
+ },
1316
+ tone: { converter: STRING_CONVERTER },
1317
+ loading: { converter: BOOLEAN_CONVERTER },
1318
+ duration: { converter: NUMBER_CONVERTER },
1319
+ removeDelay: {
1320
+ converter: NUMBER_CONVERTER,
1321
+ attribute: "remove-delay"
1322
+ },
1323
+ closable: { converter: BOOLEAN_CONVERTER },
1324
+ pauseOnPageIdle: {
1325
+ converter: BOOLEAN_CONVERTER,
1326
+ attribute: "pause-on-page-idle"
1327
+ },
1328
+ paused: { converter: BOOLEAN_CONVERTER },
1329
+ translations: { attribute: false }
1330
+ };
1331
+ notifyStatus = (details) => {
1332
+ this.dispatchEvent(new CustomEvent("status-change", {
1333
+ detail: details,
1334
+ bubbles: true,
1335
+ composed: true
1336
+ }));
1337
+ };
1338
+ notifyAction = (details) => {
1339
+ this.dispatchEvent(new CustomEvent("action", {
1340
+ detail: details,
1341
+ bubbles: true,
1342
+ composed: true
1343
+ }));
1344
+ };
1345
+ ctrl = new MachineController(this, toastMachine, () => this.machineProps());
1346
+ machineProps() {
1347
+ return {
1348
+ id: this.toastId,
1349
+ title: this.titleText,
1350
+ description: this.descriptionText,
1351
+ tone: this.tone,
1352
+ loading: this.loading,
1353
+ duration: this.duration,
1354
+ removeDelay: this.removeDelay,
1355
+ closable: this.closable,
1356
+ pauseOnPageIdle: this.pauseOnPageIdle,
1357
+ paused: this.paused,
1358
+ translations: this.translations,
1359
+ onStatusChange: this.notifyStatus,
1360
+ onAction: this.notifyAction
1361
+ };
1362
+ }
1363
+ /** 这个部件的文字归不归作者:头一回见到它时定案,之后不再改口。 */
1364
+ authorOwnsText = /* @__PURE__ */ new WeakMap();
1365
+ /**
1366
+ * 作者没往部件里写内容时,用 props 上的文案填进去。
1367
+ *
1368
+ * 归属只判一次:判完本轮就会把文字写进去,再回读分不清是作者写的还是自己写的,
1369
+ * 从那以后 title 属性改了也再刷不动(Vue 侧是响应式渲染,会跟着改,两侧就此分叉)。
1370
+ */
1371
+ fillText(el, text) {
1372
+ if (!el) return;
1373
+ let authored = this.authorOwnsText.get(el);
1374
+ if (authored === void 0) {
1375
+ authored = el.childElementCount > 0 || (el.textContent ?? "").trim() !== "";
1376
+ this.authorOwnsText.set(el, authored);
1377
+ }
1378
+ if (authored) return;
1379
+ const next = text ?? "";
1380
+ if (el.textContent !== next) el.textContent = next;
1381
+ }
1382
+ wire() {
1383
+ const api = connectToast(this.ctrl.service, wcNormalize);
1384
+ const put = (name, props) => {
1385
+ const el = this.getPart(name);
1386
+ if (el) this.spreader.spread(el, props);
1387
+ };
1388
+ put("root", api.getRootProps());
1389
+ put("indicator", api.getIndicatorProps());
1390
+ put("content", api.getContentProps());
1391
+ put("title", api.getTitleProps());
1392
+ put("description", api.getDescriptionProps());
1393
+ put("action-trigger", api.getActionTriggerProps());
1394
+ put("progress", api.getProgressProps());
1395
+ put("close-trigger", api.getCloseTriggerProps());
1396
+ this.fillText(this.getPart("title"), api.title);
1397
+ this.fillText(this.getPart("description"), api.description);
1398
+ this.setPartHidden(this.getPart("root"), api.status === "unmounted");
1399
+ this.setPartHidden(this.getPart("close-trigger"), !api.closable);
1400
+ }
1401
+ };
1402
+ //#endregion
1403
+ export { XhDialogElement as a, createOverlayExit as c, XhLoadingBarElement as i, XhElement as l, XhNotificationElement as n, XhPortalHostElement as o, XhNotificationItemElement as r, XhButtonElement as s, XhToastElement as t };
1404
+
1405
+ //# sourceMappingURL=toast-BTlM12YF.js.map