@greypan/web-ui 4.0.0 → 5.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 (158) hide show
  1. package/README.CN.md +248 -139
  2. package/README.md +255 -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 +29 -0
  34. package/dist/components/context-menu/index.js +167 -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 +2 -0
  47. package/dist/components/dropdown/index.js +23 -13
  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 +2 -1
  70. package/dist/components/popover/index.js +8 -4
  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 +32 -28
  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.js +2 -2
  112. package/dist/components/tooltip/index.js.map +1 -1
  113. package/dist/components/tooltip/style.js +1 -1
  114. package/dist/components/tooltip/style.js.map +1 -1
  115. package/dist/icons/generated/heroicons-arrow-down-16-solid.d.ts +6 -0
  116. package/dist/icons/generated/heroicons-arrow-up-16-solid.d.ts +6 -0
  117. package/dist/icons/generated/heroicons-bars-arrow-down-16-solid.d.ts +6 -0
  118. package/dist/icons/generated/heroicons-bars-arrow-down-16-solid.js +10 -0
  119. package/dist/icons/generated/heroicons-bars-arrow-down-16-solid.js.map +1 -0
  120. package/dist/icons/generated/heroicons-bars-arrow-up-16-solid.d.ts +6 -0
  121. package/dist/icons/generated/heroicons-bars-arrow-up-16-solid.js +10 -0
  122. package/dist/icons/generated/heroicons-bars-arrow-up-16-solid.js.map +1 -0
  123. package/dist/icons/generated/lucide-code.d.ts +6 -0
  124. package/dist/icons/generated/lucide-code.js +10 -0
  125. package/dist/icons/generated/lucide-code.js.map +1 -0
  126. package/dist/icons/generated/lucide-ellipsis-vertical.d.ts +6 -0
  127. package/dist/icons/generated/lucide-ellipsis-vertical.js +10 -0
  128. package/dist/icons/generated/lucide-ellipsis-vertical.js.map +1 -0
  129. package/dist/icons/generated/lucide-list-filter.d.ts +6 -0
  130. package/dist/icons/generated/lucide-list-filter.js +10 -0
  131. package/dist/icons/generated/lucide-list-filter.js.map +1 -0
  132. package/dist/icons/generated/lucide-list-restart.d.ts +6 -0
  133. package/dist/icons/generated/lucide-list-restart.js +10 -0
  134. package/dist/icons/generated/lucide-list-restart.js.map +1 -0
  135. package/dist/icons/generated/lucide-tag.d.ts +6 -0
  136. package/dist/icons/generated/lucide-tag.js +10 -0
  137. package/dist/icons/generated/lucide-tag.js.map +1 -0
  138. package/dist/icons/generated/tabler-sort-ascending-letters.d.ts +6 -0
  139. package/dist/icons/generated/tabler-sort-ascending-letters.js +10 -0
  140. package/dist/icons/generated/tabler-sort-ascending-letters.js.map +1 -0
  141. package/dist/icons/index.d.ts +8 -0
  142. package/dist/icons/index.js +18 -10
  143. package/dist/shared/gesture/drag-gesture.js +133 -0
  144. package/dist/shared/gesture/drag-gesture.js.map +1 -0
  145. package/dist/shared/gesture/physics.js +45 -0
  146. package/dist/shared/gesture/physics.js.map +1 -0
  147. package/dist/shared/group-management/index.js +49 -47
  148. package/dist/shared/group-management/index.js.map +1 -1
  149. package/dist/shared/menu-portal/menu-portal.js +22 -10
  150. package/dist/shared/menu-portal/menu-portal.js.map +1 -1
  151. package/dist/shared/menu-portal/menu-tree.js +4 -2
  152. package/dist/shared/menu-portal/menu-tree.js.map +1 -1
  153. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  154. package/dist/shared/overlay/nested-drawer-layers.js +55 -0
  155. package/dist/shared/overlay/nested-drawer-layers.js.map +1 -0
  156. package/dist/shared/overlay/portal.js +44 -13
  157. package/dist/shared/overlay/portal.js.map +1 -1
  158. package/package.json +5 -3
