@greypan/web-ui 6.4.0 → 8.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 (148) hide show
  1. package/README.CN.md +211 -50
  2. package/README.md +222 -61
  3. package/dist/assets/glass.js +1 -1
  4. package/dist/assets/glass.js.map +1 -1
  5. package/dist/assets/selection-control.js +6 -0
  6. package/dist/assets/selection-control.js.map +1 -0
  7. package/dist/assets/toast-containers.js.map +1 -1
  8. package/dist/components/autocomplete/index.d.ts +19 -5
  9. package/dist/components/autocomplete/index.js +142 -108
  10. package/dist/components/autocomplete/index.js.map +1 -1
  11. package/dist/components/autocomplete/style.js +1 -1
  12. package/dist/components/autocomplete/style.js.map +1 -1
  13. package/dist/components/avatar/index.js +4 -4
  14. package/dist/components/badge/style.js.map +1 -1
  15. package/dist/components/button/index.js +15 -15
  16. package/dist/components/button/style.js.map +1 -1
  17. package/dist/components/button-group/index.js +5 -5
  18. package/dist/components/checkbox/index.d.ts +9 -2
  19. package/dist/components/checkbox/index.js +50 -26
  20. package/dist/components/checkbox/index.js.map +1 -1
  21. package/dist/components/checkbox/style.js +1 -1
  22. package/dist/components/checkbox/style.js.map +1 -1
  23. package/dist/components/checkbox-group/index.d.ts +3 -0
  24. package/dist/components/checkbox-group/index.js +27 -16
  25. package/dist/components/checkbox-group/index.js.map +1 -1
  26. package/dist/components/checkbox-group/style.js +1 -1
  27. package/dist/components/checkbox-group/style.js.map +1 -1
  28. package/dist/components/collapse/index.js.map +1 -1
  29. package/dist/components/context-menu/index.d.ts +10 -0
  30. package/dist/components/context-menu/index.js +51 -34
  31. package/dist/components/context-menu/index.js.map +1 -1
  32. package/dist/components/dialog/index.d.ts +6 -0
  33. package/dist/components/dialog/index.js +49 -21
  34. package/dist/components/dialog/index.js.map +1 -1
  35. package/dist/components/dialog/style.js +1 -1
  36. package/dist/components/dialog/style.js.map +1 -1
  37. package/dist/components/drawer/index.d.ts +22 -4
  38. package/dist/components/drawer/index.js +141 -110
  39. package/dist/components/drawer/index.js.map +1 -1
  40. package/dist/components/drawer/style.js +1 -1
  41. package/dist/components/drawer/style.js.map +1 -1
  42. package/dist/components/dropdown/index.d.ts +21 -1
  43. package/dist/components/dropdown/index.js +77 -51
  44. package/dist/components/dropdown/index.js.map +1 -1
  45. package/dist/components/editable-text/index.d.ts +471 -0
  46. package/dist/components/editable-text/index.js +206 -0
  47. package/dist/components/editable-text/index.js.map +1 -0
  48. package/dist/components/editable-text/style.js +6 -0
  49. package/dist/components/editable-text/style.js.map +1 -0
  50. package/dist/components/image-preview/index.js +103 -80
  51. package/dist/components/image-preview/index.js.map +1 -1
  52. package/dist/components/image-preview/style.js +1 -1
  53. package/dist/components/image-preview/style.js.map +1 -1
  54. package/dist/components/index.d.ts +1 -0
  55. package/dist/components/index.js +39 -38
  56. package/dist/components/input/index.d.ts +2 -0
  57. package/dist/components/input/index.js +12 -6
  58. package/dist/components/input/index.js.map +1 -1
  59. package/dist/components/input/style.js.map +1 -1
  60. package/dist/components/input-number/index.js +15 -15
  61. package/dist/components/layout/index.js +9 -9
  62. package/dist/components/layout/index.js.map +1 -1
  63. package/dist/components/layout/style.js +1 -1
  64. package/dist/components/layout/style.js.map +1 -1
  65. package/dist/components/popover/index.d.ts +8 -2
  66. package/dist/components/popover/index.js +53 -55
  67. package/dist/components/popover/index.js.map +1 -1
  68. package/dist/components/radio/index.d.ts +1 -1
  69. package/dist/components/radio/index.js +22 -21
  70. package/dist/components/radio/index.js.map +1 -1
  71. package/dist/components/radio/style.js +1 -1
  72. package/dist/components/radio/style.js.map +1 -1
  73. package/dist/components/radio-group/index.d.ts +3 -0
  74. package/dist/components/radio-group/index.js +27 -16
  75. package/dist/components/radio-group/index.js.map +1 -1
  76. package/dist/components/radio-group/style.js +1 -1
  77. package/dist/components/radio-group/style.js.map +1 -1
  78. package/dist/components/segmented/index.d.ts +4 -1
  79. package/dist/components/segmented/index.js +42 -29
  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/style.js +1 -1
  84. package/dist/components/segmented-trigger/style.js.map +1 -1
  85. package/dist/components/select/index.d.ts +2 -0
  86. package/dist/components/select/index.js +38 -34
  87. package/dist/components/select/index.js.map +1 -1
  88. package/dist/components/slider/index.js +11 -11
  89. package/dist/components/slider/index.js.map +1 -1
  90. package/dist/components/slider/style.js +1 -1
  91. package/dist/components/slider/style.js.map +1 -1
  92. package/dist/components/svg-draw-lines/index.d.ts +13 -2
  93. package/dist/components/svg-draw-lines/index.js +44 -31
  94. package/dist/components/svg-draw-lines/index.js.map +1 -1
  95. package/dist/components/switch/index.js +12 -12
  96. package/dist/components/switch/index.js.map +1 -1
  97. package/dist/components/switch/style.js.map +1 -1
  98. package/dist/components/textarea/index.js +6 -6
  99. package/dist/components/textarea/index.js.map +1 -1
  100. package/dist/components/textarea/style.js.map +1 -1
  101. package/dist/components/theme/index.d.ts +15 -0
  102. package/dist/components/theme/index.js +178 -21
  103. package/dist/components/theme/index.js.map +1 -1
  104. package/dist/components/theme/style.js +1 -1
  105. package/dist/components/theme/style.js.map +1 -1
  106. package/dist/components/toast/index.d.ts +1 -1
  107. package/dist/components/toast/manager.d.ts +1 -3
  108. package/dist/components/toast/manager.js +92 -49
  109. package/dist/components/toast/manager.js.map +1 -1
  110. package/dist/components/toast/style.js.map +1 -1
  111. package/dist/components/toast/toast.d.ts +37 -0
  112. package/dist/components/toast/toast.js +74 -21
  113. package/dist/components/toast/toast.js.map +1 -1
  114. package/dist/components/toast/types.d.ts +0 -4
  115. package/dist/components/tooltip/index.d.ts +6 -1
  116. package/dist/components/tooltip/index.js +21 -19
  117. package/dist/components/tooltip/index.js.map +1 -1
  118. package/dist/shared/gesture/drag-gesture.js +66 -64
  119. package/dist/shared/gesture/drag-gesture.js.map +1 -1
  120. package/dist/shared/gesture/physics.js +7 -29
  121. package/dist/shared/gesture/physics.js.map +1 -1
  122. package/dist/shared/menu-behavior/index.js +3 -4
  123. package/dist/shared/menu-behavior/index.js.map +1 -1
  124. package/dist/shared/option-portal/combobox-shell.js +48 -7
  125. package/dist/shared/option-portal/combobox-shell.js.map +1 -1
  126. package/dist/shared/option-portal/index.js +9 -9
  127. package/dist/shared/overlay/anchored-panel.js +36 -31
  128. package/dist/shared/overlay/anchored-panel.js.map +1 -1
  129. package/dist/shared/overlay/native-dialog-presence.js +16 -0
  130. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  131. package/dist/shared/overlay/nested-drawer-layers.js +7 -4
  132. package/dist/shared/overlay/nested-drawer-layers.js.map +1 -1
  133. package/dist/shared/overlay/open-overlay.js +198 -0
  134. package/dist/shared/overlay/open-overlay.js.map +1 -0
  135. package/dist/shared/overlay/presence.js +19 -22
  136. package/dist/shared/overlay/presence.js.map +1 -1
  137. package/dist/shared/theme/duration.js +9 -0
  138. package/dist/shared/theme/duration.js.map +1 -0
  139. package/dist/shared/theme/reduced-motion.js +15 -0
  140. package/dist/shared/theme/reduced-motion.js.map +1 -0
  141. package/dist/shared/theme/root-sync.js +42 -0
  142. package/dist/shared/theme/root-sync.js.map +1 -0
  143. package/package.json +1 -1
  144. package/dist/shared/overlay/composition.js +0 -57
  145. package/dist/shared/overlay/composition.js.map +0 -1
  146. package/dist/shared/overlay/lifecycle.js +0 -41
  147. package/dist/shared/overlay/lifecycle.js.map +0 -1
  148. package/dist/vite.svg +0 -1
