@greypan/web-ui 4.0.0 → 6.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 (162) hide show
  1. package/README.CN.md +259 -139
  2. package/README.md +296 -139
  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/components/autocomplete/index.d.ts +8 -0
  8. package/dist/components/autocomplete/index.js +108 -50
  9. package/dist/components/autocomplete/index.js.map +1 -1
  10. package/dist/components/autocomplete/style.js +1 -1
  11. package/dist/components/autocomplete/style.js.map +1 -1
  12. package/dist/components/back-top/index.js +1 -1
  13. package/dist/components/back-top/index.js.map +1 -1
  14. package/dist/components/back-top/style.js +1 -1
  15. package/dist/components/back-top/style.js.map +1 -1
  16. package/dist/components/button/index.js +33 -32
  17. package/dist/components/button/index.js.map +1 -1
  18. package/dist/components/button/style.js +1 -1
  19. package/dist/components/button/style.js.map +1 -1
  20. package/dist/components/button-group/index.js +14 -12
  21. package/dist/components/button-group/index.js.map +1 -1
  22. package/dist/components/button-group/style.js +1 -1
  23. package/dist/components/button-group/style.js.map +1 -1
  24. package/dist/components/checkbox/index.js +23 -16
  25. package/dist/components/checkbox/index.js.map +1 -1
  26. package/dist/components/checkbox/style.js +1 -1
  27. package/dist/components/checkbox/style.js.map +1 -1
  28. package/dist/components/collapse/index.d.ts +66 -0
  29. package/dist/components/collapse/index.js +159 -0
  30. package/dist/components/collapse/index.js.map +1 -0
  31. package/dist/components/collapse/style.js +6 -0
  32. package/dist/components/collapse/style.js.map +1 -0
  33. package/dist/components/context-menu/index.d.ts +9 -0
  34. package/dist/components/context-menu/index.js +109 -51
  35. package/dist/components/context-menu/index.js.map +1 -1
  36. package/dist/components/dialog/index.d.ts +6 -0
  37. package/dist/components/dialog/index.js +28 -7
  38. package/dist/components/dialog/index.js.map +1 -1
  39. package/dist/components/dialog/style.js +1 -1
  40. package/dist/components/dialog/style.js.map +1 -1
  41. package/dist/components/drawer/index.d.ts +32 -2
  42. package/dist/components/drawer/index.js +229 -39
  43. package/dist/components/drawer/index.js.map +1 -1
  44. package/dist/components/drawer/style.js +1 -1
  45. package/dist/components/drawer/style.js.map +1 -1
  46. package/dist/components/dropdown/index.d.ts +8 -0
  47. package/dist/components/dropdown/index.js +84 -40
  48. package/dist/components/dropdown/index.js.map +1 -1
  49. package/dist/components/dropdown-item/style.js +1 -1
  50. package/dist/components/dropdown-item/style.js.map +1 -1
  51. package/dist/components/empty/index.d.ts +1 -0
  52. package/dist/components/empty/index.js +23 -21
  53. package/dist/components/empty/index.js.map +1 -1
  54. package/dist/components/empty/style.js +1 -1
  55. package/dist/components/empty/style.js.map +1 -1
  56. package/dist/components/index.d.ts +1 -0
  57. package/dist/components/index.js +28 -27
  58. package/dist/components/input/style.js +1 -1
  59. package/dist/components/input/style.js.map +1 -1
  60. package/dist/components/input-number/style.js +1 -1
  61. package/dist/components/input-number/style.js.map +1 -1
  62. package/dist/components/layout/index.d.ts +23 -0
  63. package/dist/components/layout/index.js +134 -30
  64. package/dist/components/layout/index.js.map +1 -1
  65. package/dist/components/layout/style.js +1 -1
  66. package/dist/components/layout/style.js.map +1 -1
  67. package/dist/components/option/style.js +1 -1
  68. package/dist/components/option/style.js.map +1 -1
  69. package/dist/components/popover/index.d.ts +4 -1
  70. package/dist/components/popover/index.js +18 -6
  71. package/dist/components/popover/index.js.map +1 -1
  72. package/dist/components/popover/style.js +1 -1
  73. package/dist/components/popover/style.js.map +1 -1
  74. package/dist/components/radio/index.js +18 -15
  75. package/dist/components/radio/index.js.map +1 -1
  76. package/dist/components/radio/style.js +1 -1
  77. package/dist/components/radio/style.js.map +1 -1
  78. package/dist/components/segmented/index.d.ts +8 -1
  79. package/dist/components/segmented/index.js +87 -21
  80. package/dist/components/segmented/index.js.map +1 -1
  81. package/dist/components/segmented/style.js +1 -1
  82. package/dist/components/segmented/style.js.map +1 -1
  83. package/dist/components/segmented-trigger/index.js +17 -14
  84. package/dist/components/segmented-trigger/index.js.map +1 -1
  85. package/dist/components/segmented-trigger/style.js +1 -1
  86. package/dist/components/segmented-trigger/style.js.map +1 -1
  87. package/dist/components/select/index.js +33 -29
  88. package/dist/components/select/index.js.map +1 -1
  89. package/dist/components/select/style.js +1 -1
  90. package/dist/components/select/style.js.map +1 -1
  91. package/dist/components/slider/index.d.ts +3 -2
  92. package/dist/components/slider/index.js +65 -47
  93. package/dist/components/slider/index.js.map +1 -1
  94. package/dist/components/slider/style.js +1 -1
  95. package/dist/components/slider/style.js.map +1 -1
  96. package/dist/components/spinner/index.js.map +1 -1
  97. package/dist/components/switch/index.d.ts +8 -0
  98. package/dist/components/switch/index.js +72 -25
  99. package/dist/components/switch/index.js.map +1 -1
  100. package/dist/components/switch/style.js +1 -1
  101. package/dist/components/switch/style.js.map +1 -1
  102. package/dist/components/textarea/index.js +1 -4
  103. package/dist/components/textarea/index.js.map +1 -1
  104. package/dist/components/textarea/style.js +1 -1
  105. package/dist/components/textarea/style.js.map +1 -1
  106. package/dist/components/theme/style.js +1 -1
  107. package/dist/components/theme/style.js.map +1 -1
  108. package/dist/components/toast/style.js +1 -1
  109. package/dist/components/toast/style.js.map +1 -1
  110. package/dist/components/toast/toast.js.map +1 -1
  111. package/dist/components/tooltip/index.d.ts +1 -0
  112. package/dist/components/tooltip/index.js +9 -4
  113. package/dist/components/tooltip/index.js.map +1 -1
  114. package/dist/components/tooltip/style.js +1 -1
  115. package/dist/components/tooltip/style.js.map +1 -1
  116. package/dist/icons/generated/heroicons-arrow-down-16-solid.d.ts +6 -0
  117. package/dist/icons/generated/heroicons-arrow-up-16-solid.d.ts +6 -0
  118. package/dist/icons/generated/heroicons-bars-arrow-down-16-solid.d.ts +6 -0
  119. package/dist/icons/generated/heroicons-bars-arrow-down-16-solid.js +10 -0
  120. package/dist/icons/generated/heroicons-bars-arrow-down-16-solid.js.map +1 -0
  121. package/dist/icons/generated/heroicons-bars-arrow-up-16-solid.d.ts +6 -0
  122. package/dist/icons/generated/heroicons-bars-arrow-up-16-solid.js +10 -0
  123. package/dist/icons/generated/heroicons-bars-arrow-up-16-solid.js.map +1 -0
  124. package/dist/icons/generated/lucide-code.d.ts +6 -0
  125. package/dist/icons/generated/lucide-code.js +10 -0
  126. package/dist/icons/generated/lucide-code.js.map +1 -0
  127. package/dist/icons/generated/lucide-ellipsis-vertical.d.ts +6 -0
  128. package/dist/icons/generated/lucide-ellipsis-vertical.js +10 -0
  129. package/dist/icons/generated/lucide-ellipsis-vertical.js.map +1 -0
  130. package/dist/icons/generated/lucide-list-filter.d.ts +6 -0
  131. package/dist/icons/generated/lucide-list-filter.js +10 -0
  132. package/dist/icons/generated/lucide-list-filter.js.map +1 -0
  133. package/dist/icons/generated/lucide-list-restart.d.ts +6 -0
  134. package/dist/icons/generated/lucide-list-restart.js +10 -0
  135. package/dist/icons/generated/lucide-list-restart.js.map +1 -0
  136. package/dist/icons/generated/lucide-tag.d.ts +6 -0
  137. package/dist/icons/generated/lucide-tag.js +10 -0
  138. package/dist/icons/generated/lucide-tag.js.map +1 -0
  139. package/dist/icons/generated/tabler-sort-ascending-letters.d.ts +6 -0
  140. package/dist/icons/generated/tabler-sort-ascending-letters.js +10 -0
  141. package/dist/icons/generated/tabler-sort-ascending-letters.js.map +1 -0
  142. package/dist/icons/index.d.ts +8 -0
  143. package/dist/icons/index.js +18 -10
  144. package/dist/shared/gesture/drag-gesture.js +133 -0
  145. package/dist/shared/gesture/drag-gesture.js.map +1 -0
  146. package/dist/shared/gesture/physics.js +45 -0
  147. package/dist/shared/gesture/physics.js.map +1 -0
  148. package/dist/shared/group-management/index.js +49 -47
  149. package/dist/shared/group-management/index.js.map +1 -1
  150. package/dist/shared/menu-portal/menu-portal.js +22 -10
  151. package/dist/shared/menu-portal/menu-portal.js.map +1 -1
  152. package/dist/shared/menu-portal/menu-tree.js +63 -2
  153. package/dist/shared/menu-portal/menu-tree.js.map +1 -1
  154. package/dist/shared/option-portal/index.js +1 -1
  155. package/dist/shared/option-portal/index.js.map +1 -1
  156. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  157. package/dist/shared/overlay/nested-drawer-layers.js +55 -0
  158. package/dist/shared/overlay/nested-drawer-layers.js.map +1 -0
  159. package/dist/shared/overlay/overlay.js.map +1 -1
  160. package/dist/shared/overlay/portal.js +119 -16
  161. package/dist/shared/overlay/portal.js.map +1 -1
  162. package/package.json +8 -3
