@greypan/web-ui 8.1.0 → 9.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.
Files changed (163) hide show
  1. package/README.CN.md +309 -135
  2. package/README.md +311 -135
  3. package/dist/assets/glass.js +1 -1
  4. package/dist/assets/glass.js.map +1 -1
  5. package/dist/assets/menu-portal.js +1 -1
  6. package/dist/assets/menu-portal.js.map +1 -1
  7. package/dist/assets/modal-surface.js +6 -0
  8. package/dist/assets/modal-surface.js.map +1 -0
  9. package/dist/assets/selection-control.js +1 -1
  10. package/dist/assets/selection-control.js.map +1 -1
  11. package/dist/assets/toast-containers.js +1 -1
  12. package/dist/assets/toast-containers.js.map +1 -1
  13. package/dist/components/autocomplete/index.d.ts +3 -2
  14. package/dist/components/autocomplete/index.js +40 -44
  15. package/dist/components/autocomplete/index.js.map +1 -1
  16. package/dist/components/autocomplete/style.js +1 -1
  17. package/dist/components/autocomplete/style.js.map +1 -1
  18. package/dist/components/avatar/index.js.map +1 -1
  19. package/dist/components/avatar/style.js +1 -1
  20. package/dist/components/avatar/style.js.map +1 -1
  21. package/dist/components/back-top/style.js +1 -1
  22. package/dist/components/back-top/style.js.map +1 -1
  23. package/dist/components/badge/style.js +1 -1
  24. package/dist/components/badge/style.js.map +1 -1
  25. package/dist/components/button/style.js +1 -1
  26. package/dist/components/button/style.js.map +1 -1
  27. package/dist/components/button-group/index.js +0 -1
  28. package/dist/components/button-group/index.js.map +1 -1
  29. package/dist/components/button-group/style.js +1 -1
  30. package/dist/components/button-group/style.js.map +1 -1
  31. package/dist/components/checkbox/index.js +4 -4
  32. package/dist/components/checkbox/index.js.map +1 -1
  33. package/dist/components/checkbox-group/style.js +1 -1
  34. package/dist/components/checkbox-group/style.js.map +1 -1
  35. package/dist/components/context-menu/index.d.ts +182 -3
  36. package/dist/components/context-menu/index.js +166 -91
  37. package/dist/components/context-menu/index.js.map +1 -1
  38. package/dist/components/context-menu/scrim.js +6 -0
  39. package/dist/components/context-menu/scrim.js.map +1 -0
  40. package/dist/components/context-menu/style.js +1 -1
  41. package/dist/components/context-menu/style.js.map +1 -1
  42. package/dist/components/dialog/index.d.ts +9 -0
  43. package/dist/components/dialog/index.js +68 -46
  44. package/dist/components/dialog/index.js.map +1 -1
  45. package/dist/components/dialog/style.js +1 -1
  46. package/dist/components/dialog/style.js.map +1 -1
  47. package/dist/components/drawer/index.d.ts +45 -0
  48. package/dist/components/drawer/index.js +122 -53
  49. package/dist/components/drawer/index.js.map +1 -1
  50. package/dist/components/drawer/style.js +1 -1
  51. package/dist/components/drawer/style.js.map +1 -1
  52. package/dist/components/dropdown/index.d.ts +12 -0
  53. package/dist/components/dropdown/index.js +13 -10
  54. package/dist/components/dropdown/index.js.map +1 -1
  55. package/dist/components/dropdown-divider/style.js +1 -1
  56. package/dist/components/dropdown-divider/style.js.map +1 -1
  57. package/dist/components/dropdown-header/style.js +1 -1
  58. package/dist/components/dropdown-header/style.js.map +1 -1
  59. package/dist/components/dropdown-item/style.js +1 -1
  60. package/dist/components/dropdown-item/style.js.map +1 -1
  61. package/dist/components/editable-text/index.d.ts +39 -0
  62. package/dist/components/editable-text/index.js +6 -2
  63. package/dist/components/editable-text/index.js.map +1 -1
  64. package/dist/components/empty/index.d.ts +21 -4
  65. package/dist/components/empty/index.js +37 -32
  66. package/dist/components/empty/index.js.map +1 -1
  67. package/dist/components/empty/style.js +1 -1
  68. package/dist/components/empty/style.js.map +1 -1
  69. package/dist/components/image-preview/index.js +75 -63
  70. package/dist/components/image-preview/index.js.map +1 -1
  71. package/dist/components/image-preview/style.js +1 -1
  72. package/dist/components/image-preview/style.js.map +1 -1
  73. package/dist/components/index.d.ts +1 -0
  74. package/dist/components/index.js +38 -37
  75. package/dist/components/input/style.js +1 -1
  76. package/dist/components/input/style.js.map +1 -1
  77. package/dist/components/input-number/style.js +1 -1
  78. package/dist/components/input-number/style.js.map +1 -1
  79. package/dist/components/layout/index.d.ts +21 -6
  80. package/dist/components/layout/index.js +47 -27
  81. package/dist/components/layout/index.js.map +1 -1
  82. package/dist/components/layout/style.js +1 -1
  83. package/dist/components/layout/style.js.map +1 -1
  84. package/dist/components/middle-ellipsis/index.d.ts +100 -0
  85. package/dist/components/middle-ellipsis/index.js +112 -0
  86. package/dist/components/middle-ellipsis/index.js.map +1 -0
  87. package/dist/components/middle-ellipsis/style.js +6 -0
  88. package/dist/components/middle-ellipsis/style.js.map +1 -0
  89. package/dist/components/middle-ellipsis/truncate.d.ts +62 -0
  90. package/dist/components/middle-ellipsis/truncate.js +45 -0
  91. package/dist/components/middle-ellipsis/truncate.js.map +1 -0
  92. package/dist/components/option/style.js +1 -1
  93. package/dist/components/option/style.js.map +1 -1
  94. package/dist/components/popover/index.js.map +1 -1
  95. package/dist/components/popover/style.js +1 -1
  96. package/dist/components/popover/style.js.map +1 -1
  97. package/dist/components/radio/style.js +1 -1
  98. package/dist/components/radio/style.js.map +1 -1
  99. package/dist/components/radio-group/style.js +1 -1
  100. package/dist/components/radio-group/style.js.map +1 -1
  101. package/dist/components/segmented/index.d.ts +5 -0
  102. package/dist/components/segmented/index.js +45 -28
  103. package/dist/components/segmented/index.js.map +1 -1
  104. package/dist/components/segmented/style.js +1 -1
  105. package/dist/components/segmented/style.js.map +1 -1
  106. package/dist/components/segmented-trigger/style.js +1 -1
  107. package/dist/components/segmented-trigger/style.js.map +1 -1
  108. package/dist/components/select/index.js +0 -1
  109. package/dist/components/select/index.js.map +1 -1
  110. package/dist/components/select/style.js +1 -1
  111. package/dist/components/select/style.js.map +1 -1
  112. package/dist/components/slider/style.js +1 -1
  113. package/dist/components/slider/style.js.map +1 -1
  114. package/dist/components/spinner/style.js +1 -1
  115. package/dist/components/spinner/style.js.map +1 -1
  116. package/dist/components/svg-draw-lines/index.js.map +1 -1
  117. package/dist/components/switch/style.js +1 -1
  118. package/dist/components/switch/style.js.map +1 -1
  119. package/dist/components/textarea/index.js +2 -2
  120. package/dist/components/textarea/index.js.map +1 -1
  121. package/dist/components/textarea/style.js +1 -1
  122. package/dist/components/textarea/style.js.map +1 -1
  123. package/dist/components/theme/style.js +1 -1
  124. package/dist/components/theme/style.js.map +1 -1
  125. package/dist/components/toast/style.js +1 -1
  126. package/dist/components/toast/style.js.map +1 -1
  127. package/dist/components/tooltip/index.js.map +1 -1
  128. package/dist/components/tooltip/style.js +1 -1
  129. package/dist/components/tooltip/style.js.map +1 -1
  130. package/dist/icons/generated/akar-icons-circle-check.d.ts +6 -0
  131. package/dist/icons/generated/akar-icons-circle-check.js +10 -0
  132. package/dist/icons/generated/akar-icons-circle-check.js.map +1 -0
  133. package/dist/icons/generated/bi-check-lg.d.ts +6 -0
  134. package/dist/icons/generated/bi-check-lg.js +10 -0
  135. package/dist/icons/generated/bi-check-lg.js.map +1 -0
  136. package/dist/icons/generated/fluent-tag-search-24-regular.d.ts +6 -0
  137. package/dist/icons/generated/fluent-tag-search-24-regular.js +10 -0
  138. package/dist/icons/generated/fluent-tag-search-24-regular.js.map +1 -0
  139. package/dist/icons/generated/mdi-tag-search-outline.d.ts +6 -0
  140. package/dist/icons/generated/mdi-tag-search-outline.js +10 -0
  141. package/dist/icons/generated/mdi-tag-search-outline.js.map +1 -0
  142. package/dist/icons/index.d.ts +4 -0
  143. package/dist/icons/index.js +5 -1
  144. package/dist/scrollbars/index.d.ts +33 -0
  145. package/dist/scrollbars/index.js +18 -0
  146. package/dist/scrollbars/index.js.map +1 -0
  147. package/dist/scrollbars/style.css +10 -0
  148. package/dist/shared/element-height/index.js +47 -0
  149. package/dist/shared/element-height/index.js.map +1 -0
  150. package/dist/shared/element-width/index.js +37 -0
  151. package/dist/shared/element-width/index.js.map +1 -0
  152. package/dist/shared/menu-portal/menu-portal.js +11 -11
  153. package/dist/shared/menu-portal/menu-portal.js.map +1 -1
  154. package/dist/shared/menu-portal/menu-tree.js +2 -2
  155. package/dist/shared/menu-portal/menu-tree.js.map +1 -1
  156. package/dist/shared/overlay/anchored-panel.js.map +1 -1
  157. package/dist/shared/overlay/native-dialog-presence.js +23 -11
  158. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  159. package/dist/shared/overlay/nested-drawer-layers.js +30 -23
  160. package/dist/shared/overlay/nested-drawer-layers.js.map +1 -1
  161. package/dist/shared/scroll-lock/scroll-lock.js +3 -3
  162. package/dist/shared/scroll-lock/scroll-lock.js.map +1 -1
  163. package/package.json +19 -5
@@ -13,21 +13,38 @@ import { customElement as f, property as p, state as m } from "lit/decorators.js
13
13
  import { classMap as h } from "lit/directives/class-map.js";
14
14
  import { styleMap as g } from "lit/directives/style-map.js";
15
15
  //#region src/components/layout/index.ts