package/README.md CHANGED
@@ -275,6 +275,7 @@ All form controls participate in native `FormData`, constraint validation, `form
275
275
  | ---------------------- | --------------------------------------------------------- |
276
276
  | **Form Controls** | [`<web-ui-input>`](#web-ui-input) |
277
277
  | | [`<web-ui-textarea>`](#web-ui-textarea) |
278
+ | | [`<web-ui-editable-text>`](#web-ui-editable-text) |
278
279
  | | [`<web-ui-input-number>`](#web-ui-input-number) |
279
280
  | | [`<web-ui-select>`](#web-ui-select) |
280
281
  | | [`<web-ui-autocomplete>`](#web-ui-autocomplete) |
@@ -337,6 +338,8 @@ Text input with clearable, prefix/suffix slots.
337
338
 
338
339
  **Events:** `input`, `change`, `focus`, `blur`
339
340
 
341
+ **Methods:** `focus()`, `blur()` — delegate to the internal native input (the host itself is not focusable)
342
+
340
343
  **Slots:** `prefix`, `default`, `suffix`
341
344
 
342
345
  **CSS Custom Properties:**
@@ -381,6 +384,41 @@ Multi-line text input with auto-resize.
381
384
  | `--wui-textarea-width` | `200px` | Textarea width |
382
385
  | `--wui-textarea-clear-color` | `var(--wui-color-text-tertiary)` | Clear button color |
383
386
 
387
+ #### `<web-ui-editable-text>`
388
+
389
+ Inline plain-text editor: click the text to edit in place, `Enter` and `blur` commit, `Escape` cancels. The text layer and the editing layer share one box, so entering edit mode does not move a single pixel.
390
+
391
+ | Attribute | Type | Default | Description |
392
+ | ------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
393
+ | `value` | `string` | `''` | Current value; the declarative value captured on first connection becomes the `form.reset()` default; later attribute or property changes do not update it |
394
+ | `placeholder` | `string` | `''` | Text shown while the value is empty |
395
+ | `name` | `string` | `''` | Form field name |
396
+ | `disabled` | `boolean` | `false` | Disabled state; behavior only, no visual dimming |
397
+ | `aria-label` | `string` | — | Accessible label |
398
+
399
+ **Events:** `input` (per keystroke), `change` (commit), `cancel` (cancel; shares its name with the native `<dialog>` `cancel`, so it neither bubbles nor crosses shadow boundaries and is dispatched on the component itself only). React has no synthetic event covering `cancel`: listen with `addEventListener('cancel', ...)`
400
+
401
+ **Methods:** `focus()`, `blur()`, `select()`
402
+
403
+ Clicking places the caret at the clicked offset; keyboard focus places it at the end of the text. `Enter` and `blur` both commit the draft and dispatch `change` exactly once; `Enter` does not insert a newline and returns focus to the host, while `blur` leaves the focus wherever the user moved it. `Escape` alone cancels: the value returns to what it was when editing started, `cancel` is dispatched with no `change`, focus returns to the host, and the key is consumed by the editing layer, so an outer overlay (drawer, menu) is not closed by the same press. The `cancel` event neither bubbles nor crosses shadow boundaries and is dispatched on the component itself only: when the component is projected inside an overlay's shadow root, such as a drawer title, it never reaches the overlay's native `cancel` close pipeline, so listen on the component itself. A value that already contains newlines still renders as several lines; only typing `Enter` no longer adds one. An empty value keeps showing the placeholder, and the editing layer always sizes itself to its own content, so an empty or whitespace-only draft still has room for the caret even where the host itself collapses (a flex item with `min-width: 0`, a table cell).
404
+
405
+ `select()` enters edit mode with the whole content selected, and re-selects it when already editing. While `disabled` it does nothing, matching `focus()`.
406
+
407
+ The host is an inline-level box: it sizes to its content unless a width is set, and grows with wrapped lines. Font, color, text alignment and white space are inherited from the surrounding context, so the component reads as ordinary text until it is edited.
408
+
409
+ The caret follows the shared `--wui-color-accent` semantic token by default; set that token on the host or any ancestor to recolor it. Both layers wrap a long unbroken string (letters and digits with no spaces) at the same points, so a token wider than the box breaks inside the box instead of overflowing it while idle.
410
+
411
+ **CSS Custom Properties:**
412
+
413
+ | Property | Default | Description |
414
+ | ----------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
415
+ | `--wui-editable-text-white-space` | `pre-wrap` | White-space handling of both text layers |
416
+ | `--wui-editable-text-overflow-wrap` | `anywhere` | Overflow-wrap handling of both text layers; the default breaks a long unbroken string inside the box, matching the native editing layer |
417
+
418
+ The shared box comes with two constraints: keep `line-height` at `1` or above, because below that the native editing layer grows taller than its own box and shifts the text by a pixel; and with `nowrap` plus a fixed width, text wider than the box overflows while idle and scrolls inside the box while editing.
419
+
420
+ **Interaction change:** `blur` commits again: both `Enter` and `blur` commit the draft, and only `Escape` cancels, restoring the value from the moment editing started. `Enter` still does not insert a newline. The `cancel` event no longer bubbles: a bubbling custom `cancel` used to travel through the slot into the `<dialog>` inside an outer overlay's shadow root (`composed` made no difference) and was treated as a close request, closing the overlay along with the edit (issue #159). Listen on the component itself.
421
+
384
422
  #### `<web-ui-input-number>`
385
423
 
386
424
  Numeric input with step buttons and keyboard control.
@@ -461,10 +499,26 @@ Editable combobox with input filtering and single option selection.
461
499
 
462
500
  **Events:** `input`, `change`, `focus`, `blur`, `open-change` (`CustomEvent<{ open: boolean }>`)
463
501
 
464
- **Slots:** `default` (project `<web-ui-option>` elements), `empty` (replace the “no matches” state; falls back to “No matches”)
502
+ **Methods:** `focus()`, `blur()`
503
+
504
+ **Slots:** `default` (project `<web-ui-option>` elements), `trigger` (custom trigger content — replaces the default input), `empty` (replace the “no matches” state; falls back to “No matches”)
465
505
 
466
506
  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.
467
507
 
508
+ **Trigger:** the default trigger is an internal `web-ui-input`. Put any editable component in the `trigger` slot to replace it — the wrapper keeps the combobox ARIA and marks itself with `data-custom-trigger`, and the dropdown stays anchored to the trigger element. Programmatic `focus()` / `blur()` delegate to the active trigger: `web-ui-input` and `web-ui-textarea` move focus to their native control, while a custom trigger without its own focus redirection is focused as the host itself.
509
+
510
+ ```html
511
+ <web-ui-autocomplete placeholder="Describe the issue">
512
+ <web-ui-textarea slot="trigger" rows="3"></web-ui-textarea>
513
+ <web-ui-option value="bug" label="Bug"></web-ui-option>
514
+ <web-ui-option value="feature" label="Feature"></web-ui-option>
515
+ </web-ui-autocomplete>
516
+ ```
517
+
518
+ The custom trigger exposes the same contract as the default one: a string `value` for the text, focusable, and the events the component delegates from it (`input`, `click`, `focus`, `blur`). `web-ui-input` and `web-ui-textarea` satisfy it as-is; a custom element is usable when it exposes a string `value` property. The wrapper div is not a tab stop (`tabindex="-1"`): sequential focus belongs to the trigger itself, so a custom trigger has to be focusable for keyboard users to reach the combobox.
519
+
520
+ Multiline triggers (a trigger whose editable element is a `<textarea>`) keep Enter for newlines: Enter never selects the highlighted option and never commits a custom value, so close the panel with Escape or blur. Selecting an option still writes its label back to the trigger. A single-line custom trigger keeps the default Enter behavior. ArrowUp/ArrowDown follow the same exception while the panel is open: they move the text caret instead of navigating options, so keyboard option navigation is unavailable in that state — select with a pointer click. With the panel closed, ArrowDown/ArrowUp still open it.
521
+
468
522
  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.
469
523
 
470
524
  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.
@@ -510,8 +564,7 @@ Supports ArrowLeft/Right/Up/Down, Home/End, PageUp/PageDown keyboard navigation.
510
564
  | `--wui-slider-height` | `var(--wui-slider-track-size, 6px)` | Track thickness |
511
565
  | `--wui-slider-track-size` | `6px` | Track size |
512
566
  | `--wui-slider-thumb-width` | `24px` | Thumb width |
513
- | `--wui-slider-thumb-height` | `18px` | Thumb height |
514
- | `--wui-slider-thumb-radius` | `8px` | Thumb border radius |
567
+ | `--wui-slider-thumb-height` | `20px` | Thumb height |
515
568
  | `--wui-slider-marks-inset` | `0` | Marks inset from edges |
516
569
 
517
570
  #### `<web-ui-checkbox>`
@@ -532,6 +585,12 @@ Individual checkbox.
532
585
 
533
586
  Uses native `<label>` with `role="checkbox"` and `aria-checked`. Enter/Space keyboard toggle.
534
587
 
588
+ **Layout:** the host is an inline-flex box whose height is set by its content, so the inherited page line-height can no longer inflate it or shift the indicator up and down; it aligns against surrounding text with `vertical-align: middle`. The indicator measures `--wui-selection-control-size` (`18px`), and `<web-ui-radio>` shares the same box contract.
589
+
590
+ **Check animation:** the checkmark is the control's own stroked path rather than a `<web-ui-icon>` asset, wrapped in `<web-ui-svg-draw-lines>` (with `no-autoplay`, so a control that mounts already checked shows a static check). Checking draws it in left to right at a constant speed over `--wui-duration-trigger` (160ms by default), read from the resolved theme at the time of the toggle so the stroke lands on the same beat as the indicator's background transition; unchecking retracts the same path back to blank instead of only fading out. Neither plays inside a `motion="reduced"` theme scope, where both states switch instantly.
591
+
592
+ **Idle states:** the unchecked indicator is filled with `--wui-color-surface-control`, the same control surface neutral buttons use, so it stays separable from `--wui-color-page` in dark mode. Hovering anywhere in the trigger row — indicator, gap or slotted label — tints that surface with a 6% state layer over `--wui-color-surface-control`. Hover applies only on `(hover: hover) and (pointer: fine)` devices; there is no pressed state, and checked and disabled controls keep their own surface. `<web-ui-radio>` shares the same states.
593
+
535
594
  #### `<web-ui-radio>`
536
595
 
537
596
  Individual radio button.
@@ -548,6 +607,8 @@ Individual radio button.
548
607
 
549
608
  **Slots:** `default` (label text)
550
609
 
610
+ **Layout:** shares the selection-control box contract with `<web-ui-checkbox>` — host height comes from its content, alignment against surrounding text is `vertical-align: middle`, and the indicator measures `--wui-selection-control-size`.
611
+
551
612
  #### `<web-ui-switch>`
552
613
 
553
614
  Toggle switch.
@@ -571,56 +632,97 @@ Uses `role="switch"` and `aria-checked`. Pointer events for pressed visual state
571
632
 
572
633
  Segmented control — single-select button group.
573
634
 
574
- | Attribute | Type | Default | Description |
575
- | ---------- | --------- | ------- | ---------------------- |
576
- | `value` | `string` | `''` | Selected trigger value |
577
- | `name` | `string` | `''` | Form field name |
578
- | `disabled` | `boolean` | `false` | Disables all triggers |
579
- | `required` | `boolean` | `false` | Required validation |
635
+ | Attribute | Type | Default | Description |
636
+ | ---------- | ------------------- | ------- | ------------------------------------ |
637
+ | `value` | `string` | `''` | Selected trigger value |
638
+ | `name` | `string` | `''` | Form field name |
639
+ | `disabled` | `boolean` | `false` | Disables all triggers |
640
+ | `required` | `boolean` | `false` | Required validation |
641
+ | `variant` | `inset` \| `raised` | `inset` | Track and resting indicator material |
580
642
 
581
643
  **Events:** `input`, `change`
582
644
 
583
645
  **Slots:** `default` (project `<web-ui-segmented-trigger>` elements)
584
646
 
647
+ **Variants:** `inset` (default, sunken) renders an opaque `--wui-color-surface` track with a constant 1px ring and drop shadow, and a solid `--wui-color-surface-segmented` resting indicator that reads as embedded in the track. In light appearance that token equals `--wui-color-page`, so the track sits flush with the page and the recess reads from the ring and shadow alone; in dark appearance `--wui-color-surface` is the one-step-above-page elevation, so the track keeps lifting off the page. `raised` renders the classic flat `--wui-color-surface-segmented` track (no ring, no shadow) with a solid `--wui-color-surface-selected` resting indicator carrying a soft shadow that floats above the track. In both variants the pressed/dragged indicator is transparent glass — backdrop blur, ring fading in over 80ms, 1.5x scale — easing back to the resting surface on release. Illegal values fall back to `inset`.
648
+
585
649
  Form-associated: integrates with native `<form>` via `ElementInternals`.
586
650
 
587
651
  Manages child trigger `checked` state based on `value`. `disabled` supplies inherited effective disabled state without changing a trigger's own `disabled` property. Setting `value` directly does not dispatch `input`/`change`.
588
652
 
589
653
  Press the selected segment and drag horizontally to slide the indicator; on release it snaps to the nearest option (a fast flick switches by velocity). Touch surfaces declare `touch-action: none` and suppress `touchmove` defaulting during an active drag, so iOS Safari does not interrupt the gesture.
590
654
 
655
+ **Pressed state:** the track is an opaque surface (`--wui-color-surface`, page-colored in light appearance) with no backdrop filter in every state — its 1px ring and drop shadow never change. The indicator is the layer that turns to glass. At rest it is a solid `--wui-color-surface-segmented` surface with all glass output off. While pressed or dragging it gains backdrop blur, the glass ring fading in over 80ms, and a 1.5x scale over a fully transparent background: the indicator samples its blur entirely from inside the track, and against the previous glass track a fully transparent indicator measured only +0.5 gray levels on a uniform dark backdrop while striped backdrops showed through the sample (std 1.89 gray levels inside the indicator). The opaque track makes that sample uniform (std 0), which is what gives the pressed thumb a perceptible body — no tint and no edge-decoration suppression are needed. The background never fades in — it lands on the first pressed frame — and on release the indicator eases back to the solid gray rest surface over `--wui-duration-press`.
656
+
657
+ **Label color:** every label stays `--wui-color-text-secondary` — checked or not, `inset` or `raised`, at rest and while a press or drag is active. Selection is carried by the indicator alone; no label takes the accent color.
658
+
591
659
  #### `<web-ui-checkbox-group>`
592
660
 
593
661
  Checkbox group managing multiple selection.
594
662
 
595
- | Attribute | Type | Default | Description |
596
- | ---------- | ---------- | ------- | --------------------------- |
597
- | `value` | `string[]` | `[]` | Selected values |
598
- | `name` | `string` | `''` | Form field name |
599
- | `disabled` | `boolean` | `false` | Disables all child controls |
600
- | `required` | `boolean` | `false` | Required validation |
663
+ ##### Attributes
664
+
665
+ | Attribute | Type | Default | Description |
666
+ | ----------- | ---------------------------- | ------------ | --------------------------- |
667
+ | `value` | `string[]` | `[]` | Selected values |
668
+ | `name` | `string` | `''` | Form field name |
669
+ | `disabled` | `boolean` | `false` | Disables all child controls |
670
+ | `required` | `boolean` | `false` | Required validation |
671
+ | `direction` | `'horizontal' \| 'vertical'` | `'vertical'` | Layout direction |
601
672
 
602
673
  **Events:** `input`, `change`
603
674
 
604
675
  **Slots:** `default` (project `<web-ui-checkbox>` elements)
605
676
 
606
- Syncs child checkbox `checked` state. `disabled` supplies inherited effective disabled state without changing a child's own `disabled` property. Listens to child `change` events.
677
+ Syncs child checkbox `checked` state. `disabled` supplies inherited effective disabled state without changing a child's own `disabled` property. Listens to child `change` events. An invalid `direction` value falls back to `vertical`.
678
+
679
+ ##### Tokens
680
+
681
+ | Token | Default | Description |
682
+ | -------------------------- | ------- | ----------------------------- |
683
+ | `--wui-checkbox-group-gap` | `8px` | Gap between member checkboxes |
684
+
685
+ Set it on the group element itself, or on an ancestor inside the same `<web-ui-theme>` scope. The theme host declares `8px` for its own subtree, so an override placed above a `<web-ui-theme>` element does not reach the group.
686
+
687
+ ```css
688
+ web-ui-checkbox-group {
689
+ --wui-checkbox-group-gap: 16px;
690
+ }
691
+ ```
607
692
 
608
693
  #### `<web-ui-radio-group>`
609
694
 
610
695
  Radio group managing single selection.
611
696
 
612
- | Attribute | Type | Default | Description |
613
- | ---------- | --------- | ------- | ---------------------------------------- |
614
- | `value` | `string` | `''` | Selected radio value |
615
- | `name` | `string` | `''` | Form field name (propagates to children) |
616
- | `disabled` | `boolean` | `false` | Disables all child controls |
617
- | `required` | `boolean` | `false` | Required validation |
697
+ ##### Attributes
698
+
699
+ | Attribute | Type | Default | Description |
700
+ | ----------- | ---------------------------- | ------------ | ---------------------------------------- |
701
+ | `value` | `string` | `''` | Selected radio value |
702
+ | `name` | `string` | `''` | Form field name (propagates to children) |
703
+ | `disabled` | `boolean` | `false` | Disables all child controls |
704
+ | `required` | `boolean` | `false` | Required validation |
705
+ | `direction` | `'horizontal' \| 'vertical'` | `'vertical'` | Layout direction |
618
706
 
619
707
  **Events:** `input`, `change`
620
708
 
621
709
  **Slots:** `default` (project `<web-ui-radio>` elements)
622
710
 
623
- `disabled` supplies inherited effective disabled state without changing a child's own `disabled` property.
711
+ `disabled` supplies inherited effective disabled state without changing a child's own `disabled` property. An invalid `direction` value falls back to `vertical`.
712
+
713
+ ##### Tokens
714
+
715
+ | Token | Default | Description |
716
+ | ----------------------- | ------- | ------------------------- |
717
+ | `--wui-radio-group-gap` | `8px` | Gap between member radios |
718
+
719
+ Set it on the group element itself, or on an ancestor inside the same `<web-ui-theme>` scope. The theme host declares `8px` for its own subtree, so an override placed above a `<web-ui-theme>` element does not reach the group.
720
+
721
+ ```css
722
+ web-ui-radio-group {
723
+ --wui-radio-group-gap: 16px;
724
+ }
725
+ ```
624
726
 
625
727
  ---
626
728
 
@@ -701,6 +803,8 @@ Modal dialog using native `<dialog>` with `showModal()`.
701
803
 
702
804
  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.
703
805
 
806
+ > **Escape ownership:** Escape is arbitrated by a single shared owner, so one keypress closes only the **innermost** open overlay (popover, select, autocomplete, dropdown, context-menu, drawer and dialog all take part). With a select open inside a drawer, the first Escape closes the select and only the second closes the drawer. Sibling overlays that do not nest fall back to open order, closing the most recently opened one. A panel that is still playing its exit transition stays on screen and still catches the Escape, but any overlay that is still open outranks it, so one keypress still closes exactly one layer. `image-preview` takes part as well: its native `<dialog>` is registered with the same arbiter, so Escape is decided by layer order; the component's `cancel` handler only vetoes the native instant close, keeping the top layer until the exit transition finishes.
807
+
704
808
  **CSS Custom Properties:**
705
809
 
706
810
  | Property | Default | Description |
@@ -714,7 +818,7 @@ Uses native `<dialog>` with `@cancel` prevention. Escape calls `close()` unless
714
818
  | `--wui-dialog-desc-gap` | `24px` | Spacing below the body content |
715
819
  | `--wui-dialog-footer-gap` | `10px` / `12px` (horizontal) | Spacing between footer buttons |
716
820
  | `--wui-dialog-footer-justify` | `flex-end` (default) / `center` (horizontal) | Footer `justify-content`; override to `flex-end` for right-aligned horizontal buttons |
717
- | `--wui-dialog-scale-enter` | `1.2` | Enter scale start: the panel shrinks in from `1.2` to `1`, and exit reverses it |
821
+ | `--wui-dialog-scale-enter` | `1.1` | Enter scale start: the panel shrinks in from `1.1` to `1`, and exit reverses it |
718
822
 
719
823
  #### `<web-ui-drawer>`
720
824
 
@@ -748,10 +852,18 @@ When `closable` is set, the built-in close button is positioned at the header's
748
852
  **Drag to close:** With `draggable`, a gray capsule drag bar (4×56px by default, visually centered 10px from the inner edge inside a 20px-thick hit zone) 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:
749
853
 
750
854
  - Dragging follows the pointer in real time; the backdrop fades proportionally.
751
- - 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.
752
- - 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.
855
+ - Backdrop click now only honors a genuine tap chain: the press must start on the backdrop itself and the press-to-release travel must stay within the tap magnitude. The click the browser generates for a press–drag–release lands on the common ancestor (`dialog`) regardless of where the press started — pressing on the panel content or on the backdrop itself and releasing over the backdrop used to close the drawer through that click. The component records the pointerdown origin and validates it on click, so such drag releases always rebound. A `detail`-0 click (keyboard / programmatic) never consumes the pointer record.
856
+ - Releasing with a net displacement past half the drawer size (floor of 10px), or with a fast closing flick, closes the drawer; otherwise it animates back open. The close direction is placement-aware, and each part of the decision matches Base UI's `useSwipeDismiss`:
857
+ - Both the displacement origin and the decision clock reset to the first `pointermove`, absorbing the gap between the press and that move; its whole travel is discarded, so a gesture needs at least two moves to accumulate displacement.
858
+ - The flick is judged by the **whole gesture's** average velocity — net displacement ÷ gesture duration, divisor floored at 50ms — reaching 500px/s, not by the instantaneous velocity at release. A sliding window only describes the last short stretch of the trace, so under a whole-gesture average sweeping back towards the edge can no longer read as a flick. A zero-length duration yields zero velocity instead of a floored divisor, so an unmeasurable gesture is never read as a flick.
859
+ - Any release whose net displacement isn't in the close direction animates back open.
860
+ - The distance test also uses the net displacement, so grabbing the drawer mid-rebound and releasing without dragging further never counts as "already past half way".
861
+ - A withdrawal of 10px or more from the point where the close direction was confirmed cancels the flick, so dragging out into the overscroll and sweeping back never closes. Displacement that has already crossed the distance threshold clears the mark again, so a gesture that is past half way is unaffected by a small retreat. Upstream has the same guard (`cancelledSwipe`), but never reaches it on the drawer path because `DrawerViewport.onRelease` always returns a decision.
862
+ - Dragging in the opening direction is damped by a square root (`sign(d) · |d| ** 0.5`), matching `applyDirectionalDamping`. The damping applies to the gesture's increment, added on top of the offset the drag started from, and self-limits rather than being capped at a fixed distance.
863
+ - The release velocity only shapes the settle transition's duration (180–420ms); the settle itself is a single CSS transition on `transform`, handed back by JS at release — no WAAPI animation is created.
864
+ - With `controlled`, release past the threshold only emits `open-change(false)`; the drawer holds at the closed position briefly (120ms write-back window) and animates back open if the consumer rejects or misses the write-back.
753
865
  - Drag-to-open is not supported because the closed drawer renders nothing outside the native dialog.
754
- - Under `prefers-reduced-motion`, release snaps instantly without spring animation.
866
+ - Under `prefers-reduced-motion`, release settles instantly, with no transition.
755
867
 
756
868
  **CSS Custom Properties:**
757
869
 
@@ -768,7 +880,7 @@ When `closable` is set, the built-in close button is positioned at the header's
768
880
  | `--wui-drawer-drag-bar-length` | `56px` | Drag bar capsule length (along the drawer edge) |
769
881
  | `--wui-drawer-header-padding` | `16px 20px` | Header section padding |
770
882
  | `--wui-drawer-close-top` | `16px` | Built-in close button offset from the top of the header |
771
- | `--wui-drawer-close-right` | `20px` | Built-in close button offset from the right edge of the drawer |
883
+ | `--wui-drawer-close-right` | `16px` | Built-in close button offset from the right edge of the drawer |
772
884
  | `--wui-drawer-content-padding` | `20px` | Content area padding; also drives drag bar visual center (half value) |
773
885
  | `--wui-drawer-footer-padding` | `16px 20px` | Footer section padding |
774
886
 
@@ -1226,12 +1338,14 @@ At `640px` and below, the sidebar becomes a `web-ui-drawer` with its built-in gl
1226
1338
 
1227
1339
  `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`).
1228
1340
 
1341
+ The sidebar card surface is `--wui-color-surface-sidebar`. In light mode it carries the same value as the shared `--wui-color-surface-overlay`; in dark mode it sits one notch above `--wui-color-page`, level with `--wui-color-surface`. It is a separate token because dialog, drawer and toast share `--wui-color-surface-overlay` and are not elevated along with the sidebar.
1342
+
1229
1343
  **CSS Custom Properties:**
1230
1344
 
1231
- | Property | Default | Description |
1232
- | ---------------------------------- | --------------------------------- | ------------------------------------------------ |
1233
- | `--wui-layout-sidebar-radius` | `var(--wui-radius-overlay, 28px)` | Border radius of sidebar card (desktop & mobile) |
1234
- | `--wui-layout-mobile-toggle-inset` | `8px` | Left inset of the mobile header toggle |
1345
+ | Property | Default | Description |
1346
+ | ---------------------------------- | ------- | ------------------------------------------------ |
1347
+ | `--wui-layout-sidebar-radius` | `24px` | Border radius of sidebar card (desktop & mobile) |
1348
+ | `--wui-layout-mobile-toggle-inset` | `8px` | Left inset of the mobile header toggle |
1235
1349
 
1236
1350
  #### `<web-ui-back-top>`
1237
1351
 
@@ -1265,20 +1379,25 @@ Role: `button`, keyboard Enter scrolls to top.
1265
1379
 
1266
1380
  #### `<web-ui-svg-draw-lines>`
1267
1381
 
1268
- SVG line drawing animation using `stroke-dashoffset`. Animates geometry in-place — no cloning, no DOM manipulation.
1382
+ SVG line drawing animation using `stroke-dashoffset`. Animates geometry in-place — no cloning, no DOM manipulation. Two playback directions: the reveal draws geometry from nothing to fully drawn, `replay({ reverse: true })` retracts it along the same path back to nothing.
1383
+
1384
+ | Attribute | Type | Default | Description |
1385
+ | ------------- | --------- | ---------- | ------------------------------------------------------ |
1386
+ | `duration` | `number` | `1000` | Animation duration in ms, clamped to `[0, 30000]` |
1387
+ | `easing` | `string` | `'linear'` | CSS easing function passed to `element.animate()` |
1388
+ | `no-autoplay` | `boolean` | `false` | Skip the automatic playback when content first appears |
1269
1389
 
1270
- | Attribute | Type | Default | Description |
1271
- | ---------- | -------- | ---------- | ------------------------------------------------- |
1272
- | `duration` | `number` | `1000` | Animation duration in ms, clamped to `[0, 30000]` |
1273
- | `easing` | `string` | `'linear'` | CSS easing function passed to `element.animate()` |
1390
+ All attributes are reflected.
1274
1391
 
1275
- Both attributes are reflected.
1392
+ **Auto-play:** the first time slotted content settles, one reveal plays by itself. Set `no-autoplay` to leave the geometry as authored and decide when to play — that is how `<web-ui-checkbox>` avoids drawing a checkmark that was already checked on mount.
1276
1393
 
1277
- **Methods:** `replay(): Promise<void>` — cancels running animation, re-collects geometry elements from current DOM, and starts a new animation. All targets animate in parallel with the same duration/easing. Resolves when all complete. Returns immediately without animation when the nearest theme scope uses `motion="reduced"`, or when its `motion="system"` mode matches `prefers-reduced-motion: reduce`.
1394
+ **Methods:** `replay(options?: { reverse?: boolean }): Promise<void>` — cancels running animation, re-collects geometry elements from current DOM, and starts a new animation. All targets animate in parallel with the same duration/easing. Resolves when all complete. A reverse run ends on blank and leaves that end state in the DOM: the animation itself is cancelled, while the dash values that hide the stroke stay as inline styles until the next `replay()` replaces or undoes them. A reveal run restores the authored inline styles instead. A retract always starts from the full stroke, so interrupting a reveal draws the whole path back rather than only the part that was visible. When the nearest theme scope uses `motion="reduced"`, or its `motion="system"` mode matches `prefers-reduced-motion: reduce`, no animation starts — but a previous retract's blank is still undone, so the geometry returns to as authored and visibility is left entirely to the consumer's own styles.
1395
+
1396
+ **Reverse end state:** the blank a retract leaves behind is inline style on the geometry it animated, including nodes inside nested open shadow roots such as `<web-ui-icon>`. Copying those nodes (`cloneNode`, an `innerHTML` round-trip) carries the blank along, and detaching and re-appending one keeps the stroke hidden — a finished retract is a DOM state rather than a live animation, so nothing is left to restore it. Call `replay()` to draw them again. Because the component reaches into nested open shadow roots, two `<web-ui-svg-draw-lines>` must not cover the same geometry: each records the inline dash values the first time it animates an element, so a blank left by the inner one becomes the outer one's idea of the consumer's own value.
1278
1397
 
1279
1398
  **Slots:** `default` — SVG content to animate. Accepts inline `<svg>` elements (light DOM) as well as components that render an SVG in an open shadow root, such as `<web-ui-icon>`. Closed shadow roots are skipped.
1280
1399
 
1281
- Finds `path`, `rect`, `circle`, `line`, `polyline`, `polygon`, `ellipse` elements by recursively traversing the light DOM and all open shadow roots. Paths ending with `Z`/`z` receive a temporary gap fix for proper closing-segment rendering. After animation completes or is cancelled, all in-line styles are restored.
1400
+ Finds `path`, `rect`, `circle`, `line`, `polyline`, `polygon`, `ellipse` elements by recursively traversing the light DOM and all open shadow roots. Paths ending with `Z`/`z` receive a temporary gap fix for proper closing-segment rendering. After a reveal completes or is cancelled, all in-line styles are restored.
1282
1401
 
1283
1402
  #### `<web-ui-theme>`
1284
1403
 
@@ -1295,8 +1414,14 @@ Theme provider defining CSS custom property tokens.
1295
1414
 
1296
1415
  Defines foundation, color, layer, shadow, and motion tokens for its subtree. `motion="system"` follows `prefers-reduced-motion`; use `motion="reduced"` to reduce animation in a scope or `motion="full"` in a nested theme to restore normal token values. System appearance follows `prefers-color-scheme`.
1297
1416
 
1417
+ Appearance changes are animated with the View Transitions API according to this scope's `motion`: `full` always reveals, `reduced` applies the new appearance immediately with no reveal, and `system` follows `prefers-reduced-motion`. A root theme reveals the whole page; a nested theme reveals only its own capture box. The origin is the last pointer-down position when available, otherwise the theme box or viewport center. Dark-to-light and light-to-dark directions are reversed. Unsupported browsers, reduced-motion scopes, zero durations, and another request already in the same flight fall back to applying the new appearance immediately; `appearance="system"` does not animate OS light/dark changes in this version.
1418
+
1419
+ **Migration from the removed `transition` attribute:** drop it and express the intent with `motion` instead. A theme that carried `transition` keeps the same reveal under the default `motion="system"`; add `motion="full"` to reveal even when the system prefers reduced motion. A theme that did not carry it now reveals by default, so pass `motion="reduced"` to keep appearance changes instant.
1420
+
1298
1421
  The host uses `display: contents` and does not paint any background: the library never draws on the host page, so the embedding application keeps full control of the surface behind the themed subtree. Custom properties still inherit to slotted content reliably.
1299
1422
 
1423
+ **Root page color sync:** the outermost active theme also mirrors its computed `--wui-color-page` onto `document.documentElement` as an inline custom property. That closes the one place the token cannot reach on its own: custom properties stop inheriting at the document element, so a consumer rule such as `body { background: var(--wui-color-page) }` only colors the scrollable area, while the canvas behind an overscroll bounce (macOS rubber-banding) stays at the user-agent background. With the mirrored property the bounce area follows the theme too. The write happens when a theme connects and whenever its appearance changes, including `appearance="system"` following an OS light/dark flip; the value written is the theme's computed value, so an override of `--wui-color-page` on the host is mirrored as it stands. **Not live:** the component does not observe the host's own style changes, so a runtime change to that property on the host — an inline style edit, a class toggle, a host-level media query — reaches the root only at the next write trigger, which is a connect, an `appearance` change to a different value, or an OS light/dark flip; re-assigning `appearance` to the value it already has is not a trigger, because the update is skipped when nothing changed. For a page color that has to change independently of the theme's appearance, own the variable yourself on `:root` or `body` with `!important`, which outranks this inline write, or drive the color through `appearance`. Only the outermost theme writes: nested themes keep their own scope and never touch the root value. Ownership follows connect order and passes to the next connected theme when the outermost one disconnects; when the last theme disconnects the value is deliberately kept rather than removed, so a page that swaps themes does not flash back to the user-agent background in between. The library writes exactly this one custom property on the document element and nothing else there.
1424
+
1300
1425
  **Foundation tokens:**
1301
1426
 
1302
1427
  | Property | Default | Description |
@@ -1307,6 +1432,14 @@ The host uses `display: contents` and does not paint any background: the library
1307
1432
  | `--wui-overlay-min-width` | `200px` | Minimum anchored overlay width |
1308
1433
  | `--wui-focus-ring-width` | `3px` | Focus indicator width |
1309
1434
 
1435
+ **Selection control tokens (radio, checkbox):**
1436
+
1437
+ | Property | Default | Description |
1438
+ | ------------------------------ | ------- | ----------------------------------------------- |
1439
+ | `--wui-selection-control-size` | `18px` | Indicator (circle / box) width and height |
1440
+ | `--wui-radio-group-gap` | `8px` | Member spacing inside `<web-ui-radio-group>` |
1441
+ | `--wui-checkbox-group-gap` | `8px` | Member spacing inside `<web-ui-checkbox-group>` |
1442
+
1310
1443
  **Radius tokens:**
1311
1444
 
1312
1445
  | Property | Default | Description |
@@ -1331,7 +1464,7 @@ The host uses `display: contents` and does not paint any background: the library
1331
1464
  | `--wui-layer-toast` | `200` | Toasts |
1332
1465
  | `--wui-layer-loading` | `300` | Blocking loading surfaces |
1333
1466
 
1334
- **Motion tokens:** duration defaults are `--wui-duration-press: 80ms`, `--wui-duration-feedback: 100ms`, `--wui-duration-trigger: 160ms`, `--wui-duration-focus: 200ms`, `--wui-duration-float-enter: 240ms`, `--wui-duration-float-exit: 160ms`, `--wui-duration-dialog-enter: 320ms`, `--wui-duration-dialog-exit: 260ms`, `--wui-duration-drawer-enter: 280ms`, `--wui-duration-drawer-exit: 240ms`, `--wui-duration-drawer-nested: 450ms`, `--wui-duration-toast-enter: 280ms`, `--wui-duration-toast-exit: 200ms`, `--wui-duration-collapse-enter: 200ms`, `--wui-duration-collapse-exit: 160ms`, `--wui-duration-layout: 200ms`, `--wui-duration-swipe-settle: 220ms` (image preview carousel settle), `--wui-duration-spin: 600ms` (icon rotation) and `--wui-duration-spinner: 800ms` (spinner leaf cycle). Easing tokens are `--wui-ease-enter`, `--wui-ease-dialog` (`cubic-bezier(0.2, 0, 0, 1)`), `--wui-ease-slide`, `--wui-ease-float` (`cubic-bezier(0.4, 0.38, 0.2, 1)`, a no-bounce spring fit used by anchored floating panels), and `--wui-ease-swipe` (`cubic-bezier(0.32, 0.72, 0, 1)`, the decelerating tail of the carousel settle). Enter scale tokens are `--wui-scale-enter: 0.95` (floating panels and the image preview's image surface grow in) and `--wui-dialog-scale-enter: 1.2` (the dialog shrinks in from above, reversed on exit). Under `motion="reduced"` every transition duration collapses to `0ms`, the carousel settle with them, while the two infinite loading loops keep running at a slower period (`--wui-duration-spin` / `--wui-duration-spinner`: `600ms` / `800ms` → `1600ms`). A frozen loading indicator reads as a hung UI. Hover/active background feedback switches instantly with no transition; checked, pressed and focus states do transition, and those durations collapse to 0ms under reduced motion along with the rest.
1467
+ **Motion tokens:** duration defaults are `--wui-duration-press: 80ms`, `--wui-duration-feedback: 100ms`, `--wui-duration-trigger: 160ms`, `--wui-duration-focus: 200ms`, `--wui-duration-float-enter: 240ms`, `--wui-duration-float-exit: 160ms`, `--wui-duration-dialog-enter: 320ms`, `--wui-duration-dialog-exit: 260ms`, `--wui-duration-drawer-enter: 280ms`, `--wui-duration-drawer-exit: 240ms`, `--wui-duration-drawer-nested: 450ms`, `--wui-duration-toast-enter: 280ms`, `--wui-duration-toast-exit: 200ms`, `--wui-duration-collapse-enter: 200ms`, `--wui-duration-collapse-exit: 160ms`, `--wui-duration-layout: 200ms`, `--wui-duration-swipe-settle: 220ms` (image preview carousel settle), `--wui-duration-spin: 600ms` (icon rotation), `--wui-duration-spinner: 800ms` (spinner leaf cycle) and `--wui-theme-transition-duration: 500ms` (theme appearance reveal). Easing tokens are `--wui-ease-enter`, `--wui-ease-dialog` (`cubic-bezier(0.2, 0, 0, 1)`), `--wui-ease-slide`, `--wui-ease-float` (`cubic-bezier(0.4, 0.38, 0.2, 1)`, a no-bounce spring fit used by anchored floating panels), `--wui-ease-swipe` (`cubic-bezier(0.32, 0.72, 0, 1)`, the decelerating tail of the carousel settle) and `--wui-theme-transition-easing: ease-in`. Enter scale tokens are `--wui-scale-enter: 0.95` (floating panels and the image preview's image surface grow in) and `--wui-dialog-scale-enter: 1.1` (the dialog shrinks in from above, reversed on exit). Under `motion="reduced"` every transition duration collapses to `0ms`, the carousel settle with them, while the two infinite loading loops keep running at a slower period (`--wui-duration-spin` / `--wui-duration-spinner`: `600ms` / `800ms` → `1600ms`). A frozen loading indicator reads as a hung UI. Hover/active background feedback switches instantly with no transition; checked, pressed and focus states do transition, and those durations collapse to 0ms under reduced motion along with the rest.
1335
1468
 
1336
1469
  **Color tokens:**
1337
1470
 
@@ -1342,13 +1475,14 @@ The host uses `display: contents` and does not paint any background: the library
1342
1475
  | `--wui-color-surface-raised` | `#f2f2f7` | `#2c2c2e` | Raised surface |