package/README.md CHANGED
@@ -187,49 +187,85 @@ Components own their role and interaction state. Browser-native composed events
187
187
  remain the primary interaction API. Kebab-case custom events such as `open-change` describe user-originated component
188
188
  state changes; assigning a property programmatically does not emit them.
189
189
 
190
+ ### Cursor behavior
191
+
192
+ Interactive controls use the native arrow cursor instead of the hand cursor. Disabled controls use `not-allowed`.
193
+ Slider, switch, and segmented controls keep the arrow cursor during hover and press feedback, then switch to
194
+ `grabbing` only while a drag is active. Dedicated drag handles and resize controls keep their specialized cursors.
195
+ Cursor styles live inside Shadow DOM, so a plain document-level selector cannot override them.
196
+
197
+ ### Controlled state semantics
198
+
199
+ `web-ui-*` elements do not use a single "controlled" contract; three distinct state models exist, chosen per component
200
+ family by the nature of the interaction. Do not assume `controlled` means the same thing across components.
201
+
202
+ **User-originated events only (all `open`-style components: collapse, popover, dropdown, tooltip, dialog, drawer, back-top).**
203
+ `*-change` events (`open-change`, etc.) fire only for user-originated toggles; assigning the property programmatically
204
+ (`el.open = true`) or calling an imperative method (`show()`, `close()`, `toggle()`) never emits. Components self-manage
205
+ their `open` value unless you coordinate externally — you may observe the event and write it back for your own state, but
206
+ the component does not require or wait for that write-back.
207
+
208
+ **Request-then-write-back (`controlled` on dialog and drawer only).**
209
+ With `controlled`, user close actions (Escape, backdrop, built-in close, drag-release) only _request_ `open=false`; the
210
+ component does not change its own state, and it stays open until the consumer writes `open=false`. This is the modal
211
+ confirmation semantics — closing a dialog or drawer is typically an operation that needs consumer approval (unsaved form,
212
+ confirmation), and unlike instantaneous open/close, it is not safe for the component to self-commit. Only these two modal
213
+ components use this mode; the instant open/close family (collapse, popover, dropdown, tooltip) does not need it.
214
+
215
+ **Native form-model (input, textarea, input-number, select, checkbox, radio, switch, autocomplete).**
216
+ The control manages its own value/checked state internally; user interaction flips it immediately and then dispatches
217
+ native `input`/`change` events. Assigning the property programmatically (`el.checked = true`) overrides the internal
218
+ state. This mirrors native `<input type="checkbox">` — there is no `controlled` prop because the value is always readable
219
+ and writable, and React/Vue layers provide their own controlled wrappers on top. For interactions that need an external
220
+ round-trip before the flip is durable (e.g. a switch that should only stay on after an API call succeeds), use the
221
+ optimistic-update path: flip immediately, observe `change`, set the control's `loading` during the request (this blocks
222
+ further interaction and shows a spinner), and on failure write the property back (`el.checked = false`) plus surface a
223
+ toast. The flip-before-approval window is accepted; there is no `controlled` prop for a reject-before-flip contract.
224
+
190
225
  ### Form-associated controls
191
226
 
192
227
  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
228
 
194
229
  ## All Components