16
- var _ = class extends c {
16
+ var _ = "(width <= 640px)", v = class extends c {
17
17
  constructor(...e) {
18
- super(...e), this.sidebarCollapsed = !1, this.sidebarWidth = "240px", this.collapsedWidth = "68px", this.sidebarResizable = !1, this.sidebarMinWidth = "", this.sidebarMaxWidth = "", this.sidebarOpen = !1, this.headerGlow = !1, this._isMobile = !1, this._visibleBanner = o({ onVisibleAreaChange: (e) => {
18
+ super(...e), this.sidebarCollapsed = !1, this.sidebarWidth = "240px", this.collapsedWidth = "68px", this.sidebarResizable = !1, this.sidebarMinWidth = "", this.sidebarMaxWidth = "", this.sidebarOpen = !1, this.headerGlow = !1, this._mobile = !1, this._mobileQuery = null, this._onMobileQueryChange = (e) => {
19
+ this._syncMobile(e.matches);
20
+ }, this._visibleBanner = o({ onVisibleAreaChange: (e) => {
19
21
  this.style.setProperty("--wui-layout-visible-banner-height", `${e.height}px`);
20
- } }).make(), this._visibleBannerController = new a(this, this._visibleBanner), this._resizeGesture = null, this._resizeStartWidth = 0, this._resizeWidth = null, this._resizeTimeout = null, this._handleResize = () => {
21
- this._resizeTimeout !== null && clearTimeout(this._resizeTimeout), this._resizeTimeout = setTimeout(() => {
22
- this._checkMobile(), this._resizeTimeout = null;
23
- }, 100);
24
- };
22
+ } }).make(), this._visibleBannerController = new a(this, this._visibleBanner), this._resizeGesture = null, this._resizeStartWidth = 0, this._resizeWidth = null;
25
23
  }
26
24
  static {
27
25
  this.styles = [d(t), d(s)];
28
26
  }
27
+ get mobile() {
28
+ return this._mobile;
29
+ }
30
+ _syncMobile(e) {
31
+ e !== this._mobile && (this._mobile = e, this._isResizing() && this._resetActiveResize(), this.toggleAttribute("mobile", e), this.dispatchEvent(new CustomEvent("mobile-change", {
32
+ detail: { mobile: e },
33
+ bubbles: !0,
34
+ composed: !0
35
+ })));
36
+ }
37
+ attributeChangedCallback(e, t, n) {
38
+ if (super.attributeChangedCallback(e, t, n), e !== "mobile") return;
39
+ let r = this._mobileQuery?.matches ?? !1;
40
+ if (this._mobile !== r && (this._mobile = r), r) {
41
+ this.getAttribute("mobile") !== "" && this.setAttribute("mobile", "");
42
+ return;
43
+ }
44
+ this.hasAttribute("mobile") && this.removeAttribute("mobile");
45
+ }
29
46
  _toggleSidebar() {
30
- if (this._isMobile) {
47
+ if (this._mobile) {
31
48
  this._emitSidebarOpenChange(!this.sidebarOpen);
32
49
  return;
33
50
  }
@@ -65,7 +82,7 @@ var _ = class extends c {
65
82
  return this._resizeGesture?.isDragging() ?? !1;
66
83
  }
67
84
  _handleResizePointerDown(e) {
68
- if (!this.sidebarResizable || this.sidebarCollapsed || this._isMobile || this._isResizing() || e.isPrimary === !1) return;
85
+ if (!this.sidebarResizable || this.sidebarCollapsed || this._mobile || this._isResizing() || e.isPrimary === !1) return;
69
86
  let t = this.renderRoot.querySelector("aside");
70
87
  t && (this._resizeStartWidth = this._parsePx(window.getComputedStyle(t).width, this._parsePx(this.sidebarWidth, 240)), this._resizeWidth = null, t.classList.add("is-resizing"), this._resizeGesture = r(e, {
71
88
  axis: "x",
@@ -94,7 +111,7 @@ var _ = class extends c {
94
111
  this._resizeGesture?.destroy(), this._resizeGesture = null, this.renderRoot.querySelector("aside")?.classList.remove("is-resizing"), this._resizeWidth = null;
95
112
  }
96
113
  _handleResizeKeydown(e) {
97
- if (!this.sidebarResizable || this.sidebarCollapsed || this._isMobile || this._isResizing()) return;
114
+ if (!this.sidebarResizable || this.sidebarCollapsed || this._mobile || this._isResizing()) return;
98
115
  let t = this._resolveSidebarMinWidth(), n = this._resolveSidebarMaxWidth(), r = this.renderRoot.querySelector("aside"), i = this._resizeWidth ? this._parsePx(this._resizeWidth, t) : this._parsePx(r ? window.getComputedStyle(r).width : this.sidebarWidth, t), a = e.shiftKey ? 64 : 16, o = null;
99
116
  switch (e.key) {
100
117
  case "ArrowLeft":
@@ -127,17 +144,16 @@ var _ = class extends c {
127
144
  }
128
145
  e.preventDefault(), this._resizeWidth = `${Math.round(o)}px`;
129
146
  }
130
- _checkMobile() {
131
- let e = window.innerWidth <= 640;
132
- e !== this._isMobile && this._isResizing() && this._resetActiveResize(), this._isMobile = e;
147
+ _syncMobileViewportSubscription() {
148
+ this._mobileQuery?.removeEventListener("change", this._onMobileQueryChange), this._mobileQuery = typeof window.matchMedia == "function" ? window.matchMedia(_) : null, this._mobileQuery?.addEventListener("change", this._onMobileQueryChange), this._syncMobile(this._mobileQuery?.matches ?? !1);
133
149
  }
134
150
  connectedCallback() {
135
- super.connectedCallback(), this._checkMobile(), window.addEventListener("resize", this._handleResize), this.updateComplete.then(() => {
151
+ super.connectedCallback(), this._syncMobileViewportSubscription(), this.updateComplete.then(() => {
136
152
  this.isConnected && this._syncBannerPresence();
137
153
  });
138
154
  }
139
155
  disconnectedCallback() {
140
- super.disconnectedCallback(), this._resetActiveResize(), window.removeEventListener("resize", this._handleResize), this._resizeTimeout !== null && clearTimeout(this._resizeTimeout), this._resizeTimeout = null;
156
+ super.disconnectedCallback(), this._resetActiveResize(), this._mobileQuery?.removeEventListener("change", this._onMobileQueryChange), this._mobileQuery = null;
141
157
  }
142
158
  firstUpdated() {
143
159
  this._syncBannerPresence();
@@ -150,11 +166,11 @@ var _ = class extends c {
150
166
  this._visibleBanner.setTarget(e ? t : null);
151
167
  }
152
168
  render() {
153
- let e = !this.sidebarCollapsed, t = this._isMobile ? {} : { width: this._resizeWidth ?? (this.sidebarCollapsed ? this.collapsedWidth : this.sidebarWidth) }, r = e ? "折叠侧边栏" : "展开侧边栏", i = l`
169
+ let e = !this.sidebarCollapsed, t = this._mobile ? {} : { width: this._resizeWidth ?? (this.sidebarCollapsed ? this.collapsedWidth : this.sidebarWidth) }, r = e ? "折叠侧边栏" : "展开侧边栏", i = l`
154
170
  <web-ui-button
155
171
  class="sidebar-toggle"
156
172
  icon
157
- variant="glass"
173
+ variant="secondary"
158
174
  @click="${this._toggleSidebar}"
159
175
  aria-label="${r}"
160
176
  >
@@ -234,7 +250,7 @@ var _ = class extends c {
234
250
  <div class="layout-banner">
235
251
  <slot name="banner" @slotchange="${this._onBannerSlotChange}"></slot>
236
252
  </div>
237
- ${this._isMobile ? s : o}
253
+ ${this._mobile ? s : o}
238
254
  </div>
239
255
  `;
240
256
  }
@@ -243,36 +259,40 @@ e([p({
243
259
  type: Boolean,
244
260
  attribute: "sidebar-collapsed",
245
261
  reflect: !0
246
- })], _.prototype, "sidebarCollapsed", void 0), e([p({
262
+ })], v.prototype, "sidebarCollapsed", void 0), e([p({
247
263
  type: String,
248
264
  attribute: "sidebar-width",
249
265
  reflect: !0
250
- })], _.prototype, "sidebarWidth", void 0), e([p({
266
+ })], v.prototype, "sidebarWidth", void 0), e([p({
251
267
  type: String,
252
268
  attribute: "collapsed-width",
253
269
  reflect: !0
254
- })], _.prototype, "collapsedWidth", void 0), e([p({
270
+ })], v.prototype, "collapsedWidth", void 0), e([p({
255
271
  type: Boolean,
256
272
  attribute: "sidebar-resizable",
257
273
  reflect: !0
258
- })], _.prototype, "sidebarResizable", void 0), e([p({
274
+ })], v.prototype, "sidebarResizable", void 0), e([p({
259
275
  type: String,
260
276
  attribute: "sidebar-min-width",
261
277
  reflect: !0
262
- })], _.prototype, "sidebarMinWidth", void 0), e([p({
278
+ })], v.prototype, "sidebarMinWidth", void 0), e([p({
263
279
  type: String,
264
280
  attribute: "sidebar-max-width",
265
281
  reflect: !0
266
- })], _.prototype, "sidebarMaxWidth", void 0), e([p({
282
+ })], v.prototype, "sidebarMaxWidth", void 0), e([p({
267
283
  type: Boolean,
268
284
  attribute: "sidebar-open",
269
285
  reflect: !0
270
- })], _.prototype, "sidebarOpen", void 0), e([p({
286
+ })], v.prototype, "sidebarOpen", void 0), e([p({
271
287
  type: Boolean,
272
288
  attribute: "header-glow",
273
289
  reflect: !0
274
- })], _.prototype, "headerGlow", void 0), e([m()], _.prototype, "_isMobile", void 0), e([m()], _.prototype, "_resizeWidth", void 0), _ = e([f("web-ui-layout")], _);
290
+ })], v.prototype, "headerGlow", void 0), e([p({
291
+ type: Boolean,
292
+ attribute: "mobile",
293
+ reflect: !0
294
+ })], v.prototype, "_mobile", void 0), e([m()], v.prototype, "_resizeWidth", void 0), v = e([f("web-ui-layout")], v);
275
295
  //#endregion
276
- export { _ as WebUiLayout };
296
+ export { v as WebUiLayout };
277
297
 
278
298
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/layout/index.ts"],"sourcesContent":["import { html, LitElement, nothing, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { styleMap } from 'lit/directives/style-map.js'\n\nimport glass from '@/assets/glass.css?inline'\nimport '@/components/icon'\nimport '@/components/button'\nimport '@/components/drawer'\nimport { radixIconsPanelLeftMinimized } from '@/icons'\nimport { attachDragGesture, clamp, type DragGestureHandle } from '@/shared/gesture'\nimport { defineVisibleAreaTracker, VisibleAreaController } from '@/shared/visible-area'\n\nimport style from './style.css?inline'\n\n/**\n * Layout 组件 - 提供页面布局框架,支持可折叠侧边栏\n *\n * @fires sidebar-collapsed-change - 桌面端侧边栏折叠状态变更请求时触发\n * @fires sidebar-open-change - 移动端侧边栏 Drawer 开关请求时触发\n * @fires sidebar-width-change - 桌面端侧边栏拖拽调宽结束时触发,携带最终宽度(受控请求)\n * @slot banner - 顶部 Banner 区域(全宽,随页面滚动,可选)\n * @slot header - 内容区顶部 header(sticky)\n * @slot sidebar - 侧边栏内容;Consumer 决定其内部固定区域与滚动容器\n * @slot tabbar - 底部 tabbar 区域\n * @slot default - 主内容区\n */\n@customElement('web-ui-layout')\nexport class WebUiLayout extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(style)]\n\n /**\n * 桌面端 Sidebar 的持久布局偏好。\n *\n * `true` 只会将桌面 Sidebar 收窄到 `collapsedWidth`,不会影响移动端 Drawer。\n * 它不与 `sidebarOpen` 合并:前者是跨视口保留的密度选择,后者是移动端瞬时的 modal 可见性。\n */\n @property({ type: Boolean, attribute: 'sidebar-collapsed', reflect: true })\n sidebarCollapsed = false\n\n @property({ type: String, attribute: 'sidebar-width', reflect: true })\n sidebarWidth = '240px'\n\n @property({ type: String, attribute: 'collapsed-width', reflect: true })\n collapsedWidth = '68px'\n\n /** 启用桌面端 Sidebar 拖拽调宽(折叠态隐藏 handle)。 */\n @property({ type: Boolean, attribute: 'sidebar-resizable', reflect: true })\n sidebarResizable = false\n\n /**\n * 拖拽调宽的下限(px,其他单位不解析);未设置时回退为 `collapsedWidth`,\n * 防止拖到不可用宽度。上限 `sidebar-max-width` 受内置 50vw 硬上限钳制。\n */\n @property({ type: String, attribute: 'sidebar-min-width', reflect: true })\n sidebarMinWidth = ''\n\n /** 拖拽调宽的上限(px,其他单位不解析);始终受内置 50vw 硬上限钳制。 */\n @property({ type: String, attribute: 'sidebar-max-width', reflect: true })\n sidebarMaxWidth = ''\n\n /**\n * 移动端 web-ui-drawer 的瞬时可见性。\n *\n * Drawer Toggle、Escape 和遮罩关闭都通过 `sidebar-open-change` 请求更新它;它不会改变桌面端的折叠偏好。\n * 因而不与 `sidebarCollapsed` 合并,避免响应式切换时将一次移动端交互误写为桌面端布局选择。\n */\n @property({ type: Boolean, attribute: 'sidebar-open', reflect: true })\n sidebarOpen = false\n\n /**\n * 启用 header 晕染效果\n */\n @property({ type: Boolean, attribute: 'header-glow', reflect: true })\n headerGlow = false\n\n @state() private _isMobile = false\n\n private readonly _visibleBanner = defineVisibleAreaTracker({\n onVisibleAreaChange: area => {\n this.style.setProperty('--wui-layout-visible-banner-height', `${area.height}px`)\n }\n }).make()\n\n private readonly _visibleBannerController = new VisibleAreaController(this, this._visibleBanner)\n\n private _toggleSidebar() {\n if (this._isMobile) {\n this._emitSidebarOpenChange(!this.sidebarOpen)\n return\n }\n\n this._emitSidebarCollapsedChange(!this.sidebarCollapsed)\n }\n\n private _emitSidebarCollapsedChange(collapsed: boolean) {\n this.dispatchEvent(\n new CustomEvent('sidebar-collapsed-change', {\n detail: { collapsed },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private _emitSidebarOpenChange(open: boolean) {\n this.dispatchEvent(\n new CustomEvent('sidebar-open-change', {\n detail: { open },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private _onDrawerChange(e: CustomEvent<{ open: boolean }>) {\n e.stopPropagation()\n this._emitSidebarOpenChange(e.detail.open)\n }\n\n private _resizeGesture: DragGestureHandle | null = null\n private _resizeStartWidth = 0\n // 拖拽中的临时宽度;由 render 的 styleMap 统一写入,避免与 Lit 样式管理竞争。\n @state() private _resizeWidth: string | null = null\n\n private _parsePx(value: string, fallback: number): number {\n const parsed = Number.parseFloat(value)\n return Number.isFinite(parsed) ? parsed : fallback\n }\n\n private _resolveSidebarMinWidth(): number {\n return this.sidebarMinWidth\n ? this._parsePx(this.sidebarMinWidth, this._parsePx(this.collapsedWidth, 68))\n : this._parsePx(this.collapsedWidth, 68)\n }\n\n private _resolveSidebarMaxWidth(): number {\n // 组件内置硬上限:Sidebar 最多占视口一半(无需调用方设置)。\n // 调用方 sidebar-max-width 在该上限内生效,防止拖拽占据整个视口。\n const halfViewport = Math.floor(window.innerWidth / 2)\n const configured = this.sidebarMaxWidth ? this._parsePx(this.sidebarMaxWidth, halfViewport) : halfViewport\n const upper = Math.min(configured, halfViewport)\n return Math.max(upper, this._resolveSidebarMinWidth())\n }\n\n private _isResizing(): boolean {\n return this._resizeGesture?.isDragging() ?? false\n }\n\n private _handleResizePointerDown(e: PointerEvent) {\n if (!this.sidebarResizable || this.sidebarCollapsed || this._isMobile || this._isResizing()) return\n // jsdom 等环境的 PointerEvent 可能缺失 isPrimary,仅在明确为 false(多点触控副指针)时拒绝。\n if (e.isPrimary === false) return\n\n const aside = this.renderRoot.querySelector('aside')\n if (!aside) return\n\n // 拖拽起点以 computed width 为准,并丢弃键盘未提交的调整(Enter 才提交、\n // Escape 撤回;抓取隐式放弃),保证零位移松手不会派发陈旧宽度。\n this._resizeStartWidth = this._parsePx(window.getComputedStyle(aside).width, this._parsePx(this.sidebarWidth, 240))\n this._resizeWidth = null\n aside.classList.add('is-resizing')\n\n this._resizeGesture = attachDragGesture(e, {\n axis: 'x',\n onMove: info => {\n // 向右拖增宽、向左拖收窄(handle 在右缘);钳制在 [min, max] 与视口内。\n const next = this._resizeStartWidth + info.deltaX\n const clamped = clamp(next, this._resolveSidebarMinWidth(), this._resolveSidebarMaxWidth())\n this._resizeWidth = `${Math.round(clamped)}px`\n },\n onEnd: () => {\n // 零位移松手(点击而非拖拽)不视为调宽动作:按 cancel 语义静默收尾。\n if (this._resizeWidth === null) {\n this._resetActiveResize()\n return\n }\n const width = this._resizeWidth\n this._resetActiveResize()\n // 受控契约:松手派发请求并交还 prop 管辖,由 Consumer 回写 sidebar-width。\n this.dispatchEvent(\n new CustomEvent('sidebar-width-change', {\n detail: { width },\n bubbles: true,\n composed: true\n })\n )\n },\n onCancel: () => {\n this._resetActiveResize()\n }\n })\n }\n\n // 终结进行中的拖拽:清手势状态、移除跟手 class,宽度过渡回 prop 管辖,不派发事件。\n // pointer cancel 与视口跨越移动端断点(handle 随桌面 layout 卸载)共用此收尾。\n private _resetActiveResize() {\n this._resizeGesture?.destroy()\n this._resizeGesture = null\n this.renderRoot.querySelector('aside')?.classList.remove('is-resizing')\n this._resizeWidth = null\n }\n\n // 键盘调宽(WAI-ARIA splitter 模式):方向键步进 16px(Shift 加速 64px)调整\n // 临时宽度,Enter 以受控请求派发 `sidebar-width-change`(与指针松手同语义),\n // Escape 撤回临时宽度交还 prop 管辖,Home/End 直接到 min/max。\n private _handleResizeKeydown(e: KeyboardEvent) {\n if (!this.sidebarResizable || this.sidebarCollapsed || this._isMobile || this._isResizing()) return\n\n const min = this._resolveSidebarMinWidth()\n const max = this._resolveSidebarMaxWidth()\n const aside = this.renderRoot.querySelector('aside')\n const current = this._resizeWidth\n ? this._parsePx(this._resizeWidth, min)\n : this._parsePx(aside ? window.getComputedStyle(aside).width : this.sidebarWidth, min)\n const step = e.shiftKey ? 64 : 16\n let next: number | null = null\n\n switch (e.key) {\n case 'ArrowLeft':\n next = Math.max(min, current - step)\n break\n case 'ArrowRight':\n next = Math.min(max, current + step)\n break\n case 'Home':\n next = min\n break\n case 'End':\n next = max\n break\n case 'Enter':\n // 无未提交调整时不派发(焦点停留时的回车噪声)\n if (this._resizeWidth !== null) {\n const width = this._resizeWidth\n this._resizeWidth = null\n this.dispatchEvent(\n new CustomEvent('sidebar-width-change', { detail: { width }, bubbles: true, composed: true })\n )\n }\n e.preventDefault()\n return\n case 'Escape':\n this._resizeWidth = null\n e.preventDefault()\n return\n default:\n return\n }\n e.preventDefault()\n this._resizeWidth = `${Math.round(next)}px`\n }\n\n private _checkMobile() {\n const nextMobile = window.innerWidth <= 640\n // 视口跨越移动端断点会卸载桌面 layout(handle 随之消失);必须先终结进行中的\n // 拖拽,否则 _resizePointerId 悬挂,切回桌面后所有新的拖拽在入口被拦截。\n if (nextMobile !== this._isMobile && this._isResizing()) this._resetActiveResize()\n this._isMobile = nextMobile\n }\n\n private _resizeTimeout: ReturnType<typeof setTimeout> | null = null\n\n private _handleResize = () => {\n if (this._resizeTimeout !== null) clearTimeout(this._resizeTimeout)\n this._resizeTimeout = setTimeout(() => {\n this._checkMobile()\n this._resizeTimeout = null\n }, 100)\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._checkMobile()\n window.addEventListener('resize', this._handleResize)\n void this.updateComplete.then(() => {\n if (this.isConnected) this._syncBannerPresence()\n })\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._resetActiveResize()\n window.removeEventListener('resize', this._handleResize)\n if (this._resizeTimeout !== null) clearTimeout(this._resizeTimeout)\n this._resizeTimeout = null\n }\n\n override firstUpdated() {\n this._syncBannerPresence()\n }\n\n private _onBannerSlotChange() {\n this._syncBannerPresence()\n }\n\n private _syncBannerPresence() {\n const bannerSlot = this.renderRoot.querySelector<HTMLSlotElement>('slot[name=\"banner\"]')\n const hasBanner =\n bannerSlot\n ?.assignedNodes()\n .some(\n node =>\n node.nodeType === Node.ELEMENT_NODE || (node.nodeType === Node.TEXT_NODE && !!node.textContent?.trim())\n ) ?? false\n const banner = this.shadowRoot?.querySelector<HTMLElement>('.layout-banner') ?? null\n this._visibleBanner.setTarget(hasBanner ? banner : null)\n }\n\n override render() {\n const isSidebarOpen = !this.sidebarCollapsed\n // 拖拽中的临时宽度优先于 prop 宽度;移动端宽度由 Drawer 内部变量管理。\n const sidebarStyle = this._isMobile\n ? {}\n : { width: this._resizeWidth ?? (this.sidebarCollapsed ? this.collapsedWidth : this.sidebarWidth) }\n\n const toggleLabel = isSidebarOpen ? '折叠侧边栏' : '展开侧边栏'\n\n const desktopToggle = html`\n <web-ui-button\n class=\"sidebar-toggle\"\n icon\n variant=\"glass\"\n @click=\"${this._toggleSidebar}\"\n aria-label=\"${toggleLabel}\"\n >\n <web-ui-icon .icon=${radixIconsPanelLeftMinimized}></web-ui-icon>\n </web-ui-button>\n `\n\n const sidebarViewport = html`\n <div class=\"sidebar-viewport\">\n <slot name=\"sidebar\"></slot>\n </div>\n `\n\n const desktopLayout = html`\n <div class=\"layout-body\">\n <div class=\"sidebar-wrapper\">\n <aside\n class=\"${classMap({\n collapsed: !isSidebarOpen\n })}\"\n style=\"${styleMap(sidebarStyle)}\"\n >\n <div class=\"aside-panel wui-glass\">\n ${sidebarViewport}\n <div class=\"sidebar-toggle-area\">${desktopToggle}</div>\n </div>\n ${\n this.sidebarResizable && !this.sidebarCollapsed\n ? html`\n <div\n class=\"sidebar-resize-handle\"\n role=\"separator\"\n tabindex=\"0\"\n aria-orientation=\"vertical\"\n aria-label=\"调整侧边栏宽度\"\n @pointerdown=${this._handleResizePointerDown}\n @keydown=${this._handleResizeKeydown}\n ></div>\n `\n : nothing\n }\n </aside>\n </div>\n <div class=\"layout-content\">\n <header>\n <slot name=\"header\"></slot>\n </header>\n <main><slot></slot></main>\n <footer><slot name=\"tabbar\"></slot></footer>\n </div>\n </div>\n `\n\n const mobileLayout = html`\n <div class=\"layout-content\">\n <header>\n <web-ui-button\n class=\"mobile-toggle\"\n icon\n variant=\"glass\"\n @click=\"${this._toggleSidebar}\"\n aria-label=\"打开导航菜单\"\n >\n <web-ui-icon .icon=${radixIconsPanelLeftMinimized}></web-ui-icon>\n </web-ui-button>\n <slot name=\"header\"></slot>\n </header>\n <main><slot></slot></main>\n <footer><slot name=\"tabbar\"></slot></footer>\n </div>\n\n <web-ui-drawer\n placement=\"left\"\n ?open=\"${this.sidebarOpen}\"\n controlled\n draggable\n dialog-label=\"主导航\"\n @open-change=\"${this._onDrawerChange}\"\n style=\"${styleMap({\n '--wui-drawer-width': this.sidebarWidth,\n '--wui-drawer-radius': 'var(--wui-layout-sidebar-radius, 24px)'\n })}\"\n >\n ${sidebarViewport}\n </web-ui-drawer>\n `\n\n return html`\n <div class=\"layout-page\">\n <div class=\"layout-banner\">\n <slot name=\"banner\" @slotchange=\"${this._onBannerSlotChange}\"></slot>\n </div>\n ${this._isMobile ? mobileLayout : desktopLayout}\n </div>\n `\n }\n\n declare readonly $events: {\n 'sidebar-collapsed-change': CustomEvent<{ collapsed: boolean }>\n 'sidebar-open-change': CustomEvent<{ open: boolean }>\n 'sidebar-width-change': CustomEvent<{ width: string }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-layout': WebUiLayout\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/layout/index.ts"],"sourcesContent":["import { html, LitElement, nothing, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\nimport { styleMap } from 'lit/directives/style-map.js'\n\nimport glass from '@/assets/glass.css?inline'\nimport '@/components/icon'\nimport '@/components/button'\nimport '@/components/drawer'\nimport { radixIconsPanelLeftMinimized } from '@/icons'\nimport { attachDragGesture, clamp, type DragGestureHandle } from '@/shared/gesture'\nimport { defineVisibleAreaTracker, VisibleAreaController } from '@/shared/visible-area'\n\nimport style from './style.css?inline'\n\n/**\n * 移动端断点的媒体查询。\n *\n * 必须与 `style.css` 里那份 `@media (width <= 640px)` 是同一条条件:CSS 用它在窄视口隐藏桌面\n * aside,TS 用同一条件决定渲染哪一套树,两者错开就会出现「树还在桌面、aside 已经被藏起来」\n * 的空档。两份字面量的相等由 `components/drawer/__tests__/breakpoint-parity.spec.ts` 守着。\n */\nconst MOBILE_VIEWPORT_QUERY = '(width <= 640px)'\n\n/**\n * Layout 组件 - 提供页面布局框架,支持可折叠侧边栏\n *\n * @fires sidebar-collapsed-change - 桌面端侧边栏折叠状态变更请求时触发\n * @fires sidebar-open-change - 移动端侧边栏 Drawer 开关请求时触发\n * @fires sidebar-width-change - 桌面端侧边栏拖拽调宽结束时触发,携带最终宽度(受控请求)\n * @fires mobile-change - 视口跨越移动端断点、`mobile` 翻转时触发(派生状态通知)\n * @slot banner - 顶部 Banner 区域(全宽,随页面滚动,可选)\n * @slot header - 内容区顶部 header(sticky)\n * @slot sidebar - 侧边栏内容;Consumer 决定其内部固定区域与滚动容器\n * @slot tabbar - 底部 tabbar 区域\n * @slot default - 主内容区\n */\n@customElement('web-ui-layout')\nexport class WebUiLayout extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(style)]\n\n /**\n * 桌面端 Sidebar 的持久布局偏好。\n *\n * `true` 只会将桌面 Sidebar 收窄到 `collapsedWidth`,不会影响移动端 Drawer。\n * 它不与 `sidebarOpen` 合并:前者是跨视口保留的密度选择,后者是移动端瞬时的 modal 可见性。\n *\n * 两处渲染的是同一份 `sidebar` slot 内容,所以本属性只负责桌面 aside 的几何:Consumer\n * 若让自己的 slot 内容跟着它变形态(隐藏标签、改排布),必须按 `mobile` 收窄那个条件\n * (例如 `collapsed && !layout.mobile`,或 CSS 的 `web-ui-layout[mobile]`),\n * 否则折叠态会一路渲染进移动端 Drawer(issue #195)。\n */\n @property({ type: Boolean, attribute: 'sidebar-collapsed', reflect: true })\n sidebarCollapsed = false\n\n @property({ type: String, attribute: 'sidebar-width', reflect: true })\n sidebarWidth = '240px'\n\n @property({ type: String, attribute: 'collapsed-width', reflect: true })\n collapsedWidth = '68px'\n\n /** 启用桌面端 Sidebar 拖拽调宽(折叠态隐藏 handle)。 */\n @property({ type: Boolean, attribute: 'sidebar-resizable', reflect: true })\n sidebarResizable = false\n\n /**\n * 拖拽调宽的下限(px,其他单位不解析);未设置时回退为 `collapsedWidth`,\n * 防止拖到不可用宽度。上限 `sidebar-max-width` 受内置 50vw 硬上限钳制。\n */\n @property({ type: String, attribute: 'sidebar-min-width', reflect: true })\n sidebarMinWidth = ''\n\n /** 拖拽调宽的上限(px,其他单位不解析);始终受内置 50vw 硬上限钳制。 */\n @property({ type: String, attribute: 'sidebar-max-width', reflect: true })\n sidebarMaxWidth = ''\n\n /**\n * 移动端 web-ui-drawer 的瞬时可见性。\n *\n * Drawer Toggle、Escape 和遮罩关闭都通过 `sidebar-open-change` 请求更新它;它不会改变桌面端的折叠偏好。\n * 因而不与 `sidebarCollapsed` 合并,避免响应式切换时将一次移动端交互误写为桌面端布局选择。\n */\n @property({ type: Boolean, attribute: 'sidebar-open', reflect: true })\n sidebarOpen = false\n\n @property({ type: Boolean, attribute: 'header-glow', reflect: true })\n headerGlow = false\n\n /*\n * `mobile` 是派生输出,不是第二个输入:像 `web-ui-theme` 的 `resolved-appearance` 一样由组件\n * 独占写入,读取取 `MOBILE_VIEWPORT_QUERY`。外部写 attribute 会在同一次 reaction 内被恢复,\n * 写 property 不生效(只读 accessor,严格模式下抛错)——两种写法都不构成本属性之外的\n * 第三个视口来源。\n */\n @property({ type: Boolean, attribute: 'mobile', reflect: true })\n private _mobile = false\n\n /**\n * 当前视口是否落在移动端分支(即 `MOBILE_VIEWPORT_QUERY` 命中)。\n *\n * 与组件渲染哪一套树、以及 `mobile-change` 的派发是同一个值:翻转瞬间三者一起变,\n * Consumer 不必自己写断点判断,也不会与 layout 内部的树切换错开半拍。派生状态只读。\n */\n get mobile(): boolean {\n return this._mobile\n }\n\n private _mobileQuery: MediaQueryList | null = null\n\n private readonly _onMobileQueryChange = (event: MediaQueryListEvent) => {\n this._syncMobile(event.matches)\n }\n\n /*\n * 本方法由媒体查询翻转驱动,也被连接时的初次求值调用;attribute 的恢复路径见\n * `attributeChangedCallback`,那里不改判、也不派发通知,所以 `mobile-change` 只在\n * 视口真正跨断点时出现一次(窄视口挂载时那次算第一次)。\n */\n private _syncMobile(next: boolean) {\n if (next === this._mobile) return\n this._mobile = next\n // 跨断点会卸载桌面 layout(resize handle 随之消失);必须先终结进行中的拖拽,\n // 否则手势悬挂,切回桌面后所有新拖拽都在入口被 `_isResizing()` 拦下。\n if (this._isResizing()) this._resetActiveResize()\n this.toggleAttribute('mobile', next)\n this.dispatchEvent(new CustomEvent('mobile-change', { detail: { mobile: next }, bubbles: true, composed: true }))\n }\n\n override attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {\n super.attributeChangedCallback(name, oldValue, newValue)\n // super 已把外部值写进 backing field,所以这里要按媒体查询重算,不能拿字段当真相。\n if (name !== 'mobile') return\n // 无 matchMedia 的环境没有视口可判,`mobile` 恒为 false(见 connectedCallback)。\n const derived = this._mobileQuery?.matches ?? false\n if (this._mobile !== derived) this._mobile = derived\n /*\n * 布尔 attribute 只论存在性,但值本身也要归位:`setAttribute('mobile', 'false')` 在移动端\n * 会让 backing field 与 attribute 的**存在性**一致、值却留在宿主上(`getAttribute` 会回读\n * 到 consumer 写的那串)。组件写的形态是空串,这里按同一形态恢复,主题的\n * `resolved-appearance` 先例同理。\n */\n if (derived) {\n if (this.getAttribute('mobile') !== '') this.setAttribute('mobile', '')\n return\n }\n if (this.hasAttribute('mobile')) this.removeAttribute('mobile')\n }\n\n private readonly _visibleBanner = defineVisibleAreaTracker({\n onVisibleAreaChange: area => {\n this.style.setProperty('--wui-layout-visible-banner-height', `${area.height}px`)\n }\n }).make()\n\n private readonly _visibleBannerController = new VisibleAreaController(this, this._visibleBanner)\n\n private _toggleSidebar() {\n if (this._mobile) {\n this._emitSidebarOpenChange(!this.sidebarOpen)\n return\n }\n\n this._emitSidebarCollapsedChange(!this.sidebarCollapsed)\n }\n\n private _emitSidebarCollapsedChange(collapsed: boolean) {\n this.dispatchEvent(\n new CustomEvent('sidebar-collapsed-change', {\n detail: { collapsed },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private _emitSidebarOpenChange(open: boolean) {\n this.dispatchEvent(\n new CustomEvent('sidebar-open-change', {\n detail: { open },\n bubbles: true,\n composed: true\n })\n )\n }\n\n private _onDrawerChange(e: CustomEvent<{ open: boolean }>) {\n e.stopPropagation()\n this._emitSidebarOpenChange(e.detail.open)\n }\n\n private _resizeGesture: DragGestureHandle | null = null\n private _resizeStartWidth = 0\n // 拖拽中的临时宽度;由 render 的 styleMap 统一写入,避免与 Lit 样式管理竞争。\n @state() private _resizeWidth: string | null = null\n\n private _parsePx(value: string, fallback: number): number {\n const parsed = Number.parseFloat(value)\n return Number.isFinite(parsed) ? parsed : fallback\n }\n\n private _resolveSidebarMinWidth(): number {\n return this.sidebarMinWidth\n ? this._parsePx(this.sidebarMinWidth, this._parsePx(this.collapsedWidth, 68))\n : this._parsePx(this.collapsedWidth, 68)\n }\n\n private _resolveSidebarMaxWidth(): number {\n // 组件内置硬上限:Sidebar 最多占视口一半(无需调用方设置)。\n // 调用方 sidebar-max-width 在该上限内生效,防止拖拽占据整个视口。\n const halfViewport = Math.floor(window.innerWidth / 2)\n const configured = this.sidebarMaxWidth ? this._parsePx(this.sidebarMaxWidth, halfViewport) : halfViewport\n const upper = Math.min(configured, halfViewport)\n return Math.max(upper, this._resolveSidebarMinWidth())\n }\n\n private _isResizing(): boolean {\n return this._resizeGesture?.isDragging() ?? false\n }\n\n private _handleResizePointerDown(e: PointerEvent) {\n if (!this.sidebarResizable || this.sidebarCollapsed || this._mobile || this._isResizing()) return\n // jsdom 等环境的 PointerEvent 可能缺失 isPrimary,仅在明确为 false(多点触控副指针)时拒绝。\n if (e.isPrimary === false) return\n\n const aside = this.renderRoot.querySelector('aside')\n if (!aside) return\n\n // 拖拽起点以 computed width 为准,并丢弃键盘未提交的调整(Enter 才提交、\n // Escape 撤回;抓取隐式放弃),保证零位移松手不会派发陈旧宽度。\n this._resizeStartWidth = this._parsePx(window.getComputedStyle(aside).width, this._parsePx(this.sidebarWidth, 240))\n this._resizeWidth = null\n aside.classList.add('is-resizing')\n\n this._resizeGesture = attachDragGesture(e, {\n axis: 'x',\n onMove: info => {\n // 向右拖增宽、向左拖收窄(handle 在右缘);钳制在 [min, max] 与视口内。\n const next = this._resizeStartWidth + info.deltaX\n const clamped = clamp(next, this._resolveSidebarMinWidth(), this._resolveSidebarMaxWidth())\n this._resizeWidth = `${Math.round(clamped)}px`\n },\n onEnd: () => {\n // 零位移松手(点击而非拖拽)不视为调宽动作:按 cancel 语义静默收尾。\n if (this._resizeWidth === null) {\n this._resetActiveResize()\n return\n }\n const width = this._resizeWidth\n this._resetActiveResize()\n // 受控契约:松手派发请求并交还 prop 管辖,由 Consumer 回写 sidebar-width。\n this.dispatchEvent(\n new CustomEvent('sidebar-width-change', {\n detail: { width },\n bubbles: true,\n composed: true\n })\n )\n },\n onCancel: () => {\n this._resetActiveResize()\n }\n })\n }\n\n // 终结进行中的拖拽:清手势状态、移除跟手 class,宽度过渡回 prop 管辖,不派发事件。\n // pointer cancel 与视口跨越移动端断点(handle 随桌面 layout 卸载)共用此收尾。\n private _resetActiveResize() {\n this._resizeGesture?.destroy()\n this._resizeGesture = null\n this.renderRoot.querySelector('aside')?.classList.remove('is-resizing')\n this._resizeWidth = null\n }\n\n // 键盘调宽(WAI-ARIA splitter 模式):方向键步进 16px(Shift 加速 64px)调整\n // 临时宽度,Enter 以受控请求派发 `sidebar-width-change`(与指针松手同语义),\n // Escape 撤回临时宽度交还 prop 管辖,Home/End 直接到 min/max。\n private _handleResizeKeydown(e: KeyboardEvent) {\n if (!this.sidebarResizable || this.sidebarCollapsed || this._mobile || this._isResizing()) return\n\n const min = this._resolveSidebarMinWidth()\n const max = this._resolveSidebarMaxWidth()\n const aside = this.renderRoot.querySelector('aside')\n const current = this._resizeWidth\n ? this._parsePx(this._resizeWidth, min)\n : this._parsePx(aside ? window.getComputedStyle(aside).width : this.sidebarWidth, min)\n const step = e.shiftKey ? 64 : 16\n let next: number | null = null\n\n switch (e.key) {\n case 'ArrowLeft':\n next = Math.max(min, current - step)\n break\n case 'ArrowRight':\n next = Math.min(max, current + step)\n break\n case 'Home':\n next = min\n break\n case 'End':\n next = max\n break\n case 'Enter':\n // 无未提交调整时不派发(焦点停留时的回车噪声)\n if (this._resizeWidth !== null) {\n const width = this._resizeWidth\n this._resizeWidth = null\n this.dispatchEvent(\n new CustomEvent('sidebar-width-change', { detail: { width }, bubbles: true, composed: true })\n )\n }\n e.preventDefault()\n return\n case 'Escape':\n this._resizeWidth = null\n e.preventDefault()\n return\n default:\n return\n }\n e.preventDefault()\n this._resizeWidth = `${Math.round(next)}px`\n }\n\n /*\n * 视口判定订阅媒体查询,而不是监听 window resize:跨断点每次只派发一次 change,没有\n * resize 事件流要挡。判定、CSS 的 `@media (width <= 640px)` 与对外暴露的 `mobile` 因此\n * 在同一时刻翻转,不会留下「树还在桌面、aside 已被 CSS 藏起来」的空档。\n */\n private _syncMobileViewportSubscription() {\n this._mobileQuery?.removeEventListener('change', this._onMobileQueryChange)\n // jsdom 等没有 matchMedia 的环境没有视口可判:`mobile` 停在 false,订阅与渲染照常。\n this._mobileQuery = typeof window.matchMedia === 'function' ? window.matchMedia(MOBILE_VIEWPORT_QUERY) : null\n this._mobileQuery?.addEventListener('change', this._onMobileQueryChange)\n this._syncMobile(this._mobileQuery?.matches ?? false)\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._syncMobileViewportSubscription()\n void this.updateComplete.then(() => {\n if (this.isConnected) this._syncBannerPresence()\n })\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._resetActiveResize()\n this._mobileQuery?.removeEventListener('change', this._onMobileQueryChange)\n this._mobileQuery = null\n }\n\n override firstUpdated() {\n this._syncBannerPresence()\n }\n\n private _onBannerSlotChange() {\n this._syncBannerPresence()\n }\n\n private _syncBannerPresence() {\n const bannerSlot = this.renderRoot.querySelector<HTMLSlotElement>('slot[name=\"banner\"]')\n const hasBanner =\n bannerSlot\n ?.assignedNodes()\n .some(\n node =>\n node.nodeType === Node.ELEMENT_NODE || (node.nodeType === Node.TEXT_NODE && !!node.textContent?.trim())\n ) ?? false\n const banner = this.shadowRoot?.querySelector<HTMLElement>('.layout-banner') ?? null\n this._visibleBanner.setTarget(hasBanner ? banner : null)\n }\n\n override render() {\n const isSidebarOpen = !this.sidebarCollapsed\n // 拖拽中的临时宽度优先于 prop 宽度;移动端宽度由 Drawer 内部变量管理。\n const sidebarStyle = this._mobile\n ? {}\n : { width: this._resizeWidth ?? (this.sidebarCollapsed ? this.collapsedWidth : this.sidebarWidth) }\n\n const toggleLabel = isSidebarOpen ? '折叠侧边栏' : '展开侧边栏'\n\n const desktopToggle = html`\n <web-ui-button\n class=\"sidebar-toggle\"\n icon\n variant=\"secondary\"\n @click=\"${this._toggleSidebar}\"\n aria-label=\"${toggleLabel}\"\n >\n <web-ui-icon .icon=${radixIconsPanelLeftMinimized}></web-ui-icon>\n </web-ui-button>\n `\n\n const sidebarViewport = html`\n <div class=\"sidebar-viewport\">\n <slot name=\"sidebar\"></slot>\n </div>\n `\n\n const desktopLayout = html`\n <div class=\"layout-body\">\n <div class=\"sidebar-wrapper\">\n <aside\n class=\"${classMap({\n collapsed: !isSidebarOpen\n })}\"\n style=\"${styleMap(sidebarStyle)}\"\n >\n <div class=\"aside-panel wui-glass\">\n ${sidebarViewport}\n <div class=\"sidebar-toggle-area\">${desktopToggle}</div>\n </div>\n ${\n this.sidebarResizable && !this.sidebarCollapsed\n ? html`\n <div\n class=\"sidebar-resize-handle\"\n role=\"separator\"\n tabindex=\"0\"\n aria-orientation=\"vertical\"\n aria-label=\"调整侧边栏宽度\"\n @pointerdown=${this._handleResizePointerDown}\n @keydown=${this._handleResizeKeydown}\n ></div>\n `\n : nothing\n }\n </aside>\n </div>\n <div class=\"layout-content\">\n <header>\n <slot name=\"header\"></slot>\n </header>\n <main><slot></slot></main>\n <footer><slot name=\"tabbar\"></slot></footer>\n </div>\n </div>\n `\n\n const mobileLayout = html`\n <div class=\"layout-content\">\n <header>\n <web-ui-button\n class=\"mobile-toggle\"\n icon\n variant=\"glass\"\n @click=\"${this._toggleSidebar}\"\n aria-label=\"打开导航菜单\"\n >\n <web-ui-icon .icon=${radixIconsPanelLeftMinimized}></web-ui-icon>\n </web-ui-button>\n <slot name=\"header\"></slot>\n </header>\n <main><slot></slot></main>\n <footer><slot name=\"tabbar\"></slot></footer>\n </div>\n\n <web-ui-drawer\n placement=\"left\"\n ?open=\"${this.sidebarOpen}\"\n controlled\n draggable\n dialog-label=\"主导航\"\n @open-change=\"${this._onDrawerChange}\"\n style=\"${styleMap({\n '--wui-drawer-width': this.sidebarWidth,\n '--wui-drawer-radius': 'var(--wui-layout-sidebar-radius, 24px)'\n })}\"\n >\n ${sidebarViewport}\n </web-ui-drawer>\n `\n\n return html`\n <div class=\"layout-page\">\n <div class=\"layout-banner\">\n <slot name=\"banner\" @slotchange=\"${this._onBannerSlotChange}\"></slot>\n </div>\n ${this._mobile ? mobileLayout : desktopLayout}\n </div>\n `\n }\n\n declare readonly $events: {\n 'sidebar-collapsed-change': CustomEvent<{ collapsed: boolean }>\n 'sidebar-open-change': CustomEvent<{ open: boolean }>\n 'sidebar-width-change': CustomEvent<{ width: string }>\n 'mobile-change': CustomEvent<{ mobile: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-layout': WebUiLayout\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  //#region src/components/layout/style.css?inline
2
- var e = "*,:before,:after{box-sizing:border-box}:host{--wui-layout-visible-banner-height:0px;--wui-layout-header-glow-color:var(--wui-color-page,#fff);display:block}.layout-page{flex-direction:column;min-height:100dvh;display:flex}.layout-banner{z-index:var(--wui-layer-banner,30);position:relative}.layout-body{flex:1;display:flex}.sidebar-wrapper{align-self:stretch;position:relative}.sidebar-viewport{flex:1;min-height:0}aside{z-index:var(--wui-layer-sidebar,50);max-width:50vw;height:calc(100vh - var(--wui-layout-visible-banner-height));height:calc(100dvh - var(--wui-layout-visible-banner-height));max-height:calc(100vh - var(--wui-layout-visible-banner-height));max-height:calc(100dvh - var(--wui-layout-visible-banner-height));transition:width var(--wui-duration-layout,.2s) var(--wui-ease-enter,cubic-bezier(.23, 1, .32, 1));flex-direction:column;flex-shrink:0;padding:8px 0 8px 8px;display:flex;position:sticky;top:0}aside .aside-panel{--wui-glass-corner-radius:var(--wui-layout-sidebar-radius,24px);border-radius:var(--wui-layout-sidebar-radius,24px);background-color:var(--wui-color-surface-sidebar,#e9e9e9d1);flex-direction:column;flex:1;min-height:0;display:flex}aside .sidebar-toggle-area{flex-shrink:0;align-items:center;display:flex}aside .sidebar-toggle{--wui-button-width:44px;margin:8px}aside.collapsed .sidebar-viewport{overflow:hidden}.sidebar-resize-handle{touch-action:none;cursor:col-resize;z-index:1;width:12px;position:absolute;top:0;bottom:0;right:-6px}.sidebar-resize-handle:before{content:\"\";border-radius:var(--wui-radius-control,calc(infinity * 1px));opacity:0;background:var(--wui-color-accent,#08f);width:3px;transition:opacity var(--wui-duration-feedback,.1s) ease;position:absolute;top:0;bottom:0;left:50%;translate:-50%}.sidebar-resize-handle:hover:before,.sidebar-resize-handle:focus-visible:before,aside.is-resizing .sidebar-resize-handle:before{opacity:1}aside.is-resizing{transition:none}.layout-content{flex-direction:column;flex:1;min-width:0;display:flex}header{isolation:isolate;z-index:var(--wui-layer-header,10);align-items:center;gap:8px;display:flex;position:sticky;top:0;overflow-x:clip}.mobile-toggle{margin-left:var(--wui-layout-mobile-toggle-inset,8px)}:host([header-glow]) header:before{pointer-events:none;content:\"\";z-index:-1;opacity:.95;background-color:var(--wui-layout-header-glow-color);filter:blur(10px);position:absolute;inset:0;transform:translateY(-50%)scale(1.05,2)}main{flex:1}footer{z-index:var(--wui-layer-tabbar,40);position:sticky;bottom:0}@media (width<=640px){aside,.sidebar-wrapper{display:none}header{align-items:flex-start}.mobile-toggle{align-self:flex-start;margin-top:8px}}@media (prefers-reduced-motion:reduce){aside,.sidebar-resize-handle:before{transition:none}}";
2
+ var e = "*,:before,:after{box-sizing:border-box}:host{--wui-layout-visible-banner-height:0px;display:block}.layout-page{flex-direction:column;min-height:100dvh;display:flex}.layout-banner{z-index:var(--wui-layer-banner,30);position:relative}.layout-body{flex:1;display:flex}.sidebar-wrapper{align-self:stretch;position:relative}.sidebar-viewport{flex:1;min-height:0}aside{z-index:var(--wui-layer-sidebar,50);max-width:50vw;height:calc(100vh - var(--wui-layout-visible-banner-height));height:calc(100dvh - var(--wui-layout-visible-banner-height));max-height:calc(100vh - var(--wui-layout-visible-banner-height));max-height:calc(100dvh - var(--wui-layout-visible-banner-height));padding:var(--wui-space-2,8px) 0 var(--wui-space-2,8px) var(--wui-space-2,8px);transition:width var(--wui-duration-layout,.2s) var(--wui-ease-enter,cubic-bezier(.23, 1, .32, 1));flex-direction:column;flex-shrink:0;display:flex;position:sticky;top:0}aside .aside-panel{--wui-glass-corner-radius:var(--wui-layout-sidebar-radius,24px);border-radius:var(--wui-layout-sidebar-radius,24px);background-color:var(--wui-color-surface-sidebar,#e9e9e9d1);flex-direction:column;flex:1;min-height:0;display:flex}aside .sidebar-toggle-area{flex-shrink:0;align-items:center;display:flex}aside .sidebar-toggle{--wui-button-width:var(--wui-layout-sidebar-toggle-width,44px);margin:var(--wui-space-2,8px)}aside.collapsed .sidebar-viewport{overflow:hidden}.sidebar-resize-handle{touch-action:none;cursor:col-resize;z-index:1;width:12px;position:absolute;top:0;bottom:0;right:-6px}.sidebar-resize-handle:before{content:\"\";border-radius:var(--wui-radius-control,calc(infinity * 1px));opacity:0;background:var(--wui-color-accent,#08f);width:3px;transition:opacity var(--wui-duration-feedback,.1s) ease;position:absolute;top:0;bottom:0;left:50%;translate:-50%}.sidebar-resize-handle:hover:before,.sidebar-resize-handle:focus-visible:before,aside.is-resizing .sidebar-resize-handle:before{opacity:1}.sidebar-resize-handle:focus-visible{border-radius:var(--wui-radius-control,calc(infinity * 1px));outline:var(--wui-focus-ring-width,3px) solid var(--wui-color-focus-ring,color-mix(in srgb, var(--wui-color-accent,#08f) 40%, transparent));outline-offset:2px}aside.is-resizing{transition:none}.layout-content{flex-direction:column;flex:1;min-width:0;display:flex;overflow-x:clip}header{isolation:isolate;z-index:var(--wui-layer-header,10);gap:var(--wui-space-2,8px);align-items:center;display:flex;position:sticky;top:0}.mobile-toggle{margin-left:var(--wui-layout-mobile-toggle-inset,8px)}:host([header-glow]) header:before{pointer-events:none;content:\"\";z-index:-1;background:linear-gradient(to bottom, var(--wui-color-page,#fff), transparent);margin:-10px;position:absolute;inset:0}:host([header-glow]) header:after{pointer-events:none;content:\"\";z-index:-1;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);margin:0 -30px -30px;position:absolute;inset:0;-webkit-mask-image:linear-gradient(#000 0% 45%,#0000 100%);mask-image:linear-gradient(#000 0% 45%,#0000 100%);-webkit-mask-size:100% 100%;mask-size:100% 100%;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}main{flex:1}footer{z-index:var(--wui-layer-tabbar,40);position:sticky;bottom:0}@media (width<=640px){aside,.sidebar-wrapper{display:none}header{align-items:flex-start}.mobile-toggle{margin-top:var(--wui-space-2,8px);align-self:flex-start}}@media (prefers-reduced-motion:reduce){aside,.sidebar-resize-handle:before{transition:none}}";
3
3
  //#endregion
4
4
  export { e as default };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/layout/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n --wui-layout-visible-banner-height: 0px;\n --wui-layout-header-glow-color: var(--wui-color-page, #fff);\n\n display: block;\n}\n\n/* 页面级滚动容器。Banner 位于正文前,随页面一起滚动。 */\n.layout-page {\n display: flex;\n flex-direction: column;\n min-height: 100vh;\n min-height: 100dvh;\n}\n\n.layout-banner {\n position: relative;\n z-index: var(--wui-layer-banner, 30);\n}\n\n/* 参考 book-fe:正文区域负责撑高 sidebar 的 relative wrapper。 */\n.layout-body {\n display: flex;\n flex: 1;\n}\n\n.sidebar-wrapper {\n position: relative;\n align-self: stretch;\n}\n\n/* Layout 只提供可用高度;桌面端由 Consumer 决定 scrollport,移动端由 Drawer content 提供。 */\n.sidebar-viewport {\n flex: 1;\n min-height: 0;\n}\n\n/*\n * Banner 可见时,组件用它的当前可见高度缩短 aside;Banner 滚出后该值归零。\n * 因而圆角卡片始终完整可见,同时 sticky Sidebar 可在 Banner 消失后填满视口。\n */\naside {\n position: sticky;\n z-index: var(--wui-layer-sidebar, 50);\n top: 0;\n\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n\n max-width: 50vw;\n height: calc(100vh - var(--wui-layout-visible-banner-height));\n height: calc(100dvh - var(--wui-layout-visible-banner-height));\n max-height: calc(100vh - var(--wui-layout-visible-banner-height));\n max-height: calc(100dvh - var(--wui-layout-visible-banner-height));\n padding: 8px 0 8px 8px;\n\n transition: width var(--wui-duration-layout, 200ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n\n .aside-panel {\n --wui-glass-corner-radius: var(--wui-layout-sidebar-radius, 24px);\n\n display: flex;\n flex: 1;\n flex-direction: column;\n\n min-height: 0;\n border-radius: var(--wui-layout-sidebar-radius, 24px);\n\n /* 深色下比 page 浅一档的专用表面 token。不直接复用 --wui-color-surface-overlay:\n 该 token 同时服务 dialog/drawer/toast,整体抬升会带偏不在本次范围内的表面。\n 浅色保持中性分层,无 theme 时回退到同一字面量。 */\n background-color: var(--wui-color-surface-sidebar, rgb(233 233 233 / 0.82));\n }\n\n .sidebar-toggle-area {\n display: flex;\n flex-shrink: 0;\n align-items: center;\n }\n\n .sidebar-toggle {\n --wui-button-width: 44px;\n margin: 8px;\n }\n\n &.collapsed {\n .sidebar-viewport {\n overflow: hidden;\n }\n }\n}\n\n/*\n * 命中条贴 aside 右缘(约 12px 宽),hover 或拖拽中显示 3px accent 竖线。\n * aside 有 8px padding,handle 定位在 padding 区外缘,不遮挡 aside-panel 内容。\n */\n.sidebar-resize-handle {\n touch-action: none;\n cursor: col-resize;\n\n position: absolute;\n z-index: 1;\n top: 0;\n right: -6px;\n bottom: 0;\n\n width: 12px;\n}\n\n.sidebar-resize-handle::before {\n content: '';\n\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n translate: -50%;\n\n width: 3px;\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n\n opacity: 0;\n background: var(--wui-color-accent, #08f);\n\n transition: opacity var(--wui-duration-feedback, 100ms) ease;\n}\n\n.sidebar-resize-handle:hover::before,\n.sidebar-resize-handle:focus-visible::before,\naside.is-resizing .sidebar-resize-handle::before {\n opacity: 1;\n}\n\n/* 拖拽中禁用宽度过渡,保证实时跟手。 */\naside.is-resizing {\n transition: none;\n}\n\n.layout-content {\n display: flex;\n flex: 1;\n flex-direction: column;\n min-width: 0;\n}\n\nheader {\n /* 将负层级 glow 限定在 Header 的局部 stacking context,slot 内容自然位于其上。 */\n isolation: isolate;\n position: sticky;\n z-index: var(--wui-layer-header, 10);\n top: 0;\n\n /*\n * glow 伪元素用 scale + translate 向外扩展,会溢出 header 盒子并撑出页面横向滚动条\n * (移动端约 30px)。这里只裁横向:纵向保持 visible,下拉浮层等槽位内容仍可溢出。\n * clip 不创建滚动容器,因此不影响 header 自身的 sticky。\n */\n overflow-x: clip;\n display: flex;\n gap: 8px;\n align-items: center;\n}\n\n/* 移动端 Toggle 属于组件自身,不随 Consumer 的 header 内边距缩进;\n 默认 8px 使其脱离视口边缘,Consumer 可用同名变量对齐自身内容左缘。 */\n.mobile-toggle {\n margin-left: var(--wui-layout-mobile-toggle-inset, 8px);\n}\n\n/*\n * Header 的装饰性背景晕染\n * ========================\n *\n * 伪元素随 header 的实际盒子尺寸和 sticky 位置变化,自然适配 header slot 的\n * 高度与位置,无需单独维护一层 DOM。\n */\n:host([header-glow]) header::before {\n pointer-events: none;\n content: '';\n\n position: absolute;\n z-index: -1;\n inset: 0;\n transform: translateY(-50%) scale(1.05, 2);\n\n opacity: 0.95;\n background-color: var(--wui-layout-header-glow-color);\n filter: blur(10px);\n}\n\nmain {\n flex: 1;\n}\n\nfooter {\n position: sticky;\n z-index: var(--wui-layer-tabbar, 40);\n bottom: 0;\n}\n\n@media (width <= 640px) {\n aside,\n .sidebar-wrapper {\n display: none;\n }\n\n /* Slot 可能包含展开后的多行 header;Toggle 对齐首行,而不是被整段 header 高度居中。 */\n header {\n align-items: flex-start;\n }\n\n .mobile-toggle {\n align-self: flex-start;\n margin-top: 8px;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n aside {\n transition: none;\n }\n\n .sidebar-resize-handle::before {\n transition: none;\n }\n}\n"],"mappings":""}
1
+ {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/layout/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n --wui-layout-visible-banner-height: 0px;\n\n display: block;\n}\n\n/* 页面级滚动容器。Banner 位于正文前,随页面一起滚动。 */\n.layout-page {\n display: flex;\n flex-direction: column;\n min-height: 100vh;\n min-height: 100dvh;\n}\n\n.layout-banner {\n position: relative;\n z-index: var(--wui-layer-banner, 30);\n}\n\n/* 参考 book-fe:正文区域负责撑高 sidebar 的 relative wrapper。 */\n.layout-body {\n display: flex;\n flex: 1;\n}\n\n.sidebar-wrapper {\n position: relative;\n align-self: stretch;\n}\n\n/* Layout 只提供可用高度;桌面端由 Consumer 决定 scrollport,移动端由 Drawer content 提供。 */\n.sidebar-viewport {\n flex: 1;\n min-height: 0;\n}\n\n/*\n * Banner 可见时,组件用它的当前可见高度缩短 aside;Banner 滚出后该值归零。\n * 因而圆角卡片始终完整可见,同时 sticky Sidebar 可在 Banner 消失后填满视口。\n */\naside {\n position: sticky;\n z-index: var(--wui-layer-sidebar, 50);\n top: 0;\n\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n\n max-width: 50vw;\n height: calc(100vh - var(--wui-layout-visible-banner-height));\n height: calc(100dvh - var(--wui-layout-visible-banner-height));\n max-height: calc(100vh - var(--wui-layout-visible-banner-height));\n max-height: calc(100dvh - var(--wui-layout-visible-banner-height));\n padding: var(--wui-space-2, 8px) 0 var(--wui-space-2, 8px) var(--wui-space-2, 8px);\n\n transition: width var(--wui-duration-layout, 200ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n\n .aside-panel {\n --wui-glass-corner-radius: var(--wui-layout-sidebar-radius, 24px);\n\n display: flex;\n flex: 1;\n flex-direction: column;\n\n min-height: 0;\n border-radius: var(--wui-layout-sidebar-radius, 24px);\n\n /* 深色下比 page 浅一档的专用表面 token。不直接复用 --wui-color-surface-overlay:\n 该 token 同时服务 dialog/drawer/toast,整体抬升会带偏不在本次范围内的表面。\n 浅色保持中性分层,无 theme 时回退到同一字面量。 */\n background-color: var(--wui-color-surface-sidebar, rgb(233 233 233 / 0.82));\n }\n\n .sidebar-toggle-area {\n display: flex;\n flex-shrink: 0;\n align-items: center;\n }\n\n .sidebar-toggle {\n /*\n * 走可覆盖的 var:`.sidebar-toggle` 自身声明的 `--wui-button-width` 优先于从\n * 祖先继承的同名变量,app 侧无法直接改写它。要让折叠态的 toggle 跟着折叠宽度\n * 一起变,只能由这里显式接一个出口。取值由 app 决定,本组件不假设折叠宽度。\n * 默认值与历史硬编码一致,不设置时行为不变。\n */\n --wui-button-width: var(--wui-layout-sidebar-toggle-width, 44px);\n\n margin: var(--wui-space-2, 8px);\n }\n\n &.collapsed {\n .sidebar-viewport {\n overflow: hidden;\n }\n }\n}\n\n/*\n * 命中条贴 aside 右缘(约 12px 宽),hover 或拖拽中显示 3px accent 竖线。\n * aside 有 8px padding,handle 定位在 padding 区外缘,不遮挡 aside-panel 内容。\n */\n.sidebar-resize-handle {\n touch-action: none;\n cursor: col-resize;\n\n position: absolute;\n z-index: 1;\n top: 0;\n right: -6px;\n bottom: 0;\n\n width: 12px;\n}\n\n.sidebar-resize-handle::before {\n content: '';\n\n position: absolute;\n top: 0;\n bottom: 0;\n left: 50%;\n translate: -50%;\n\n width: 3px;\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n\n opacity: 0;\n background: var(--wui-color-accent, #08f);\n\n transition: opacity var(--wui-duration-feedback, 100ms) ease;\n}\n\n.sidebar-resize-handle:hover::before,\n.sidebar-resize-handle:focus-visible::before,\naside.is-resizing .sidebar-resize-handle::before {\n opacity: 1;\n}\n\n/*\n * 键盘 focus 同时给出两样东西:竖条与 hover / 拖拽同形,ring 与其余组件同一套浅蓝\n * 样式。竖条说明「命中条在哪」,ring 说明「键盘落点在哪」——handle 宽 12px 且通高,\n * 命中区远大于 3px 竖条本身,只靠竖条不足以说明焦点位置。\n *\n * ring 沿用 --wui-focus-ring-width / --wui-color-focus-ring 两个 token。圆角取\n * control token 后 outline 呈细长胶囊,与 button 的胶囊 ring 同一形状语言;offset\n * 2px 让 ring 骑在 aside 右缘内外两侧,不被 aside 裁掉。\n */\n.sidebar-resize-handle:focus-visible {\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n outline: var(--wui-focus-ring-width, 3px) solid\n var(--wui-color-focus-ring, color-mix(in srgb, var(--wui-color-accent, #08f) 40%, transparent));\n outline-offset: 2px;\n}\n\n/* 拖拽中禁用宽度过渡,保证实时跟手。 */\naside.is-resizing {\n transition: none;\n}\n\n.layout-content {\n /*\n * glow 的两层伪元素用负 margin 向外扩展(见 header::before / ::after),会溢出内容列\n * 并撑出页面横向滚动条(移动端约 30px),横向裁剪因此归属本层。它有 min-width: 0,\n * 是容纳横向溢出的位置。\n * clip 不创建滚动容器,因此不影响 header / footer 自身的 sticky;也不用 hidden——\n * 那会把本层变成滚动容器,sticky 随即失效。\n *\n * 这一层不负责纵向羽化,也切不到它:glow 的下缘断层来自 header 自身的裁剪上下文\n * (见 header 的注释),真 Safari 上把本层改成 overflow-x: visible / unset\n * 与保持 clip 的截图逐像素相同。\n */\n overflow-x: clip;\n display: flex;\n flex: 1;\n flex-direction: column;\n\n min-width: 0;\n}\n\nheader {\n /*\n * isolation: isolate 既把负层级 glow 限定在本 Header 的局部 stacking context\n * (slot 内容自然位于其上),也顺带使本层成为该上下文的裁剪边界:z-index: -1 的\n * glow 伪元素溢出 header 盒的部分会被切掉。横向裁剪上移到 .layout-content。\n *\n * 「纵向必须 visible 才留得住下拉浮层的溢出空间」仍然成立——overflow-* 一旦生效\n * 就会裁剪该轴,visible 是这些槽位内容唯一的溢出出口。\n */\n isolation: isolate;\n position: sticky;\n z-index: var(--wui-layer-header, 10);\n top: 0;\n\n display: flex;\n gap: var(--wui-space-2, 8px);\n align-items: center;\n}\n\n/* 移动端 Toggle 属于组件自身,不随 Consumer 的 header 内边距缩进;\n 默认 8px 使其脱离视口边缘,Consumer 可用同名变量对齐自身内容左缘。 */\n.mobile-toggle {\n margin-left: var(--wui-layout-mobile-toggle-inset, 8px);\n}\n\n/*\n * Header 的装饰性背景晕染\n * ========================\n *\n * 两层伪元素随 header 的实际盒子尺寸和 sticky 位置变化,自然适配 header slot 的\n * 高度与位置,无需单独维护一层 DOM。::before 是晕染色本身,::after 在更外一圈叠一层\n * 真实模糊(backdrop-filter)。职责分开后,模糊强度与晕染浓度互不牵制。\n *\n * 两层都用负 margin 撑开,而不是 transform: scale。渐变完全画在盒内,底缘在\n * 触及 header 盒之前就归零,盒外那一段不带可见不透明度。\n *\n * ::after 只向下、向两侧撑开,**不向上越出 header 盒上缘**(横向 -30px 由祖先\n * .layout-content 的 overflow-x: clip 裁掉)。原因是 WebKit 会把元素盒在视口之外\n * 那一段的**视口外画面**也采进 backdrop:sticky header 贴住视口顶端时它上缘就是\n * 视口上缘,向上越出的 30px 正落在视口外,Safari 于是把那条的颜色——文档顶端以上\n * 的浏览器/窗口底色,或滚动上去的页面内容——模糊后晕染进 header 顶缘。系统暗色 +\n * 页面浅色时那条底色是黑的,页顶就出现一条黑色阴影(#197)。Chrome 不采样视口外,\n * 所以只在 Safari 复现;`clip-path` 也拦不住(裁的是绘制范围,不是采样范围),\n * 必须让盒子本身不越界。::before 是纯渐变、没有 backdrop-filter,不采样,保留\n * 原有的 -10px。\n *\n * ::after 的 backdrop-filter 按规范会把模糊延伸到盒外,而 `isolation: isolate` 让\n * header 成为 z-index: -1 伪元素的裁剪边界,WebKit 在这条边界上会切掉盒外那段\n * (真 Safari 实测 11 级台阶)。mask 把模糊沿高度收到 0,但**残余不为 0**:它的盒\n * 比 header 高 30px,mask 在盒高 45% 之前保持不透明、之后线性归零。45% 是折算来的:\n * 旧盒高 H+60、截止 60%,衰减起点在 header 顶下 0.6H+6;新盒高 H+30,同一绝对起点\n * 换算成 (0.6H+6)/(H+30),header 高 40px 时 43%、110px 时 51%,故取中段的 45%。实测\n * 衰减段(header 底缘以下)在 header 高 52px(demo 的取值)与改动前逐像素一致,\n * 40~110px 全区间衰减段差异 ≤1/255;header 盒内因衰减起点上移有几级差别(≤9/255)。\n * 改回 60% 会把 header 底缘推到衰减段起点(H=52 时该处 alpha 由 0.67 升到 0.91),\n * header 正下方的模糊明显加重。\n *\n * 端点用裸 `transparent`(即 rgb(0 0 0 / 0))而不是\n * `color-mix(in srgb, var(--wui-color-page) 100%, transparent)`:混色按预乘 alpha\n * 进行,后者算出来是完全不透明的原色(alpha = 1×1 + 0×0 = 1),终点没有羽化,实测\n * 台阶反而从 11 级恶化到 152 级。裸 transparent 在渐变插值中同样按预乘处理,各引擎\n * 一致淡到 0,且不引入新色相。\n */\n:host([header-glow]) header::before {\n pointer-events: none;\n content: '';\n\n position: absolute;\n z-index: -1;\n inset: 0;\n\n margin: -10px;\n\n background: linear-gradient(to bottom, var(--wui-color-page, #fff), transparent);\n}\n\n:host([header-glow]) header::after {\n pointer-events: none;\n content: '';\n\n position: absolute;\n z-index: -1;\n inset: 0;\n\n margin: 0 -30px -30px;\n\n backdrop-filter: blur(4px);\n\n mask-image: linear-gradient(to bottom, black 0%, black 45%, transparent 100%);\n mask-repeat: no-repeat;\n mask-size: 100% 100%;\n}\n\nmain {\n flex: 1;\n}\n\nfooter {\n position: sticky;\n z-index: var(--wui-layer-tabbar, 40);\n bottom: 0;\n}\n\n@media (width <= 640px) {\n aside,\n .sidebar-wrapper {\n display: none;\n }\n\n /* Slot 可能包含展开后的多行 header;Toggle 对齐首行,而不是被整段 header 高度居中。 */\n header {\n align-items: flex-start;\n }\n\n .mobile-toggle {\n align-self: flex-start;\n margin-top: var(--wui-space-2, 8px);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n aside {\n transition: none;\n }\n\n .sidebar-resize-handle::before {\n transition: none;\n }\n}\n"],"mappings":""}
@@ -0,0 +1,100 @@
1
+ import { LitElement, PropertyValues } from 'lit';
2
+ /**
3
+ * 单行中间省略:把超长文本截成 `首段…尾段`,省略号出现在中间而不是末尾。
4
+ *
5
+ * 适用面与 `text-overflow: ellipsis` 相同——**单行、且有确定宽度**。文本不换行,宽度由容器
6
+ * 给出(块级盒子的包含块、flex 主轴的 `flex: 1`、grid 轨道都行)。宿主自撑宽度时不存在
7
+ * 「可用空间」可言,组件按当时量到的宽度切一次就不再收缩(见 `_onWidth` 的收敛守卫)。
8
+ *
9
+ * 被丢掉的是中间那段,因此**选中复制拿到的是屏幕上这串**,不是原文;原文经 `title` 暴露,
10
+ * 悬停可见。这条与 CSS 的 `text-overflow` 不同(那里文本节点没被动过、复制得到全文),是
11
+ * JS 方案绕不开的代价,见 docs/research/web-ui-middle-ellipsis-261007.md。
12
+ */
13
+ export declare class WebUiMiddleEllipsis extends LitElement {
14
+ static styles: import('lit').CSSResult[];
15
+ /** 要显示的完整文本。组件只读它,不回写。 */
16
+ text: string;
17
+ /** 中间插入的标记,默认 `…`(U+2026)。空串即「只截断不给信号」,是否这样用由调用方判断。 */
18
+ set marker(value: string);
19
+ get marker(): string;
20
+ private _marker;
21
+ /**
22
+ * 标记所在的横向比例,0–100,默认 `50`(居中)。
23
+ *
24
+ * 与 CSSWG css-overflow-5 的 `text-overflow: ellipsis <length-percentage>` 同向:量的是标记
25
+ * 结束边距行末边的距离占可用行内空间的比例。`0` 只剩头部(等价于末尾省略),`100` 只剩尾部。
26
+ * 文件名这类「头部是路径噪声、尾部才是扩展名」的场景用**大于** 50 的值让尾部多留一些。
27
+ */
28
+ set markerPosition(value: number);
29
+ get markerPosition(): number;
30
+ private _markerPosition;
31
+ private _display;
32
+ private _truncated;
33
+ private _root?;
34
+ private _textEl?;
35
+ private _measure?;
36
+ private _markerProbe?;
37
+ /** 最近一次切分依据的可用宽度(px);-1 表示还没量到过布局。 */
38
+ private _available;
39
+ /** 当前渲染出来的文本实际占宽(px)。收敛守卫靠它判断现有输出是否已经放得下。 */
40
+ private _rendered;
41
+ /** 输入变过、还没按新输入切过。 */
42
+ private _dirty;
43
+ private _refineRound;
44
+ private readonly _range;
45
+ private readonly _width;
46
+ private readonly _probe;
47
+ willUpdate(changed: PropertyValues<this>): void;
48
+ updated(): void;
49
+ /**
50
+ * 容器宽度变化。
51
+ *
52
+ * 收缩方向带一条收敛守卫:宿主宽度由内容决定时(flex 主轴 `auto`、或 `width: fit-content` 在内容
53
+ * 缩到容器以内之后),把自己截短会反过来把宿主量窄,于是排下一次尺寸回调。这个自驱动收缩**可能**
54
+ * 一路棘轮到只剩标记,是否发生取决于度量:
55
+ *
56
+ * - flex + 等宽 16px + 默认 50%:缩掉的量小于一个簇宽时,重新二分落回同一组切点,走一步就停。
57
+ * - `width: fit-content` + `marker-position: 80` + CJK:每个簇都是整宽块,切短一簇与宿主窄一簇
58
+ * 互相咬合,摘掉本守卫后 300px 容器里逐帧 282→266→…→10,九步塌到只剩标记(同仓 browser spec
59
+ * 的「fit-content 宿主 + 非居中标记」用例钉住了这一条)。
60
+ *
61
+ * 判据因此取「当前输出已经放得下」:放得下就没有必须重切的理由,自驱动收缩在第一步就停,与度量
62
+ * 无关;真正的容器收缩会让输出宽于新宽度,那时才重切。放长方向没有这个问题,一律重切(可能能
63
+ * 少截一些)。
64
+ */
65
+ private _onWidth;
66
+ /**
67
+ * 把一次重切排到当前更新周期之外。
68
+ *
69
+ * 重切只能在渲染之后量到宽度才做得了,所以第二次更新是必然的;区别只在它从哪里排出去。
70
+ * 微任务仍排在下一帧渲染之前,画面不会先闪一版全文,而 `updated()` 返回时更新已不再是
71
+ * pending,Lit 的 `change-in-update` 警告不会触发。
72
+ */
73
+ private _defer;
74
+ /**
75
+ * 同步可读的可用行内空间。
76
+ *
77
+ * ResizeObserver 的回调在布局之后才到,属性刚改完的那一帧还没有宽度可用;用 `.root` 的
78
+ * 内容盒补上,免得先按全文画一帧再切。jsdom 没有布局,读到的 0 由 `_apply` 跳过。
79
+ */
80
+ private _currentWidth;
81
+ /**
82
+ * 按可用宽度切分并写回渲染结果。
83
+ *
84
+ * 宽度为 0(没有布局)时什么都不做,**并且保留 `_dirty`**:宿主不可见期间改 text 时这一趟
85
+ * 必然被跳过,若在这里就把「有未落的拟合」清掉,等它重新有布局时就没有任何东西记得要切了。
86
+ * 清标记因此只发生在真的切完之后。
87
+ */
88
+ private _apply;
89
+ private _settle;
90
+ /** 把全文装进隐藏量具。它没有 Lit 绑定,改它不会触发重渲染。 */
91
+ private _syncMeasure;
92
+ /** `[from, to)` 这段原文在量具里的渲染宽度。空区间恒为 0——Range 塌缩矩形的宽度不可靠。 */
93
+ private _rangeWidth;
94
+ render(): import('lit').TemplateResult<1>;
95
+ }
96
+ declare global {
97
+ interface HTMLElementTagNameMap {
98
+ 'web-ui-middle-ellipsis': WebUiMiddleEllipsis;
99
+ }
100
+ }
@@ -0,0 +1,112 @@
1
+ import e from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
2
+ import { normalizeNumber as t } from "../../shared/normalize/index.js";
3
+ import { ElementWidthController as n } from "../../shared/element-width/index.js";
4
+ import r from "./style.js";
5
+ import { truncateMiddle as i } from "./truncate.js";
6
+ import { LitElement as a, html as o, unsafeCSS as s } from "lit";
7
+ import { customElement as c, property as l, query as u } from "lit/decorators.js";
8
+ import { ifDefined as d } from "lit/directives/if-defined.js";
9
+ //#region src/components/middle-ellipsis/index.ts
10
+ var f = 0, p = 100, m = 50, h = .5, g = 3, _ = .5, v = class extends a {
11
+ constructor(...e) {
12
+ super(...e), this.text = "", this._marker = "…", this._markerPosition = m, this._display = "", this._truncated = !1, this._available = -1, this._rendered = 0, this._dirty = !0, this._refineRound = 0, this._range = document.createRange(), this._width = new n(this, (e) => this._onWidth(e)), this._probe = {
13
+ prefix: (e) => this._rangeWidth(0, e),
14
+ suffix: (e) => this._rangeWidth(this.text.length - e, this.text.length),
15
+ marker: () => this._markerProbe?.getBoundingClientRect().width ?? 0
16
+ };
17
+ }
18
+ static {
19
+ this.styles = [s(r)];
20
+ }
21
+ set marker(e) {
22
+ let t = e ?? "…";
23
+ if (this._marker !== t) {
24
+ let e = this._marker;
25
+ this._marker = t, this.requestUpdate("marker", e);
26
+ }
27
+ this.getAttribute("marker") !== t && this.setAttribute("marker", t);
28
+ }
29
+ get marker() {
30
+ return this._marker;
31
+ }
32
+ set markerPosition(e) {
33
+ let n = t(e, f, p, m);
34
+ if (this._markerPosition !== n) {
35
+ let e = this._markerPosition;
36
+ this._markerPosition = n, this.requestUpdate("markerPosition", e);
37
+ }
38
+ this.getAttribute("marker-position") !== String(n) && this.setAttribute("marker-position", String(n));
39
+ }
40
+ get markerPosition() {
41
+ return this._markerPosition;
42
+ }
43
+ willUpdate(e) {
44
+ (e.has("text") || e.has("marker") || e.has("markerPosition")) && (this._dirty = !0, this._display = this.text, this._truncated = !1);
45
+ }
46
+ updated() {
47
+ let e = this._textEl;
48
+ if (e) {
49
+ if (this._rendered = e.getBoundingClientRect().width, this._dirty) {
50
+ this._refineRound = 0, this._defer(() => this._apply(this._currentWidth()));
51
+ return;
52
+ }
53
+ if (this._truncated && this._rendered > this._available + h) {
54
+ if (this._refineRound < g) {
55
+ this._refineRound += 1;
56
+ let e = this._available - (this._rendered - this._available) - h;
57
+ this._defer(() => this._apply(e));
58
+ }
59
+ return;
60
+ }
61
+ this._refineRound = 0;
62
+ }
63
+ }
64
+ _onWidth(e) {
65
+ e > 0 && (!this._dirty && (this._available >= 0 && Math.abs(e - this._available) <= _ || this._available >= 0 && e < this._available && this._rendered <= e + h) || (this._refineRound = 0, this._apply(e)));
66
+ }
67
+ _defer(e) {
68
+ queueMicrotask(e);
69
+ }
70
+ _currentWidth() {
71
+ return this._root?.clientWidth ?? 0;
72
+ }
73
+ _apply(e) {
74
+ if (!(e > 0)) return;
75
+ this._syncMeasure();
76
+ let t = i(this.text, this._marker, this._markerPosition, e, this._probe);
77
+ this._available = e, this._dirty = !1, this._settle(t.truncated ? t.head + this._marker + t.tail : this.text, t.truncated);
78
+ }
79
+ _settle(e, t) {
80
+ (this._display !== e || this._truncated !== t) && (this._display = e, this._truncated = t, this.requestUpdate());
81
+ }
82
+ _syncMeasure() {
83
+ let e = this._measure;
84
+ e && e.textContent !== this.text && (e.textContent = this.text);
85
+ }
86
+ _rangeWidth(e, t) {
87
+ if (t <= e) return 0;
88
+ let n = this._measure?.firstChild;
89
+ return n instanceof Text ? (this._range.setStart(n, e), this._range.setEnd(n, t), this._range.getBoundingClientRect().width) : 0;
90
+ }
91
+ render() {
92
+ return o`
93
+ <div class="root">
94
+ <span class="text" title=${d(this._truncated ? this.text : void 0)}>${this._display}</span>
95
+ <span class="measure"></span>
96
+ <span class="marker-probe" aria-hidden="true">${this._marker}</span>
97
+ </div>
98
+ `;
99
+ }
100
+ };
101
+ e([l({ type: String })], v.prototype, "text", void 0), e([l({
102
+ type: String,
103
+ reflect: !0
104
+ })], v.prototype, "marker", null), e([l({
105
+ type: Number,
106
+ reflect: !0,
107
+ attribute: "marker-position"
108
+ })], v.prototype, "markerPosition", null), e([u(".root")], v.prototype, "_root", void 0), e([u(".text")], v.prototype, "_textEl", void 0), e([u(".measure")], v.prototype, "_measure", void 0), e([u(".marker-probe")], v.prototype, "_markerProbe", void 0), v = e([c("web-ui-middle-ellipsis")], v);
109
+ //#endregion
110
+ export { v as WebUiMiddleEllipsis };
111
+
112
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/middle-ellipsis/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { ifDefined } from 'lit/directives/if-defined.js'\n\nimport { ElementWidthController } from '@/shared/element-width'\nimport { normalizeNumber } from '@/shared/normalize'\n\nimport style from './style.css?inline'\nimport { DEFAULT_MARKER, truncateMiddle, type WidthProbe } from './truncate'\n\nconst MIN_POSITION = 0\nconst MAX_POSITION = 100\nconst DEFAULT_POSITION = 50\n\n/**\n * 合串复核的容差与轮数上限。\n *\n * 分段预算看不见两个接缝处的字距,实测偏差在亚像素量级:容差取 0.5px 吸收它。\n * 轮数上限是纯防御,不是常规路径:实测四种形态(等宽 ASCII 文件名、CJK 长串、无空格长串、\n * emoji/ZWJ 混排)各挂载一次,`_apply` 每次都只被调用一次——复核自己会再调一次 `_apply`,\n * 所以调用次数就是拟合次数,一次意味着复核一轮都没触发。这条上限防的是「标记被调用方撑得比\n * 容器还宽」这类退化输入。\n */\nconst OVERFLOW_TOLERANCE = 0.5\nconst MAX_REFINE_ROUNDS = 3\n\n/**\n * 可用宽度变化的感知阈(px)。\n *\n * `_currentWidth()` 走 `clientWidth`(整数),ResizeObserver 报的是 `contentRect.width`(分数),\n * 同一个盒子在两条路径上最多差 0.5px 的取整误差。不设阈值时,观察者的首次回调会被当成一次「变宽」\n * 而整份重算——实测挂载时两次 `_apply` 的入参都是同一个 240,也就是每次挂载白做一次二分。\n * 0.5 同时压住了「跳过略小于半个像素的真实收缩」的代价:那个量级下输出最多溢出 0.5px,\n * 与 `OVERFLOW_TOLERANCE` 视为可忽略的量级一致。\n */\nconst WIDTH_EPSILON = 0.5\n\n/**\n * 单行中间省略:把超长文本截成 `首段…尾段`,省略号出现在中间而不是末尾。\n *\n * 适用面与 `text-overflow: ellipsis` 相同——**单行、且有确定宽度**。文本不换行,宽度由容器\n * 给出(块级盒子的包含块、flex 主轴的 `flex: 1`、grid 轨道都行)。宿主自撑宽度时不存在\n * 「可用空间」可言,组件按当时量到的宽度切一次就不再收缩(见 `_onWidth` 的收敛守卫)。\n *\n * 被丢掉的是中间那段,因此**选中复制拿到的是屏幕上这串**,不是原文;原文经 `title` 暴露,\n * 悬停可见。这条与 CSS 的 `text-overflow` 不同(那里文本节点没被动过、复制得到全文),是\n * JS 方案绕不开的代价,见 docs/research/web-ui-middle-ellipsis-261007.md。\n */\n@customElement('web-ui-middle-ellipsis')\nexport class WebUiMiddleEllipsis extends LitElement {\n static override styles = [unsafeCSS(style)]\n\n /** 要显示的完整文本。组件只读它,不回写。 */\n @property({ type: String }) text = ''\n\n /** 中间插入的标记,默认 `…`(U+2026)。空串即「只截断不给信号」,是否这样用由调用方判断。 */\n @property({ type: String, reflect: true })\n set marker(value: string) {\n const next = value ?? DEFAULT_MARKER\n if (this._marker !== next) {\n const old = this._marker\n this._marker = next\n this.requestUpdate('marker', old)\n }\n if (this.getAttribute('marker') !== next) this.setAttribute('marker', next)\n }\n\n get marker(): string {\n return this._marker\n }\n\n private _marker = DEFAULT_MARKER\n\n /**\n * 标记所在的横向比例,0–100,默认 `50`(居中)。\n *\n * 与 CSSWG css-overflow-5 的 `text-overflow: ellipsis <length-percentage>` 同向:量的是标记\n * 结束边距行末边的距离占可用行内空间的比例。`0` 只剩头部(等价于末尾省略),`100` 只剩尾部。\n * 文件名这类「头部是路径噪声、尾部才是扩展名」的场景用**大于** 50 的值让尾部多留一些。\n */\n @property({ type: Number, reflect: true, attribute: 'marker-position' })\n set markerPosition(value: number) {\n const normalized = normalizeNumber(value, MIN_POSITION, MAX_POSITION, DEFAULT_POSITION)\n if (this._markerPosition !== normalized) {\n const old = this._markerPosition\n this._markerPosition = normalized\n this.requestUpdate('markerPosition', old)\n }\n if (this.getAttribute('marker-position') !== String(normalized)) {\n this.setAttribute('marker-position', String(normalized))\n }\n }\n\n get markerPosition(): number {\n return this._markerPosition\n }\n\n private _markerPosition = DEFAULT_POSITION\n\n /*\n * 渲染结果用普通字段承载,不用 `@state`:切分只能在渲染之后量到真实宽度才算得出来,也就是说\n * 「改渲染结果」必然发生在更新周期里,而周期内的 `requestUpdate` 会被 Lit 记一条\n * `change-in-update` 警告——一个实例一条,一张千行的表就是一千条。改成普通字段 + 只在周期外\n * (ResizeObserver 回调、微任务)显式 `requestUpdate()`,渲染时机不变,警告消失。\n */\n private _display = ''\n private _truncated = false\n\n @query('.root') private _root?: HTMLElement\n @query('.text') private _textEl?: HTMLElement\n @query('.measure') private _measure?: HTMLElement\n @query('.marker-probe') private _markerProbe?: HTMLElement\n\n /** 最近一次切分依据的可用宽度(px);-1 表示还没量到过布局。 */\n private _available = -1\n /** 当前渲染出来的文本实际占宽(px)。收敛守卫靠它判断现有输出是否已经放得下。 */\n private _rendered = 0\n /** 输入变过、还没按新输入切过。 */\n private _dirty = true\n private _refineRound = 0\n\n private readonly _range = document.createRange()\n\n private readonly _width = new ElementWidthController(this, width => this._onWidth(width))\n\n private readonly _probe: WidthProbe = {\n prefix: units => this._rangeWidth(0, units),\n suffix: units => this._rangeWidth(this.text.length - units, this.text.length),\n marker: () => this._markerProbe?.getBoundingClientRect().width ?? 0\n }\n\n override willUpdate(changed: PropertyValues<this>): void {\n if (!changed.has('text') && !changed.has('marker') && !changed.has('markerPosition')) return\n\n /*\n * 输入变了就先把渲染退回全文:上一版的切分是按旧文本算的,拿它渲染新文本会显示一段两个\n * 版本都不属于的串。真正的重切由 `updated()` 排进微任务,在同一帧渲染之前完成,所以用户\n * 看不到这个中间态。这里写普通字段,正是为了不在更新周期里再排一次更新。\n */\n this._dirty = true\n this._display = this.text\n this._truncated = false\n }\n\n override updated(): void {\n const textEl = this._textEl\n if (!textEl) return\n\n this._rendered = textEl.getBoundingClientRect().width\n\n if (this._dirty) {\n this._refineRound = 0\n this._defer(() => this._apply(this._currentWidth()))\n return\n }\n\n /*\n * 合串复核:分段预算只保证「首段和尾段各自没超额度」,接缝处的字距与连字能把合串推过\n * 边界,而这件事只有渲染出来的盒子量得到。超出时按超出量回收额度重切;轮数上限兜住\n * 「怎么切都放不下」的退化情形(例如调用方把标记撑得比容器还宽)。\n */\n if (this._truncated && this._rendered > this._available + OVERFLOW_TOLERANCE) {\n if (this._refineRound < MAX_REFINE_ROUNDS) {\n this._refineRound += 1\n const tighter = this._available - (this._rendered - this._available) - OVERFLOW_TOLERANCE\n this._defer(() => this._apply(tighter))\n }\n return\n }\n\n this._refineRound = 0\n }\n\n /**\n * 容器宽度变化。\n *\n * 收缩方向带一条收敛守卫:宿主宽度由内容决定时(flex 主轴 `auto`、或 `width: fit-content` 在内容\n * 缩到容器以内之后),把自己截短会反过来把宿主量窄,于是排下一次尺寸回调。这个自驱动收缩**可能**\n * 一路棘轮到只剩标记,是否发生取决于度量:\n *\n * - flex + 等宽 16px + 默认 50%:缩掉的量小于一个簇宽时,重新二分落回同一组切点,走一步就停。\n * - `width: fit-content` + `marker-position: 80` + CJK:每个簇都是整宽块,切短一簇与宿主窄一簇\n * 互相咬合,摘掉本守卫后 300px 容器里逐帧 282→266→…→10,九步塌到只剩标记(同仓 browser spec\n * 的「fit-content 宿主 + 非居中标记」用例钉住了这一条)。\n *\n * 判据因此取「当前输出已经放得下」:放得下就没有必须重切的理由,自驱动收缩在第一步就停,与度量\n * 无关;真正的容器收缩会让输出宽于新宽度,那时才重切。放长方向没有这个问题,一律重切(可能能\n * 少截一些)。\n */\n private _onWidth(width: number): void {\n if (!(width > 0)) return\n\n /*\n * 两条短路都只在「没有未落的拟合」时才成立,它们的依据都是「当前输出就是按这个宽度切出来的」——\n * 而 `_dirty` 为真恰恰说明它不是(输入换过了,切分还没跟上)。宿主无布局期间改 text 就是这样:\n * `_apply` 按宽度 ≤ 0 直接返回,`_dirty` 留着,恢复显示时宽度与上次**相同**,若这里按\n * 「宽度没变」短路,就再也没有任何回调会把它切回来——屏幕上留着一段被 `overflow: hidden`\n * 硬裁的全文,没有标记也没有 title。\n */\n if (!this._dirty) {\n // 与上次拟合用的宽度相同(含亚像素取整差)就没有可算的:当前结果正是按这个宽度切出来的。\n // 这一档实测存在——观察者的首次回调报的就是 `_currentWidth()` 量到的同一个宽度。\n if (this._available >= 0 && Math.abs(width - this._available) <= WIDTH_EPSILON) return\n if (this._available >= 0 && width < this._available && this._rendered <= width + OVERFLOW_TOLERANCE) {\n return\n }\n }\n\n this._refineRound = 0\n this._apply(width)\n }\n\n /**\n * 把一次重切排到当前更新周期之外。\n *\n * 重切只能在渲染之后量到宽度才做得了,所以第二次更新是必然的;区别只在它从哪里排出去。\n * 微任务仍排在下一帧渲染之前,画面不会先闪一版全文,而 `updated()` 返回时更新已不再是\n * pending,Lit 的 `change-in-update` 警告不会触发。\n */\n private _defer(fit: () => void): void {\n queueMicrotask(fit)\n }\n\n /**\n * 同步可读的可用行内空间。\n *\n * ResizeObserver 的回调在布局之后才到,属性刚改完的那一帧还没有宽度可用;用 `.root` 的\n * 内容盒补上,免得先按全文画一帧再切。jsdom 没有布局,读到的 0 由 `_apply` 跳过。\n */\n private _currentWidth(): number {\n return this._root?.clientWidth ?? 0\n }\n\n /**\n * 按可用宽度切分并写回渲染结果。\n *\n * 宽度为 0(没有布局)时什么都不做,**并且保留 `_dirty`**:宿主不可见期间改 text 时这一趟\n * 必然被跳过,若在这里就把「有未落的拟合」清掉,等它重新有布局时就没有任何东西记得要切了。\n * 清标记因此只发生在真的切完之后。\n */\n private _apply(available: number): void {\n if (!(available > 0)) return\n\n // 量具必须在切分之前装上当前文本:首次更新时量具还不存在,只靠输入变化那次同步会让第一次\n // 切分读到空量具,`prefix(text.length)` 量出 0 而被误判成「放得下」。\n this._syncMeasure()\n\n const result = truncateMiddle(this.text, this._marker, this._markerPosition, available, this._probe)\n this._available = available\n this._dirty = false\n this._settle(result.truncated ? result.head + this._marker + result.tail : this.text, result.truncated)\n }\n\n private _settle(display: string, truncated: boolean): void {\n if (this._display === display && this._truncated === truncated) return\n\n this._display = display\n this._truncated = truncated\n // 周期内的调用会被 Lit 记 change-in-update:这里的两条来路(尺寸回调、微任务)都在周期之外。\n this.requestUpdate()\n }\n\n /** 把全文装进隐藏量具。它没有 Lit 绑定,改它不会触发重渲染。 */\n private _syncMeasure(): void {\n const measure = this._measure\n if (measure && measure.textContent !== this.text) measure.textContent = this.text\n }\n\n /** `[from, to)` 这段原文在量具里的渲染宽度。空区间恒为 0——Range 塌缩矩形的宽度不可靠。 */\n private _rangeWidth(from: number, to: number): number {\n if (to <= from) return 0\n\n const node = this._measure?.firstChild\n if (!(node instanceof Text)) return 0\n\n this._range.setStart(node, from)\n this._range.setEnd(node, to)\n return this._range.getBoundingClientRect().width\n }\n\n override render() {\n return html`\n <div class=\"root\">\n <span class=\"text\" title=${ifDefined(this._truncated ? this.text : undefined)}>${this._display}</span>\n <span class=\"measure\"></span>\n <span class=\"marker-probe\" aria-hidden=\"true\">${this._marker}</span>\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-middle-ellipsis': WebUiMiddleEllipsis\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ //#region src/components/middle-ellipsis/style.css?inline
2
+ var e = ":host{display:block;overflow:hidden}.root{white-space:nowrap;position:relative;overflow:hidden}.text{white-space:nowrap}.measure,.marker-probe{pointer-events:none;white-space:nowrap;visibility:hidden;position:absolute;top:0;left:0}";
3
+ //#endregion
4
+ export { e as default };
5
+
6
+ //# sourceMappingURL=style.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/middle-ellipsis/style.css?inline"],"sourcesContent":[":host {\n overflow: hidden;\n display: block;\n}\n\n.root {\n position: relative;\n overflow: hidden;\n white-space: nowrap;\n}\n\n.text {\n white-space: nowrap;\n}\n\n/*\n * 两个隐藏量具。必须是出流盒子:留在流内的话宿主的 max-content 会被全文撑开,\n * 而截断的前提正是「宽度来自容器」。\n */\n.measure,\n.marker-probe {\n pointer-events: none;\n\n position: absolute;\n top: 0;\n left: 0;\n\n white-space: nowrap;\n\n visibility: hidden;\n}\n"],"mappings":""}