1343
1476
  | `--wui-color-surface-control` | `#dfdfdf` | `#3a3a3c` | Neutral control surface |
1344
1477
  | `--wui-color-surface-track` | `#e5e5ea` | `#444446` | Slider and switch track surface |
1345
- | `--wui-color-surface-menu` | `rgb(246 246 246 / 0.82)` | `rgb(30 30 32 / 0.92)` | Menu and floating panel surface |
1478
+ | `--wui-color-surface-menu` | `rgb(246 246 246 / 0.82)` | `rgb(44 44 46 / 0.78)` | Menu and floating panel surface |
1346
1479
  | `--wui-color-surface-glass` | `rgb(250 250 250 / 0.34)` | `rgb(44 44 46 / 0.42)` | Liquid glass surface |
1347
1480
  | `--wui-color-surface-glass-hover` | `color-mix(... text 6%, surface-glass)` | `color-mix(... text 6%, surface-glass)` | Full glass hover background |
1348
1481
  | `--wui-color-surface-glass-active` | `color-mix(... text 15%, surface-glass)` | `color-mix(... text 15%, surface-glass)` | Full glass pressed background |
1349
1482
  | `--wui-color-surface-overlay` | `rgb(246 246 246 / 0.82)` | `rgb(32 34 34 / 0.9)` | Translucent overlay surface |