package/README.md CHANGED
@@ -123,10 +123,51 @@ useEffect(() => {
123
123
  <web-ui-dropdown-item ref={itemRef}>Paste and close</web-ui-dropdown-item>
124
124
  ```
125
125
 
126
+ In `portal` mode the library physically moves panel content into the overlay Shadow DOM. React removes nodes
127
+ through its recorded insertion parent, so a bare conditional child (`{condition && <el/>}`) whose node was
128
+ already migrated cannot be removed while the panel is open: the commit fails and the node can reappear when
129
+ the panel closes. Conditionally render inside a stable wrapper element instead — React records the wrapper as
130
+ the insertion parent, so wrapper-local additions and removals keep working while the panel is open and across
131
+ open/close cycles:
132
+
133
+ ```tsx
134
+ <web-ui-popover portal open={open} trigger="manual">
135
+ <div className="panel-body">{editing && <TagEditor />}</div>
136
+ </web-ui-popover>
137
+ ```
138
+
139
+ Bare conditional additions while the panel is open (appending to the host) keep working and migrate into the
140
+ panel automatically. Keeping content mounted and toggling visibility remains a valid fallback.
141
+
142
+ ### Lit
143
+
144
+ Portal panels support live rendering for Lit child parts: `${condition ? html`<el/>` : nothing}` inside
145
+ `web-ui-popover`, `web-ui-tooltip`, `web-ui-select` and `web-ui-autocomplete` migrates into the open panel
146
+ automatically, removals and element-to-element swaps work while the panel is open, and on close content is
147
+ restored to its original anchor position. lit-html child-part markers stay in the host light DOM, so
148
+ subsequent patches keep anchoring correctly. Boundary: reordering a `repeat()` list while a panel is open is
149
+ not supported — lit inserts relative to host part markers while sibling items already live in the panel, the
150
+ same cross-container anchoring boundary as Vue's keyed `v-for`. Apply list reorders while the panel is closed.
151
+
152
+ `web-ui-dropdown` manages menu items through the shared menu portal, which reconciles from both the host and
153
+ the panel: child-part additions while the panel is open work, but lit cannot reach items that already live in
154
+ the panel, so removals and element swaps there are not supported — apply them while the panel is closed.
155
+ `web-ui-context-menu` does not observe host-side insertions and is not covered by these live-rendering
156
+ guarantees.
157
+
126
158
  ### Vue
127
159
 
128
160
  Requires `vue >= 3.5` as optional peer dependency.
129
161
 
162
+ Portal panels support live rendering: content conditionally added or removed while a panel is open (Vue
163
+ `v-if` inside `web-ui-select`, `web-ui-autocomplete`, `web-ui-popover`, `web-ui-tooltip`, `web-ui-dropdown`,
164
+ `web-ui-context-menu`) migrates into the open panel automatically in template order, and on close everything
165
+ is restored to its original anchor position so subsequent patches keep working. Boundary: splicing or
166
+ reordering a keyed `v-for` list while a panel is open is not supported — Vue's keyed children diff resolves
167
+ insertion anchors against sibling nodes already migrated into the panel, so its patch fails and items can be
168
+ lost, regardless of compilation path (compiled templates included). Apply such list updates while the panel
169
+ is closed, or keep to appends/removals at the list tail.
170
+
130
171
  ```ts
131
172
  // vite.config.ts
132
173
  import vue from '@vitejs/plugin-vue'
@@ -187,49 +228,85 @@ Components own their role and interaction state. Browser-native composed events
187
228
  remain the primary interaction API. Kebab-case custom events such as `open-change` describe user-originated component
188
229
  state changes; assigning a property programmatically does not emit them.
189
230
 
231
+ ### Cursor behavior
232
+
233
+ Interactive controls use the native arrow cursor instead of the hand cursor. Disabled controls use `not-allowed`.
234
+ Slider, switch, and segmented controls keep the arrow cursor during hover and press feedback, then switch to
235
+ `grabbing` only while a drag is active. Dedicated drag handles and resize controls keep their specialized cursors.
236
+ Cursor styles live inside Shadow DOM, so a plain document-level selector cannot override them.
237
+
238
+ ### Controlled state semantics
239
+
240
+ `web-ui-*` elements do not use a single "controlled" contract; three distinct state models exist, chosen per component
241
+ family by the nature of the interaction. Do not assume `controlled` means the same thing across components.
242
+
243
+ **User-originated events only (all `open`-style components: collapse, popover, dropdown, tooltip, dialog, drawer, back-top).**
244
+ `*-change` events (`open-change`, etc.) fire only for user-originated toggles; assigning the property programmatically
245
+ (`el.open = true`) or calling an imperative method (`show()`, `close()`, `toggle()`) never emits. Components self-manage
246
+ their `open` value unless you coordinate externally — you may observe the event and write it back for your own state, but
247
+ the component does not require or wait for that write-back.
248
+
249
+ **Request-then-write-back (`controlled` on dialog and drawer only).**
250
+ With `controlled`, user close actions (Escape, backdrop, built-in close, drag-release) only _request_ `open=false`; the
251
+ component does not change its own state, and it stays open until the consumer writes `open=false`. This is the modal
252
+ confirmation semantics — closing a dialog or drawer is typically an operation that needs consumer approval (unsaved form,
253
+ confirmation), and unlike instantaneous open/close, it is not safe for the component to self-commit. Only these two modal
254
+ components use this mode; the instant open/close family (collapse, popover, dropdown, tooltip) does not need it.
255
+
256
+ **Native form-model (input, textarea, input-number, select, checkbox, radio, switch, autocomplete).**
257
+ The control manages its own value/checked state internally; user interaction flips it immediately and then dispatches
258
+ native `input`/`change` events. Assigning the property programmatically (`el.checked = true`) overrides the internal
259
+ state. This mirrors native `<input type="checkbox">` — there is no `controlled` prop because the value is always readable
260
+ and writable, and React/Vue layers provide their own controlled wrappers on top. For interactions that need an external
261
+ round-trip before the flip is durable (e.g. a switch that should only stay on after an API call succeeds), use the
262
+ optimistic-update path: flip immediately, observe `change`, set the control's `loading` during the request (this blocks
263
+ further interaction and shows a spinner), and on failure write the property back (`el.checked = false`) plus surface a
264
+ toast. The flip-before-approval window is accepted; there is no `controlled` prop for a reject-before-flip contract.
265
+
190
266
  ### Form-associated controls
191
267
 
192
268
  All form controls participate in native `FormData`, constraint validation, `form.reset()` and browser form-state restoration. The control captures its reset default once, after declarative attributes have been applied on its first connection. Later runtime property updates do not redefine that default. A disabled ancestor `fieldset` disables validation and interaction without mutating the control's public `disabled` property. For checkbox/radio groups, the group is the single submission, reset and restoration owner; managed child controls do not submit or restore an independent state.
193
269
 
194
270
  ## All Components
195
271
 
196
- | Category | Component |
197
- | -------------------- | --------------------------------------------------------- |
198
- | **Form Controls** | [`<web-ui-input>`](#web-ui-input) |
199
- | | [`<web-ui-textarea>`](#web-ui-textarea) |
200
- | | [`<web-ui-input-number>`](#web-ui-input-number) |
201
- | | [`<web-ui-select>`](#web-ui-select) |
202
- | | [`<web-ui-autocomplete>`](#web-ui-autocomplete) |
203
- | | [`<web-ui-slider>`](#web-ui-slider) |
204
- | | [`<web-ui-checkbox>`](#web-ui-checkbox) |
205
- | | [`<web-ui-radio>`](#web-ui-radio) |
206
- | | [`<web-ui-switch>`](#web-ui-switch) |
207
- | | [`<web-ui-segmented>`](#web-ui-segmented) |
208
- | | [`<web-ui-checkbox-group>`](#web-ui-checkbox-group) |
209
- | | [`<web-ui-radio-group>`](#web-ui-radio-group) |
210
- | **Buttons** | [`<web-ui-button>`](#web-ui-button) |
211
- | | [`<web-ui-button-group>`](#web-ui-button-group) |
212
- | **Overlay / Modal** | [`<web-ui-dialog>`](#web-ui-dialog) |
213
- | | [`<web-ui-drawer>`](#web-ui-drawer) |
214
- | **Floating** | [`<web-ui-popover>`](#web-ui-popover) |
215
- | | [`<web-ui-tooltip>`](#web-ui-tooltip) |
216
- | | [`<web-ui-context-menu>`](#web-ui-context-menu) |
217
- | **Menu** | [`<web-ui-dropdown>`](#web-ui-dropdown) |
218
- | | [`<web-ui-dropdown-item>`](#web-ui-dropdown-item) |
219
- | | [`<web-ui-dropdown-divider>`](#web-ui-dropdown-divider) |
220
- | | [`<web-ui-dropdown-header>`](#web-ui-dropdown-header) |
221
- | **Data Display** | [`<web-ui-avatar>`](#web-ui-avatar) |
222
- | | [`<web-ui-badge>`](#web-ui-badge) |
223
- | | [`<web-ui-empty>`](#web-ui-empty) |
224
- | | [`<web-ui-icon>`](#web-ui-icon) |
225
- | | [`<web-ui-spinner>`](#web-ui-spinner) |
226
- | **Layout & Utility** | [`<web-ui-layout>`](#web-ui-layout) |
227
- | | [`<web-ui-back-top>`](#web-ui-back-top) |
228
- | | [`<web-ui-svg-draw-lines>`](#web-ui-svg-draw-lines) |
229
- | | [`<web-ui-theme>`](#web-ui-theme) |
230
- | **Notification** | [`<web-ui-toast>`](#web-ui-toast) |
231
- | **Sub-items** | [`<web-ui-option>`](#web-ui-option) |
232
- | | [`<web-ui-segmented-trigger>`](#web-ui-segmented-trigger) |
272
+ | Category | Component |
273
+ | ---------------------- | --------------------------------------------------------- |
274
+ | **Form Controls** | [`<web-ui-input>`](#web-ui-input) |
275
+ | | [`<web-ui-textarea>`](#web-ui-textarea) |
276
+ | | [`<web-ui-input-number>`](#web-ui-input-number) |
277
+ | | [`<web-ui-select>`](#web-ui-select) |
278
+ | | [`<web-ui-autocomplete>`](#web-ui-autocomplete) |
279
+ | | [`<web-ui-slider>`](#web-ui-slider) |
280
+ | | [`<web-ui-checkbox>`](#web-ui-checkbox) |
281
+ | | [`<web-ui-radio>`](#web-ui-radio) |
282
+ | | [`<web-ui-switch>`](#web-ui-switch) |
283
+ | | [`<web-ui-segmented>`](#web-ui-segmented) |
284
+ | | [`<web-ui-checkbox-group>`](#web-ui-checkbox-group) |
285
+ | | [`<web-ui-radio-group>`](#web-ui-radio-group) |
286
+ | **Buttons** | [`<web-ui-button>`](#web-ui-button) |
287
+ | | [`<web-ui-button-group>`](#web-ui-button-group) |
288
+ | **Overlay / Modal** | [`<web-ui-dialog>`](#web-ui-dialog) |
289
+ | | [`<web-ui-drawer>`](#web-ui-drawer) |
290
+ | **In-flow Disclosure** | [`<web-ui-collapse>`](#web-ui-collapse) |
291
+ | **Floating** | [`<web-ui-popover>`](#web-ui-popover) |
292
+ | | [`<web-ui-tooltip>`](#web-ui-tooltip) |
293
+ | | [`<web-ui-context-menu>`](#web-ui-context-menu) |
294
+ | **Menu** | [`<web-ui-dropdown>`](#web-ui-dropdown) |
295
+ | | [`<web-ui-dropdown-item>`](#web-ui-dropdown-item) |
296
+ | | [`<web-ui-dropdown-divider>`](#web-ui-dropdown-divider) |
297
+ | | [`<web-ui-dropdown-header>`](#web-ui-dropdown-header) |
298
+ | **Data Display** | [`<web-ui-avatar>`](#web-ui-avatar) |
299
+ | | [`<web-ui-badge>`](#web-ui-badge) |
300
+ | | [`<web-ui-empty>`](#web-ui-empty) |
301
+ | | [`<web-ui-icon>`](#web-ui-icon) |
302
+ | | [`<web-ui-spinner>`](#web-ui-spinner) |
303
+ | **Layout & Utility** | [`<web-ui-layout>`](#web-ui-layout) |
304
+ | | [`<web-ui-back-top>`](#web-ui-back-top) |
305
+ | | [`<web-ui-svg-draw-lines>`](#web-ui-svg-draw-lines) |
306
+ | | [`<web-ui-theme>`](#web-ui-theme) |
307
+ | **Notification** | [`<web-ui-toast>`](#web-ui-toast) |
308
+ | **Sub-items** | [`<web-ui-option>`](#web-ui-option) |
309
+ | | [`<web-ui-segmented-trigger>`](#web-ui-segmented-trigger) |
233
310
 
234
311
  ## API Reference
235
312
 
@@ -241,19 +318,19 @@ Form controls implement `static formAssociated = true` and integrate with native
241
318
 
242
319
  Text input with clearable, prefix/suffix slots.
243
320
 
244
- | Attribute | Type | Default | Description |
245
- | ------------- | --------- | -------- | ------------------- |
246
- | `value` | `string` | `''` | Input value |
247
- | `type` | `string` | `'text'` | HTML input type |
248
- | `placeholder` | `string` | `''` | Placeholder text |
249
- | `name` | `string` | `''` | Form field name |
250
- | `disabled` | `boolean` | `false` | Disabled state |
251
- | `readonly` | `boolean` | `false` | Read-only state |
252
- | `required` | `boolean` | `false` | Required validation |
253
- | `clearable` | `boolean` | `false` | Show clear button |
254
- | `full` | `boolean` | `false` | Full width |
255
- | `borderless` | `boolean` | `false` | No border |
256
- | `aria-label` | `string` | — | Accessible label |
321
+ | Attribute | Type | Default | Description |
322
+ | ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------- |
323
+ | `value` | `string` | `''` | Input value |
324
+ | `type` | `string` | `'text'` | HTML input type |
325
+ | `placeholder` | `string` | `''` | Placeholder text |
326
+ | `name` | `string` | `''` | Form field name |
327
+ | `disabled` | `boolean` | `false` | Disabled state |
328
+ | `readonly` | `boolean` | `false` | Read-only state |
329
+ | `required` | `boolean` | `false` | Required validation |
330
+ | `clearable` | `boolean` | `false` | Show clear button |
331
+ | `full` | `boolean` | `false` | Full width |
332
+ | `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring |
333
+ | `aria-label` | `string` | — | Accessible label |
257
334
 
258
335
  **Events:** `input`, `change`, `focus`, `blur`
259
336
 
@@ -269,24 +346,24 @@ Text input with clearable, prefix/suffix slots.
269
346
 
270
347
  Multi-line text input with auto-resize.
271
348
 
272
- | Attribute | Type | Default | Description |
273
- | ----------------- | --------- | ------- | ----------------------------------------- |
274
- | `value` | `string` | `''` | Textarea value |
275
- | `placeholder` | `string` | `''` | Placeholder text |
276
- | `rows` | `number` | `3` | Visible rows |
277
- | `name` | `string` | `''` | Form field name |
278
- | `disabled` | `boolean` | `false` | Disabled state |
279
- | `readonly` | `boolean` | `false` | Read-only state |
280
- | `required` | `boolean` | `false` | Required validation |
281
- | `clearable` | `boolean` | `false` | Show clear button |
282
- | `full` | `boolean` | `false` | Full width |
283
- | `borderless` | `boolean` | `false` | No border |
284
- | `autosize` | `boolean` | `false` | Auto-resize height |
285
- | `max-height` | `number` | `0` | Autosize max height (px); `0` = unlimited |
286
- | `minlength` | `number` | — | Minimum length validation |
287
- | `maxlength` | `number` | — | Maximum length validation |
288
- | `aria-label` | `string` | — | Accessible label |
289
- | `aria-labelledby` | `string` | — | Accessible label reference |
349
+ | Attribute | Type | Default | Description |
350
+ | ----------------- | --------- | ------- | ------------------------------------------------------------------------------------------------- |
351
+ | `value` | `string` | `''` | Textarea value |
352
+ | `placeholder` | `string` | `''` | Placeholder text |
353
+ | `rows` | `number` | `3` | Visible rows |
354
+ | `name` | `string` | `''` | Form field name |
355
+ | `disabled` | `boolean` | `false` | Disabled state |
356
+ | `readonly` | `boolean` | `false` | Read-only state |
357
+ | `required` | `boolean` | `false` | Required validation |
358
+ | `clearable` | `boolean` | `false` | Show clear button |
359
+ | `full` | `boolean` | `false` | Full width |
360
+ | `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring |
361
+ | `autosize` | `boolean` | `false` | Auto-resize height |
362
+ | `max-height` | `number` | `0` | Autosize max height (px); `0` = unlimited |
363
+ | `minlength` | `number` | — | Minimum length validation |
364
+ | `maxlength` | `number` | — | Maximum length validation |
365
+ | `aria-label` | `string` | — | Accessible label |
366
+ | `aria-labelledby` | `string` | — | Accessible label reference |
290
367
 
291
368
  **Events:** `input`, `change`, `focus`, `blur`
292
369
 
@@ -347,43 +424,57 @@ Child `<web-ui-option>` elements register via `option-register` / `option-unregi
347
424
 
348
425
  **CSS Custom Properties:**
349
426
 
350
- | Property | Default | Description |
351
- | ------------------------- | ------- | ------------------ |
352
- | `--wui-select-max-width` | `500px` | Dropdown max width |
353
- | `--wui-overlay-min-width` | `200px` | Dropdown min width |
427
+ | Property | Default | Description |
428
+ | ------------------------- | ---------------------------------- | ------------------------------- |
429
+ | `--wui-select-max-width` | `500px` | Dropdown max width |
430
+ | `--wui-select-max-height` | `200px` | Dropdown scroll area max height |
431
+ | `--wui-overlay-min-width` | `200px` | Dropdown min width |
432
+ | `--wui-select-bg-hover` | `--wui-color-surface-glass-hover` | Trigger hover background |
433
+ | `--wui-select-bg-active` | `--wui-color-surface-glass-active` | Trigger pressed/open background |
434
+
435
+ Portal panels mirror these variables from the host element when created; change the host before opening a portal panel for a new value to apply.
354
436
 
355
437
  #### `<web-ui-autocomplete>`
356
438
 
357
439
  Editable combobox with input filtering and single option selection.
358
440
 
359
- | Attribute | Type | Default | Description |
360
- | ------------------ | ---------------------------------- | ------------ | ------------------------------------------------------------------------- |
361
- | `value` | `string` | `''` | Current input text (form value) |
362
- | `selected-value` | `string` | `''` | Value of the option exactly matching the input label (derived, read-only) |
363
- | `placeholder` | `string` | `''` | Placeholder text |
364
- | `filter` | `'none' \| 'prefix' \| 'contains'` | `'contains'` | Candidate filtering mode (matched against option label) |
365
- | `name` | `string` | `''` | Form field name |
366
- | `disabled` | `boolean` | `false` | Disabled state |
367
- | `readonly` | `boolean` | `false` | Read-only state (no typing, no dropdown) |
368
- | `required` | `boolean` | `false` | Required validation |
369
- | `portal` | `boolean` | `false` | Render dropdown in theme overlay |
370
- | `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
371
- | `overlayContainer` | `HTMLElement \| () => HTMLElement` | — | Explicit portal container |
372
- | `aria-label` | `string` | — | Accessible name |
373
- | `aria-labelledby` | `string` | — | Accessible name references |
441
+ | Attribute | Type | Default | Description |
442
+ | -------------------- | ---------------------------------- | ------------ | ------------------------------------------------------------------------------------------------- |
443
+ | `value` | `string` | `''` | Current input text (form value) |
444
+ | `selected-value` | `string` | `''` | Value of the enabled option exactly matching the input label (derived, read-only) |
445
+ | `placeholder` | `string` | `''` | Placeholder text |
446
+ | `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring |
447
+ | `filter` | `'none' \| 'prefix' \| 'contains'` | `'contains'` | Candidate filtering mode (matched against option label) |
448
+ | `name` | `string` | `''` | Form field name |
449
+ | `disabled` | `boolean` | `false` | Disabled state |
450
+ | `readonly` | `boolean` | `false` | Read-only state (no typing, no dropdown) |
451
+ | `required` | `boolean` | `false` | Required validation |
452
+ | `allow-custom-value` | `boolean` | `false` | Allow Enter to submit a custom value that is not among candidates |
453
+ | `portal` | `boolean` | `false` | Render dropdown in theme overlay |
454
+ | `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
455
+ | `overlayContainer` | `HTMLElement \| () => HTMLElement` | — | Explicit portal container |
456
+ | `aria-label` | `string` | — | Accessible name |
457
+ | `aria-labelledby` | `string` | — | Accessible name references |
374
458
 
375
459
  **Events:** `input`, `change`, `focus`, `blur`, `open-change` (`CustomEvent<{ open: boolean }>`)
376
460
 
377
- **Slots:** `default` (project `<web-ui-option>` elements)
461
+ **Slots:** `default` (project `<web-ui-option>` elements), `empty` (replace the “no matches” state; falls back to “No matches”)
378
462
 
379
463
  Typing filters the option list by label (`contains` or `prefix`, or `none` to disable filtering). Selecting an option fills the input with its label and exposes the option's value via `selected-value`; `change` fires on selection commit. Supports ArrowDown/ArrowUp/Enter/Escape keyboard navigation.
380
464
 
465
+ When `allow-custom-value` is enabled, pressing Enter with no active option and no matching candidate commits the raw input as a custom value and closes the panel. `change` fires and `selected-value` remains empty. The component does not create an option automatically or trim the raw value. Text matching a disabled option cannot bypass the disabled state or derive as a selected option.
466
+
467
+ Use `<div slot="empty">…</div>` for static, non-interactive empty-state content. Portal rendering moves this node into the floating panel and restores it to the host after close.
468
+
381
469
  **CSS Custom Properties:**
382
470
 
383
- | Property | Default | Description |
384
- | ------------------------------ | ------- | ---------------------- |
385
- | `--wui-autocomplete-max-width` | `500px` | Dropdown max width |
386
- | `--wui-overlay-min-width` | `200px` | Dropdown minimum width |
471
+ | Property | Default | Description |
472
+ | ------------------------------- | ------- | ------------------------------- |
473
+ | `--wui-autocomplete-max-width` | `500px` | Dropdown max width |
474
+ | `--wui-autocomplete-max-height` | `200px` | Dropdown scroll area max height |
475
+ | `--wui-overlay-min-width` | `200px` | Dropdown minimum width |
476
+
477
+ Portal panels mirror these variables from the host element when created; change the host before opening a portal panel for a new value to apply.
387
478
 
388
479
  #### `<web-ui-slider>`
389
480
 
@@ -415,8 +506,8 @@ Supports ArrowLeft/Right/Up/Down, Home/End, PageUp/PageDown keyboard navigation.
415
506
  | `--wui-slider-vertical-height` | `200px` | Vertical slider height |
416
507
  | `--wui-slider-height` | `var(--wui-slider-track-size, 6px)` | Track thickness |
417
508
  | `--wui-slider-track-size` | `6px` | Track size |
418
- | `--wui-slider-thumb-width` | `24px` | Thumb short axis |
419
- | `--wui-slider-thumb-height` | `32px` | Thumb long axis |
509
+ | `--wui-slider-thumb-width` | `30px` | Thumb width |
510
+ | `--wui-slider-thumb-height` | `20px` | Thumb height |
420
511
  | `--wui-slider-marks-inset` | `0` | Marks inset from edges |
421
512
 
422
513
  #### `<web-ui-checkbox>`
@@ -538,7 +629,7 @@ Styled button with variants and loading state.
538
629
  | `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger' \| 'glass'` | `'glass'` | Button variant |
539
630
  | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Inner-button type; invalid values become `button` |
540
631
  | `disabled` | `boolean` | `false` | Disabled state |
541
- | `loading` | `boolean` | `false` | Loading spinner |
632
+ | `loading` | `boolean` | `false` | Loading spinner; with `icon`, the spinner replaces the icon content |
542
633
  | `full` | `boolean` | `false` | Full width |
543
634
  | `icon` | `boolean` | `false` | Icon-only mode |
544
635
  | `size` | `string` | `''` | Button height in px (icon mode: also sets min-width for square default) |
@@ -553,6 +644,9 @@ form-associated control when external form submission behavior is required.
553
644
 
554
645
  Disabled and loading states prevent `click` events.
555
646
 
647
+ When `icon` and `loading` are both set, the button renders only the spinner, and the default-slot icon is not
648
+ projected. The icon-mode geometry stays square by default, except for `full` or an explicit width.
649
+
556
650
  **CSS Custom Properties:**
557
651
 
558
652
  | Property | Default | Description |
@@ -585,31 +679,33 @@ Applies the direction to the grouped button layout without changing child button
585
679
 
586
680
  Modal dialog using native `<dialog>` with `showModal()`.
587
681
 
588
- | Attribute | Type | Default | Description |
589
- | ------------------- | --------- | ------- | ----------------------------------- |
590
- | `open` | `boolean` | `false` | Dialog visibility |
591
- | `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
592
- | `no-backdrop-close` | `boolean` | `false` | Do not close on backdrop click |
593
- | `no-escape-close` | `boolean` | `false` | Do not close when Escape is pressed |
682
+ | Attribute | Type | Default | Description |
683
+ | ------------------- | --------- | ------- | ------------------------------------------------------------------------------ |
684
+ | `open` | `boolean` | `false` | Dialog visibility |
685
+ | `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
686
+ | `no-backdrop-close` | `boolean` | `false` | Do not close on backdrop click |
687
+ | `no-escape-close` | `boolean` | `false` | Do not close when Escape is pressed |
688
+ | `controlled` | `boolean` | `false` | Escape and backdrop only request `open=false`; the consumer must update `open` |
594
689
 
595
- **Events:** `open-change` (`CustomEvent<{ open: boolean }>`)
690
+ **Events:** `open-change` (`CustomEvent<{ open: boolean }>`). With `controlled`, Escape and backdrop clicks only request `open=false` and the dialog stays open until the consumer writes `open=false`. Programmatic APIs (`showModal()`, `close()`, assigning `open`) stay direct and never emit. Native dialog closure (e.g. form `method="dialog"`) is restored to the controlled open state and re-emits the request.
596
691
 
597
692
  **Slots:** `body`, `title`, `default`, `footer`
598
693
 
599
694
  **Methods:** `showModal()`, `close()`
600
695
 
601
- Uses native `<dialog>` with `@cancel` prevention. Escape calls `close()` unless `no-escape-close` is present. Click on backdrop closes dialog unless `no-backdrop-close` is present.
696
+ Uses native `<dialog>` with `@cancel` prevention. Escape calls `close()` unless `no-escape-close` is present. Click on backdrop closes dialog unless `no-backdrop-close` is present. With `controlled`, both only emit the close request instead.
602
697
 
603
698
  **CSS Custom Properties:**
604
699
 
605
- | Property | Default | Description |
606
- | ------------------------- | --------------------------- | ------------------- |
607
- | `--wui-dialog-max-width` | `360px` | Dialog max width |
608
- | `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | Backdrop background |
700
+ | Property | Default | Description |
701
+ | ------------------------- | ---------------------------------- | -------------------------------------------------------------- |
702
+ | `--wui-dialog-max-width` | `360px` | Dialog max width |
703
+ | `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | Backdrop background |
704
+ | `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | Glass card background, falls back to `rgb(246 246 246 / 0.88)` |
609
705
 
610
706
  #### `<web-ui-drawer>`
611
707
 
612
- Side drawer using native `<dialog>` with closing animation.
708
+ Side drawer using native `<dialog>` with closing animation. In non-headless mode the drawer renders as a floating rounded card inset from all viewport edges (matching the layout sidebar's card language), so elastic drag distances read as margin changes rather than gaps.
613
709
 
614
710
  | Attribute | Type | Default | Description |
615
711
  | ------------------- | ---------------------------------------- | --------- | --------------------------------------------------------------------------------- |
@@ -619,11 +715,12 @@ Side drawer using native `<dialog>` with closing animation.
619
715
  | `closable` | `boolean` | `false` | Show close button |
620
716
  | `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
621
717
  | `no-backdrop-close` | `boolean` | `false` | Do not close on backdrop click |
622
- | `request-only` | `boolean` | `false` | User close actions only request `open=false`; the consumer must update `open` |
718
+ | `controlled` | `boolean` | `false` | User close actions only request `open=false`; the consumer must update `open` |
623
719
  | `headless` | `boolean` | `false` | Keep only overlay behavior and render the default slot without built-in drawer UI |
624
720
  | `dialog-label` | `string` | `''` | Accessible name for the internal native dialog; required in headless mode |
721
+ | `draggable` | `boolean` | `false` | Show a drag bar on the inner edge for drag-to-close gestures |
625
722
 
626
- **Events:** `open-change` (`CustomEvent<{ open: boolean }>`). With `request-only`, Escape, backdrop and the built-in close button only request `open=false`; the drawer remains open until the consumer writes `open=false`. If native dialog closure occurs while that request is rejected, the drawer restores its open top-layer state and emits the same request.
723
+ **Events:** `open-change` (`CustomEvent<{ open: boolean }>`). With `controlled`, Escape, backdrop, the built-in close button and drag-release only request `open=false`; the drawer remains open until the consumer writes `open=false`. Programmatic APIs (`show()`, `close()`, assigning `open`) stay direct and never emit. If native dialog closure occurs while a request is pending, the drawer restores its open top-layer state and emits the same request. With nested drawers, an inner `open-change` bubbles through the outer root (composed event); distinguish by `event.target`.
627
724
 
628
725
  **Slots:** `header`, `default`, `footer` — with `headless`, only the `default` slot is rendered.
629
726
 
@@ -633,14 +730,54 @@ Side drawer using native `<dialog>` with closing animation.
633
730
 
634
731
  Closing keeps the native dialog in the top layer until the `--wui-duration-drawer-exit` transition completes (240ms by default), then calls `dialog.close()`. Escape always follows this close path; `no-backdrop-close` controls backdrop clicks only.
635
732
 
733
+ **Drag to close:** With `draggable`, a gray capsule drag bar appears on the drawer's inner edge (left edge for `right`, right edge for `left`, bottom edge for `top`, top edge for `bottom`) while open. Dragging follows the pointer in real time (backdrop fades proportionally); releasing past ~1/3 of the drawer size or with a fast closing flick springs the drawer shut, otherwise it springs back open. The close direction is placement-aware. With `controlled`, release past the threshold only emits `open-change(false)`; the drawer holds at the closed position briefly (120ms write-back window) and springs back open if the consumer rejects or misses the write-back. Drag-to-open is not supported because the closed drawer renders nothing outside the native dialog. Under `prefers-reduced-motion`, release snaps instantly without spring animation.
734
+
636
735
  **CSS Custom Properties:**
637
736
 
638
- | Property | Default | Description |
639
- | ------------------------- | ---------------------------------- | -------------------------- |
640
- | `--wui-drawer-width` | `320px` | Drawer width |
641
- | `--wui-drawer-height` | `300px` | Drawer height (top/bottom) |
642
- | `--wui-drawer-bg` | `var(--wui-color-surface-overlay)` | Drawer body background |
643
- | `--wui-drawer-overlay-bg` | `rgb(0 0 0 / 0.12)` | Backdrop background |
737
+ | Property | Default | Description |
738
+ | ------------------------- | ---------------------------------- | ---------------------------------------------------------------------------- |
739
+ | `--wui-drawer-width` | `320px` | Drawer width |
740
+ | `--wui-drawer-height` | `300px` | Drawer height (top/bottom) |
741
+ | `--wui-drawer-bg` | `var(--wui-color-surface-overlay)` | Drawer body background |
742
+ | `--wui-drawer-radius` | `28px` | Floating card corner radius (non-headless) |
743
+ | `--wui-drawer-inset` | `8px` | Floating card viewport inset (non-headless); `0` gives edge-to-edge geometry |
744
+ | `--wui-drawer-overlay-bg` | `rgb(0 0 0 / 0.12)` | Backdrop background |
745
+
746
+ ---
747
+
748
+ ### In-flow Disclosure
749
+
750
+ #### `<web-ui-collapse>`
751
+
752
+ In-flow expand/collapse container with animated height (or width) transition. Single element with two slots; no portal, no scroll lock, no focus management.
753
+
754
+ | Attribute | Type | Default | Description |
755
+ | -------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
756
+ | `open` | `boolean` | `false` | Expanded state; self-managed on interaction, `open-change` emits only for user-originated toggles |
757
+ | `disabled` | `boolean` | `false` | Ignores trigger clicks and sets `aria-disabled` on the trigger element; expanded content is kept |
758
+ | `horizontal` | `boolean` | `false` | Animate width instead of height |
759
+ | `keep-mounted` | `boolean` | `false` | Closed state keeps the content in the 0fr track with `inert` (scroll position and layout measurable) instead of `hidden` |
760
+
761
+ **Events:** `open-change` (`CustomEvent<{ open: boolean }>`). Emitted only for user-originated toggles (trigger click). Programmatic writes (`open`, `show()`, `close()`, `toggle()`) never emit. Nested collapses: an inner `open-change` bubbles through the outer root (composed event); distinguish by `event.target`.
762
+
763
+ **Slots:** `default` (trigger) + `content` (collapsible content)
764
+
765
+ **Methods:** `show()`, `close()`, `toggle()`
766
+
767
+ ```html
768
+ <web-ui-collapse>
769
+ <button type="button">Toggle me</button>
770
+ <div slot="content">Collapsible content</div>
771
+ </web-ui-collapse>
772
+ ```
773
+
774
+ The initial `open` attribute settles instantly without playing the animation. Nested collapses are supported.
775
+
776
+ **Trigger semantics:** interaction comes from the element you put in the default slot — a native `<button>`, `<web-ui-button>`, or any other interactive element supplies Enter/Space activation and focus natively. The collapse writes `aria-expanded` / `aria-controls` (pointing at its content track) and `aria-disabled` onto the first assigned element in the trigger slot. Use an interactive element as the trigger: a plain `<span>` is clickable but has no keyboard/focus semantics.
777
+
778
+ **Closed-state semantics:** the consumer's light DOM is never moved or unmounted. Default closed state applies `hidden` to the internal content container; with `keep-mounted` the inner container is marked `inert` while staying measurable inside the collapsed track.
779
+
780
+ **Known limitations:** the `horizontal` animation reflows content while width changes.
644
781
 
645
782
  ---
646
783
 
@@ -668,6 +805,8 @@ Popover overlay anchored to trigger element.
668
805
 
669
806
  Hover mode uses `pointerenter`/`pointerleave` with delay. Click mode toggles on trigger click. Manual mode only responds to imperative `show()`/`close()`.
670
807
 
808
+ **Trigger ARIA:** open/close state is written back to the trigger element: `aria-expanded` and `aria-controls` (pointing at the panel) are set on the first assigned element in the `trigger` slot (the wrapper retains them for backward compatibility). Put an interactive element (native button, `web-ui-button`) in the trigger slot.
809
+
671
810
  #### `<web-ui-tooltip>`
672
811
 
673
812
  Tooltip overlay using pointer/focus triggers.
@@ -688,7 +827,7 @@ Tooltip overlay using pointer/focus triggers.
688
827
 
689
828
  **Slots:** `default` (trigger), `content` (tooltip panel)
690
829
 
691
- `open` is a controlled visibility property. Pointer/focus triggers update it, and direct updates synchronize the local or portal panel. Adjacent tooltips open immediately after the first tooltip is visible; pointer/focus triggers otherwise use delay timers.
830
+ `open` is a self-managed visibility property. Pointer/focus triggers update it, and direct property writes synchronize the local or portal panel; `open-change` emits only for user-originated open/close. Adjacent tooltips open immediately after the first tooltip is visible; pointer/focus triggers otherwise use delay timers.
692
831
 
693
832
  **CSS Custom Properties:**
694
833
 
@@ -714,6 +853,8 @@ Right-click context menu.
714
853
 
715
854
  Opens on `contextmenu` event. Menu items: `<web-ui-dropdown-item>`, `<web-ui-dropdown-divider>`, `<web-ui-dropdown-header>`. Supports keyboard navigation (Arrow keys, Enter, Escape) and submenu hover with `pointerenter`.
716
855
 
856
+ While the menu is open, consumer code may conditionally render, move, or remove menu items (e.g. Vue `v-if`) without re-inserting them at the host element; changes inside the portal are reconciled automatically, and framework anchors are returned to the host on close so subsequent framework patches keep working.
857
+
717
858
  ---
718
859
 
719
860
  ### Menu
@@ -739,6 +880,8 @@ Dropdown menu with multi-level submenu support.
739
880
 
740
881
  Items: `<web-ui-dropdown-item>`, `<web-ui-dropdown-divider>`, `<web-ui-dropdown-header>`. Submenus via `submenu` attribute on `<web-ui-dropdown-item>`. Full keyboard navigation (Arrow keys, Home, End, Enter, Space, Escape).
741
882
 
883
+ **Trigger ARIA:** `aria-haspopup="menu"`, `aria-expanded` and `aria-controls` (pointing at the root menu panel) are written back to the first assigned element in the `trigger` slot. Put an interactive element in the trigger slot.
884
+
742
885
  #### `<web-ui-dropdown-item>`
743
886
 
744
887
  Menu item for dropdown or context-menu.
@@ -878,15 +1021,20 @@ WebUiSpinner.hide() // hide
878
1021
 
879
1022
  Responsive page layout with an optional full-width banner, a collapsible desktop sidebar, and a headless-drawer mobile sidebar. The page itself scrolls; the desktop sidebar and header stick to the viewport after the banner scrolls away.
880
1023
 
881
- | Attribute | Type | Default | Description |
882
- | ------------------- | --------- | --------- | ----------------------------------------------------- |
883
- | `sidebar-collapsed` | `boolean` | `false` | Controlled desktop sidebar collapsed state |
884
- | `sidebar-open` | `boolean` | `false` | Controlled mobile sidebar drawer open state |
885
- | `header-glow` | `boolean` | `false` | Decorative Header background glow behind slot content |
886
- | `sidebar-width` | `string` | `'240px'` | Expanded desktop and mobile sidebar width |
887
- | `collapsed-width` | `string` | `'72px'` | Collapsed desktop sidebar width |
1024
+ | Attribute | Type | Default | Description |
1025
+ | ------------------- | --------- | --------- | -------------------------------------------------------------------------------------------------- |
1026
+ | `sidebar-collapsed` | `boolean` | `false` | Controlled desktop sidebar collapsed state |
1027
+ | `sidebar-open` | `boolean` | `false` | Controlled mobile sidebar drawer open state |
1028
+ | `header-glow` | `boolean` | `false` | Decorative Header background glow behind slot content |
1029
+ | `sidebar-width` | `string` | `'240px'` | Expanded desktop and mobile sidebar width |
1030
+ | `collapsed-width` | `string` | `'72px'` | Collapsed desktop sidebar width |
1031
+ | `sidebar-resizable` | `boolean` | `false` | Enable drag-to-resize on the desktop sidebar edge |
1032
+ | `sidebar-min-width` | `string` | — | Resize lower bound (px); falls back to `collapsed-width` |
1033
+ | `sidebar-max-width` | `string` | — | Resize upper bound (px); clamped to half the viewport, which always wins over the configured value |
888
1034
 
889
- **Events:** `sidebar-collapsed-change` (`CustomEvent<{ collapsed: boolean }>`) requests a desktop collapse-state update. `sidebar-open-change` (`CustomEvent<{ open: boolean }>`) requests a mobile drawer open-state update. Consumers must write the requested value back to the corresponding controlled property.
1035
+ **Events:** `sidebar-collapsed-change` (`CustomEvent<{ collapsed: boolean }>`) requests a desktop collapse-state update. `sidebar-open-change` (`CustomEvent<{ open: boolean }>`) requests a mobile drawer open-state update. `sidebar-width-change` (`CustomEvent<{ width: string }>`) requests a sidebar width update after a resize drag ends. Consumers must write the requested value back to the corresponding controlled property.
1036
+
1037
+ **Sidebar resize:** With `sidebar-resizable`, a resize handle appears on the desktop sidebar's right edge (hidden while collapsed). Hovering or dragging shows a 3px accent vertical line with a `col-resize` cursor. Dragging updates the width in real time (transition suppressed, clamped to `[min, max]` and the viewport); releasing emits `sidebar-width-change` with the final pixel width and returns control to the `sidebar-width` property once the consumer writes it back. `pointercancel` restores the property-controlled width without emitting. The handle is keyboard-operable (WAI-ARIA splitter pattern): focus it and use ←/→ to step by 16px (Shift for 64px), Home/End to jump to min/max, Enter to commit via the same `sidebar-width-change` request, and Escape to revert an uncommitted adjustment. The mobile drawer always supports drag-to-close via its built-in `draggable` drawer.
890
1038
 
891
1039
  | Slot | Description |
892
1040
  | --------- | ----------------------------------------------------------------------------------------- |
@@ -898,15 +1046,16 @@ Responsive page layout with an optional full-width banner, a collapsible desktop
898
1046
 
899
1047
  `web-ui-layout` constrains the sidebar card and owns the desktop toggle area, but does not create a sidebar scrollport. To make only part of the sidebar scroll, make the `sidebar` slot root a `height: 100%; min-height: 0` flex column and apply `overflow-y: auto` to the intended child. This keeps consumer-defined headers and footers fixed without adding extra public slots.
900
1048
 
901
- At `640px` and below, the sidebar becomes a headless `web-ui-drawer`. The consumer content is rendered in the same rounded sidebar card; the mobile toggle appears in the header row.
1049
+ At `640px` and below, the sidebar becomes a headless `web-ui-drawer`. The consumer content is rendered in the same rounded sidebar card; the mobile toggle appears in the header row as a glass button. Its left inset defaults to `8px`; align it with your own header padding via `--wui-layout-mobile-toggle-inset`.
902
1050
 
903
1051
  `header-glow` adds a pointer-transparent decorative glow behind header-slot content and the mobile toggle. It is a Header background rather than a foreground layer, so slotted content remains above it. Override its color with `--wui-layout-header-glow-color` (default: `--wui-color-page`). The glow concentration and spread are controlled by the internal variable `--wui-layout-header-glow-height` (default: `150%`); increase for stronger coverage, decrease for a subtler effect. Layout layers are ordered as Header (`10`) < Auxiliary (`20`) < Banner (`30`) < Tabbar (`40`) < Sidebar (`50`).
904
1052
 
905
1053
  **CSS Custom Properties:**
906
1054
 
907
- | Property | Default | Description |
908
- | ----------------------------- | ------- | ------------------------------------------------ |
909
- | `--wui-layout-sidebar-radius` | `28px` | Border radius of sidebar card (desktop & mobile) |
1055
+ | Property | Default | Description |
1056
+ | ---------------------------------- | ------- | ------------------------------------------------ |
1057
+ | `--wui-layout-sidebar-radius` | `24px` | Border radius of sidebar card (desktop & mobile) |
1058
+ | `--wui-layout-mobile-toggle-inset` | `8px` | Left inset of the mobile header toggle |
910
1059
 
911
1060
  #### `<web-ui-back-top>`
912
1061
 
@@ -974,7 +1123,7 @@ Defines foundation, color, layer, shadow, and motion tokens for its subtree. `mo
974
1123
  | ------------------------- | ------- | ------------------------------------------------ |
975
1124
  | `--wui-font-size` | `14px` | Base font size for controls |
976
1125
  | `--wui-input-width` | `200px` | Default width for compact form controls |
977
- | `--wui-control-size` | `40px` | Default height and square min-width for controls |
1126
+ | `--wui-control-size` | `36px` | Default height and square min-width for controls |
978
1127
  | `--wui-overlay-min-width` | `200px` | Minimum anchored overlay width |
979
1128
  | `--wui-focus-ring-width` | `3px` | Focus indicator width |
980
1129
 
@@ -994,32 +1143,38 @@ Defines foundation, color, layer, shadow, and motion tokens for its subtree. `mo
994
1143
  | `--wui-layer-toast` | `200` | Toasts |
995
1144
  | `--wui-layer-loading` | `300` | Blocking loading surfaces |
996
1145
 
997
- **Motion tokens:** duration defaults are `--wui-duration-press: 80ms`, `--wui-duration-feedback: 100ms`, `--wui-duration-trigger: 160ms`, `--wui-duration-focus: 200ms`, `--wui-duration-menu-enter: 140ms`, `--wui-duration-menu-exit: 100ms`, `--wui-duration-overlay-enter: 180ms`, `--wui-duration-overlay-exit: 140ms`, `--wui-duration-drawer-enter: 280ms`, `--wui-duration-drawer-exit: 240ms`, `--wui-duration-layout: 200ms`. Easing tokens are `--wui-ease-enter` and `--wui-ease-slide`; enter scale is `--wui-scale-enter: 0.97`.
1146
+ **Motion tokens:** duration defaults are `--wui-duration-press: 80ms`, `--wui-duration-feedback: 100ms`, `--wui-duration-trigger: 160ms`, `--wui-duration-focus: 200ms`, `--wui-duration-menu-enter: 140ms`, `--wui-duration-menu-exit: 100ms`, `--wui-duration-overlay-enter: 180ms`, `--wui-duration-overlay-exit: 140ms`, `--wui-duration-drawer-enter: 280ms`, `--wui-duration-drawer-exit: 240ms`, `--wui-duration-drawer-nested: 450ms`, `--wui-duration-collapse-enter: 200ms`, `--wui-duration-collapse-exit: 160ms`, `--wui-duration-layout: 200ms`. Easing tokens are `--wui-ease-enter` and `--wui-ease-slide`; enter scale is `--wui-scale-enter: 0.97`. Hover/active background feedback switches instantly with no transition; checked/pressed/focus and overlay enter/exit transitions are unaffected.
998
1147
 
999
1148
  **Color tokens:**
1000
1149
 
1001
1150
  | Property | Light default | Dark default | Description |
1002
1151
  | ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------- |
1003
- | `--wui-color-page` | `#fff` | `#18181a` | Page background |
1152
+ | `--wui-color-page` | `#fff` | `#242628` | Page background |
1004
1153
  | `--wui-color-surface` | `#fff` | `#2c2c2e` | Plain surface |
1005
1154
  | `--wui-color-surface-raised` | `#f2f2f7` | `#2c2c2e` | Raised surface |
1006
1155
  | `--wui-color-surface-control` | `#dfdfdf` | `#3a3a3c` | Neutral control surface |
1007
1156
  | `--wui-color-surface-track` | `#e5e5ea` | `#444446` | Slider and switch track surface |
1157
+ | `--wui-color-surface-menu` | `rgb(246 246 246 / 0.82)` | `rgb(30 30 32 / 0.92)` | Menu and floating panel surface |
1008
1158
  | `--wui-color-surface-glass` | `rgb(250 250 250 / 0.34)` | `rgb(44 44 46 / 0.42)` | Liquid glass surface |
1009
1159
  | `--wui-color-surface-glass-hover` | `color-mix(... text 6%, surface-glass)` | `color-mix(... text 6%, surface-glass)` | Full glass hover background |
1010
1160
  | `--wui-color-surface-glass-active` | `color-mix(... text 15%, surface-glass)` | `color-mix(... text 15%, surface-glass)` | Full glass pressed background |
1011
- | `--wui-color-surface-overlay` | `rgb(246 246 246 / 0.82)` | `rgb(44 44 46 / 0.82)` | Translucent overlay surface |
1012
- | `--wui-color-text` | `#1b1b1b` | `#f5f5f7` | Primary text |
1161
+ | `--wui-color-surface-overlay` | `rgb(246 246 246 / 0.82)` | `rgb(32 34 34 / 0.9)` | Translucent overlay surface |
1162
+ | `--wui-color-surface-segmented` | `#e5e5ea` | `#3a3a3c` | Segmented track surface |
1163
+ | `--wui-color-surface-selected` | `#fff` | `#5c5c5e` | Segmented selected indicator |
1164
+ | `--wui-color-text` | `#1b1b1b` | `#e9eaea` | Primary text |
1013
1165
  | `--wui-color-text-secondary` | `#6a6a6a` | `#a1a1a6` | Secondary text |
1014
1166
  | `--wui-color-text-tertiary` | `color-mix(in srgb, var(--wui-color-text) 35%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 42%, transparent)` | Tertiary text and quiet icons |
1015
1167
  | `--wui-color-text-disabled` | `color-mix(in srgb, var(--wui-color-text) 32%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 38%, transparent)` | Disabled foreground text |
1016
1168
  | `--wui-color-state-layer-hover` | `color-mix(in srgb, var(--wui-color-text) 6%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 6%, transparent)` | Transparent hover overlay |
1017
1169
  | `--wui-color-state-layer-active` | `color-mix(in srgb, var(--wui-color-text) 15%, transparent)` | `color-mix(in srgb, var(--wui-color-text) 15%, transparent)` | Transparent pressed overlay |
1018
1170
  | `--wui-color-border` | `rgb(0 0 0 / 0.1)` | `rgb(255 255 255 / 0.14)` | Normal border and divider |
1019
- | `--wui-color-glass-border` | `rgb(51 51 51 / 0.12)` | `rgb(255 255 255 / 0.16)` | Glass border tint |
1020
- | `--wui-color-glass-highlight` | `rgb(255 255 255 / 0.9)` | `rgb(255 255 255 / 0.22)` | Glass edge highlight |
1171
+ | `--wui-color-glass-border` | `transparent` | `rgb(255 255 255 / 0.05)` | Glass border tint |
1172
+ | `--wui-color-glass-highlight` | `rgb(255 255 255 / 0.9)` | `rgb(255 255 255 / 0.1)` | Glass edge highlight |
1173
+ | `--wui-color-glass-corner` | `rgb(255 255 255 / 0.08)` | `rgb(255 255 255 / 0.2)` | Glass corner sheen on border |
1174
+ | `--wui-color-glass-shade` | `rgb(0 0 0 / 0.06)` | `rgb(0 0 0 / 0.5)` | Glass corner shade on border |
1021
1175
  | `--wui-color-accent` | `#08f` | `#0a84ff` | Accent and focused input border |
1022
1176
  | `--wui-color-on-accent` | `#fff` | `#fff` | Foreground on accent |
1177
+ | `--wui-color-on-control` | `#fff` | `#f2f2f7` | Foreground on control inner |
1023
1178
  | `--wui-color-success` | `#16a34a` | `#30d158` | Success |
1024
1179
  | `--wui-color-warning` | `#d97706` | `#ff9f0a` | Warning |
1025
1180
  | `--wui-color-danger` | `#dc2626` | `#ff453a` | Danger |
@@ -1031,9 +1186,11 @@ Defines foundation, color, layer, shadow, and motion tokens for its subtree. `mo
1031
1186
 
1032
1187
  | Property | Light default | Dark default | Description |
1033
1188
  | ---------------------- | -------------------------------- | ------------------------------- | --------------------------- |
1034
- | `--wui-shadow-overlay` | `2px 16px 40px rgb(0 0 0 / 0.4)` | `0 18px 48px rgb(0 0 0 / 0.54)` | Modal and drawer shadow |
1035
- | `--wui-shadow-panel` | `0 3px 9px rgb(0 0 0 / 0.27)` | `0 4px 16px rgb(0 0 0 / 0.35)` | Small floating panel shadow |
1036
- | `--wui-shadow-glass` | four-layer diffuse shadow | `0 12px 32px rgb(0 0 0 / 0.38)` | Base liquid glass shadow |
1189
+ | `--wui-shadow-overlay` | `2px 16px 40px rgb(0 0 0 / 0.4)` | `0 18px 48px rgb(0 0 0 / 0.48)` | Modal and drawer shadow |
1190
+ | `--wui-shadow-panel` | `0 3px 9px rgb(0 0 0 / 0.27)` | `0 4px 16px rgb(0 0 0 / 0.32)` | Small floating panel shadow |
1191
+ | `--wui-shadow-glass` | four-layer diffuse shadow | `0 8px 24px rgb(0 0 0 / 0.08)` | Base liquid glass shadow |
1192
+
1193
+ **Glass effect tokens:** `--wui-glass-brightness` is `1.06` in light mode and `1.02` in dark mode. `--wui-glass-corner-radius` defaults to `32px`; glass components set it from their own corner radius so the diagonal border lighting aligns with their shape.
1037
1194
 
1038
1195
  **Internal tokens:** variables prefixed `--wui-internal-*` are private wiring between shadow DOM parts; they are not part of the public token API and must not be overridden by consumers.
1039
1196