@greypan/web-ui 8.1.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.CN.md +309 -135
- package/README.md +311 -135
- package/dist/assets/glass.js +1 -1
- package/dist/assets/glass.js.map +1 -1
- package/dist/assets/menu-portal.js +1 -1
- package/dist/assets/menu-portal.js.map +1 -1
- package/dist/assets/modal-surface.js +6 -0
- package/dist/assets/modal-surface.js.map +1 -0
- package/dist/assets/selection-control.js +1 -1
- package/dist/assets/selection-control.js.map +1 -1
- package/dist/assets/toast-containers.js +1 -1
- package/dist/assets/toast-containers.js.map +1 -1
- package/dist/components/autocomplete/index.d.ts +3 -2
- package/dist/components/autocomplete/index.js +40 -44
- package/dist/components/autocomplete/index.js.map +1 -1
- package/dist/components/autocomplete/style.js +1 -1
- package/dist/components/autocomplete/style.js.map +1 -1
- package/dist/components/avatar/index.js.map +1 -1
- package/dist/components/avatar/style.js +1 -1
- package/dist/components/avatar/style.js.map +1 -1
- package/dist/components/back-top/style.js +1 -1
- package/dist/components/back-top/style.js.map +1 -1
- package/dist/components/badge/style.js +1 -1
- package/dist/components/badge/style.js.map +1 -1
- package/dist/components/button/style.js +1 -1
- package/dist/components/button/style.js.map +1 -1
- package/dist/components/button-group/index.js +0 -1
- package/dist/components/button-group/index.js.map +1 -1
- package/dist/components/button-group/style.js +1 -1
- package/dist/components/button-group/style.js.map +1 -1
- package/dist/components/checkbox/index.js +4 -4
- package/dist/components/checkbox/index.js.map +1 -1
- package/dist/components/checkbox-group/style.js +1 -1
- package/dist/components/checkbox-group/style.js.map +1 -1
- package/dist/components/context-menu/index.d.ts +182 -3
- package/dist/components/context-menu/index.js +166 -91
- package/dist/components/context-menu/index.js.map +1 -1
- package/dist/components/context-menu/scrim.js +6 -0
- package/dist/components/context-menu/scrim.js.map +1 -0
- package/dist/components/context-menu/style.js +1 -1
- package/dist/components/context-menu/style.js.map +1 -1
- package/dist/components/dialog/index.d.ts +9 -0
- package/dist/components/dialog/index.js +68 -46
- package/dist/components/dialog/index.js.map +1 -1
- package/dist/components/dialog/style.js +1 -1
- package/dist/components/dialog/style.js.map +1 -1
- package/dist/components/drawer/index.d.ts +45 -0
- package/dist/components/drawer/index.js +122 -53
- package/dist/components/drawer/index.js.map +1 -1
- package/dist/components/drawer/style.js +1 -1
- package/dist/components/drawer/style.js.map +1 -1
- package/dist/components/dropdown/index.d.ts +12 -0
- package/dist/components/dropdown/index.js +13 -10
- package/dist/components/dropdown/index.js.map +1 -1
- package/dist/components/dropdown-divider/style.js +1 -1
- package/dist/components/dropdown-divider/style.js.map +1 -1
- package/dist/components/dropdown-header/style.js +1 -1
- package/dist/components/dropdown-header/style.js.map +1 -1
- package/dist/components/dropdown-item/style.js +1 -1
- package/dist/components/dropdown-item/style.js.map +1 -1
- package/dist/components/editable-text/index.d.ts +39 -0
- package/dist/components/editable-text/index.js +6 -2
- package/dist/components/editable-text/index.js.map +1 -1
- package/dist/components/empty/index.d.ts +21 -4
- package/dist/components/empty/index.js +37 -32
- package/dist/components/empty/index.js.map +1 -1
- package/dist/components/empty/style.js +1 -1
- package/dist/components/empty/style.js.map +1 -1
- package/dist/components/image-preview/index.js +75 -63
- package/dist/components/image-preview/index.js.map +1 -1
- package/dist/components/image-preview/style.js +1 -1
- package/dist/components/image-preview/style.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +38 -37
- package/dist/components/input/style.js +1 -1
- package/dist/components/input/style.js.map +1 -1
- package/dist/components/input-number/style.js +1 -1
- package/dist/components/input-number/style.js.map +1 -1
- package/dist/components/layout/index.d.ts +21 -6
- package/dist/components/layout/index.js +47 -27
- package/dist/components/layout/index.js.map +1 -1
- package/dist/components/layout/style.js +1 -1
- package/dist/components/layout/style.js.map +1 -1
- package/dist/components/middle-ellipsis/index.d.ts +100 -0
- package/dist/components/middle-ellipsis/index.js +112 -0
- package/dist/components/middle-ellipsis/index.js.map +1 -0
- package/dist/components/middle-ellipsis/style.js +6 -0
- package/dist/components/middle-ellipsis/style.js.map +1 -0
- package/dist/components/middle-ellipsis/truncate.d.ts +62 -0
- package/dist/components/middle-ellipsis/truncate.js +45 -0
- package/dist/components/middle-ellipsis/truncate.js.map +1 -0
- package/dist/components/option/style.js +1 -1
- package/dist/components/option/style.js.map +1 -1
- package/dist/components/popover/index.js.map +1 -1
- package/dist/components/popover/style.js +1 -1
- package/dist/components/popover/style.js.map +1 -1
- package/dist/components/radio/style.js +1 -1
- package/dist/components/radio/style.js.map +1 -1
- package/dist/components/radio-group/style.js +1 -1
- package/dist/components/radio-group/style.js.map +1 -1
- package/dist/components/segmented/index.d.ts +5 -0
- package/dist/components/segmented/index.js +45 -28
- package/dist/components/segmented/index.js.map +1 -1
- package/dist/components/segmented/style.js +1 -1
- package/dist/components/segmented/style.js.map +1 -1
- package/dist/components/segmented-trigger/style.js +1 -1
- package/dist/components/segmented-trigger/style.js.map +1 -1
- package/dist/components/select/index.js +0 -1
- package/dist/components/select/index.js.map +1 -1
- package/dist/components/select/style.js +1 -1
- package/dist/components/select/style.js.map +1 -1
- package/dist/components/slider/style.js +1 -1
- package/dist/components/slider/style.js.map +1 -1
- package/dist/components/spinner/style.js +1 -1
- package/dist/components/spinner/style.js.map +1 -1
- package/dist/components/svg-draw-lines/index.js.map +1 -1
- package/dist/components/switch/style.js +1 -1
- package/dist/components/switch/style.js.map +1 -1
- package/dist/components/textarea/index.js +2 -2
- package/dist/components/textarea/index.js.map +1 -1
- package/dist/components/textarea/style.js +1 -1
- package/dist/components/textarea/style.js.map +1 -1
- package/dist/components/theme/style.js +1 -1
- package/dist/components/theme/style.js.map +1 -1
- package/dist/components/toast/style.js +1 -1
- package/dist/components/toast/style.js.map +1 -1
- package/dist/components/tooltip/index.js.map +1 -1
- package/dist/components/tooltip/style.js +1 -1
- package/dist/components/tooltip/style.js.map +1 -1
- package/dist/icons/generated/akar-icons-circle-check.d.ts +6 -0
- package/dist/icons/generated/akar-icons-circle-check.js +10 -0
- package/dist/icons/generated/akar-icons-circle-check.js.map +1 -0
- package/dist/icons/generated/bi-check-lg.d.ts +6 -0
- package/dist/icons/generated/bi-check-lg.js +10 -0
- package/dist/icons/generated/bi-check-lg.js.map +1 -0
- package/dist/icons/generated/fluent-tag-search-24-regular.d.ts +6 -0
- package/dist/icons/generated/fluent-tag-search-24-regular.js +10 -0
- package/dist/icons/generated/fluent-tag-search-24-regular.js.map +1 -0
- package/dist/icons/generated/mdi-tag-search-outline.d.ts +6 -0
- package/dist/icons/generated/mdi-tag-search-outline.js +10 -0
- package/dist/icons/generated/mdi-tag-search-outline.js.map +1 -0
- package/dist/icons/index.d.ts +4 -0
- package/dist/icons/index.js +5 -1
- package/dist/scrollbars/index.d.ts +33 -0
- package/dist/scrollbars/index.js +18 -0
- package/dist/scrollbars/index.js.map +1 -0
- package/dist/scrollbars/style.css +10 -0
- package/dist/shared/element-height/index.js +47 -0
- package/dist/shared/element-height/index.js.map +1 -0
- package/dist/shared/element-width/index.js +37 -0
- package/dist/shared/element-width/index.js.map +1 -0
- package/dist/shared/menu-portal/menu-portal.js +11 -11
- package/dist/shared/menu-portal/menu-portal.js.map +1 -1
- package/dist/shared/menu-portal/menu-tree.js +2 -2
- package/dist/shared/menu-portal/menu-tree.js.map +1 -1
- package/dist/shared/overlay/anchored-panel.js.map +1 -1
- package/dist/shared/overlay/native-dialog-presence.js +23 -11
- package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
- package/dist/shared/overlay/nested-drawer-layers.js +30 -23
- package/dist/shared/overlay/nested-drawer-layers.js.map +1 -1
- package/dist/shared/scroll-lock/scroll-lock.js +3 -3
- package/dist/shared/scroll-lock/scroll-lock.js.map +1 -1
- package/package.json +19 -5
package/README.md
CHANGED
|
@@ -25,10 +25,37 @@ import '@greypan/web-ui'
|
|
|
25
25
|
// import '@greypan/web-ui/components/button'
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
A subpath import registers only the component it names, so import every tag the page uses.
|
|
29
|
+
|
|
28
30
|
```html
|
|
29
31
|
<web-ui-button variant="primary">Click me</web-ui-button> <web-ui-icon .icon="${lucidePlus}"></web-ui-icon>
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
## Scrollbars
|
|
35
|
+
|
|
36
|
+
`@greypan/web-ui/scrollbars` carries the repo-wide scrollbar behavior for [OverlayScrollbars](https://github.com/KingSora/OverlayScrollbars)
|
|
37
|
+
hosts: a macOS-aligned theme, auto-hide that reveals the bar while scrolling, and clicking the track scrolling to that spot.
|
|
38
|
+
Import the stylesheet once per document and pass the options to whichever adapter you initialize with.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import '@greypan/web-ui/scrollbars.css'
|
|
42
|
+
import { webUiScrollbarsOptions } from '@greypan/web-ui/scrollbars'
|
|
43
|
+
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react' // or overlayscrollbars-vue
|
|
44
|
+
|
|
45
|
+
;<OverlayScrollbarsComponent options={webUiScrollbarsOptions}>{children}</OverlayScrollbarsComponent>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `overlayscrollbars` is an optional peer dependency and is only needed by this subpath. The stylesheet already
|
|
49
|
+
contains the library's structural styles, so it replaces `import 'overlayscrollbars/overlayscrollbars.css'`.
|
|
50
|
+
- Keep the container's own `overflow` declaration (`overflow-y-auto`, …). OverlayScrollbars overrides it once it
|
|
51
|
+
takes over, but it is what keeps the container scrollable until then.
|
|
52
|
+
- The target must be a plain scroll container. OverlayScrollbars turns the host into a flex row and moves its
|
|
53
|
+
children into a generated viewport, so a host whose own layout carries meaning — a grid, a centred box, a `ul`/`ol`
|
|
54
|
+
— loses it.
|
|
55
|
+
- OverlayScrollbars only changes the element it is initialized on; children keep their native scrollbars. To cover
|
|
56
|
+
the document scroller, initialize `document.body` — `window.scrollY` / `window.scrollTo` keep their native
|
|
57
|
+
semantics there.
|
|
58
|
+
|
|
32
59
|
## Framework Setup
|
|
33
60
|
|
|
34
61
|
### Cross-framework API conventions
|
|
@@ -304,6 +331,7 @@ All form controls participate in native `FormData`, constraint validation, `form
|
|
|
304
331
|
| **Data Display** | [`<web-ui-avatar>`](#web-ui-avatar) |
|
|
305
332
|
| | [`<web-ui-badge>`](#web-ui-badge) |
|
|
306
333
|
| | [`<web-ui-empty>`](#web-ui-empty) |
|
|
334
|
+
| | [`<web-ui-middle-ellipsis>`](#web-ui-middle-ellipsis) |
|
|
307
335
|
| | [`<web-ui-icon>`](#web-ui-icon) |
|
|
308
336
|
| | [`<web-ui-spinner>`](#web-ui-spinner) |
|
|
309
337
|
| **Layout & Utility** | [`<web-ui-layout>`](#web-ui-layout) |
|
|
@@ -354,24 +382,24 @@ Text input with clearable, prefix/suffix slots.
|
|
|
354
382
|
|
|
355
383
|
Multi-line text input with auto-resize.
|
|
356
384
|
|
|
357
|
-
| Attribute | Type | Default | Description
|
|
358
|
-
| ----------------- | --------- | ------- |
|
|
359
|
-
| `value` | `string` | `''` | Textarea value
|
|
360
|
-
| `placeholder` | `string` | `''` | Placeholder text
|
|
361
|
-
| `rows` | `number` | `3` | Visible rows
|
|
362
|
-
| `name` | `string` | `''` | Form field name
|
|
363
|
-
| `disabled` | `boolean` | `false` | Disabled state
|
|
364
|
-
| `readonly` | `boolean` | `false` | Read-only state
|
|
365
|
-
| `required` | `boolean` | `false` | Required validation
|
|
366
|
-
| `clearable` | `boolean` | `false` | Show clear button
|
|
367
|
-
| `full` | `boolean` | `false` | Full width
|
|
368
|
-
| `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring
|
|
369
|
-
| `autosize` | `boolean` | `false` | Auto-resize height
|
|
370
|
-
| `max-height` | `number` | `0` | Autosize max height (px); `0` = unlimited
|
|
371
|
-
| `minlength` | `number` | — | Minimum length validation
|
|
372
|
-
| `maxlength` | `number` | — | Maximum length validation
|
|
373
|
-
| `aria-label` | `string` | — | Accessible label
|
|
374
|
-
| `aria-labelledby` | `string` | — | Accessible label reference
|
|
385
|
+
| Attribute | Type | Default | Description |
|
|
386
|
+
| ----------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------- |
|
|
387
|
+
| `value` | `string` | `''` | Textarea value |
|
|
388
|
+
| `placeholder` | `string` | `''` | Placeholder text |
|
|
389
|
+
| `rows` | `number` | `3` | Visible rows |
|
|
390
|
+
| `name` | `string` | `''` | Form field name |
|
|
391
|
+
| `disabled` | `boolean` | `false` | Disabled state |
|
|
392
|
+
| `readonly` | `boolean` | `false` | Read-only state |
|
|
393
|
+
| `required` | `boolean` | `false` | Required validation |
|
|
394
|
+
| `clearable` | `boolean` | `false` | Show clear button |
|
|
395
|
+
| `full` | `boolean` | `false` | Full width |
|
|
396
|
+
| `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring |
|
|
397
|
+
| `autosize` | `boolean` | `false` | Auto-resize height; also forces `resize: none`, because the native grip fights a script-owned height |
|
|
398
|
+
| `max-height` | `number` | `0` | Autosize max height (px); `0` = unlimited |
|
|
399
|
+
| `minlength` | `number` | — | Minimum length validation |
|
|
400
|
+
| `maxlength` | `number` | — | Maximum length validation |
|
|
401
|
+
| `aria-label` | `string` | — | Accessible label |
|
|
402
|
+
| `aria-labelledby` | `string` | — | Accessible label reference |
|
|
375
403
|
|
|
376
404
|
**Events:** `input`, `change`, `focus`, `blur`
|
|
377
405
|
|
|
@@ -390,14 +418,15 @@ Multi-line text input with auto-resize.
|
|
|
390
418
|
|
|
391
419
|
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.
|
|
392
420
|
|
|
393
|
-
| Attribute
|
|
394
|
-
|
|
|
395
|
-
| `value`
|
|
396
|
-
| `placeholder`
|
|
397
|
-
| `name`
|
|
398
|
-
| `disabled`
|
|
399
|
-
| `readonly`
|
|
400
|
-
| `
|
|
421
|
+
| Attribute | Type | Default | Description |
|
|
422
|
+
| ------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
423
|
+
| `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 |
|
|
424
|
+
| `placeholder` | `string` | `''` | Text shown while the value is empty |
|
|
425
|
+
| `name` | `string` | `''` | Form field name |
|
|
426
|
+
| `disabled` | `boolean` | `false` | Disabled state; behavior only, no visual dimming |
|
|
427
|
+
| `readonly` | `boolean` | `false` | Read-only state; focus, selection, and copying remain available, but edits cannot commit |
|
|
428
|
+
| `commit-on-unmount` | `boolean` | `false` | End an in-progress edit by committing when the element is unmounted; see below |
|
|
429
|
+
| `aria-label` | `string` | — | Accessible label |
|
|
401
430
|
|
|
402
431
|
**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', ...)`
|
|
403
432
|
|
|
@@ -407,6 +436,18 @@ Clicking places the caret at the clicked offset; keyboard focus places it at the
|
|
|
407
436
|
|
|
408
437
|
`select()` enters edit mode with the whole content selected, and re-selects it when already editing. While `disabled` it does nothing, matching `focus()`. While `readonly`, focus and selection remain available, but input is rejected and leaving edit mode does not dispatch `change`.
|
|
409
438
|
|
|
439
|
+
**Unmounting mid-edit.** What happens when an element being edited is removed from the DOM depends on the engine, and the difference is the reason this property exists:
|
|
440
|
+
|
|
441
|
+
- **Chromium** dispatches `blur` on the editing layer, so a plain `value` commit already happens today and the default `false` leaves that untouched. `blur` and the `change` it triggers are dispatched synchronously _after_ the unmount callback has run, so the commit arrives after whatever state the caller updates in response to the removal has already been applied.
|
|
442
|
+
- **WebKit (Safari, iOS)** dispatches no `blur` at all. With the default `false` nothing is committed and the draft is dropped along with the element.
|
|
443
|
+
- **Gecko (Firefox)** is **unverified**: no Firefox build would start on the machine this was measured on, so nothing is claimed here. Treat Gecko as unknown rather than as matching either column above, and enable the property if you need the unmount commit to be the thing that saves the draft.
|
|
444
|
+
|
|
445
|
+
If your code relies on a draft surviving the row being recycled, **enable `commit-on-unmount`**: on Safari and iOS it is the only thing that saves the draft, and in Chromium it moves the commit ahead of the removal-driven `blur`. In both engines the unmount then ends an open editing session by committing — the draft becomes the new value, edit mode exits, and `change` is dispatched exactly once, from the same code `Enter` and `blur` use. Measured on Chromium and WebKit; the browser test suite runs on Chromium only, so the WebKit result comes from a one-off cross-engine run rather than from CI, and Gecko was not measured at all.
|
|
446
|
+
|
|
447
|
+
Unlike `Enter` and `blur`, which a user action triggers and which therefore always count as an intent to commit, unmount carries no such intent. The commit therefore only fires when the draft really differs from the value captured when editing began, so a session that changed nothing does not produce an empty `change`. An element that is not editing at all has nothing to commit and dispatches nothing, and neither does one that is `disabled` or `readonly` — matching the `blur` path, which dispatches no commit in either state. What happens to a draft that already exists differs between the two, though: under `disabled` the draft stays on the element, while under `readonly` it is genuinely lost. `readonly` does not retract input typed earlier, and on unmount there is no `blur` left to commit it — on WebKit there is no `blur` at all. The path is reachable: enter editing while editable, type a draft, flip to `readonly`, then unmount. It is not treated as a cancel, because cancelling means restoring the value editing started from and there is no focus to return or host to hand a result to at unmount; the user simply sees their typing disappear, with neither `change` nor `cancel`.
|
|
448
|
+
|
|
449
|
+
The commit fires from the unmount callback, so `change` still travels the composed path and reaches listeners on the component or any ancestor, including ancestors that are themselves being unmounted: dispatching does not require an ancestor to still be in the document. Listeners must therefore not assume the component is still connected; hand the result to state outside the host tree explicitly (for example in a `queueMicrotask`) when it has to outlive the removal. Note that this reach is not guaranteed for an unmount React drives from its own render: such a removal never reaches React's delegated event root, so `onChange` will not fire there and the value has to be read from the element instead — the same caveat that applies to `cancel` above. Setting `display: none` on an ancestor is not a removal — the component stays connected and dispatches nothing in either engine, so end the edit explicitly in that case.
|
|
450
|
+
|
|
410
451
|
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.
|
|
411
452
|
|
|
412
453
|
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.
|
|
@@ -504,9 +545,9 @@ Editable combobox with input filtering and single option selection.
|
|
|
504
545
|
|
|
505
546
|
**Methods:** `focus()`, `blur()`
|
|
506
547
|
|
|
507
|
-
**Slots:** `default` (project `<web-ui-option>` elements), `trigger` (custom trigger content, replacing the default input)
|
|
548
|
+
**Slots:** `default` (project `<web-ui-option>` elements), `trigger` (custom trigger content, replacing the default input)
|
|
508
549
|
|
|
509
|
-
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.
|
|
550
|
+
Typing filters the option list by label (`contains` or `prefix`, or `none` to disable filtering). When no option matches, the dropdown stays closed; deleting characters so at least one option matches reopens it. 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.
|
|
510
551
|
|
|
511
552
|
**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.
|
|
512
553
|
|
|
@@ -522,9 +563,7 @@ The custom trigger exposes the same contract as the default one: a string `value
|
|
|
522
563
|
|
|
523
564
|
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; 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.
|
|
524
565
|
|
|
525
|
-
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
|
|
526
|
-
|
|
527
|
-
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.
|
|
566
|
+
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 keeps the dropdown closed. `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.
|
|
528
567
|
|
|
529
568
|
**CSS Custom Properties:**
|
|
530
569
|
|
|
@@ -681,11 +720,11 @@ Syncs child checkbox `checked` state. `disabled` supplies inherited effective di
|
|
|
681
720
|
|
|
682
721
|
##### Tokens
|
|
683
722
|
|
|
684
|
-
| Token | Default
|
|
685
|
-
| -------------------------- |
|
|
686
|
-
| `--wui-checkbox-group-gap` | `
|
|
723
|
+
| Token | Default | Description |
|
|
724
|
+
| -------------------------- | -------------------- | ----------------------------- |
|
|
725
|
+
| `--wui-checkbox-group-gap` | `var(--wui-space-2)` | Gap between member checkboxes |
|
|
687
726
|
|
|
688
|
-
Set it on the group element itself, or on an ancestor inside the same `<web-ui-theme>` scope. The theme host declares
|
|
727
|
+
Set it on the group element itself, or on an ancestor inside the same `<web-ui-theme>` scope. The theme host declares the spacing step for its own subtree, so an override placed above a `<web-ui-theme>` element does not reach the group. Its default now resolves through `--wui-space-2`, so overriding that step on the theme host also moves the gap between members; setting this token still wins over both.
|
|
689
728
|
|
|
690
729
|
```css
|
|
691
730
|
web-ui-checkbox-group {
|
|
@@ -715,11 +754,11 @@ Radio group managing single selection.
|
|
|
715
754
|
|
|
716
755
|
##### Tokens
|
|
717
756
|
|
|
718
|
-
| Token | Default
|
|
719
|
-
| ----------------------- |
|
|
720
|
-
| `--wui-radio-group-gap` | `
|
|
757
|
+
| Token | Default | Description |
|
|
758
|
+
| ----------------------- | -------------------- | ------------------------- |
|
|
759
|
+
| `--wui-radio-group-gap` | `var(--wui-space-2)` | Gap between member radios |
|
|
721
760
|
|
|
722
|
-
Set it on the group element itself, or on an ancestor inside the same `<web-ui-theme>` scope. The theme host declares
|
|
761
|
+
Set it on the group element itself, or on an ancestor inside the same `<web-ui-theme>` scope. The theme host declares the spacing step for its own subtree, so an override placed above a `<web-ui-theme>` element does not reach the group. Its default now resolves through `--wui-space-2`, so overriding that step on the theme host also moves the gap between members; setting this token still wins over both.
|
|
723
762
|
|
|
724
763
|
```css
|
|
725
764
|
web-ui-radio-group {
|
|
@@ -802,6 +841,7 @@ Modal dialog using native `<dialog>` with `showModal()`.
|
|
|
802
841
|
| Attribute | Type | Default | Description |
|
|
803
842
|
| ------------------- | --------- | ------- | ------------------------------------------------------------------------------ |
|
|
804
843
|
| `open` | `boolean` | `false` | Dialog visibility |
|
|
844
|
+
| `closable` | `boolean` | `false` | Show the built-in close button |
|
|
805
845
|
| `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
|
|
806
846
|
| `no-backdrop-close` | `boolean` | `false` | Do not close on backdrop click |
|
|
807
847
|
| `no-escape-close` | `boolean` | `false` | Do not close when Escape is pressed |
|
|
@@ -815,22 +855,31 @@ Modal dialog using native `<dialog>` with `showModal()`.
|
|
|
815
855
|
|
|
816
856
|
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.
|
|
817
857
|
|
|
858
|
+
With `closable`, a built-in close button is rendered in both content modes, and it follows the same close path as Escape and backdrop clicks (so `controlled` applies to it identically). Both modes place it the same way: it is absolutely positioned at the card's top-right corner, offset from the card edges by `--wui-dialog-close-top` / `--wui-dialog-close-right` (both default to `20px`; the drawer close button remains at `16px`). Only a `body` slot makes the title row disappear; the title-mode title reserves room for the button so a long title never runs under it. It uses the same `ooui:close` icon as the drawer close button. Omitting `closable` renders nothing at all — no button and no extra wrapper.
|
|
859
|
+
|
|
818
860
|
> **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.
|
|
819
861
|
|
|
820
862
|
**CSS Custom Properties:**
|
|
821
863
|
|
|
822
|
-
| Property
|
|
823
|
-
|
|
|
824
|
-
| `--wui-dialog-width`
|
|
825
|
-
| `--wui-dialog-max-height`
|
|
826
|
-
| `--wui-dialog-
|
|
827
|
-
| `--wui-dialog-
|
|
828
|
-
| `--wui-dialog-
|
|
829
|
-
| `--wui-dialog-
|
|
830
|
-
| `--wui-dialog-
|
|
831
|
-
| `--wui-dialog-
|
|
832
|
-
| `--wui-dialog-
|
|
833
|
-
| `--wui-dialog-
|
|
864
|
+
| Property | Default | Description |
|
|
865
|
+
| --------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
866
|
+
| `--wui-dialog-width` | `360px` | Dialog width |
|
|
867
|
+
| `--wui-dialog-max-height` | `90vh` / `min(90vh, 90dvh)` (dvh engines) | Max height of the content area (`.desc`), not of the card. Two fallback tiers: `90vh` on the base rule, `min(90vh, 90dvh)` inside `@supports (height: 100dvh)` — an engine without `dvh` support only ever gets `90vh`. See the note below for the chrome conversion |
|
|
868
|
+
| `--wui-dialog-close-top` | `20px` | `closable` button offset from the card top |
|
|
869
|
+
| `--wui-dialog-close-right` | `20px` | `closable` button offset from the card right edge |
|
|
870
|
+
| `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | Backdrop background |
|
|
871
|
+
| `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | Glass card background, falls back to `rgb(248 248 248 / 0.92)` |
|
|
872
|
+
| `--wui-dialog-padding` | `20px 24px 24px` | Dialog surface padding |
|
|
873
|
+
| `--wui-dialog-title-gap` | `16px` | Spacing below the title |
|
|
874
|
+
| `--wui-dialog-desc-gap` | `24px` | Spacing below the body content |
|
|
875
|
+
| `--wui-dialog-desc-focus-padding` | `6px` | Focus-ring margin reserved on **all four** sides of the content area. `.desc` is a scroll container and, because one axis must be non-`visible`, a clip box as well, so a control flush against any of its four edges would have its ring clipped by the padding box; the margin covers the `5px` ring (`offset: 2` + `width: 3`) on every axis. Hosts do not need to reserve this themselves. Paired negative margins keep the chrome height unchanged, so this can be retuned to a platform's font metrics without moving the card |
|
|
876
|
+
| `--wui-dialog-footer-gap` | `10px` / `12px` (horizontal) | Spacing between footer buttons |
|
|
877
|
+
| `--wui-dialog-footer-justify` | `flex-end` (default) / `center` (horizontal) | Footer `justify-content`; override to `flex-end` for right-aligned horizontal buttons |
|
|
878
|
+
| `--wui-dialog-scale-enter` | `1.1` | Enter scale start: the panel shrinks in from `1.1` to `1`, and exit reverses it |
|
|
879
|
+
|
|
880
|
+
> **Height semantics:** the token caps the content area, not the card. The title row and the footer stay sized by their content, and anything past the cap scrolls inside `.desc`; the card itself is bounded only by a literal `100vh` / `100dvh` backstop, which catches the case where the viewport is too short to fit chrome plus the cap. A host that wants to bound the **whole card** must still measure its own chrome (card padding + title row + gaps + footer) and subtract it from the token — the three dialogs in the Interweave frontend subtract 142, 142 and 106 respectively. What no longer needs that number is making the content area scroll: before, `.desc` had neither a cap nor `overflow`, so the only element that could scroll was a host-sized inner box, and a host that wanted overflow to scroll had to compute that height itself.
|
|
881
|
+
>
|
|
882
|
+
> **`body` mode has no `.desc`**, so the token does not apply there. The `body` slot is wrapped in `.wui-dialog-content`, which is the scroll area, and the card stays content-sized until the same literal `100vh` / `100dvh` backstop kicks in; the built-in close button keeps its `--wui-dialog-close-top` / `--wui-dialog-close-right` offset while that content scrolls.
|
|
834
883
|
|
|
835
884
|
#### `<web-ui-drawer>`
|
|
836
885
|
|
|
@@ -859,9 +908,17 @@ Side drawer using native `<dialog>` with closing animation. In non-headless mode
|
|
|
859
908
|
|
|
860
909
|
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.
|
|
861
910
|
|
|
862
|
-
When `closable` is set, the built-in close button is positioned at the header's top-right corner (defaults `16px` down and `
|
|
911
|
+
When `closable` is set, the built-in close button is positioned at the header's top-right corner (defaults `16px` down and `16px` in from the edge) instead of stretching across the drawer body. The `16px` down resolves through `--wui-space-4` so it tracks the drawer's header padding; the `16px` in is the button's own offset to the panel's right edge, which is not the header's horizontal padding (`20px`), so it stays a literal.
|
|
912
|
+
|
|
913
|
+
**Drag to close:** With `draggable`, a gray capsule drag bar (4×56px by default, visually centered 10px from the inner edge) 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:
|
|
914
|
+
|
|
915
|
+
The visual center follows half of `--wui-drawer-content-padding`, floored at half the bar thickness plus 4px: zeroing that padding to let content fill the panel edge-to-edge still leaves the capsule a visible inset, so it neither crosses the panel's inner edge nor sits flush against it.
|
|
916
|
+
|
|
917
|
+
The capsule is a visual marker only: it is `pointer-events: none` and never swallows a press. What you press is a hit surface. On `left` / `right` that is the drag zone alone. On `top` / `bottom` there are three: the drag zone, a band along the grab edge (exactly as wide as the capsule, and at least `--wui-drawer-drag-edge-size` thick), and the header (`placement=bottom`) or footer (`placement=top`) section in full. The edge band's height is the larger of the capsule's own outer edge and that token, so it covers the capsule's band by construction — pressing the capsule starts a drag and hovering it lights it up, on every placement.
|
|
863
918
|
|
|
864
|
-
|
|
919
|
+
The drag zone yields to the header (`placement=bottom`) or the footer (`placement=top`), so that section never pushes the capsule away from the edge — and that section takes the drag itself instead of sitting inert: a press anywhere in it starts a drag, title text included. Controls are kept clear **one by one, at the event layer**, rather than by cutting the hit surface geometrically. A press whose composed path contains an interactive element — native `button` / `input` / `select` / `textarea` / `label` / `a[href]` / `summary`, anything `contenteditable`, an interactive ARIA `role` (`button`, `checkbox`, `switch`, `tab`, `menuitem`, `option`, `slider`, …), or any explicit `tabindex` other than `-1` — reaches the control and starts no drag. A custom control that is not natively interactive has to carry one of those markers to be recognised; adding `role` or `tabindex` is the way in, and it is the right call for accessibility anyway. Geometric cutting was rejected on purpose: it would have to measure each control's position, so it breaks the moment a control moves, and it relocates the A6 defect rather than removing it. The trade is explicit. The built-in close button sits against the panel's right edge, outside the capsule's span, so it stays clickable; a control placed inside the capsule's horizontal span is still covered by the drag surface over the band's height.
|
|
920
|
+
|
|
921
|
+
One consequence follows from the handle occupying that span: a control placed inside it whose top edge falls within the band's height (`--wui-drawer-drag-edge-size`, `20px` by default, or the capsule's own outer edge when that is thicker) has its upper part covered by the drag surface, since the handle is drawn there and a drag has to start where the handle is. `z-index` does not lift it clear — Chromium keeps the band above slotted content — so the levers are `--wui-drawer-drag-bar-length`, which sets the capsule length and the band width together, and `--wui-drawer-drag-edge-size`, which sets the band's thickness.
|
|
865
922
|
|
|
866
923
|
- Dragging follows the pointer in real time; the backdrop fades proportionally.
|
|
867
924
|
- 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.
|
|
@@ -879,22 +936,29 @@ When `closable` is set, the built-in close button is positioned at the header's
|
|
|
879
936
|
|
|
880
937
|
**CSS Custom Properties:**
|
|
881
938
|
|
|
882
|
-
| Property | Default | Description
|
|
883
|
-
| --------------------------------- | ---------------------------------- |
|
|
884
|
-
| `--wui-drawer-width` | `320px` | Drawer width
|
|
885
|
-
| `--wui-drawer-height` | `300px` | Drawer height (top/bottom)
|
|
886
|
-
| `--wui-drawer-bg` | `var(--wui-color-surface-overlay)` | Drawer body background
|
|
887
|
-
| `--wui-drawer-radius` | `var(--wui-radius-overlay, 28px)` | Floating card corner radius (non-headless)
|
|
888
|
-
| `--wui-drawer-inset` | `8px` | Floating card viewport inset (non-headless); `0` gives edge-to-edge geometry
|
|
889
|
-
| `--wui-drawer-
|
|
890
|
-
| `--wui-drawer-
|
|
891
|
-
| `--wui-drawer-drag-
|
|
892
|
-
| `--wui-drawer-drag-
|
|
893
|
-
| `--wui-drawer-
|
|
894
|
-
| `--wui-drawer-
|
|
895
|
-
| `--wui-drawer-
|
|
896
|
-
| `--wui-drawer-
|
|
897
|
-
| `--wui-drawer-
|
|
939
|
+
| Property | Default | Description |
|
|
940
|
+
| --------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
941
|
+
| `--wui-drawer-width` | `320px` | Drawer width |
|
|
942
|
+
| `--wui-drawer-height` | `300px` | Drawer height (top/bottom) |
|
|
943
|
+
| `--wui-drawer-bg` | `var(--wui-color-surface-overlay)` | Drawer body background |
|
|
944
|
+
| `--wui-drawer-radius` | `var(--wui-radius-overlay, 28px)` | Floating card corner radius (non-headless) |
|
|
945
|
+
| `--wui-drawer-inset` | `8px` | Floating card viewport inset (non-headless); `0` gives edge-to-edge geometry |
|
|
946
|
+
| `--wui-drawer-nested-peek-base` | `43.2px` | Nested-stack reveal step base `A` (`28.8px` at `width <= 640px`); total stack width grows as `A · ln(n)`, so a single layer never moves and each extra layer reveals less. `0` disables the reveal. See the note below. |
|
|
947
|
+
| `--wui-drawer-overlay-bg` | `rgb(0 0 0 / 0.12)` | Backdrop background |
|
|
948
|
+
| `--wui-drawer-drag-zone-size` | `20px` | Drag-to-close hit zone thickness on the inner edge (draggable) |
|
|
949
|
+
| `--wui-drawer-drag-zone-inset` | `var(--wui-space-1, 4px)` | Breathing gap between the drag hit zone and the header (`placement=bottom`) or footer (`placement=top`) it yields to. `0` when that section is absent |
|
|
950
|
+
| `--wui-drawer-drag-bar-thickness` | `4px` | Drag bar capsule thickness (short axis) |
|
|
951
|
+
| `--wui-drawer-drag-bar-length` | `56px` | Drag bar capsule length (along the drawer edge) |
|
|
952
|
+
| `--wui-drawer-drag-edge-size` | `20px` | Thickness of the capsule's hit band along the grab edge (`top` / `bottom` only). Floored by the capsule's own outer edge, so the capsule band is always covered; the default matches `--wui-drawer-drag-zone-size`, so the handle is never harder to hit than the strip beside it |
|
|
953
|
+
| `--wui-drawer-header-padding` | `16px 20px` | Header section padding |
|
|
954
|
+
| `--wui-drawer-close-top` | `16px` | Built-in close button offset from the top of the header |
|
|
955
|
+
| `--wui-drawer-close-right` | `16px` | Built-in close button offset from the right edge of the drawer |
|
|
956
|
+
| `--wui-drawer-content-padding` | `20px` | Content area padding; also drives drag bar visual center (half value, floored at half the bar thickness + 4px) |
|
|
957
|
+
| `--wui-drawer-footer-padding` | `16px 20px` | Footer section padding |
|
|
958
|
+
|
|
959
|
+
`--wui-drawer-nested-peek-base` needs no declaration of yours: the `43.2px` default is the `initialValue` of the property the component registers with `CSS.registerProperty` (in `components/drawer/index.ts`), and narrow viewports get a smaller `28.8px` base from the `@media (width <= 640px)` rule in `components/drawer/style.css`. Those two places are where the shipped defaults live. Setting the token on the host (or any ancestor) overrides both, at any viewport width.
|
|
960
|
+
|
|
961
|
+
With the shipped defaults, a four-layer stack of `320px` drawers reveals `29.94px` / `17.52px` / `12.43px` per step on desktop and `19.96px` / `11.68px` / `8.29px` on narrow viewports, for a total stack width of `59.89px` and `39.93px` respectively. A single layer never moves, because `ln(1)` is `0`.
|
|
898
962
|
|
|
899
963
|
#### `imagePreview()`
|
|
900
964
|
|
|
@@ -1101,10 +1165,12 @@ Tooltip overlay using pointer/focus triggers.
|
|
|
1101
1165
|
|
|
1102
1166
|
Right-click context menu.
|
|
1103
1167
|
|
|
1104
|
-
| Attribute
|
|
1105
|
-
|
|
|
1106
|
-
| `disabled`
|
|
1107
|
-
| `no-scroll-lock`
|
|
1168
|
+
| Attribute | Type | Default | Description |
|
|
1169
|
+
| ------------------ | --------- | ------- | ------------------------------------------------------------- |
|
|
1170
|
+
| `disabled` | `boolean` | `false` | Right-click does not open the menu; no visual dimming |
|
|
1171
|
+
| `no-scroll-lock` | `boolean` | `false` | Allow background scrolling |
|
|
1172
|
+
| `long-press` | `boolean` | `false` | Open on a touch long press (opt-in; mouse uses `contextmenu`) |
|
|
1173
|
+
| `long-press-delay` | `number` | `500` | Hold duration in ms before `long-press` opens the menu |
|
|
1108
1174
|
|
|
1109
1175
|
**Events:** `open-change` (`CustomEvent<{ open: boolean }>`)
|
|
1110
1176
|
|
|
@@ -1112,10 +1178,26 @@ Right-click context menu.
|
|
|
1112
1178
|
|
|
1113
1179
|
**Methods:** `openAt(x: number, y: number)`, `close()`
|
|
1114
1180
|
|
|
1181
|
+
**CSS Custom Properties:**
|
|
1182
|
+
|
|
1183
|
+
| Property | Default | Description |
|
|
1184
|
+
| ------------------------------------- | ---------------------------------- | -------------------------------- |
|
|
1185
|
+
| `--wui-context-menu-safe-area-bottom` | `env(safe-area-inset-bottom, 0px)` | Bottom inset a touch menu clears |
|
|
1186
|
+
|
|
1115
1187
|
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`.
|
|
1116
1188
|
|
|
1189
|
+
While the menu is open it is modal: it renders into a transparent `<dialog>` in the browser's top layer, so nothing on the page below can be clicked or hit-tested. Clicking outside therefore dismisses the menu without also activating whatever was underneath — a row, a checkbox, a link. The scrim is deliberately invisible; it exists to absorb the hit, not to dim the page. One consequence to know about: for the ~160ms exit transition after a menu item is activated, the scrim has already left the top layer, so the page below is briefly clickable again and the menu's stacking level is that of the normal flow.
|
|
1190
|
+
|
|
1191
|
+
`long-press` is opt-in and touch-only: a pointer held for `long-press-delay` (default `500` ms, matching the platform long press) opens the menu, through the same path as a right-click. It only reacts to `pointerType === 'touch'`, and holding while moving more than 10px cancels it, so scrolling never opens a menu. Because the browser also reports that same hold as a native gesture, the `contextmenu` and `click` it emits afterwards are absorbed rather than allowed to reopen — or immediately light-dismiss — the menu the long press just opened.
|
|
1192
|
+
|
|
1193
|
+
A `long-press` menu opens downward from the press point: its top edge sits at the press `y` and it is centred horizontally on the press `x`, which is how native iOS and Android long-press menus appear. When there is not enough room below, the panel flips above the press point rather than running off the screen, and both axes are clamped into the viewport while clearing `--wui-context-menu-safe-area-bottom` so it does not sit under a home indicator. The split is by input, not by viewport width: a narrow window still anchors a mouse right-click at the cursor's top-left corner. Only the horizontal alignment differs — both paths run the same `computePosition` pass, so touch and mouse cannot drift apart.
|
|
1194
|
+
|
|
1195
|
+
`disabled` suppresses menu behaviour only: right-click and `openAt()` do not open a menu. It does not dim or otherwise restyle what you put in the default slot — the component renders no menu surface of its own to dim, and disabling the right-click menu is not the same as disabling your trigger. If a disabled state should read as disabled, render it on your own content.
|
|
1196
|
+
|
|
1117
1197
|
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.
|
|
1118
1198
|
|
|
1199
|
+
On close, focus returns to whatever was focused when the menu opened. That is the browser's own modal-dialog behaviour rather than something the component reimplements, which is also why it only works while the menu is the top layer. Activating a menu item is the one path that hands focus back early — at the click, before the exit transition — so that an item action which moves focus elsewhere (opening an inline editor, say) ends up keeping it: the closing menu never reaches in and pulls focus back out of the new target, so nothing that commits on blur fires a spurious edit.
|
|
1200
|
+
|
|
1119
1201
|
---
|
|
1120
1202
|
|
|
1121
1203
|
### Menu
|
|
@@ -1205,24 +1287,72 @@ Badge / notification count.
|
|
|
1205
1287
|
|
|
1206
1288
|
Empty state placeholder.
|
|
1207
1289
|
|
|
1208
|
-
| Attribute | Type
|
|
1209
|
-
| ------------- |
|
|
1210
|
-
| `title` | `string`
|
|
1211
|
-
| `description` | `string`
|
|
1212
|
-
| `size` | `
|
|
1290
|
+
| Attribute | Type | Default | Description |
|
|
1291
|
+
| ------------- | -------- | ------- | -------------------------------------------------------------- |
|
|
1292
|
+
| `title` | `string` | `''` | Title text |
|
|
1293
|
+
| `description` | `string` | `''` | Description text |
|
|
1294
|
+
| `size` | `number` | `56` | Layout scale in px; invalid or non-positive values become `56` |
|
|
1295
|
+
|
|
1296
|
+
`size` is the one knob for the whole placeholder. It drives the icon container, `min-height`, padding, and the title/description font size, so a large icon no longer sits in medium-sized whitespace:
|
|
1297
|
+
|
|
1298
|
+
| `size` | Icon box | Glyph | `min-height` | Padding block/inline | Title | Description |
|
|
1299
|
+
| ------ | -------- | ------ | ------------ | -------------------- | ------ | ----------- |
|
|
1300
|
+
| `40` | `40px` | `17px` | `171px` | `23px/17px` | `14px` | `13px` |
|
|
1301
|
+
| `56` | `56px` | `24px` | `240px` | `32px/24px` | `16px` | `14px` |
|
|
1302
|
+
| `72` | `72px` | `31px` | `309px` | `41px/31px` | `16px` | `14px` |
|
|
1303
|
+
|
|
1304
|
+
The derived values are `min-height: round(size * 30 / 7)`, `padding-block: round(size * 4 / 7)`, and `padding-inline: round(size * 3 / 7)`; the default glyph is `round(size * 3 / 7)`.
|
|
1305
|
+
|
|
1306
|
+
Font size is one step rather than a scale: below `56` the title is `14px` and the description `13px`, and from `56` up they are `16px` and `14px`. A bigger placeholder gets its presence from whitespace, not from bigger text.
|
|
1307
|
+
|
|
1308
|
+
**Migrating from the `'small' \| 'medium' \| 'large'` enum:** `size` is now a number, and the removed tiers had hand-tuned metrics that these ratios only approach — `small` was `160px` tall with `20px 16px` padding, `large` was `320px` with `48px 32px`. Section margins and content width never followed the tiers.
|
|
1309
|
+
|
|
1310
|
+
| Old value | New value | Default glyph |
|
|
1311
|
+
| --------- | --------- | ------------- |
|
|
1312
|
+
| `small` | `40` | `17` |
|
|
1313
|
+
| `medium` | `56` | `24` |
|
|
1314
|
+
| `large` | `72` | `31` |
|
|
1213
1315
|
|
|
1214
1316
|
**Slots:** `default` (title, overrides `title` prop), `icon`, `description`, `action`
|
|
1215
1317
|
|
|
1216
1318
|
**CSS Custom Properties:**
|
|
1217
1319
|
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
|
1221
|
-
|
|
|
1222
|
-
| `--wui-empty-
|
|
1223
|
-
| `--wui-empty-
|
|
1224
|
-
| `--wui-empty-
|
|
1225
|
-
| `--wui-empty-
|
|
1320
|
+
Each one overrides the value `size` derives, so all eight stay authoritative — including the two font sizes. Defaults are the values at the default `size` of `56`.
|
|
1321
|
+
|
|
1322
|
+
| Property | Default | Description |
|
|
1323
|
+
| ----------------------------------- | ----------- | ------------------------------------------- |
|
|
1324
|
+
| `--wui-empty-min-height` | `240px` | Min height |
|
|
1325
|
+
| `--wui-empty-padding` | `32px 24px` | Padding |
|
|
1326
|
+
| `--wui-empty-icon-size` | `56px` | Icon container size; overrides `size` |
|
|
1327
|
+
| `--wui-empty-content-width` | `480px` | Max width of title, description, and action |
|
|
1328
|
+
| `--wui-empty-title-margin-top` | `16px` | Space between the icon and the title |
|
|
1329
|
+
| `--wui-empty-title-font-size` | `16px` | Title font size |
|
|
1330
|
+
| `--wui-empty-description-font-size` | `14px` | Description font size |
|
|
1331
|
+
| `--wui-empty-action-margin-top` | `20px` | Space above the action slot |
|
|
1332
|
+
|
|
1333
|
+
#### `<web-ui-middle-ellipsis>`
|
|
1334
|
+
|
|
1335
|
+
Single-line middle truncation: keeps both ends and drops the middle — `very-long-file-…-abcdefghij.txt` — where `text-overflow: ellipsis` can only keep the head.
|
|
1336
|
+
|
|
1337
|
+
| Attribute | Type | Default | Description |
|
|
1338
|
+
| ----------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1339
|
+
| `text` | `string` | `''` | Full text. Read-only from the component's side. |
|
|
1340
|
+
| `marker` | `string` | `'…'` | Marker inserted between the two ends. An empty string truncates without any visible signal. |
|
|
1341
|
+
| `marker-position` | `number` | `50` | Where the marker sits, 0–100. `0` puts it at the line-end (head only), `100` at the line-start (tail only). Out-of-range clamps, `NaN` falls back to `50`. |
|
|
1342
|
+
|
|
1343
|
+
It needs what `text-overflow: ellipsis` needs: **one line with a definite inline size**. A block-level box, `flex: 1; min-width: 0` in a flex row, or a grid track all work. A host that sizes itself to its own content has no available space to truncate against, so the component settles on the width it measured and stops shrinking instead of collapsing to the marker.
|
|
1344
|
+
|
|
1345
|
+
`marker-position` is the knob for file names, where the head is path noise and the tail carries the extension. The rows below are illustrative, not reproducible — exact cut points depend on the font; they were taken at 16px `system-ui` in a 240px box:
|
|
1346
|
+
|
|
1347
|
+
| `marker-position` | In a 240px box |
|
|
1348
|
+
| ----------------- | --------------------------------- |
|
|
1349
|
+
| `0` | `very-long-file-name-abcdefghi…` |
|
|
1350
|
+
| `50` _(default)_ | `very-long-file-…-abcdefghij.txt` |
|
|
1351
|
+
| `80` | `very-l…file-name-abcdefghij.txt` |
|
|
1352
|
+
|
|
1353
|
+
Cut points always fall on **grapheme cluster** boundaries: surrogate pairs, combining marks and ZWJ emoji sequences are never split. While truncated, the full string is exposed as the `title` of the text element and removed again once everything fits. The host must be `direction: ltr`; under `direction: rtl` the composed string is reordered by the bidi algorithm.
|
|
1354
|
+
|
|
1355
|
+
**Known limitation — copy and assistive tech.** The element holds the string you can see, so selecting and copying yields the truncated text rather than the original, and screen readers and in-page find match the truncated string too. CSS `text-overflow` has neither problem because it never touches the text node; this is the structural price of computing the split in JavaScript. `title` carries the original for hover. See `docs/research/web-ui-middle-ellipsis-261007.md` for the alternatives that keep the full string in the DOM and what they cost.
|
|
1226
1356
|
|
|
1227
1357
|
#### `<web-ui-icon>`
|
|
1228
1358
|
|
|
@@ -1297,13 +1427,24 @@ Responsive page layout with an optional full-width banner, a collapsible desktop
|
|
|
1297
1427
|
| `sidebar-collapsed` | `boolean` | `false` | Controlled desktop sidebar collapsed state |
|
|
1298
1428
|
| `sidebar-open` | `boolean` | `false` | Controlled mobile sidebar drawer open state |
|
|
1299
1429
|
| `header-glow` | `boolean` | `false` | Decorative Header background glow behind slot content |
|
|
1430
|
+
| `mobile` | `boolean` | `false` | Read-only, derived: whether the viewport is at or below the mobile breakpoint |
|
|
1300
1431
|
| `sidebar-width` | `string` | `'240px'` | Expanded desktop and mobile sidebar width |
|
|
1301
1432
|
| `collapsed-width` | `string` | `'72px'` | Collapsed desktop sidebar width |
|
|
1302
1433
|
| `sidebar-resizable` | `boolean` | `false` | Enable drag-to-resize on the desktop sidebar edge |
|
|
1303
1434
|
| `sidebar-min-width` | `string` | — | Resize lower bound (px); falls back to `collapsed-width` |
|
|
1304
1435
|
| `sidebar-max-width` | `string` | — | Resize upper bound (px); clamped to half the viewport, which always wins over the configured value |
|
|
1305
1436
|
|
|
1306
|
-
**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.
|
|
1437
|
+
**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. `mobile-change` (`CustomEvent<{ mobile: boolean }>`) is not a request: it reports the layout's own derived viewport state.
|
|
1438
|
+
|
|
1439
|
+
**Mobile viewport state:** `mobile` is derived, not a second input — the layout writes it, the property is read-only (assignments take no effect and throw for strict-mode callers), and setting the attribute is reverted in the same reaction, value included, with no event for either. It flips with the media query the layout renders by (`@media (width <= 640px)`) and the same condition the layout CSS uses to hide the desktop sidebar, so the tree, the layout's own CSS and what consumers read can never disagree; there is no resize debounce, the flip lands on the media-query change itself. `mobile-change` fires once per crossing, including the connect-time evaluation when the element mounts on a narrow viewport. Read `layout.mobile` for the current value and subscribe to `mobile-change` for updates; CSS can target `web-ui-layout[mobile]`.
|
|
1440
|
+
|
|
1441
|
+
**Consuming it in a framework.** The layout re-renders in a microtask, so a consumer that reacts on a scheduler macrotask still reads the old value while the new tree is already on screen:
|
|
1442
|
+
|
|
1443
|
+
- **Vue** — bind `@mobile-change` in the template and hold the value in a `ref`. Vue attaches template listeners before the element is inserted (so they receive the connect-time event too) and flushes in a microtask, which lands ahead of the layout's own update.
|
|
1444
|
+
- **React** — take the element through a ref, then read `layout.mobile` and subscribe inside `useLayoutEffect`, and write the value back with `flushSync`. Reading gives you the mount-time value that a later-attached listener misses, and `flushSync` keeps a runtime crossing ahead of the layout's re-render: React flushes updates from DOM listeners on a macrotask, which otherwise leaves the drawer rendered with a stale consumer value for a frame (measured at 2 of 221 frames in this repository's React demo before the fix).
|
|
1445
|
+
- **CSS** — style off `web-ui-layout[mobile]`, either directly or through a variant. A Tailwind v4 app can write the mobile side of the boundary once instead of per utility: `@custom-variant mobile (&:where(web-ui-layout[mobile], web-ui-layout[mobile] *));`. Two limits: the attribute only reaches nodes inside the layout subtree, so content an overlay portals to `document.body` still needs a media query; and do not use a custom variant for the min-width side — Tailwind emits `@custom-variant` rules after the theme breakpoints, so a `desktop:` variant on the same property silently overrides `md:`/`lg:` at every width above them.
|
|
1446
|
+
|
|
1447
|
+
**Desktop-only collapse:** `sidebar-collapsed` only narrows the desktop sidebar; the layout applies no collapsed geometry to the mobile drawer. Both branches host the same `sidebar` slot content, so a consumer whose sidebar content changes density with that property must gate it on `mobile` (for example `collapsed && !layout.mobile`, or CSS under `web-ui-layout[mobile]`) — otherwise a collapsed desktop preference renders inside the drawer. See issue #195.
|
|
1307
1448
|
|
|
1308
1449
|
**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.
|
|
1309
1450
|
|
|
@@ -1348,18 +1489,19 @@ Responsive page layout with an optional full-width banner, a collapsible desktop
|
|
|
1348
1489
|
}
|
|
1349
1490
|
```
|
|
1350
1491
|
|
|
1351
|
-
At `640px` and below
|
|
1492
|
+
At `640px` and below — the same condition `mobile` reports — the sidebar becomes a `web-ui-drawer` with its built-in glass body, scrollable content, and drag zone. The layout maps `sidebar-width` to `--wui-drawer-width` and `--wui-layout-sidebar-radius` to `--wui-drawer-radius`. 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`.
|
|
1352
1493
|
|
|
1353
|
-
`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.
|
|
1494
|
+
`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. The glow takes its colour from `--wui-color-page`, so it follows light and dark appearance on its own; override that property on `web-ui-layout`, on the theme, or anywhere above it to change it. Two pseudo-elements build it. `::before` paints the colour as a `linear-gradient` inside the header box, fading to fully transparent, and `::after` adds a real `backdrop-filter: blur(4px)` layer whose `mask` fades that blur toward zero alpha at the bottom edge. Keeping the two apart means blur strength and colour strength do not pull on each other. Both expand with a negative `margin` rather than a `transform`, and the blur layer stops at the header's top edge — it only extends downward and to the sides. Horizontal clipping is owned by `.layout-content`, so the glow never produces a horizontal scrollbar. Layout layers are ordered as Header (`10`) < Auxiliary (`20`) < Banner (`30`) < Tabbar (`40`) < Sidebar (`50`).
|
|
1354
1495
|
|
|
1355
1496
|
The sidebar card surface is `--wui-color-surface-sidebar`. In light mode it keeps its translucent neutral layer; 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 may use a different surface.
|
|
1356
1497
|
|
|
1357
1498
|
**CSS Custom Properties:**
|
|
1358
1499
|
|
|
1359
|
-
| Property
|
|
1360
|
-
|
|
|
1361
|
-
| `--wui-layout-sidebar-radius`
|
|
1362
|
-
| `--wui-layout-mobile-toggle-inset`
|
|
1500
|
+
| Property | Default | Description |
|
|
1501
|
+
| ----------------------------------- | ------- | ------------------------------------------------------------------------------------ |
|
|
1502
|
+
| `--wui-layout-sidebar-radius` | `24px` | Border radius of sidebar card (desktop & mobile) |
|
|
1503
|
+
| `--wui-layout-mobile-toggle-inset` | `8px` | Left inset of the mobile header toggle |
|
|
1504
|
+
| `--wui-layout-sidebar-toggle-width` | `44px` | Width of the desktop sidebar collapse toggle; floored at `--wui-control-size` (36px) |
|
|
1363
1505
|
|
|
1364
1506
|
#### `<web-ui-back-top>`
|
|
1365
1507
|
|
|
@@ -1445,19 +1587,54 @@ The host uses `display: contents` and does not paint any background: the library
|
|
|
1445
1587
|
|
|
1446
1588
|
| Property | Default | Description |
|
|
1447
1589
|
| ------------------------- | ------- | ------------------------------------------------ |
|
|
1448
|
-
| `--wui-font-size` | `14px` | Base font size for controls |
|
|
1449
1590
|
| `--wui-input-width` | `200px` | Default width for compact form controls |
|
|
1450
1591
|
| `--wui-control-size` | `36px` | Default height and square min-width for controls |
|
|
1451
1592
|
| `--wui-overlay-min-width` | `200px` | Minimum anchored overlay width |
|
|
1452
1593
|
| `--wui-focus-ring-width` | `3px` | Focus indicator width |
|
|
1453
1594
|
|
|
1595
|
+
**Typography tokens:** font sizes are named by the role the text plays, not by scale position — `caption` for dense chrome labels, `readout` for numeric readouts, the unsuffixed base for body copy, and `title` for bounded card headings. Weights are named by weight, and line heights by how much vertical air the text needs. These are appearance-independent: they stay the same under both light and dark.
|
|
1596
|
+
|
|
1597
|
+
| Property | Default | Description |
|
|
1598
|
+
| ---------------------------- | ------- | --------------------------------------------------------------------------- |
|
|
1599
|
+
| `--wui-font-size-caption` | `12px` | Dense chrome labels (badge, toast timestamp, menu section head) |
|
|
1600
|
+
| `--wui-font-size-readout` | `13px` | Numeric readouts (image preview count and zoom ratio) |
|
|
1601
|
+
| `--wui-font-size` | `14px` | Base font size for body copy and controls, including their placeholder text |
|
|
1602
|
+
| `--wui-font-size-title` | `18px` | Bounded card headings (dialog, drawer) |
|
|
1603
|
+
| `--wui-font-weight-medium` | `500` | Medium weight for emphasized inline text |
|
|
1604
|
+
| `--wui-font-weight-semibold` | `600` | Semibold weight for headings |
|
|
1605
|
+
| `--wui-line-height-tight` | `1.2` | Tight leading for large headings |
|
|
1606
|
+
| `--wui-line-height-snug` | `1.4` | Snug leading for mid-size headings |
|
|
1607
|
+
| `--wui-line-height-normal` | `1.5` | Normal leading for body copy |
|
|
1608
|
+
| `--wui-line-height-relaxed` | `1.6` | Relaxed leading for long-form reading |
|
|
1609
|
+
|
|
1610
|
+
`line-height: 1` and `line-height: 0` inside components are intentionally not tokenized: the former vertically centers a single-line control label, the latter collapses a wrapper box onto its content. Neither is typographic leading, and tokenizing them would invite callers to retune them as if they were.
|
|
1611
|
+
|
|
1612
|
+
**Spacing tokens:** a six-step scale on a 4px base, stopping at 24px — the largest spacing value the component layer writes as an _effective_ static rhythm literal. (The one wider padding, `<web-ui-empty>`'s `32px 24px`, is derived in JS from `--wui-empty-size`, so it belongs to that size axis rather than to the rhythm; see the exclusions below.) These do not replace the per-component override tokens (`--wui-button-px`, `--wui-dialog-padding`, and friends) — they act as the fallback default _for_ those tokens, so each component still decides its own padding while the scale says where that value sits in the overall rhythm. Overriding a step therefore gives the embedding application a density lever across every migrated call site — a lever over rhythm, not over structure; see the exclusions below.
|
|
1613
|
+
|
|
1614
|
+
| Property | Default | Description |
|
|
1615
|
+
| --------------- | ------- | ---------------------------------- |
|
|
1616
|
+
| `--wui-space-1` | `4px` | Tight inset (badge, menu) |
|
|
1617
|
+
| `--wui-space-2` | `8px` | Control gap, group gap, inline pad |
|
|
1618
|
+
| `--wui-space-3` | `12px` | Horizontal control padding |
|
|
1619
|
+
| `--wui-space-4` | `16px` | Standard section separation |
|
|
1620
|
+
| `--wui-space-5` | `20px` | Overlay content padding |
|
|
1621
|
+
| `--wui-space-6` | `24px` | Wide overlay inset |
|
|
1622
|
+
|
|
1623
|
+
The scale deliberately omits `1px`, `2px`, `6px`, `7.5px`, `10px`, and negative values. `1px` is the structural width of hairlines and stroke rings; `2px`, `6px`, and `7.5px` are optical corrections; `10px` is a half-step coupling value off the 4px grid; negative margins cancel a flex gap or a button's padding. Each answers a different "why this number", none of them share spacing semantics, and folding them into one ruler would make overriding any one of them disturb the rest.
|
|
1624
|
+
|
|
1625
|
+
A second exclusion is by meaning rather than by value: offsets that align to the viewport edge or to the host's own content edge stay off the scale even when they land on the 4px grid — `--wui-image-preview-edge-gap`, `--wui-toast-viewport-gap`, `--wui-layout-mobile-toggle-inset`, `--wui-back-top-right` / `--wui-back-top-left` / `--wui-back-top-bottom`, `--wui-drawer-inset`, and `--wui-drawer-close-right`. Their reference frame is the screen or the host's content edge, not the rhythm between neighbours, so folding them in would make "tighten `--wui-space-2`" unexpectedly move a component away from the screen edge. `--wui-drawer-close-top` is the counter-example: it does resolve through `--wui-space-4`, because it tracks the drawer's header padding and the two must move together.
|
|
1626
|
+
|
|
1627
|
+
A third exclusion is spacing that a component derives from a **size** prop instead of writing as rhythm: `<web-ui-empty>` scales its `padding`, `min-block-size`, and glyph with `--wui-empty-size`, so its padding is `23px 17px` / `32px 24px` / `41px 31px` at the `40` / `56` / `72` steps. Four of those six values are odd, so a 4px scale cannot represent them at all. The `56` step's `32px 24px` landing on the base is a coincidence of the size choice, not a sign that the value belongs to the rhythm: `56` is the only one of the three that divides by 7, its quotient is exactly 8, and 8 is itself a multiple of 4 — no rounding happens on that step at all. A density lever that is silently inert on two of three sizes and coincidentally live on the third is worse than no lever, and the whole set moves whenever `size` does — so these stay off the scale. Reach for a literal `px` there instead.
|
|
1628
|
+
|
|
1629
|
+
Overrides must sit inside the `<web-ui-theme>` scope, since the theme host declares the steps for its own subtree. One known boundary: the overlay styles in `assets/*.css` are injected either into a theme-owned overlay root, where the scale applies, or into the document-level fallback overlay root — a plain `div` that sees no theme tokens — where overlay spacing always resolves to its literal fallback.
|
|
1630
|
+
|
|
1454
1631
|
**Selection control tokens (radio, checkbox):**
|
|
1455
1632
|
|
|
1456
|
-
| Property | Default
|
|
1457
|
-
| ------------------------------ |
|
|
1458
|
-
| `--wui-selection-control-size` | `18px`
|
|
1459
|
-
| `--wui-
|
|
1460
|
-
| `--wui-
|
|
1633
|
+
| Property | Default | Description |
|
|
1634
|
+
| ------------------------------ | -------------------- | ----------------------------------------------- |
|
|
1635
|
+
| `--wui-selection-control-size` | `18px` | Indicator (circle / box) width and height |
|
|
1636
|
+
| `--wui-checkbox-group-gap` | `var(--wui-space-2)` | Member spacing inside `<web-ui-checkbox-group>` |
|
|
1637
|
+
| `--wui-radio-group-gap` | `var(--wui-space-2)` | Member spacing inside `<web-ui-radio-group>` |
|
|
1461
1638
|
|
|
1462
1639
|
**Radius tokens:**
|
|
1463
1640
|
|
|
@@ -1471,7 +1648,6 @@ The host uses `display: contents` and does not paint any background: the library
|
|
|
1471
1648
|
|
|
1472
1649
|
| Property | Default | Description |
|
|
1473
1650
|
| ---------------------------- | ------- | ---------------------------- |
|
|
1474
|
-
| `--wui-layer-base` | `0` | Base content |
|
|
1475
1651
|
| `--wui-layer-inline-overlay` | `1` | Overlay local to a component |
|
|
1476
1652
|
| `--wui-layer-header` | `10` | Page header |
|
|
1477
1653
|
| `--wui-layer-auxiliary` | `20` | Floating utility controls |
|
|
@@ -1487,41 +1663,41 @@ The host uses `display: contents` and does not paint any background: the library
|
|
|
1487
1663
|
|
|
1488
1664
|
**Color tokens:**
|
|
1489
1665
|
|
|
1490
|
-
| Property | Light default
|
|
1491
|
-
| ---------------------------------- |
|
|
1492
|
-
| `--wui-color-page` | `#fff`
|
|
1493
|
-
| `--wui-color-surface` | `#fff`
|
|
1494
|
-
| `--wui-color-surface-raised` | `#f2f2f7`
|
|
1495
|
-
| `--wui-color-surface-control` | `#dfdfdf`
|
|
1496
|
-
| `--wui-color-surface-track` | `#e5e5ea`
|
|
1497
|
-
| `--wui-color-surface-menu` | `rgb(
|
|
1498
|
-
| `--wui-color-surface-glass` | `rgb(250 250 250 / 0.34)`
|
|
1499
|
-
| `--wui-color-surface-glass-hover` | `color-mix(... text 6%, surface-glass)`
|
|
1500
|
-
| `--wui-color-surface-glass-active` | `color-mix(... text 15%, surface-glass)`
|
|
1501
|
-
| `--wui-color-surface-overlay` | `rgb(
|
|
1502
|
-
| `--wui-color-surface-sidebar` | `rgb(233 233 233 / 0.82)`
|
|
1503
|
-
| `--wui-color-surface-segmented` | `#e5e5ea`
|
|
1504
|
-
| `--wui-color-surface-selected` | `#fff`
|
|
1505
|
-
| `--wui-color-text` | `#1b1b1b`
|
|
1506
|
-
| `--wui-color-text-secondary` | `#6a6a6a`
|
|
1507
|
-
| `--wui-color-text-tertiary` | `color-mix(in srgb, var(--wui-color-text) 35%, transparent)`
|
|
1508
|
-
| `--wui-color-text-disabled` | `color-mix(in srgb, var(--wui-color-text) 32%, transparent)`
|
|
1509
|
-
| `--wui-color-state-layer-hover` | `color-mix(in srgb, var(--wui-color-text) 6%, transparent)`
|
|
1510
|
-
| `--wui-color-state-layer-active` | `color-mix(in srgb, var(--wui-color-text) 15%, transparent)`
|
|
1511
|
-
| `--wui-color-border` | `rgb(0 0 0 / 0.1)`
|
|
1512
|
-
| `--wui-color-glass-
|
|
1513
|
-
| `--wui-color-glass-
|
|
1514
|
-
| `--wui-color-glass-
|
|
1515
|
-
| `--wui-color-glass-shade`
|
|
1516
|
-
| `--wui-color-accent` | `#08f`
|
|
1517
|
-
| `--wui-color-on-accent` | `#fff`
|
|
1518
|
-
| `--wui-color-on-control` | `#fff`
|
|
1519
|
-
| `--wui-color-success` | `#16a34a`
|
|
1520
|
-
| `--wui-color-warning` | `#d97706`
|
|
1521
|
-
| `--wui-color-danger` | `#dc2626`
|
|
1522
|
-
| `--wui-color-info` | `#2563eb`
|
|
1523
|
-
| `--wui-color-backdrop` | `rgb(0 0 0 / 0.12)`
|
|
1524
|
-
| `--wui-color-focus-ring` | `
|
|
1666
|
+
| Property | Light default | Dark default | Description |
|
|
1667
|
+
| ---------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------- |
|
|
1668
|
+
| `--wui-color-page` | `#fff` | `#242628` | Page background |
|
|
1669
|
+
| `--wui-color-surface` | `#fff` | `#2c2c2e` | Plain surface |
|
|
1670
|
+
| `--wui-color-surface-raised` | `#f2f2f7` | `#2c2c2e` | Raised surface |
|
|
1671
|
+
| `--wui-color-surface-control` | `#dfdfdf` | `#3a3a3c` | Neutral control surface |
|
|
1672
|
+
| `--wui-color-surface-track` | `#e5e5ea` | `#444446` | Slider and switch track surface |
|
|
1673
|
+
| `--wui-color-surface-menu` | `rgb(250 250 250 / 0.76)` | `rgb(49 48 50 / 0.74)` | Menu and floating panel surface |
|
|
1674
|
+
| `--wui-color-surface-glass` | `rgb(250 250 250 / 0.34)` | `rgb(44 44 46 / 0.42)` | Liquid glass surface |
|
|
1675
|
+
| `--wui-color-surface-glass-hover` | `color-mix(... text 6%, surface-glass)` | `color-mix(... text 6%, surface-glass)` | Full glass hover background |
|
|
1676
|
+
| `--wui-color-surface-glass-active` | `color-mix(... text 15%, surface-glass)` | `color-mix(... text 15%, surface-glass)` | Full glass pressed background |
|
|
1677
|
+
| `--wui-color-surface-overlay` | `rgb(248 248 248 / 0.92)` | `rgb(32 34 34 / 0.92)` | Translucent overlay surface |
|
|
1678
|
+
| `--wui-color-surface-sidebar` | `rgb(233 233 233 / 0.82)` | `rgb(44 44 46 / 0.8)` | Sidebar panel surface |
|
|
1679
|
+
| `--wui-color-surface-segmented` | `#e5e5ea` | `#3a3a3c` | Segmented indicator surface |
|
|
1680
|
+
| `--wui-color-surface-selected` | `#fff` | `#5c5c5e` | Selected surface |
|
|
1681
|
+
| `--wui-color-text` | `#1b1b1b` | `#e9eaea` | Primary text |
|
|
1682
|
+
| `--wui-color-text-secondary` | `#6a6a6a` | `#a1a1a6` | Secondary text |
|
|
1683
|
+
| `--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 |
|
|
1684
|
+
| `--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 |
|
|
1685
|
+
| `--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 |
|
|
1686
|
+
| `--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 |
|
|
1687
|
+
| `--wui-color-border` | `rgb(0 0 0 / 0.1)` | `rgb(255 255 255 / 0.14)` | Normal border and divider |
|
|
1688
|
+
| `--wui-color-glass-highlight` | `rgb(255 255 255 / 0.9)` | `rgb(255 255 255 / 0.1)` | Inset highlight on the surface |
|
|
1689
|
+
| `--wui-color-glass-ring` | `rgb(0 0 0 / 0.05)` | `rgb(255 255 255 / 0.05)` | Base tint of the 1px ring |
|
|
1690
|
+
| `--wui-color-glass-ring-sheen` | `rgb(255 255 255 / 0.5)` | `rgb(255 255 255 / 0.2)` | Corner sheen on the ring |
|
|
1691
|
+
| `--wui-color-glass-ring-shade` | `rgb(0 0 0 / 0.03)` | `rgb(0 0 0 / 0.5)` | Corner shade on the ring |
|
|
1692
|
+
| `--wui-color-accent` | `#08f` | `#0a84ff` | Accent and focused input border |
|
|
1693
|
+
| `--wui-color-on-accent` | `#fff` | `#fff` | Foreground on accent |
|
|
1694
|
+
| `--wui-color-on-control` | `#fff` | `#f2f2f7` | Foreground on control inner |
|
|
1695
|
+
| `--wui-color-success` | `#16a34a` | `#30d158` | Success |
|
|
1696
|
+
| `--wui-color-warning` | `#d97706` | `#ff9f0a` | Warning |
|
|
1697
|
+
| `--wui-color-danger` | `#dc2626` | `#ff453a` | Danger |
|
|
1698
|
+
| `--wui-color-info` | `#2563eb` | `#64d2ff` | Info |
|
|
1699
|
+
| `--wui-color-backdrop` | `rgb(0 0 0 / 0.12)` | `rgb(0 0 0 / 0.48)` | Modal backdrop |
|
|
1700
|
+
| `--wui-color-focus-ring` | `color-mix(in srgb, var(--wui-color-accent, #08f) 40%, transparent)` | `color-mix(in srgb, var(--wui-color-accent, #0a84ff) 62%, transparent)` | Focus indicator color, derived from the accent |
|
|
1525
1701
|
|
|
1526
1702
|
**Shadow tokens:**
|
|
1527
1703
|
|