1350
- | `--wui-color-surface-segmented` | `#e5e5ea` | `#3a3a3c` | Segmented track surface |
1351
- | `--wui-color-surface-selected` | `#fff` | `#5c5c5e` | Segmented selected indicator |
1483
+ | `--wui-color-surface-sidebar` | `rgb(246 246 246 / 0.82)` | `rgb(44 44 46 / 0.8)` | Sidebar panel surface |
1484
+ | `--wui-color-surface-segmented` | `#e5e5ea` | `#3a3a3c` | Segmented indicator surface |
1485
+ | `--wui-color-surface-selected` | `#fff` | `#5c5c5e` | Selected surface |
1352
1486
  | `--wui-color-text` | `#1b1b1b` | `#e9eaea` | Primary text |
1353
1487
  | `--wui-color-text-secondary` | `#6a6a6a` | `#a1a1a6` | Secondary text |
1354
1488
  | `--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 |
@@ -1358,8 +1492,8 @@ The host uses `display: contents` and does not paint any background: the library
1358
1492
  | `--wui-color-border` | `rgb(0 0 0 / 0.1)` | `rgb(255 255 255 / 0.14)` | Normal border and divider |
1359
1493
  | `--wui-color-glass-border` | `transparent` | `rgb(255 255 255 / 0.05)` | Glass border tint |
1360
1494
  | `--wui-color-glass-highlight` | `rgb(255 255 255 / 0.9)` | `rgb(255 255 255 / 0.1)` | Glass edge highlight |
1361
- | `--wui-color-glass-corner` | `rgb(255 255 255 / 0.08)` | `rgb(255 255 255 / 0.2)` | Glass corner sheen on border |
1362
- | `--wui-color-glass-shade` | `rgb(0 0 0 / 0.06)` | `rgb(0 0 0 / 0.5)` | Glass corner shade on border |
1495
+ | `--wui-color-glass-corner` | `rgb(255 255 255 / 0.5)` | `rgb(255 255 255 / 0.2)` | Glass corner sheen on border |
1496
+ | `--wui-color-glass-shade` | `rgb(0 0 0 / 0.03)` | `rgb(0 0 0 / 0.5)` | Glass corner shade on border |
1363
1497
  | `--wui-color-accent` | `#08f` | `#0a84ff` | Accent and focused input border |