195
230
 
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) |
231
+ | Category | Component |
232
+ | ---------------------- | --------------------------------------------------------- |
233
+ | **Form Controls** | [`<web-ui-input>`](#web-ui-input) |
234
+ | | [`<web-ui-textarea>`](#web-ui-textarea) |
235
+ | | [`<web-ui-input-number>`](#web-ui-input-number) |
236
+ | | [`<web-ui-select>`](#web-ui-select) |
237
+ | | [`<web-ui-autocomplete>`](#web-ui-autocomplete) |
238
+ | | [`<web-ui-slider>`](#web-ui-slider) |
239
+ | | [`<web-ui-checkbox>`](#web-ui-checkbox) |
240
+ | | [`<web-ui-radio>`](#web-ui-radio) |
241
+ | | [`<web-ui-switch>`](#web-ui-switch) |
242
+ | | [`<web-ui-segmented>`](#web-ui-segmented) |
243
+ | | [`<web-ui-checkbox-group>`](#web-ui-checkbox-group) |
244
+ | | [`<web-ui-radio-group>`](#web-ui-radio-group) |
245
+ | **Buttons** | [`<web-ui-button>`](#web-ui-button) |
246
+ | | [`<web-ui-button-group>`](#web-ui-button-group) |
247
+ | **Overlay / Modal** | [`<web-ui-dialog>`](#web-ui-dialog) |
248
+ | | [`<web-ui-drawer>`](#web-ui-drawer) |
249
+ | **In-flow Disclosure** | [`<web-ui-collapse>`](#web-ui-collapse) |
250
+ | **Floating** | [`<web-ui-popover>`](#web-ui-popover) |
251
+ | | [`<web-ui-tooltip>`](#web-ui-tooltip) |
252
+ | | [`<web-ui-context-menu>`](#web-ui-context-menu) |
253
+ | **Menu** | [`<web-ui-dropdown>`](#web-ui-dropdown) |
254
+ | | [`<web-ui-dropdown-item>`](#web-ui-dropdown-item) |
255
+ | | [`<web-ui-dropdown-divider>`](#web-ui-dropdown-divider) |
256
+ | | [`<web-ui-dropdown-header>`](#web-ui-dropdown-header) |
257
+ | **Data Display** | [`<web-ui-avatar>`](#web-ui-avatar) |
258
+ | | [`<web-ui-badge>`](#web-ui-badge) |
259
+ | | [`<web-ui-empty>`](#web-ui-empty) |
260
+ | | [`<web-ui-icon>`](#web-ui-icon) |
261
+ | | [`<web-ui-spinner>`](#web-ui-spinner) |
262
+ | **Layout & Utility** | [`<web-ui-layout>`](#web-ui-layout) |
263
+ | | [`<web-ui-back-top>`](#web-ui-back-top) |
264
+ | | [`<web-ui-svg-draw-lines>`](#web-ui-svg-draw-lines) |
265
+ | | [`<web-ui-theme>`](#web-ui-theme) |
266
+ | **Notification** | [`<web-ui-toast>`](#web-ui-toast) |
267
+ | **Sub-items** | [`<web-ui-option>`](#web-ui-option) |
268
+ | | [`<web-ui-segmented-trigger>`](#web-ui-segmented-trigger) |
233
269
 
234
270
  ## API Reference
235
271
 
@@ -241,19 +277,19 @@ Form controls implement `static formAssociated = true` and integrate with native
241
277
 
242
278
  Text input with clearable, prefix/suffix slots.
243
279
 
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 |
280
+ | Attribute | Type | Default | Description |
281
+ | ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------- |
282
+ | `value` | `string` | `''` | Input value |
283
+ | `type` | `string` | `'text'` | HTML input type |
284
+ | `placeholder` | `string` | `''` | Placeholder text |
285
+ | `name` | `string` | `''` | Form field name |
286
+ | `disabled` | `boolean` | `false` | Disabled state |
287
+ | `readonly` | `boolean` | `false` | Read-only state |
288
+ | `required` | `boolean` | `false` | Required validation |
289
+ | `clearable` | `boolean` | `false` | Show clear button |
290
+ | `full` | `boolean` | `false` | Full width |
291
+ | `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring |
292
+ | `aria-label` | `string` | — | Accessible label |
257
293
 
258
294
  **Events:** `input`, `change`, `focus`, `blur`
259
295
 
@@ -269,24 +305,24 @@ Text input with clearable, prefix/suffix slots.
269
305
 
270
306
  Multi-line text input with auto-resize.
271
307
 
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 |
308
+ | Attribute | Type | Default | Description |
309
+ | ----------------- | --------- | ------- | ------------------------------------------------------------------------------------------------- |
310
+ | `value` | `string` | `''` | Textarea value |
311
+ | `placeholder` | `string` | `''` | Placeholder text |
312
+ | `rows` | `number` | `3` | Visible rows |
313
+ | `name` | `string` | `''` | Form field name |
314
+ | `disabled` | `boolean` | `false` | Disabled state |
315
+ | `readonly` | `boolean` | `false` | Read-only state |
316
+ | `required` | `boolean` | `false` | Required validation |
317
+ | `clearable` | `boolean` | `false` | Show clear button |
318
+ | `full` | `boolean` | `false` | Full width |
319
+ | `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring |
320
+ | `autosize` | `boolean` | `false` | Auto-resize height |
321
+ | `max-height` | `number` | `0` | Autosize max height (px); `0` = unlimited |
322
+ | `minlength` | `number` | — | Minimum length validation |
323
+ | `maxlength` | `number` | — | Maximum length validation |
324
+ | `aria-label` | `string` | — | Accessible label |
325
+ | `aria-labelledby` | `string` | — | Accessible label reference |
290
326
 
291
327
  **Events:** `input`, `change`, `focus`, `blur`
292
328
 
@@ -347,43 +383,57 @@ Child `<web-ui-option>` elements register via `option-register` / `option-unregi
347
383
 
348
384
  **CSS Custom Properties:**
349
385
 
350
- | Property | Default | Description |
351
- | ------------------------- | ------- | ------------------ |
352
- | `--wui-select-max-width` | `500px` | Dropdown max width |
353
- | `--wui-overlay-min-width` | `200px` | Dropdown min width |
386
+ | Property | Default | Description |
387
+ | ------------------------- | ---------------------------------- | ------------------------------- |
388
+ | `--wui-select-max-width` | `500px` | Dropdown max width |
389
+ | `--wui-select-max-height` | `200px` | Dropdown scroll area max height |
390
+ | `--wui-overlay-min-width` | `200px` | Dropdown min width |
391
+ | `--wui-select-bg-hover` | `--wui-color-surface-glass-hover` | Trigger hover background |
392
+ | `--wui-select-bg-active` | `--wui-color-surface-glass-active` | Trigger pressed/open background |
393
+
394
+ 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
395
 
355
396
  #### `<web-ui-autocomplete>`
356
397
 
357
398
  Editable combobox with input filtering and single option selection.
358
399
 
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 |
400
+ | Attribute | Type | Default | Description |
401
+ | -------------------- | ---------------------------------- | ------------ | ------------------------------------------------------------------------------------------------- |
402
+ | `value` | `string` | `''` | Current input text (form value) |
403
+ | `selected-value` | `string` | `''` | Value of the enabled option exactly matching the input label (derived, read-only) |
404
+ | `placeholder` | `string` | `''` | Placeholder text |
405
+ | `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring |
406
+ | `filter` | `'none' \| 'prefix' \| 'contains'` | `'contains'` | Candidate filtering mode (matched against option label) |
407
+ | `name` | `string` | `''` | Form field name |
408
+ | `disabled` | `boolean` | `false` | Disabled state |
409
+ | `readonly` | `boolean` | `false` | Read-only state (no typing, no dropdown) |
410
+ | `required` | `boolean` | `false` | Required validation |
411
+ | `allow-custom-value` | `boolean` | `false` | Allow Enter to submit a custom value that is not among candidates |
412
+ | `portal` | `boolean` | `false` | Render dropdown in theme overlay |
413
+ | `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
414
+ | `overlayContainer` | `HTMLElement \| () => HTMLElement` | — | Explicit portal container |
415
+ | `aria-label` | `string` | — | Accessible name |
416
+ | `aria-labelledby` | `string` | — | Accessible name references |
374
417
 
375
418
  **Events:** `input`, `change`, `focus`, `blur`, `open-change` (`CustomEvent<{ open: boolean }>`)
376
419
 
377
- **Slots:** `default` (project `<web-ui-option>` elements)
420
+ **Slots:** `default` (project `<web-ui-option>` elements), `empty` (replace the “no matches” state; falls back to “No matches”)
378
421
 
379
422
  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
423
 
424
+ 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.
425
+
426
+ 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.
427
+
381
428
  **CSS Custom Properties:**
382
429
 
383
- | Property | Default | Description |
384
- | ------------------------------ | ------- | ---------------------- |
385
- | `--wui-autocomplete-max-width` | `500px` | Dropdown max width |
386
- | `--wui-overlay-min-width` | `200px` | Dropdown minimum width |
430
+ | Property | Default | Description |
431
+ | ------------------------------- | ------- | ------------------------------- |
432
+ | `--wui-autocomplete-max-width` | `500px` | Dropdown max width |
433
+ | `--wui-autocomplete-max-height` | `200px` | Dropdown scroll area max height |
434
+ | `--wui-overlay-min-width` | `200px` | Dropdown minimum width |
435
+
436
+ 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
437
 
388
438
  #### `<web-ui-slider>`
389
439
 
@@ -415,8 +465,8 @@ Supports ArrowLeft/Right/Up/Down, Home/End, PageUp/PageDown keyboard navigation.
415
465
  | `--wui-slider-vertical-height` | `200px` | Vertical slider height |
416
466
  | `--wui-slider-height` | `var(--wui-slider-track-size, 6px)` | Track thickness |
417
467
  | `--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 |
468
+ | `--wui-slider-thumb-width` | `30px` | Thumb width |
469
+ | `--wui-slider-thumb-height` | `20px` | Thumb height |
420
470
  | `--wui-slider-marks-inset` | `0` | Marks inset from edges |
421
471
 
422
472
  #### `<web-ui-checkbox>`
@@ -538,7 +588,7 @@ Styled button with variants and loading state.
538
588
  | `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger' \| 'glass'` | `'glass'` | Button variant |
539
589
  | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Inner-button type; invalid values become `button` |
540
590
  | `disabled` | `boolean` | `false` | Disabled state |
541
- | `loading` | `boolean` | `false` | Loading spinner |
591
+ | `loading` | `boolean` | `false` | Loading spinner; with `icon`, the spinner replaces the icon content |
542
592
  | `full` | `boolean` | `false` | Full width |
543
593
  | `icon` | `boolean` | `false` | Icon-only mode |
544
594
  | `size` | `string` | `''` | Button height in px (icon mode: also sets min-width for square default) |
@@ -553,6 +603,9 @@ form-associated control when external form submission behavior is required.
553
603
 
554
604
  Disabled and loading states prevent `click` events.
555
605
 
606
+ When `icon` and `loading` are both set, the button renders only the spinner, and the default-slot icon is not
607
+ projected. The icon-mode geometry stays square by default, except for `full` or an explicit width.
608
+
556
609
  **CSS Custom Properties:**
557
610
 
558
611
  | Property | Default | Description |
@@ -585,31 +638,33 @@ Applies the direction to the grouped button layout without changing child button
585
638
 
586
639
  Modal dialog using native `<dialog>` with `showModal()`.
587
640
 
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 |
641
+ | Attribute | Type | Default | Description |
642
+ | ------------------- | --------- | ------- | ------------------------------------------------------------------------------ |
643
+ | `open` | `boolean` | `false` | Dialog visibility |
644
+ | `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
645
+ | `no-backdrop-close` | `boolean` | `false` | Do not close on backdrop click |
646
+ | `no-escape-close` | `boolean` | `false` | Do not close when Escape is pressed |
647
+ | `controlled` | `boolean` | `false` | Escape and backdrop only request `open=false`; the consumer must update `open` |
594
648
 
595
- **Events:** `open-change` (`CustomEvent<{ open: boolean }>`)
649
+ **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
650
 
597
651
  **Slots:** `body`, `title`, `default`, `footer`
598
652
 
599
653
  **Methods:** `showModal()`, `close()`
600
654
 
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.
655
+ 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
656
 
603
657
  **CSS Custom Properties:**
604
658
 
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 |
659
+ | Property | Default | Description |
660
+ | ------------------------- | ---------------------------------- | -------------------------------------------------------------- |
661
+ | `--wui-dialog-max-width` | `360px` | Dialog max width |
662
+ | `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | Backdrop background |
663
+ | `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | Glass card background, falls back to `rgb(246 246 246 / 0.88)` |
609
664
 
610
665
  #### `<web-ui-drawer>`
611
666
 
612
- Side drawer using native `<dialog>` with closing animation.
667
+ 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
668
 
614
669
  | Attribute | Type | Default | Description |
615
670
  | ------------------- | ---------------------------------------- | --------- | --------------------------------------------------------------------------------- |
@@ -619,11 +674,12 @@ Side drawer using native `<dialog>` with closing animation.
619
674
  | `closable` | `boolean` | `false` | Show close button |
620
675
  | `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
621
676
  | `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` |
677
+ | `controlled` | `boolean` | `false` | User close actions only request `open=false`; the consumer must update `open` |
623
678
  | `headless` | `boolean` | `false` | Keep only overlay behavior and render the default slot without built-in drawer UI |
624
679
  | `dialog-label` | `string` | `''` | Accessible name for the internal native dialog; required in headless mode |
680
+ | `draggable` | `boolean` | `false` | Show a drag bar on the inner edge for drag-to-close gestures |
625
681
 
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.
682
+ **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
683
 
628
684
  **Slots:** `header`, `default`, `footer` — with `headless`, only the `default` slot is rendered.
629
685
 
@@ -633,14 +689,54 @@ Side drawer using native `<dialog>` with closing animation.
633
689
 
634
690
  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
691
 
692
+ **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.
693
+
636
694
  **CSS Custom Properties:**
637
695
 
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 |
696
+ | Property | Default | Description |
697
+ | ------------------------- | ---------------------------------- | ---------------------------------------------------------------------------- |
698
+ | `--wui-drawer-width` | `320px` | Drawer width |
699
+ | `--wui-drawer-height` | `300px` | Drawer height (top/bottom) |
700
+ | `--wui-drawer-bg` | `var(--wui-color-surface-overlay)` | Drawer body background |
701
+ | `--wui-drawer-radius` | `28px` | Floating card corner radius (non-headless) |
702
+ | `--wui-drawer-inset` | `8px` | Floating card viewport inset (non-headless); `0` gives edge-to-edge geometry |
703
+ | `--wui-drawer-overlay-bg` | `rgb(0 0 0 / 0.12)` | Backdrop background |
704
+
705
+ ---
706
+
707
+ ### In-flow Disclosure
708
+
709
+ #### `<web-ui-collapse>`
710
+
711
+ In-flow expand/collapse container with animated height (or width) transition. Single element with two slots; no portal, no scroll lock, no focus management.
712
+
713
+ | Attribute | Type | Default | Description |
714
+ | -------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
715
+ | `open` | `boolean` | `false` | Expanded state; self-managed on interaction, `open-change` emits only for user-originated toggles |
716
+ | `disabled` | `boolean` | `false` | Ignores trigger clicks and sets `aria-disabled` on the trigger element; expanded content is kept |
717
+ | `horizontal` | `boolean` | `false` | Animate width instead of height |
718
+ | `keep-mounted` | `boolean` | `false` | Closed state keeps the content in the 0fr track with `inert` (scroll position and layout measurable) instead of `hidden` |
719
+
720
+ **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`.
721
+
722
+ **Slots:** `default` (trigger) + `content` (collapsible content)
723
+
724
+ **Methods:** `show()`, `close()`, `toggle()`
725
+
726
+ ```html
727
+ <web-ui-collapse>
728
+ <button type="button">Toggle me</button>
729
+ <div slot="content">Collapsible content</div>
730
+ </web-ui-collapse>
731
+ ```
732
+
733
+ The initial `open` attribute settles instantly without playing the animation. Nested collapses are supported.
734
+
735
+ **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.
736
+
737
+ **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.
738
+
739
+ **Known limitations:** the `horizontal` animation reflows content while width changes.
644
740
 
645
741
  ---
646
742
 
@@ -668,6 +764,8 @@ Popover overlay anchored to trigger element.
668
764
 
669
765
  Hover mode uses `pointerenter`/`pointerleave` with delay. Click mode toggles on trigger click. Manual mode only responds to imperative `show()`/`close()`.
670
766
 
767
+ **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.
768
+
671
769
  #### `<web-ui-tooltip>`
672
770
 
673
771
  Tooltip overlay using pointer/focus triggers.
@@ -688,7 +786,7 @@ Tooltip overlay using pointer/focus triggers.
688
786
 
689
787
  **Slots:** `default` (trigger), `content` (tooltip panel)
690
788
 
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.
789
+ `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
790
 
693
791
  **CSS Custom Properties:**
694
792
 
@@ -714,6 +812,8 @@ Right-click context menu.
714
812
 
715
813
  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
814
 
815
+ 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.
816
+
717
817
  ---
718
818
 
719
819
  ### Menu
@@ -739,6 +839,8 @@ Dropdown menu with multi-level submenu support.
739
839
 
740
840
  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
841
 
842
+ **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.
843
+
742
844
  #### `<web-ui-dropdown-item>`
743
845
 
744
846
  Menu item for dropdown or context-menu.
@@ -878,15 +980,20 @@ WebUiSpinner.hide() // hide
878
980
 
879
981
  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
982
 
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 |
983
+ | Attribute | Type | Default | Description |
984
+ | ------------------- | --------- | --------- | -------------------------------------------------------------------------------------------------- |
985
+ | `sidebar-collapsed` | `boolean` | `false` | Controlled desktop sidebar collapsed state |
986
+ | `sidebar-open` | `boolean` | `false` | Controlled mobile sidebar drawer open state |
987
+ | `header-glow` | `boolean` | `false` | Decorative Header background glow behind slot content |
988
+ | `sidebar-width` | `string` | `'240px'` | Expanded desktop and mobile sidebar width |
989
+ | `collapsed-width` | `string` | `'72px'` | Collapsed desktop sidebar width |
990
+ | `sidebar-resizable` | `boolean` | `false` | Enable drag-to-resize on the desktop sidebar edge |
991
+ | `sidebar-min-width` | `string` | — | Resize lower bound (px); falls back to `collapsed-width` |
992
+ | `sidebar-max-width` | `string` | — | Resize upper bound (px); clamped to half the viewport, which always wins over the configured value |
993
+
994
+ **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.
888
995
 
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.
996
+ **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
997
 
891
998
  | Slot | Description |
892
999
  | --------- | ----------------------------------------------------------------------------------------- |
@@ -898,15 +1005,16 @@ Responsive page layout with an optional full-width banner, a collapsible desktop
898
1005
 
899
1006
  `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
1007
 
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.
1008
+ 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
1009
 
903
1010
  `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
1011
 
905
1012
  **CSS Custom Properties:**
906
1013
 
907
- | Property | Default | Description |
908
- | ----------------------------- | ------- | ------------------------------------------------ |
909
- | `--wui-layout-sidebar-radius` | `28px` | Border radius of sidebar card (desktop & mobile) |
1014
+ | Property | Default | Description |
1015
+ | ---------------------------------- | ------- | ------------------------------------------------ |
1016
+ | `--wui-layout-sidebar-radius` | `24px` | Border radius of sidebar card (desktop & mobile) |
1017
+ | `--wui-layout-mobile-toggle-inset` | `8px` | Left inset of the mobile header toggle |
910
1018
 
911
1019
  #### `<web-ui-back-top>`
912
1020
 
@@ -974,7 +1082,7 @@ Defines foundation, color, layer, shadow, and motion tokens for its subtree. `mo
974
1082
  | ------------------------- | ------- | ------------------------------------------------ |
975
1083
  | `--wui-font-size` | `14px` | Base font size for controls |
976
1084
  | `--wui-input-width` | `200px` | Default width for compact form controls |
977
- | `--wui-control-size` | `40px` | Default height and square min-width for controls |
1085
+ | `--wui-control-size` | `36px` | Default height and square min-width for controls |
978
1086
  | `--wui-overlay-min-width` | `200px` | Minimum anchored overlay width |
979
1087
  | `--wui-focus-ring-width` | `3px` | Focus indicator width |
980
1088
 
@@ -994,32 +1102,38 @@ Defines foundation, color, layer, shadow, and motion tokens for its subtree. `mo
994
1102
  | `--wui-layer-toast` | `200` | Toasts |
995
1103
  | `--wui-layer-loading` | `300` | Blocking loading surfaces |
996
1104
 
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`.
1105
+ **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
1106
 
999
1107
  **Color tokens:**
1000
1108
 
1001
1109
  | Property | Light default | Dark default | Description |
1002
1110
  | ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------- |
1003
- | `--wui-color-page` | `#fff` | `#18181a` | Page background |
1111
+ | `--wui-color-page` | `#fff` | `#242628` | Page background |
1004
1112
  | `--wui-color-surface` | `#fff` | `#2c2c2e` | Plain surface |
1005
1113
  | `--wui-color-surface-raised` | `#f2f2f7` | `#2c2c2e` | Raised surface |
1006
1114
  | `--wui-color-surface-control` | `#dfdfdf` | `#3a3a3c` | Neutral control surface |
1007
1115
  | `--wui-color-surface-track` | `#e5e5ea` | `#444446` | Slider and switch track surface |
1116
+ | `--wui-color-surface-menu` | `rgb(246 246 246 / 0.82)` | `rgb(30 30 32 / 0.92)` | Menu and floating panel surface |
1008
1117
  | `--wui-color-surface-glass` | `rgb(250 250 250 / 0.34)` | `rgb(44 44 46 / 0.42)` | Liquid glass surface |
1009
1118
  | `--wui-color-surface-glass-hover` | `color-mix(... text 6%, surface-glass)` | `color-mix(... text 6%, surface-glass)` | Full glass hover background |
1010
1119
  | `--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 |
1120
+ | `--wui-color-surface-overlay` | `rgb(246 246 246 / 0.82)` | `rgb(32 34 34 / 0.9)` | Translucent overlay surface |
1121
+ | `--wui-color-surface-segmented` | `#e5e5ea` | `#3a3a3c` | Segmented track surface |
1122
+ | `--wui-color-surface-selected` | `#fff` | `#5c5c5e` | Segmented selected indicator |
1123
+ | `--wui-color-text` | `#1b1b1b` | `#e9eaea` | Primary text |
1013
1124
  | `--wui-color-text-secondary` | `#6a6a6a` | `#a1a1a6` | Secondary text |
1014
1125
  | `--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
1126
  | `--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
1127
  | `--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
1128
  | `--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
1129
  | `--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 |
1130
+ | `--wui-color-glass-border` | `transparent` | `rgb(255 255 255 / 0.05)` | Glass border tint |
1131
+ | `--wui-color-glass-highlight` | `rgb(255 255 255 / 0.9)` | `rgb(255 255 255 / 0.1)` | Glass edge highlight |
1132
+ | `--wui-color-glass-corner` | `rgb(255 255 255 / 0.08)` | `rgb(255 255 255 / 0.2)` | Glass corner sheen on border |
1133
+ | `--wui-color-glass-shade` | `rgb(0 0 0 / 0.06)` | `rgb(0 0 0 / 0.5)` | Glass corner shade on border |
1021
1134
  | `--wui-color-accent` | `#08f` | `#0a84ff` | Accent and focused input border |
1022
1135
  | `--wui-color-on-accent` | `#fff` | `#fff` | Foreground on accent |
1136
+ | `--wui-color-on-control` | `#fff` | `#f2f2f7` | Foreground on control inner |
1023
1137
  | `--wui-color-success` | `#16a34a` | `#30d158` | Success |
1024
1138
  | `--wui-color-warning` | `#d97706` | `#ff9f0a` | Warning |
1025
1139
  | `--wui-color-danger` | `#dc2626` | `#ff453a` | Danger |
@@ -1031,9 +1145,11 @@ Defines foundation, color, layer, shadow, and motion tokens for its subtree. `mo
1031
1145
 
1032
1146
  | Property | Light default | Dark default | Description |
1033
1147
  | ---------------------- | -------------------------------- | ------------------------------- | --------------------------- |
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 |
1148
+ | `--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 |
1149
+ | `--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 |
1150
+ | `--wui-shadow-glass` | four-layer diffuse shadow | `0 8px 24px rgb(0 0 0 / 0.08)` | Base liquid glass shadow |
1151
+
1152
+ **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
1153
 
1038
1154
  **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
1155