1364
1498
  | `--wui-color-on-accent` | `#fff` | `#fff` | Foreground on accent |
1365
1499
  | `--wui-color-on-control` | `#fff` | `#f2f2f7` | Foreground on control inner |
@@ -1410,29 +1544,56 @@ const id = toast({ message: 'Custom', type: 'info', position: 'bottom-right', du
1410
1544
  toast.close(id)
1411
1545
  toast.clear()
1412
1546
 
1413
- // Update visible content without resetting auto-close timing
1414
- toast.updateMessage(id, { message: 'Upload 60% complete', heading: 'Uploading' })
1547
+ // Upsert: calling again with the same id updates that one toast instead of adding another
1548
+ toast.error('Network lost, retrying…', { id: 'network' })
1549
+ toast.error('Network lost (retry 2)', { id: 'network' })
1550
+
1551
+ // One toast through its whole lifecycle
1552
+ toast.info('Uploading 0%', { id: 'upload', duration: 0 })
1553
+ toast.info('Uploading 60%', { id: 'upload' }) // duration omitted: timer keeps running
1554
+ toast.success('Upload complete', { id: 'upload', duration: 3000 }) // new type, timer restarts
1415
1555
  ```
1416
1556
 
1557
+ Every call returns the final id of that toast, so callers never branch on create vs. update.
1558
+
1417
1559
  **ToastOptions:**
1418
1560
 
1419
- | Option | Type | Default | Description |
1420
- | ----------- | ------------------------------------------------------------------------------------------------- | ------------------------- | --------------------------------------------- |
1421
- | `message` | `string` | — | Notification text |
1422
- | `type` | `'success' \| 'info' \| 'warning' \| 'error'` | `'info'` | Toast type |
1423
- | `duration` | `number` | `3000` (`5000` for error) | Auto-close duration (0 = no auto-close) |
1424
- | `closable` | `boolean` | `true` | Show close button |
1425
- | `id` | `string` | auto | Deduplication identifier |
1426
- | `heading` | `string` | `''` | Bold heading text |
1427
- | `position` | `'top-left' \| 'top-center' \| 'top-right' \| 'bottom-left' \| 'bottom-center' \| 'bottom-right'` | `'top-right'` | Screen position |
1428
- | `target` | `Element` | — | Used to find nearest theme-owned overlay root |
1429
- | `container` | `HTMLElement` | — | Explicit mount container (highest priority) |
1561
+ | Option | Type | Default | Description |
1562
+ | ----------- | ------------------------------------------------------------------------------------------------- | ------------------------- | ----------------------------------------------- |
1563
+ | `message` | `string` | — | Notification text |
1564
+ | `type` | `'success' \| 'info' \| 'warning' \| 'error'` | `'info'` | Toast type |
1565
+ | `duration` | `number` | `3000` (`5000` for error) | Auto-close duration (0 = no auto-close) |
1566
+ | `closable` | `boolean` | `true` | Show close button |
1567
+ | `id` | `string` | auto | Merge key: calls sharing an id update one toast |
1568
+ | `heading` | `string` | `''` | Bold heading text |
1569
+ | `position` | `'top-left' \| 'top-center' \| 'top-right' \| 'bottom-left' \| 'bottom-center' \| 'bottom-right'` | `'top-right'` | Screen position |
1570
+ | `target` | `Element` | — | Used to find nearest theme-owned overlay root |
1571
+ | `container` | `HTMLElement` | — | Explicit mount container (highest priority) |
1572
+
1573
+ **Upsert semantics** — what a second call with the same `id` does:
1574
+
1575
+ | Target state | Result |
1576
+ | ----------------------------- | ------------------------------------------------------------------------------- |
1577
+ | Mounted | Same id returned; supplied fields overwrite, omitted ones keep their value |
1578
+ | Still queued in the same tick | The queued options are patched and applied on mount; no second toast is created |
1579
+ | Closed, or still exiting | A new toast is created; the one leaving finishes its exit animation on its own |
1580
+
1581
+ `duration` restarts the countdown only when it is passed explicitly (the one exception is a hover pause: the new value is recorded without starting the timer, and the countdown runs with it once the pointer leaves); `message`, `heading` and `type` are plain properties and leave the timer alone. The `error` shortcut's 5000 ms default is applied when the toast is created, so it does not count as an explicit `duration` — repeated `toast.error(msg, { id })` calls do not reset the countdown. Changing `position` moves the element to the new container and keeps the remaining time (via `moveBefore` where available, with a pause/resume fallback elsewhere); if the countdown already elapsed while the main thread was blocked, the fallback closes the toast on resume instead of leaving it open. `container` and `target` are read from the first call only — moving a toast to a different overlay root is not supported.
1582
+
1583
+ **Close semantics** — `toast.close(id)` covers every state a toast can be in, including the two before it becomes visible:
1584
+
1585
+ | Target state | Result |
1586
+ | ---------------------------------- | --------------------------------------------------------------- |
1587
+ | Visible | Plays the exit animation, then emits `toast-close` |
1588
+ | Mounted, `show()` has not run yet | No exit animation to play: emits `toast-close` immediately |
1589
+ | Still queued in the same microtask | Dropped before it mounts — nothing appears, no event is emitted |
1590
+ | Already exiting | No-op; that toast finishes its own exit |
1430
1591
 
1431
- **`toast.updateMessage(id, options)`** updates the visible toast's `message` and, when supplied, `heading`. It does not restart the auto-close timer. `options` is `ToastMessageUpdateOptions`: `{ message: string; heading?: string }`.
1592
+ `toast.clear()` has the same scope: it drops queued entries and closes everything mounted.
1432
1593
 
1433
1594
  **Events:** `toast-close` (`CustomEvent<{ id: string; reason: 'auto' | 'manual' | 'programmatic' | 'clear' }>`)
1434
1595
 
1435
- Hover pauses auto-close timer (uses `pointerenter`/`pointerleave`). Batch-mounts toasts created in the same microtask.
1596
+ Hover pauses the auto-close timer (uses `pointerenter`/`pointerleave`); leaving resumes the **remaining** time instead of restarting the full duration. If the `pointerleave` is missed while paused — the pointer is dragged out of the window, or the element is moved or removed while hovered — a document-level `pointerover`/`pointerout`/`pointerleave` fallback resumes the countdown, so a hovered toast cannot stay on screen forever. Batch-mounts toasts created in the same microtask.
1436
1597
 
1437
1598
  **CSS Custom Properties:**
1438
1599
 
@@ -1,5 +1,5 @@
1
1
  //#region src/assets/glass.css?inline
2
- var e = ".wui-glass{--wui-internal-glass-shadow:var(--wui-shadow-glass,0 0 1px #0000000d, 0 0 8px #00000008, 0 2px 16px #00000006, 0 8px 32px #00000004);background-color:var(--wui-color-surface-glass,#fafafa57);-webkit-backdrop-filter:blur(4px) saturate(160%) brightness(var(--wui-glass-brightness,1.06));backdrop-filter:blur(4px) saturate(160%) brightness(var(--wui-glass-brightness,1.06));box-shadow:inset 0 0 0 .5px color-mix(in srgb, var(--wui-color-glass-highlight,#ffffffe6) 38%, transparent), inset 0 1px .5px color-mix(in srgb, var(--wui-color-glass-highlight,#ffffffe6) 52%, transparent), inset 0 -.5px 1px #00000008, var(--wui-internal-glass-shadow,var(--wui-shadow-glass,0 0 1px #0000000d, 0 0 8px #00000008, 0 2px 16px #00000006, 0 8px 32px #00000004));background-clip:padding-box;border:none;position:relative}.wui-glass:before{pointer-events:none;content:\"\";border-radius:inherit;background-color:var(--wui-color-glass-border,transparent);background-image:radial-gradient(var(--wui-glass-corner-radius,32px) at 100% 0, var(--wui-color-glass-shade,#0000000f), transparent), radial-gradient(var(--wui-glass-corner-radius,32px) at 0 100%, var(--wui-color-glass-shade,#0000000f), transparent), radial-gradient(calc(var(--wui-glass-corner-radius,32px) * 2) at 0 0, var(--wui-color-glass-corner,#ffffff14), transparent), radial-gradient(calc(var(--wui-glass-corner-radius,32px) * 2) at 100% 100%, var(--wui-color-glass-corner,#ffffff14), transparent);-webkit-mask-composite:xor;-webkit-mask-source-type:auto,auto;padding:1px;position:absolute;inset:0;-webkit-mask-image:linear-gradient(#000 0 0),linear-gradient(#000 0 0);mask-image:linear-gradient(#000 0 0),linear-gradient(#000 0 0);-webkit-mask-position:0 0,0 0;mask-position:0 0,0 0;-webkit-mask-size:auto,auto;mask-size:auto,auto;-webkit-mask-repeat:repeat,repeat;mask-repeat:repeat,repeat;-webkit-mask-clip:content-box,border-box;mask-clip:content-box,border-box;-webkit-mask-origin:content-box,border-box;mask-origin:content-box,border-box;-webkit-mask-composite:xor;mask-composite:exclude;-webkit-mask-source-type:auto,auto;mask-mode:match-source,match-source}";
2
+ var e = ".wui-glass{--wui-internal-glass-shadow:var(--wui-shadow-glass,0 0 1px #00000005, 0 0 8px #00000008, 0 2px 16px #00000006, 0 8px 32px #00000004);background-color:var(--wui-color-surface-glass,#fafafa57);-webkit-backdrop-filter:blur(4px) saturate(160%) brightness(var(--wui-glass-brightness,1.06));backdrop-filter:blur(4px) saturate(160%) brightness(var(--wui-glass-brightness,1.06));box-shadow:inset 0 0 0 .5px color-mix(in srgb, var(--wui-color-glass-highlight,#ffffffe6) 38%, transparent), inset 0 1px .5px color-mix(in srgb, var(--wui-color-glass-highlight,#ffffffe6) 52%, transparent), inset 0 -.5px 1px #00000008, var(--wui-internal-glass-shadow,var(--wui-shadow-glass,0 0 1px #00000005, 0 0 8px #00000008, 0 2px 16px #00000006, 0 8px 32px #00000004));background-clip:padding-box;border:none;position:relative}.wui-glass:before{pointer-events:none;content:\"\";border-radius:inherit;background-color:var(--wui-color-glass-border,transparent);background-image:radial-gradient(var(--wui-glass-corner-radius,32px) at 100% 0, var(--wui-color-glass-shade,#00000008), transparent), radial-gradient(var(--wui-glass-corner-radius,32px) at 0 100%, var(--wui-color-glass-shade,#00000008), transparent), radial-gradient(calc(var(--wui-glass-corner-radius,32px) * 2) at 0 0, var(--wui-color-glass-corner,#ffffff80), transparent), radial-gradient(calc(var(--wui-glass-corner-radius,32px) * 2) at 100% 100%, var(--wui-color-glass-corner,#ffffff80), transparent);-webkit-mask-composite:xor;-webkit-mask-source-type:auto,auto;padding:1px;position:absolute;inset:0;-webkit-mask-image:linear-gradient(#000 0 0),linear-gradient(#000 0 0);mask-image:linear-gradient(#000 0 0),linear-gradient(#000 0 0);-webkit-mask-position:0 0,0 0;mask-position:0 0,0 0;-webkit-mask-size:auto,auto;mask-size:auto,auto;-webkit-mask-repeat:repeat,repeat;mask-repeat:repeat,repeat;-webkit-mask-clip:content-box,border-box;mask-clip:content-box,border-box;-webkit-mask-origin:content-box,border-box;mask-origin:content-box,border-box;-webkit-mask-composite:xor;mask-composite:exclude;-webkit-mask-source-type:auto,auto;mask-mode:match-source,match-source}";
3
3
  //#endregion
4
4
  export { e as default };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"glass.js","names":[],"sources":["../../src/assets/glass.css?inline"],"sourcesContent":["/*\n * 玻璃公共样式\n */\n\n.wui-glass {\n /*\n * 内部阴影变量在 glass 元素上自声明默认值,切断从祖先 glass 容器(drawer/dialog\n * body 等声明了 overlay 阴影的元素)沿 flattened tree 继承进 slotted 内容的\n * 通道;各组件在自身元素上的同变量配置(特异性/源顺序恒胜出)不受影响。\n */\n --wui-internal-glass-shadow: var(\n --wui-shadow-glass,\n 0 0 1px rgb(0 0 0 / 0.05),\n 0 0 8px rgb(0 0 0 / 0.03),\n 0 2px 16px rgb(0 0 0 / 0.025),\n 0 8px 32px rgb(0 0 0 / 0.015)\n );\n\n position: relative;\n\n border: none;\n\n background-color: var(--wui-color-surface-glass, rgb(250 250 250 / 0.34));\n background-clip: padding-box;\n\n /* Safari 16.4 只支持带 -webkit- 前缀的 backdrop-filter(无前缀 18 才支持);\n 前缀由 lightningcss 按 ?inline 管线默认 targets(含 ios16.4)自动生成,无需手写。 */\n backdrop-filter: blur(4px) saturate(160%) brightness(var(--wui-glass-brightness, 1.06));\n box-shadow:\n inset 0 0 0 0.5px color-mix(in srgb, var(--wui-color-glass-highlight, rgb(255 255 255 / 0.9)) 38%, transparent),\n inset 0 1px 0.5px color-mix(in srgb, var(--wui-color-glass-highlight, rgb(255 255 255 / 0.9)) 52%, transparent),\n inset 0 -0.5px 1px rgb(0 0 0 / 0.03),\n var(\n --wui-internal-glass-shadow,\n var(\n --wui-shadow-glass,\n 0 0 1px rgb(0 0 0 / 0.05),\n 0 0 8px rgb(0 0 0 / 0.03),\n 0 2px 16px rgb(0 0 0 / 0.025),\n 0 8px 32px rgb(0 0 0 / 0.015)\n )\n );\n}\n\n/*\n * 玻璃描边:底环 + 对角受光/背光。background-color 用 border 色铺满整圈(含圆角\n * 弧段,修复胶囊弧段无着色发暗的问题);叠两枚 corner→transparent radial 作左上/\n * 右下受光光泽,再叠两枚 shade→transparent radial(置于最上层)压暗右上/左下背光\n * 角,形成对角光影的玻璃立体感。四枚渐变均为锚定角点、半径 --wui-glass-corner-radius\n * 的固定物理尺寸正圆——视觉覆盖范围不随元素宽高缩放(百分比方案在大面板上暗边\n * 过长、小控件上过弱),各组件用与自身 border-radius 同源的值覆盖该变量即可联动。\n * 颜色由 theme 按明暗主题提供 --wui-color-glass-corner / --wui-color-glass-shade /\n * --wui-color-glass-border;-webkit-mask-* 旧语法(含 mask-composite:xor)由\n * lightningcss 按 targets 自动生成,无前缀 mask-composite 留给 Chrome 120+。\n */\n.wui-glass::before {\n pointer-events: none;\n content: '';\n\n position: absolute;\n inset: 0;\n\n padding: 1px;\n border-radius: inherit;\n\n background-color: var(--wui-color-glass-border, transparent);\n background-image:\n radial-gradient(\n var(--wui-glass-corner-radius, 32px) at 100% 0,\n var(--wui-color-glass-shade, rgb(0 0 0 / 0.06)),\n transparent\n ),\n radial-gradient(\n var(--wui-glass-corner-radius, 32px) at 0 100%,\n var(--wui-color-glass-shade, rgb(0 0 0 / 0.06)),\n transparent\n ),\n radial-gradient(\n calc(var(--wui-glass-corner-radius, 32px) * 2) at 0 0,\n var(--wui-color-glass-corner, rgb(255 255 255 / 0.08)),\n transparent\n ),\n radial-gradient(\n calc(var(--wui-glass-corner-radius, 32px) * 2) at 100% 100%,\n var(--wui-color-glass-corner, rgb(255 255 255 / 0.08)),\n transparent\n );\n\n mask:\n linear-gradient(#000 0 0) content-box,\n linear-gradient(#000 0 0);\n mask-composite: exclude;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"glass.js","names":[],"sources":["../../src/assets/glass.css?inline"],"sourcesContent":[".wui-glass {\n /*\n * 内部阴影变量在 glass 元素上自声明默认值,切断从祖先 glass 容器(drawer/dialog\n * body 等声明了 overlay 阴影的元素)沿 flattened tree 继承进 slotted 内容的\n * 通道;各组件在自身元素上的同变量配置(特异性/源顺序恒胜出)不受影响。\n */\n --wui-internal-glass-shadow: var(\n --wui-shadow-glass,\n 0 0 1px rgb(0 0 0 / 0.02),\n 0 0 8px rgb(0 0 0 / 0.03),\n 0 2px 16px rgb(0 0 0 / 0.025),\n 0 8px 32px rgb(0 0 0 / 0.015)\n );\n\n position: relative;\n\n border: none;\n\n background-color: var(--wui-color-surface-glass, rgb(250 250 250 / 0.34));\n background-clip: padding-box;\n\n /* Safari 16.4 只支持带 -webkit- 前缀的 backdrop-filter(无前缀 18 才支持);\n 前缀由 lightningcss 按 ?inline 管线默认 targets(含 ios16.4)自动生成,无需手写。 */\n backdrop-filter: blur(4px) saturate(160%) brightness(var(--wui-glass-brightness, 1.06));\n box-shadow:\n inset 0 0 0 0.5px color-mix(in srgb, var(--wui-color-glass-highlight, rgb(255 255 255 / 0.9)) 38%, transparent),\n inset 0 1px 0.5px color-mix(in srgb, var(--wui-color-glass-highlight, rgb(255 255 255 / 0.9)) 52%, transparent),\n inset 0 -0.5px 1px rgb(0 0 0 / 0.03),\n var(\n --wui-internal-glass-shadow,\n var(\n --wui-shadow-glass,\n 0 0 1px rgb(0 0 0 / 0.02),\n 0 0 8px rgb(0 0 0 / 0.03),\n 0 2px 16px rgb(0 0 0 / 0.025),\n 0 8px 32px rgb(0 0 0 / 0.015)\n )\n );\n}\n\n/*\n * 玻璃描边:底环 + 对角受光/背光。background-color 用 border 色铺满整圈(含圆角\n * 弧段,修复胶囊弧段无着色发暗的问题);叠两枚 corner→transparent radial 作左上/\n * 右下受光光泽,再叠两枚 shade→transparent radial(置于最上层)压暗右上/左下背光\n * 角,形成对角光影的玻璃立体感。四枚渐变均为锚定角点、半径 --wui-glass-corner-radius\n * 的固定物理尺寸正圆——视觉覆盖范围不随元素宽高缩放(百分比方案在大面板上暗边\n * 过长、小控件上过弱),各组件用与自身 border-radius 同源的值覆盖该变量即可联动。\n * 颜色由 theme 按明暗主题提供 --wui-color-glass-corner / --wui-color-glass-shade /\n * --wui-color-glass-border;-webkit-mask-* 旧语法(含 mask-composite:xor)由\n * lightningcss 按 targets 自动生成,无前缀 mask-composite 留给 Chrome 120+。\n */\n.wui-glass::before {\n pointer-events: none;\n content: '';\n\n position: absolute;\n inset: 0;\n\n padding: 1px;\n border-radius: inherit;\n\n background-color: var(--wui-color-glass-border, transparent);\n background-image:\n radial-gradient(\n var(--wui-glass-corner-radius, 32px) at 100% 0,\n var(--wui-color-glass-shade, rgb(0 0 0 / 0.03)),\n transparent\n ),\n radial-gradient(\n var(--wui-glass-corner-radius, 32px) at 0 100%,\n var(--wui-color-glass-shade, rgb(0 0 0 / 0.03)),\n transparent\n ),\n radial-gradient(\n calc(var(--wui-glass-corner-radius, 32px) * 2) at 0 0,\n var(--wui-color-glass-corner, rgb(255 255 255 / 0.5)),\n transparent\n ),\n radial-gradient(\n calc(var(--wui-glass-corner-radius, 32px) * 2) at 100% 100%,\n var(--wui-color-glass-corner, rgb(255 255 255 / 0.5)),\n transparent\n );\n\n mask:\n linear-gradient(#000 0 0) content-box,\n linear-gradient(#000 0 0);\n mask-composite: exclude;\n}\n"],"mappings":""}
@@ -0,0 +1,6 @@
1
+ //#region src/assets/selection-control.css?inline
2
+ var e = "*,:before,:after{box-sizing:border-box}:host{cursor:default;vertical-align:middle;align-items:center;display:inline-flex}:host([disabled]){cursor:not-allowed}:where(.wui-radio,.wui-checkbox){cursor:inherit;-webkit-user-select:none;user-select:none;font-size:var(--wui-font-size,14px);color:var(--wui-color-text,#1b1b1b);align-items:center;gap:10px;line-height:1;display:inline-flex}:where(.wui-radio,.wui-checkbox):focus-visible{border-radius:var(--wui-radius-control,calc(infinity * 1px));outline:var(--wui-focus-ring-width,3px) solid var(--wui-color-focus-ring,#08f6);outline-offset:2px}:host([data-wui-pointer-focus]) :where(.wui-radio,.wui-checkbox):focus-visible{outline-color:#0000}:where(.wui-radio.is-disabled,.wui-checkbox.is-disabled){cursor:not-allowed;color:var(--wui-color-text-secondary,#6a6a6a);opacity:.4}:where(.wui-radio-circle,.wui-checkbox-box){isolation:isolate;width:var(--wui-selection-control-size,18px);height:var(--wui-selection-control-size,18px);background-color:var(--wui-color-surface-control,#dfdfdf);transition:background-color var(--wui-duration-trigger,.16s) ease;flex-shrink:0;justify-content:center;align-items:center;display:flex;position:relative}:where(.wui-radio-circle,.wui-checkbox-box):before{content:\"\";z-index:-1;border-radius:inherit;position:absolute;inset:0}:where(.wui-radio-label,.wui-checkbox-label){pointer-events:none}@media (hover:hover) and (pointer:fine){:where(.wui-radio,.wui-checkbox):hover:not(.is-disabled,.is-checked) :where(.wui-radio-circle,.wui-checkbox-box):before{background-color:color-mix(in srgb, var(--wui-color-text,#1b1b1b) 6%, transparent)}}";
3
+ //#endregion
4
+ export { e as default };
5
+
6
+ //# sourceMappingURL=selection-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selection-control.js","names":[],"sources":["../../src/assets/selection-control.css?inline"],"sourcesContent":["/*\n * radio / checkbox 共享外壳。\n *\n * 指示器只有 18px。宿主一旦走 inline-block,页面行高会在宿主内生成匿名行盒,把盒子撑到\n * 行高(Tailwind preflight 下是 24px),且多出的空隙落在哪一侧取决于内部基线落点,radio 与\n * checkbox 因此一个贴顶一个贴底。改成 inline-flex 后宿主高度由内容决定,行高不再参与,\n * 与相邻文字的对齐交给 vertical-align 显式声明。\n */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n cursor: default;\n display: inline-flex;\n align-items: center;\n vertical-align: middle;\n}\n\n:host([disabled]) {\n cursor: not-allowed;\n}\n\n/* :where() 保持零特异性,组件级配置(圆角、选中色、内部图形)始终覆盖共享层。 */\n:where(.wui-radio, .wui-checkbox) {\n cursor: inherit;\n user-select: none;\n\n display: inline-flex;\n gap: 10px;\n align-items: center;\n\n font-size: var(--wui-font-size, 14px);\n line-height: 1;\n color: var(--wui-color-text, #1b1b1b);\n\n &:focus-visible {\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n outline: var(--wui-focus-ring-width, 3px) solid var(--wui-color-focus-ring, rgb(0 136 255 / 0.4));\n outline-offset: 2px;\n }\n}\n\n:host([data-wui-pointer-focus]) :where(.wui-radio, .wui-checkbox):focus-visible {\n outline-color: transparent;\n}\n\n:where(.wui-radio.is-disabled, .wui-checkbox.is-disabled) {\n cursor: not-allowed;\n color: var(--wui-color-text-secondary, #6a6a6a);\n opacity: 0.4;\n}\n\n/*\n * 指示器底色取控件档。surface-raised 在深色下(#2c2c2e)离页面底色(#242628)只有 8 个\n * 亮度点,未选中时几乎看不出有一个圆/方框;surface-control 才是主题里「控件自己的底」\n * 那一档(中性按钮同档),深色比页面高 22 个亮度点,浅色下也从近白的 #f2f2f7 变成可读的 #dfdfdf。\n */\n:where(.wui-radio-circle, .wui-checkbox-box) {\n isolation: isolate;\n position: relative;\n\n display: flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n\n width: var(--wui-selection-control-size, 18px);\n height: var(--wui-selection-control-size, 18px);\n\n background-color: var(--wui-color-surface-control, #dfdfdf);\n\n transition: background-color var(--wui-duration-trigger, 160ms) ease;\n}\n\n/*\n * 状态层走覆盖层,不占用指示器自己的 background-color:同一个属性没法一半过渡一半即时,\n * 而这里要的是 hover 当帧切换、选中态的 accent 按 --wui-duration-trigger 淡入淡出。\n * z-index: -1 配父级 isolation 把覆盖层夹在控件底与圆点/勾之间,半径跟着指示器走。\n */\n:where(.wui-radio-circle, .wui-checkbox-box)::before {\n content: '';\n\n position: absolute;\n z-index: -1;\n inset: 0;\n\n border-radius: inherit;\n}\n\n:where(.wui-radio-label, .wui-checkbox-label) {\n pointer-events: none;\n}\n\n/*\n * 未激活态的 hover 状态层。用半透明色而不是混进控件底:覆盖层本就压在不透明的控件底上,\n * 合成结果与 color-mix(text 6%, surface-control) 一致,深色档同样成立。\n * 标签文字带 pointer-events: none,命中落回 label 自身,所以整条触发区(含右侧文字)\n * 的 hover 都会作用到指示器。hover 只在真正的指针设备上生效,避免触屏留态。\n */\n@media (hover: hover) and (pointer: fine) {\n :where(.wui-radio, .wui-checkbox):hover:not(.is-disabled, .is-checked)\n :where(.wui-radio-circle, .wui-checkbox-box)::before {\n background-color: color-mix(in srgb, var(--wui-color-text, #1b1b1b) 6%, transparent);\n }\n}\n"],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"toast-containers.js","names":[],"sources":["../../src/assets/toast-containers.css?inline"],"sourcesContent":["/* Toast 容器定位 — 由 toast 命令式 API 动态注入到 body */\n\n.wui-toast-container {\n --wui-toast-viewport-gap: 16px;\n --wui-toast-container-padding: 40px;\n pointer-events: none;\n\n position: fixed;\n z-index: var(--wui-layer-toast, 200);\n\n overflow-y: auto;\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n gap: 8px;\n\n /* 负向定位只露出可见 gap;剩余 padding 继续作为阴影裁剪缓冲区 */\n max-height: calc(100vh + 2 * (var(--wui-toast-container-padding) - var(--wui-toast-viewport-gap)));\n\n /* padding 需要 >= 阴影偏移 + 模糊半径,否则 overflow 会裁剪子元素 box-shadow */\n padding: var(--wui-toast-container-padding);\n}\n\n.wui-toast-container > * {\n pointer-events: auto;\n}\n\n/* ===== 顶部位置 ===== */\n\n.wui-toast-top-left {\n top: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n left: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n align-items: flex-start;\n}\n\n.wui-toast-top-center {\n top: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n left: 50%;\n transform: translateX(-50%);\n align-items: center;\n}\n\n.wui-toast-top-right {\n top: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n right: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n align-items: flex-end;\n}\n\n/* ===== 底部位置 ===== */\n\n.wui-toast-bottom-left {\n bottom: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n left: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n flex-direction: column-reverse;\n align-items: flex-start;\n}\n\n.wui-toast-bottom-center {\n bottom: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n left: 50%;\n transform: translateX(-50%);\n\n flex-direction: column-reverse;\n align-items: center;\n}\n\n.wui-toast-bottom-right {\n right: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n bottom: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n flex-direction: column-reverse;\n align-items: flex-end;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"toast-containers.js","names":[],"sources":["../../src/assets/toast-containers.css?inline"],"sourcesContent":["/* Toast 容器定位 — 由 toast 命令式 API 动态注入到 body */\n\n.wui-toast-container {\n --wui-toast-viewport-gap: 16px;\n --wui-toast-container-padding: 40px;\n pointer-events: none;\n\n position: fixed;\n z-index: var(--wui-layer-toast, 200);\n\n overflow-y: auto;\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n gap: 8px;\n\n /* 负向定位只露出可见 gap;剩余 padding 继续作为阴影裁剪缓冲区 */\n max-height: calc(100vh + 2 * (var(--wui-toast-container-padding) - var(--wui-toast-viewport-gap)));\n\n /* padding 需要 >= 阴影偏移 + 模糊半径,否则 overflow 会裁剪子元素 box-shadow */\n padding: var(--wui-toast-container-padding);\n}\n\n.wui-toast-container > * {\n pointer-events: auto;\n}\n\n.wui-toast-top-left {\n top: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n left: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n align-items: flex-start;\n}\n\n.wui-toast-top-center {\n top: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n left: 50%;\n transform: translateX(-50%);\n align-items: center;\n}\n\n.wui-toast-top-right {\n top: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n right: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n align-items: flex-end;\n}\n\n.wui-toast-bottom-left {\n bottom: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n left: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n flex-direction: column-reverse;\n align-items: flex-start;\n}\n\n.wui-toast-bottom-center {\n bottom: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n left: 50%;\n transform: translateX(-50%);\n\n flex-direction: column-reverse;\n align-items: center;\n}\n\n.wui-toast-bottom-right {\n right: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n bottom: calc(var(--wui-toast-viewport-gap) - var(--wui-toast-container-padding));\n flex-direction: column-reverse;\n align-items: flex-end;\n}\n"],"mappings":""}