@greypan/web-ui 10.1.2 → 10.2.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 +41 -1547
- package/README.md +41 -1554
- package/dist/assets/overlay-motion.js +1 -1
- package/dist/assets/overlay-motion.js.map +1 -1
- package/dist/components/context-menu/index.js +3 -3
- package/dist/components/context-menu/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/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/package.json +1 -1
package/README.md
CHANGED
|
@@ -339,1559 +339,46 @@ All form controls participate in native `FormData`, constraint validation, `form
|
|
|
339
339
|
|
|
340
340
|
## All Components
|
|
341
341
|
|
|
342
|
+
Every component has its own doc page: API reference, behavior notes and interactive examples all live under [`docs/`](docs/). The table below is just the index.
|
|
343
|
+
|
|
342
344
|
| Category | Component |
|
|
343
345
|
| ---------------------- | --------------------------------------------------------- |
|
|
344
|
-
| **Form Controls** | [`<web-ui-input>`](
|
|
345
|
-
| | [`<web-ui-textarea>`](
|
|
346
|
-
| | [`<web-ui-editable-text>`](
|
|
347
|
-
| | [`<web-ui-input-number>`](
|
|
348
|
-
| | [`<web-ui-select>`](
|
|
349
|
-
| | [`<web-ui-autocomplete>`](
|
|
350
|
-
| | [`<web-ui-slider>`](
|
|
351
|
-
| | [`<web-ui-checkbox>`](
|
|
352
|
-
| | [`<web-ui-radio>`](
|
|
353
|
-
| | [`<web-ui-switch>`](
|
|
354
|
-
| | [`<web-ui-segmented>`](
|
|
355
|
-
| | [`<web-ui-checkbox-group>`](
|
|
356
|
-
| | [`<web-ui-radio-group>`](
|
|
357
|
-
| **Buttons** | [`<web-ui-button>`](
|
|
358
|
-
| | [`<web-ui-button-group>`](
|
|
359
|
-
| **Overlay / Modal** | [`<web-ui-dialog>`](
|
|
360
|
-
| | [`<web-ui-drawer>`](
|
|
361
|
-
| | [`imagePreview()`](
|
|
362
|
-
| **In-flow Disclosure** | [`<web-ui-collapse>`](
|
|
363
|
-
| **Floating** | [`<web-ui-popover>`](
|
|
364
|
-
| | [`<web-ui-tooltip>`](
|
|
365
|
-
| | [`<web-ui-context-menu>`](
|
|
366
|
-
| **Menu** | [`<web-ui-dropdown>`](
|
|
367
|
-
| | [`<web-ui-dropdown-item>`](
|
|
368
|
-
| | [`<web-ui-dropdown-divider>`](
|
|
369
|
-
| | [`<web-ui-dropdown-header>`](
|
|
370
|
-
| **Data Display** | [`<web-ui-avatar>`](
|
|
371
|
-
| | [`<web-ui-badge>`](
|
|
372
|
-
| | [`<web-ui-empty>`](
|
|
373
|
-
| | [`<web-ui-middle-ellipsis>`](
|
|
374
|
-
| | [`<web-ui-icon>`](
|
|
375
|
-
| | [`<web-ui-spinner>`](
|
|
376
|
-
| **Layout & Utility** | [`<web-ui-layout>`](
|
|
377
|
-
| | [`<web-ui-back-top>`](
|
|
378
|
-
| | [`<web-ui-svg-draw-lines>`](
|
|
379
|
-
| | [`<web-ui-theme>`](
|
|
380
|
-
| **Notification** | [`<web-ui-toast>`](
|
|
381
|
-
| **Sub-items** | [`<web-ui-option>`](
|
|
382
|
-
| | [`<web-ui-segmented-trigger>`](
|
|
383
|
-
|
|
384
|
-
## API Reference
|
|
385
|
-
|
|
386
|
-
### Form Controls
|
|
387
|
-
|
|
388
|
-
Form controls implement `static formAssociated = true` and integrate with native `<form>`: values are submitted via `FormData`, and `formResetCallback()` / `formDisabledCallback()` handle lifecycle.
|
|
389
|
-
|
|
390
|
-
#### `<web-ui-input>`
|
|
391
|
-
|
|
392
|
-
Text input with clearable, prefix/suffix slots.
|
|
393
|
-
|
|
394
|
-
| Attribute | Type | Default | Description |
|
|
395
|
-
| ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------- |
|
|
396
|
-
| `value` | `string` | `''` | Input value |
|
|
397
|
-
| `type` | `string` | `'text'` | HTML input type |
|
|
398
|
-
| `placeholder` | `string` | `''` | Placeholder text |
|
|
399
|
-
| `name` | `string` | `''` | Form field name |
|
|
400
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
401
|
-
| `readonly` | `boolean` | `false` | Read-only state |
|
|
402
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
403
|
-
| `clearable` | `boolean` | `false` | Show clear button |
|
|
404
|
-
| `full` | `boolean` | `false` | Full width |
|
|
405
|
-
| `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring |
|
|
406
|
-
| `aria-label` | `string` | — | Accessible label |
|
|
407
|
-
|
|
408
|
-
**Events:** `input`, `change`, `focus`, `blur`
|
|
409
|
-
|
|
410
|
-
**Methods:** `focus()`, `blur()`, `select()`, which delegate to the internal native input (the host itself is not focusable). `focus()` and `select()` are no-ops while `disabled`; `readonly` still allows focus and selection.
|
|
411
|
-
|
|
412
|
-
**Slots:** `prefix`, `default`, `suffix`
|
|
413
|
-
|
|
414
|
-
**CSS Custom Properties:**
|
|
415
|
-
|
|
416
|
-
| Property | Default | Description |
|
|
417
|
-
| ------------------------- | -------------------------------- | ------------------ |
|
|
418
|
-
| `--wui-input-clear-color` | `var(--wui-color-text-tertiary)` | Clear button color |
|
|
419
|
-
|
|
420
|
-
#### `<web-ui-textarea>`
|
|
421
|
-
|
|
422
|
-
Multi-line text input with auto-resize.
|
|
423
|
-
|
|
424
|
-
| Attribute | Type | Default | Description |
|
|
425
|
-
| ----------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------- |
|
|
426
|
-
| `value` | `string` | `''` | Textarea value |
|
|
427
|
-
| `placeholder` | `string` | `''` | Placeholder text |
|
|
428
|
-
| `rows` | `number` | `3` | Visible rows |
|
|
429
|
-
| `name` | `string` | `''` | Form field name |
|
|
430
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
431
|
-
| `readonly` | `boolean` | `false` | Read-only state |
|
|
432
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
433
|
-
| `clearable` | `boolean` | `false` | Show clear button |
|
|
434
|
-
| `full` | `boolean` | `false` | Full width |
|
|
435
|
-
| `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring |
|
|
436
|
-
| `autosize` | `boolean` | `false` | Auto-resize height; also forces `resize: none`, because the native grip fights a script-owned height |
|
|
437
|
-
| `max-height` | `number` | `0` | Autosize max height (px); `0` = unlimited |
|
|
438
|
-
| `minlength` | `number` | — | Minimum length validation |
|
|
439
|
-
| `maxlength` | `number` | — | Maximum length validation |
|
|
440
|
-
| `aria-label` | `string` | — | Accessible label |
|
|
441
|
-
| `aria-labelledby` | `string` | — | Accessible label reference |
|
|
442
|
-
|
|
443
|
-
**Events:** `input`, `change`, `focus`, `blur`
|
|
444
|
-
|
|
445
|
-
**Methods:** `focus()`, `blur()`, `select()`, which delegate to the internal native textarea. `focus()` and `select()` are no-ops while `disabled`; `readonly` still allows focus and selection.
|
|
446
|
-
|
|
447
|
-
**Slots:** `prefix`, `suffix`
|
|
448
|
-
|
|
449
|
-
**CSS Custom Properties:**
|
|
450
|
-
|
|
451
|
-
| Property | Default | Description |
|
|
452
|
-
| ---------------------------- | -------------------------------- | ------------------ |
|
|
453
|
-
| `--wui-textarea-width` | `200px` | Textarea width |
|
|
454
|
-
| `--wui-textarea-clear-color` | `var(--wui-color-text-tertiary)` | Clear button color |
|
|
455
|
-
|
|
456
|
-
#### `<web-ui-editable-text>`
|
|
457
|
-
|
|
458
|
-
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.
|
|
459
|
-
|
|
460
|
-
| Attribute | Type | Default | Description |
|
|
461
|
-
| ------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
462
|
-
| `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 |
|
|
463
|
-
| `placeholder` | `string` | `''` | Text shown while the value is empty |
|
|
464
|
-
| `name` | `string` | `''` | Form field name |
|
|
465
|
-
| `disabled` | `boolean` | `false` | Disabled state; behavior only, no visual dimming |
|
|
466
|
-
| `readonly` | `boolean` | `false` | Read-only state; focus, selection, and copying remain available, but edits cannot commit |
|
|
467
|
-
| `commit-on-unmount` | `boolean` | `false` | End an in-progress edit by committing when the element is unmounted; see below |
|
|
468
|
-
| `aria-label` | `string` | — | Accessible label |
|
|
469
|
-
|
|
470
|
-
**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', ...)`
|
|
471
|
-
|
|
472
|
-
**Methods:** `focus()`, `blur()`, `select()`
|
|
473
|
-
|
|
474
|
-
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).
|
|
475
|
-
|
|
476
|
-
`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`.
|
|
477
|
-
|
|
478
|
-
**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:
|
|
479
|
-
|
|
480
|
-
- **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.
|
|
481
|
-
- **WebKit (Safari, iOS)** dispatches no `blur` at all. With the default `false` nothing is committed and the draft is dropped along with the element.
|
|
482
|
-
- **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.
|
|
483
|
-
|
|
484
|
-
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.
|
|
485
|
-
|
|
486
|
-
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`.
|
|
487
|
-
|
|
488
|
-
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.
|
|
489
|
-
|
|
490
|
-
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.
|
|
491
|
-
|
|
492
|
-
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.
|
|
493
|
-
|
|
494
|
-
**CSS Custom Properties:**
|
|
495
|
-
|
|
496
|
-
| Property | Default | Description |
|
|
497
|
-
| ----------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
498
|
-
| `--wui-editable-text-white-space` | `pre-wrap` | White-space handling of both text layers |
|
|
499
|
-
| `--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 |
|
|
500
|
-
|
|
501
|
-
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.
|
|
502
|
-
|
|
503
|
-
**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.
|
|
504
|
-
|
|
505
|
-
#### `<web-ui-input-number>`
|
|
506
|
-
|
|
507
|
-
Numeric input with step buttons and keyboard control.
|
|
508
|
-
|
|
509
|
-
| Attribute | Type | Default | Description |
|
|
510
|
-
| ------------- | --------- | ---------- | ------------------- |
|
|
511
|
-
| `value` | `number` | `0` | Current value |
|
|
512
|
-
| `min` | `number` | `0` | Minimum value |
|
|
513
|
-
| `max` | `number` | `Infinity` | Maximum value |
|
|
514
|
-
| `step` | `number` | `1` | Step increment |
|
|
515
|
-
| `precision` | `number` | `0` | Decimal precision |
|
|
516
|
-
| `placeholder` | `string` | `''` | Placeholder text |
|
|
517
|
-
| `name` | `string` | `''` | Form field name |
|
|
518
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
519
|
-
| `readonly` | `boolean` | `false` | Read-only state |
|
|
520
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
521
|
-
|
|
522
|
-
**Events:** `input`, `change`
|
|
523
|
-
|
|
524
|
-
ArrowUp/ArrowDown keyboard increments and decrements the value. Empty or `-` input is ignored on commit; the value stays at the last valid number.
|
|
525
|
-
|
|
526
|
-
#### `<web-ui-select>`
|
|
527
|
-
|
|
528
|
-
Select dropdown with option items, keyboard navigation, and portal support.
|
|
529
|
-
|
|
530
|
-
| Attribute | Type | Default | Description |
|
|
531
|
-
| ------------------ | ---------------------------------- | ------- | ------------------------------------------- |
|
|
532
|
-
| `value` | `string` | `''` | Selected value |
|
|
533
|
-
| `placeholder` | `string` | `''` | Placeholder text |
|
|
534
|
-
| `name` | `string` | `''` | Form field name |
|
|
535
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
536
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
537
|
-
| `portal` | `boolean` | `false` | Render dropdown in theme-owned overlay root |
|
|
538
|
-
| `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
|
|
539
|
-
| `overlayContainer` | `HTMLElement \| () => HTMLElement` | — | Explicit portal container |
|
|
540
|
-
|
|
541
|
-
**Events:** `input`, `change`, `open-change` (`CustomEvent<{ open: boolean }>`)
|
|
542
|
-
|
|
543
|
-
**Slots:** `default` (project `<web-ui-option>` elements), `trigger` (custom trigger content, replacing the default label and chevron area)
|
|
544
|
-
|
|
545
|
-
**Methods:** none
|
|
546
|
-
|
|
547
|
-
Child `<web-ui-option>` elements register via `option-register` / `option-unregister` events. Supports ArrowDown/ArrowUp/Enter/Escape keyboard navigation.
|
|
548
|
-
|
|
549
|
-
**CSS Custom Properties:**
|
|
550
|
-
|
|
551
|
-
| Property | Default | Description |
|
|
552
|
-
| ------------------------- | ---------------------------------- | ------------------------------- |
|
|
553
|
-
| `--wui-select-max-width` | `500px` | Dropdown max width |
|
|
554
|
-
| `--wui-select-max-height` | `200px` | Dropdown scroll area max height |
|
|
555
|
-
| `--wui-overlay-min-width` | `200px` | Dropdown min width |
|
|
556
|
-
| `--wui-select-bg-hover` | `--wui-color-surface-glass-hover` | Trigger hover background |
|
|
557
|
-
| `--wui-select-bg-active` | `--wui-color-surface-glass-active` | Trigger pressed/open background |
|
|
558
|
-
|
|
559
|
-
Portal panels mirror these variables from the host element when created; change the host before opening a portal panel for a new value to apply.
|
|
560
|
-
|
|
561
|
-
#### `<web-ui-autocomplete>`
|
|
562
|
-
|
|
563
|
-
Editable combobox with input filtering and single option selection.
|
|
564
|
-
|
|
565
|
-
| Attribute | Type | Default | Description |
|
|
566
|
-
| -------------------- | ---------------------------------- | ------------ | ------------------------------------------------------------------------------------------------- |
|
|
567
|
-
| `value` | `string` | `''` | Current input text (form value) |
|
|
568
|
-
| `selected-value` | `string` | `''` | Value of the enabled option exactly matching the input label (derived, read-only) |
|
|
569
|
-
| `placeholder` | `string` | `''` | Placeholder text |
|
|
570
|
-
| `borderless` | `boolean` | `false` | Ghost form: removes border, background, and shadow; keeps padding, height metrics, and focus ring |
|
|
571
|
-
| `filter` | `'none' \| 'prefix' \| 'contains'` | `'contains'` | Candidate filtering mode (matched against option label) |
|
|
572
|
-
| `name` | `string` | `''` | Form field name |
|
|
573
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
574
|
-
| `readonly` | `boolean` | `false` | Read-only state (no typing, no dropdown) |
|
|
575
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
576
|
-
| `allow-custom-value` | `boolean` | `false` | Allow Enter to submit a custom value that is not among candidates |
|
|
577
|
-
| `portal` | `boolean` | `false` | Render dropdown in theme-owned overlay root |
|
|
578
|
-
| `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
|
|
579
|
-
| `overlayContainer` | `HTMLElement \| () => HTMLElement` | — | Explicit portal container |
|
|
580
|
-
| `aria-label` | `string` | — | Accessible name |
|
|
581
|
-
| `aria-labelledby` | `string` | — | Accessible name references |
|
|
582
|
-
|
|
583
|
-
**Events:** `input`, `change`, `focus`, `blur`, `open-change` (`CustomEvent<{ open: boolean }>`)
|
|
584
|
-
|
|
585
|
-
**Methods:** `focus()`, `blur()`
|
|
586
|
-
|
|
587
|
-
**Slots:** `default` (project `<web-ui-option>` elements), `trigger` (custom trigger content, replacing the default input)
|
|
588
|
-
|
|
589
|
-
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.
|
|
590
|
-
|
|
591
|
-
**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.
|
|
592
|
-
|
|
593
|
-
```html
|
|
594
|
-
<web-ui-autocomplete placeholder="Describe the issue">
|
|
595
|
-
<web-ui-textarea slot="trigger" rows="3"></web-ui-textarea>
|
|
596
|
-
<web-ui-option value="bug" label="Bug"></web-ui-option>
|
|
597
|
-
<web-ui-option value="feature" label="Feature"></web-ui-option>
|
|
598
|
-
</web-ui-autocomplete>
|
|
599
|
-
```
|
|
600
|
-
|
|
601
|
-
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.
|
|
602
|
-
|
|
603
|
-
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.
|
|
604
|
-
|
|
605
|
-
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.
|
|
606
|
-
|
|
607
|
-
**CSS Custom Properties:**
|
|
608
|
-
|
|
609
|
-
| Property | Default | Description |
|
|
610
|
-
| ------------------------------- | ------- | ------------------------------- |
|
|
611
|
-
| `--wui-autocomplete-max-width` | `500px` | Dropdown max width |
|
|
612
|
-
| `--wui-autocomplete-max-height` | `200px` | Dropdown scroll area max height |
|
|
613
|
-
| `--wui-overlay-min-width` | `200px` | Dropdown minimum width |
|
|
614
|
-
|
|
615
|
-
Portal panels mirror these variables from the host element when created; change the host before opening a portal panel for a new value to apply.
|
|
616
|
-
|
|
617
|
-
#### `<web-ui-slider>`
|
|
618
|
-
|
|
619
|
-
Range slider with marks and vertical orientation.
|
|
620
|
-
|
|
621
|
-
| Attribute | Type | Default | Description |
|
|
622
|
-
| ---------- | --------- | ------- | -------------------- |
|
|
623
|
-
| `value` | `number` | `0` | Current value |
|
|
624
|
-
| `min` | `number` | `0` | Minimum value |
|
|
625
|
-
| `max` | `number` | `100` | Maximum value |
|
|
626
|
-
| `step` | `number` | `1` | Step increment |
|
|
627
|
-
| `name` | `string` | `''` | Form field name |
|
|
628
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
629
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
630
|
-
| `marks` | `boolean` | `false` | Show tick marks |
|
|
631
|
-
| `vertical` | `boolean` | `false` | Vertical orientation |
|
|
632
|
-
|
|
633
|
-
**Events:** `input` (during drag), `change` (on release or keyboard)
|
|
634
|
-
|
|
635
|
-
**Methods:** `focus()`, `blur()`
|
|
636
|
-
|
|
637
|
-
Supports ArrowLeft/Right/Up/Down, Home/End, PageUp/PageDown keyboard navigation. Uses pointer capture for drag interaction across mouse, touch, and pen. Both the component host and the track declare `touch-action: none`, and an active drag suppresses `touchmove` defaulting, so iOS Safari never takes over horizontal or vertical drags mid-gesture.
|
|
638
|
-
|
|
639
|
-
**CSS Custom Properties:**
|
|
640
|
-
|
|
641
|
-
| Property | Default | Description |
|
|
642
|
-
| ------------------------------ | ----------------------------------- | ---------------------- |
|
|
643
|
-
| `--wui-slider-width` | `200px` | Slider width |
|
|
644
|
-
| `--wui-slider-vertical-height` | `200px` | Vertical slider height |
|
|
645
|
-
| `--wui-slider-height` | `var(--wui-slider-track-size, 6px)` | Track thickness |
|
|
646
|
-
| `--wui-slider-track-size` | `6px` | Track size |
|
|
647
|
-
| `--wui-slider-thumb-width` | `24px` | Thumb width |
|
|
648
|
-
| `--wui-slider-thumb-height` | `20px` | Thumb height |
|
|
649
|
-
| `--wui-slider-marks-inset` | `0` | Marks inset from edges |
|
|
650
|
-
|
|
651
|
-
#### `<web-ui-checkbox>`
|
|
652
|
-
|
|
653
|
-
Individual checkbox.
|
|
654
|
-
|
|
655
|
-
| Attribute | Type | Default | Description |
|
|
656
|
-
| ---------- | --------- | ------- | --------------------- |
|
|
657
|
-
| `checked` | `boolean` | `false` | Checked state |
|
|
658
|
-
| `value` | `string` | `''` | Form submission value |
|
|
659
|
-
| `name` | `string` | `''` | Form field name |
|
|
660
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
661
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
662
|
-
|
|
663
|
-
**Events:** `input`, `change`
|
|
664
|
-
|
|
665
|
-
**Slots:** `default` (label text)
|
|
666
|
-
|
|
667
|
-
Uses native `<label>` with `role="checkbox"` and `aria-checked`. Enter/Space keyboard toggle.
|
|
668
|
-
|
|
669
|
-
**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. The 10px between indicator and label belongs to the label: when the default slot renders nothing — it is empty, or it carries only a visually hidden accessible name such as an `.sr-only` span — the gap collapses and the host is exactly as wide as the indicator. Emptiness is decided from the label's rendered width rather than from the slot's assigned nodes, because a hidden name is assigned content that paints no box, and hiding the label with `display: none` would take that name out of the accessibility tree.
|
|
670
|
-
|
|
671
|
-
**Check animation:** the checkmark is a stroked check from `@/icons`, rendered by a nested `<web-ui-icon>` inside `<web-ui-svg-draw-lines>` (with `no-autoplay`, so a control that mounts already checked shows a static check). Checking draws it in from the left tip 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. The stroke color is `--wui-color-on-control`, passed down through `<web-ui-icon>`'s `--wui-icon-color`, because the indicator behind it is `--wui-color-accent` once checked. Two constraints follow from that arrangement: the asset has to stay stroked (`fill: none` + `stroke: currentColor`) — a solid icon animates the dash but paints nothing, so the check would just appear — and it renders at `<web-ui-icon>`'s own default 18px, since the host cannot reach the `<svg>` inside the icon's shadow root to scale it with `--wui-selection-control-size`.
|
|
672
|
-
|
|
673
|
-
**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 over the indicator, the gap or the slotted label inside the trigger row 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.
|
|
674
|
-
|
|
675
|
-
#### `<web-ui-radio>`
|
|
676
|
-
|
|
677
|
-
Individual radio button.
|
|
678
|
-
|
|
679
|
-
| Attribute | Type | Default | Description |
|
|
680
|
-
| ---------- | --------- | ------- | --------------------- |
|
|
681
|
-
| `checked` | `boolean` | `false` | Checked state |
|
|
682
|
-
| `value` | `string` | `''` | Form submission value |
|
|
683
|
-
| `name` | `string` | `''` | Form field name |
|
|
684
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
685
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
686
|
-
|
|
687
|
-
**Events:** `input`, `change`
|
|
688
|
-
|
|
689
|
-
**Slots:** `default` (label text)
|
|
690
|
-
|
|
691
|
-
**Layout:** shares the selection-control box contract with `<web-ui-checkbox>`, including the rule that the indicator-label gap only exists while the label renders something. Host height comes from its content, alignment against surrounding text is `vertical-align: middle`, and the indicator measures `--wui-selection-control-size`.
|
|
692
|
-
|
|
693
|
-
#### `<web-ui-switch>`
|
|
694
|
-
|
|
695
|
-
Toggle switch.
|
|
696
|
-
|
|
697
|
-
| Attribute | Type | Default | Description |
|
|
698
|
-
| ---------- | --------- | ------- | ----------------------- |
|
|
699
|
-
| `checked` | `boolean` | `false` | Checked (on) state |
|
|
700
|
-
| `value` | `string` | `''` | Form submission value |
|
|
701
|
-
| `name` | `string` | `''` | Form field name |
|
|
702
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
703
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
704
|
-
| `loading` | `boolean` | `false` | Loading (spinner) state |
|
|
705
|
-
|
|
706
|
-
**Events:** `input`, `change`
|
|
707
|
-
|
|
708
|
-
**Slots:** none
|
|
709
|
-
|
|
710
|
-
Uses `role="switch"` and `aria-checked`. Pointer events for pressed visual state.
|
|
711
|
-
|
|
712
|
-
#### `<web-ui-segmented>`
|
|
713
|
-
|
|
714
|
-
Segmented control: a single-select button group.
|
|
715
|
-
|
|
716
|
-
| Attribute | Type | Default | Description |
|
|
717
|
-
| ---------- | ------------------- | ------- | ------------------------------------ |
|
|
718
|
-
| `value` | `string` | `''` | Selected trigger value |
|
|
719
|
-
| `name` | `string` | `''` | Form field name |
|
|
720
|
-
| `disabled` | `boolean` | `false` | Disables all triggers |
|
|
721
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
722
|
-
| `variant` | `inset` \| `raised` | `inset` | Track and resting indicator material |
|
|
723
|
-
|
|
724
|
-
**Events:** `input`, `change`
|
|
725
|
-
|
|
726
|
-
**Slots:** `default` (project `<web-ui-segmented-trigger>` elements)
|
|
727
|
-
|
|
728
|
-
**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, with backdrop blur, a ring fading in over 80ms and a 1.5x scale, easing back to the resting surface on release. Illegal values fall back to `inset`.
|
|
729
|
-
|
|
730
|
-
Form-associated: integrates with native `<form>` via `ElementInternals`.
|
|
731
|
-
|
|
732
|
-
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`.
|
|
733
|
-
|
|
734
|
-
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). While dragging with triggers of unequal width, the indicator takes the width of the option its center is nearest to, so it previews the option it will snap to instead of keeping the pressed segment's width. Touch surfaces declare `touch-action: none` and suppress `touchmove` defaulting during an active drag, so iOS Safari does not interrupt the gesture.
|
|
735
|
-
|
|
736
|
-
**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`.
|
|
737
|
-
|
|
738
|
-
**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.
|
|
739
|
-
|
|
740
|
-
#### `<web-ui-checkbox-group>`
|
|
741
|
-
|
|
742
|
-
Checkbox group managing multiple selection.
|
|
743
|
-
|
|
744
|
-
##### Attributes
|
|
745
|
-
|
|
746
|
-
| Attribute | Type | Default | Description |
|
|
747
|
-
| ----------- | ---------------------------- | ------------ | --------------------------- |
|
|
748
|
-
| `value` | `string[]` | `[]` | Selected values |
|
|
749
|
-
| `name` | `string` | `''` | Form field name |
|
|
750
|
-
| `disabled` | `boolean` | `false` | Disables all child controls |
|
|
751
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
752
|
-
| `direction` | `'horizontal' \| 'vertical'` | `'vertical'` | Layout direction |
|
|
753
|
-
|
|
754
|
-
**Events:** `input`, `change`
|
|
755
|
-
|
|
756
|
-
**Slots:** `default` (project `<web-ui-checkbox>` elements)
|
|
757
|
-
|
|
758
|
-
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`.
|
|
759
|
-
|
|
760
|
-
##### Tokens
|
|
761
|
-
|
|
762
|
-
| Token | Default | Description |
|
|
763
|
-
| -------------------------- | -------------------- | ----------------------------- |
|
|
764
|
-
| `--wui-checkbox-group-gap` | `var(--wui-space-2)` | Gap between member checkboxes |
|
|
765
|
-
|
|
766
|
-
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.
|
|
767
|
-
|
|
768
|
-
```css
|
|
769
|
-
web-ui-checkbox-group {
|
|
770
|
-
--wui-checkbox-group-gap: 16px;
|
|
771
|
-
}
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
#### `<web-ui-radio-group>`
|
|
775
|
-
|
|
776
|
-
Radio group managing single selection.
|
|
777
|
-
|
|
778
|
-
##### Attributes
|
|
779
|
-
|
|
780
|
-
| Attribute | Type | Default | Description |
|
|
781
|
-
| ----------- | ---------------------------- | ------------ | ---------------------------------------- |
|
|
782
|
-
| `value` | `string` | `''` | Selected radio value |
|
|
783
|
-
| `name` | `string` | `''` | Form field name (propagates to children) |
|
|
784
|
-
| `disabled` | `boolean` | `false` | Disables all child controls |
|
|
785
|
-
| `required` | `boolean` | `false` | Required validation |
|
|
786
|
-
| `direction` | `'horizontal' \| 'vertical'` | `'vertical'` | Layout direction |
|
|
787
|
-
|
|
788
|
-
**Events:** `input`, `change`
|
|
789
|
-
|
|
790
|
-
**Slots:** `default` (project `<web-ui-radio>` elements)
|
|
791
|
-
|
|
792
|
-
`disabled` supplies inherited effective disabled state without changing a child's own `disabled` property. An invalid `direction` value falls back to `vertical`.
|
|
793
|
-
|
|
794
|
-
##### Tokens
|
|
795
|
-
|
|
796
|
-
| Token | Default | Description |
|
|
797
|
-
| ----------------------- | -------------------- | ------------------------- |
|
|
798
|
-
| `--wui-radio-group-gap` | `var(--wui-space-2)` | Gap between member radios |
|
|
799
|
-
|
|
800
|
-
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.
|
|
801
|
-
|
|
802
|
-
```css
|
|
803
|
-
web-ui-radio-group {
|
|
804
|
-
--wui-radio-group-gap: 16px;
|
|
805
|
-
}
|
|
806
|
-
```
|
|
807
|
-
|
|
808
|
-
---
|
|
809
|
-
|
|
810
|
-
### Buttons
|
|
811
|
-
|
|
812
|
-
#### `<web-ui-button>`
|
|
813
|
-
|
|
814
|
-
Styled button with variants and loading state.
|
|
815
|
-
|
|
816
|
-
| Attribute | Type | Default | Description |
|
|
817
|
-
| ------------ | ------------------------------------------------------------ | ---------- | ----------------------------------------------------------------------- |
|
|
818
|
-
| `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger' \| 'glass'` | `'glass'` | Button variant |
|
|
819
|
-
| `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Inner-button type; invalid values become `button` |
|
|
820
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
821
|
-
| `loading` | `boolean` | `false` | Loading spinner; with `icon`, the spinner replaces the icon content |
|
|
822
|
-
| `full` | `boolean` | `false` | Full width |
|
|
823
|
-
| `icon` | `boolean` | `false` | Icon-only mode |
|
|
824
|
-
| `size` | `string` | `''` | Button height in px (icon mode: also sets min-width for square default) |
|
|
825
|
-
| `aria-label` | `string` | — | Accessible label (delegated to inner button) |
|
|
826
|
-
|
|
827
|
-
**Events:** standard `click`
|
|
828
|
-
|
|
829
|
-
**Slots:** `prefix`, `default`, `suffix`
|
|
830
|
-
|
|
831
|
-
`submit` and `reset` forward activation through the host's form owner to its native `<form>` after the composed
|
|
832
|
-
`click` event finishes, so calling `preventDefault()` on that click cancels the action. The inner button still has no
|
|
833
|
-
form owner of its own, and `SubmitEvent.submitter` is therefore `null`. The button contributes no value to `FormData`.
|
|
834
|
-
|
|
835
|
-
Disabled, loading, and `formDisabled` states—including those caused by an ancestor `fieldset`—prevent `click` events.
|
|
836
|
-
|
|
837
|
-
When `icon` and `loading` are both set, the button renders only the spinner, and the default-slot icon is not
|
|
838
|
-
projected. The icon-mode geometry stays square by default, except for `full` or an explicit width.
|
|
839
|
-
|
|
840
|
-
**CSS Custom Properties:**
|
|
841
|
-
|
|
842
|
-
| Property | Default | Description |
|
|
843
|
-
| ------------------------ | ------------- | --------------------------------------- |
|
|
844
|
-
| `--wui-button-width` | `max-content` | Button width |
|
|
845
|
-
| `--wui-button-px` | `12px` | Horizontal padding |
|
|
846
|
-
| `--wui-button-gap` | `8px` | Gap between prefix/default/suffix slots |
|
|
847
|
-
| `--wui-button-color` | variant-based | Button text color |
|
|
848
|
-
| `--wui-button-bg` | variant-based | Button background color |
|
|
849
|
-
| `--wui-button-bg-hover` | variant-based | Button hover background |
|
|
850
|
-
| `--wui-button-bg-active` | variant-based | Button active background |
|
|
851
|
-
|
|
852
|
-
#### `<web-ui-button-group>`
|
|
853
|
-
|
|
854
|
-
Button group that manages child button layout and direction.
|
|
855
|
-
|
|
856
|
-
| Attribute | Type | Default | Description |
|
|
857
|
-
| ----------- | ---------------------------- | -------------- | ---------------- |
|
|
858
|
-
| `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | Layout direction |
|
|
859
|
-
|
|
860
|
-
**Slots:** `default` (project `<web-ui-button>` elements)
|
|
861
|
-
|
|
862
|
-
Applies the direction to the grouped button layout without changing child button attributes.
|
|
863
|
-
|
|
864
|
-
**CSS Custom Properties:**
|
|
865
|
-
|
|
866
|
-
| Property | Default | Description |
|
|
867
|
-
| ----------------------------------- | ------- | ------------------------------------------------------------------------------- |
|
|
868
|
-
| `--wui-button-group-divider-length` | `20px` | Length of the divider between adjacent buttons; its cross axis always stays 1px |
|
|
869
|
-
|
|
870
|
-
Set it on the group host. The token controls the long edge in both directions, so a horizontal group shortens the divider's height and a vertical group shortens its width.
|
|
871
|
-
|
|
872
|
-
---
|
|
873
|
-
|
|
874
|
-
### Overlay / Modal
|
|
875
|
-
|
|
876
|
-
#### `<web-ui-dialog>`
|
|
877
|
-
|
|
878
|
-
Modal dialog using native `<dialog>` with `showModal()`.
|
|
879
|
-
|
|
880
|
-
| Attribute | Type | Default | Description |
|
|
881
|
-
| ------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
882
|
-
| `open` | `boolean` | `false` | Dialog visibility |
|
|
883
|
-
| `closable` | `boolean` | `false` | Show the built-in close button |
|
|
884
|
-
| `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
|
|
885
|
-
| `no-backdrop-close` | `boolean` | `false` | Do not close on backdrop click |
|
|
886
|
-
| `no-escape-close` | `boolean` | `false` | Do not close when Escape is pressed |
|
|
887
|
-
| `controlled` | `boolean` | `false` | Escape and backdrop only request `open=false`; the consumer must update `open` |
|
|
888
|
-
| `heading` | `string` | `''` | Header text; fallback when the `header` slot is empty (ignored in `headless` mode) |
|
|
889
|
-
| `headless` | `boolean` | `false` | Keep only overlay behavior; render the `default` slot as the card's single scroll layer, without the built-in header band, content area or footer |
|
|
890
|
-
| `dialog-label` | `string` | `''` | Accessible name for the internal native dialog; required in `headless` mode |
|
|
891
|
-
|
|
892
|
-
**Events:** `open-change` (`CustomEvent<{ open: boolean }>`). With `controlled`, Escape and backdrop clicks only request `open=false` and the dialog stays open until the consumer writes `open=false`. Programmatic APIs (`showModal()`, `close()`, assigning `open`) stay direct and never emit. Native dialog closure (e.g. form `method="dialog"`) is restored to the controlled open state and re-emits the request.
|
|
893
|
-
|
|
894
|
-
**Slots:** `header`, `default`, `footer`. With `headless`, only the `default` slot is rendered.
|
|
895
|
-
|
|
896
|
-
The `header` slot is the card's chrome band: it does not scroll and it carries its own horizontal padding (`--wui-dialog-inline-padding`), so its content never touches the card edge. The `heading` attribute is its fallback — the slot wins whenever it has assigned elements, otherwise a non-empty `heading` renders a `.wui-dialog-heading`; when neither is present the band is hidden.
|
|
897
|
-
|
|
898
|
-
The `default` slot carries the body in both modes. By default it renders inside `.desc` — the card's content area — alongside the `header` band and the `footer`. With `headless` the whole card becomes one scroll layer: the `default` slot renders into `.wui-dialog-content`, and the `header` band, `.desc` and `footer` are not rendered at all, so `heading` is ignored. `headless` is the same mechanism as `<web-ui-drawer>`: an explicit boolean attribute, not a slot you have to fill to switch modes.
|
|
899
|
-
|
|
900
|
-
**Accessible name:** the internal native `<dialog>` takes `aria-label` from `dialog-label` when that is set; otherwise, in the default mode with a visible `header` band, it takes `aria-labelledby="wui-dialog-heading"` pointing at that band. `headless` renders no built-in title to point at, so **the consumer must supply the name** — `dialog-label`, or their own labelled element.
|
|
901
|
-
|
|
902
|
-
**Methods:** `showModal()`, `close()`
|
|
903
|
-
|
|
904
|
-
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.
|
|
905
|
-
|
|
906
|
-
With `closable`, a built-in close button is rendered in both the default and `headless` 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 `headless` replaces the whole card; in the default mode the `header` band reserves room for the button so long header content 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.
|
|
907
|
-
|
|
908
|
-
> **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.
|
|
909
|
-
|
|
910
|
-
**CSS Custom Properties:**
|
|
911
|
-
|
|
912
|
-
| Property | Default | Description |
|
|
913
|
-
| --------------------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
914
|
-
| `--wui-dialog-width` | `360px` | Dialog width |
|
|
915
|
-
| `--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 |
|
|
916
|
-
| `--wui-dialog-close-top` | `20px` | `closable` button offset from the card top |
|
|
917
|
-
| `--wui-dialog-close-right` | `20px` | `closable` button offset from the card right edge |
|
|
918
|
-
| `--wui-dialog-overlay-bg` | `var(--wui-color-backdrop)` | Backdrop background |
|
|
919
|
-
| `--wui-dialog-bg` | `var(--wui-color-surface-overlay)` | Glass card background, falls back to `rgb(248 248 248 / 0.92)` |
|
|
920
|
-
| `--wui-dialog-padding` | `20px 24px 24px` | Block padding of the card. Its inline component no longer applies to the card: the horizontal padding moved onto the regions, see `--wui-dialog-inline-padding` |
|
|
921
|
-
| `--wui-dialog-inline-padding` | `24px` | Horizontal padding of the `header` band, the content area (`.desc`) and the footer. The card itself has no inline padding, so the content area's scroll port (its border box) coincides with the card edge — the scrollbar sits flush — while the content keeps this inset. The default comes from `--wui-space-6` |
|
|
922
|
-
| `--wui-dialog-title-gap` | `16px` | Spacing below the title |
|
|
923
|
-
| `--wui-dialog-desc-gap` | `24px` | Spacing below the body content |
|
|
924
|
-
| `--wui-dialog-desc-focus-padding` | `6px` | Focus-ring margin reserved on the **block axis** 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 its top or bottom edge would have its ring clipped by the padding box; the margin covers the `5px` ring (`offset: 2` + `width: 3`). The horizontal ring room now comes from `--wui-dialog-inline-padding` (24px ≫ 5px), so this token only affects the block axis — its former inline component no longer applies. The paired negative top margin keeps the chrome height unchanged |
|
|
925
|
-
| `--wui-dialog-footer-gap` | `10px` / `12px` (horizontal) | Spacing between footer buttons |
|
|
926
|
-
| `--wui-dialog-footer-justify` | `flex-end` (default) / `center` (horizontal) | Footer `justify-content`; override to `flex-end` for right-aligned horizontal buttons |
|
|
927
|
-
| `--wui-dialog-scale-enter` | `1.1` | Enter scale start: the panel shrinks in from `1.1` to `1`, and exit reverses it |
|
|
928
|
-
|
|
929
|
-
> **Height semantics:** the token caps the content area, not the card. The `header` band 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 158 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.
|
|
930
|
-
>
|
|
931
|
-
> **`headless` mode has no `.desc`**, so the token does not apply there. The `headless` default 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.
|
|
932
|
-
|
|
933
|
-
#### `<web-ui-drawer>`
|
|
934
|
-
|
|
935
|
-
Side drawer using native `<dialog>` with closing animation. In non-headless mode the drawer renders as a floating rounded card inset from all viewport edges (matching the layout sidebar's card language), so elastic drag distances read as margin changes rather than gaps.
|
|
936
|
-
|
|
937
|
-
| Attribute | Type | Default | Description |
|
|
938
|
-
| ------------------- | ---------------------------------------- | --------- | --------------------------------------------------------------------------------- |
|
|
939
|
-
| `open` | `boolean` | `false` | Drawer visibility |
|
|
940
|
-
| `placement` | `'right' \| 'left' \| 'top' \| 'bottom'` | `'right'` | Slide-in direction |
|
|
941
|
-
| `heading` | `string` | `''` | Title text (fallback when no header slot) |
|
|
942
|
-
| `closable` | `boolean` | `false` | Show close button |
|
|
943
|
-
| `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
|
|
944
|
-
| `no-backdrop-close` | `boolean` | `false` | Do not close on backdrop click |
|
|
945
|
-
| `controlled` | `boolean` | `false` | User close actions only request `open=false`; the consumer must update `open` |
|
|
946
|
-
| `headless` | `boolean` | `false` | Keep only overlay behavior and render the default slot without built-in drawer UI |
|
|
947
|
-
| `dialog-label` | `string` | `''` | Accessible name for the internal native dialog; required in headless mode |
|
|
948
|
-
| `draggable` | `boolean` | `false` | Show a drag bar on the inner edge for drag-to-close gestures |
|
|
949
|
-
|
|
950
|
-
**Events:** `open-change` (`CustomEvent<{ open: boolean }>`). With `controlled`, Escape, backdrop, the built-in close button and drag-release only request `open=false`; the drawer remains open until the consumer writes `open=false`. Programmatic APIs (`show()`, `close()`, assigning `open`) stay direct and never emit. If native dialog closure occurs while a request is pending, the drawer restores its open top-layer state and emits the same request. With nested drawers, an inner `open-change` bubbles through the outer root (composed event); distinguish by `event.target`.
|
|
951
|
-
|
|
952
|
-
**Slots:** `header`, `default`, `footer`. With `headless`, only the `default` slot is rendered.
|
|
953
|
-
|
|
954
|
-
**Methods:** `show()`, `close()`
|
|
955
|
-
|
|
956
|
-
`headless` keeps the native dialog, backdrop, placement animation, Escape/backdrop close behavior, and scroll locking. It does not render the built-in glass body, header, close button, or footer; style the default-slot content completely in the consumer.
|
|
957
|
-
|
|
958
|
-
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.
|
|
959
|
-
|
|
960
|
-
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.
|
|
961
|
-
|
|
962
|
-
**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:
|
|
963
|
-
|
|
964
|
-
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.
|
|
965
|
-
|
|
966
|
-
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.
|
|
967
|
-
|
|
968
|
-
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.
|
|
969
|
-
|
|
970
|
-
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.
|
|
971
|
-
|
|
972
|
-
- Dragging follows the pointer in real time; the backdrop fades proportionally.
|
|
973
|
-
- 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.
|
|
974
|
-
- 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`:
|
|
975
|
-
- 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.
|
|
976
|
-
- The flick is judged by the **whole gesture's** average velocity. Net displacement ÷ gesture duration, divisor floored at 50ms. The flick commits when the average reaches 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.
|
|
977
|
-
- Any release whose net displacement isn't in the close direction animates back open.
|
|
978
|
-
- 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".
|
|
979
|
-
- 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.
|
|
980
|
-
- 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.
|
|
981
|
-
- 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.
|
|
982
|
-
- 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.
|
|
983
|
-
- Drag-to-open is not supported because the closed drawer renders nothing outside the native dialog.
|
|
984
|
-
- Under `prefers-reduced-motion`, release settles instantly, with no transition.
|
|
985
|
-
|
|
986
|
-
**CSS Custom Properties:**
|
|
987
|
-
|
|
988
|
-
| Property | Default | Description |
|
|
989
|
-
| --------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
990
|
-
| `--wui-drawer-width` | `320px` | Drawer width |
|
|
991
|
-
| `--wui-drawer-height` | `300px` | Drawer height (top/bottom) |
|
|
992
|
-
| `--wui-drawer-bg` | `var(--wui-color-surface-overlay)` | Drawer body background |
|
|
993
|
-
| `--wui-drawer-radius` | `var(--wui-radius-overlay, 28px)` | Floating card corner radius (non-headless) |
|
|
994
|
-
| `--wui-drawer-inset` | `8px` | Floating card viewport inset (non-headless); `0` gives edge-to-edge geometry |
|
|
995
|
-
| `--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. |
|
|
996
|
-
| `--wui-drawer-overlay-bg` | `rgb(0 0 0 / 0.12)` | Backdrop background |
|
|
997
|
-
| `--wui-drawer-drag-zone-size` | `20px` | Drag-to-close hit zone thickness on the inner edge (draggable) |
|
|
998
|
-
| `--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 |
|
|
999
|
-
| `--wui-drawer-drag-bar-thickness` | `4px` | Drag bar capsule thickness (short axis) |
|
|
1000
|
-
| `--wui-drawer-drag-bar-length` | `56px` | Drag bar capsule length (along the drawer edge) |
|
|
1001
|
-
| `--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 |
|
|
1002
|
-
| `--wui-drawer-header-padding` | `16px 20px` | Header section padding |
|
|
1003
|
-
| `--wui-drawer-close-top` | `16px` | Built-in close button offset from the top of the header |
|
|
1004
|
-
| `--wui-drawer-close-right` | `16px` | Built-in close button offset from the right edge of the drawer |
|
|
1005
|
-
| `--wui-drawer-content-padding` | `20px` | Content area padding; also drives drag bar visual center (half value, floored at half the bar thickness + 4px) |
|
|
1006
|
-
| `--wui-drawer-footer-padding` | `16px 20px` | Footer section padding |
|
|
1007
|
-
|
|
1008
|
-
`--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.
|
|
1009
|
-
|
|
1010
|
-
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`.
|
|
1011
|
-
|
|
1012
|
-
#### `imagePreview()`
|
|
1013
|
-
|
|
1014
|
-
Imperative image preview with no declarative tag contract: open it through `imagePreview()` and drive it through the returned handle. It uses the native `<dialog>` `showModal()` and mounts into the nearest `web-ui-theme` theme-owned overlay root, falling back to the global fallback overlay root when no theme-owned root exists.
|
|
1015
|
-
|
|
1016
|
-
It does not reuse the `<web-ui-dialog>` component: the preview needs a dialog that fills the viewport itself (the full-viewport backdrop scrim is rendered by a dedicated overlay layer inside the dialog, so the dialog element never fades its own opacity and the glass controls' backdrop blur stays continuous) plus its own pointer interaction, which is a different source than the dialog's glass panel and slot contract. The two share the `native-dialog-presence` and `scroll-lock` plugins, and `noScrollLock` / `noBackdropClose` mirror the same-named properties in naming and semantics.
|
|
1017
|
-
|
|
1018
|
-
```ts
|
|
1019
|
-
import { imagePreview } from '@greypan/web-ui'
|
|
1020
|
-
|
|
1021
|
-
const preview = imagePreview({
|
|
1022
|
-
images: [{ src: '/a.jpg', alt: 'Image A' }, { src: '/b.jpg' }],
|
|
1023
|
-
index: 0,
|
|
1024
|
-
nav: true,
|
|
1025
|
-
toolbar: true,
|
|
1026
|
-
closable: true,
|
|
1027
|
-
indicator: true
|
|
1028
|
-
})
|
|
1029
|
-
|
|
1030
|
-
preview.next()
|
|
1031
|
-
preview.zoomIn()
|
|
1032
|
-
|
|
1033
|
-
await preview.closed
|
|
1034
|
-
```
|
|
1035
|
-
|
|
1036
|
-
**Options:**
|
|
1037
|
-
|
|
1038
|
-
| Option | Type | Default | Description |
|
|
1039
|
-
| ----------------- | -------------------- | ------- | --------------------------------------------------------------------------- |
|
|
1040
|
-
| `images` | `ImagePreviewItem[]` | — | Image list; must contain at least one item, else it throws |
|
|
1041
|
-
| `index` | `number` | `0` | Initial index, clamped into range |
|
|
1042
|
-
| `loop` | `boolean` | `true` | Wrap around at both ends |
|
|
1043
|
-
| `target` | `Element` | — | Trigger element used to resolve the nearest theme-owned overlay root |
|
|
1044
|
-
| `container` | `HTMLElement` | — | Explicit mount container, highest priority |
|
|
1045
|
-
| `nav` | `boolean` | `false` | Show prev/next buttons; rendered only when more than one image |
|
|
1046
|
-
| `toolbar` | `boolean` | `false` | Show the zoom toolbar (out / factor / in / reset) |
|
|
1047
|
-
| `closable` | `boolean` | `false` | Show the close button |
|
|
1048
|
-
| `indicator` | `boolean` | `false` | Show the "current / total" indicator, announced via `aria-live` |
|
|
1049
|
-
| `swipe` | `boolean` | `false` | At 1x, swipe horizontally to change images; owns that axis while it applies |
|
|
1050
|
-
| `noScrollLock` | `boolean` | `false` | Do not lock page scroll while open |
|
|
1051
|
-
| `noBackdropClose` | `boolean` | `false` | Clicking the blank area outside the image does not close |
|
|
1052
|
-
|
|
1053
|
-
`ImagePreviewItem` is `{ src: string; alt?: string }`; a missing `alt` defaults to an empty string.
|
|
1054
|
-
|
|
1055
|
-
Every presentation option defaults to off: with no options passed only the image itself is rendered, so navigation, toolbar, close button and indicator all have to be enabled explicitly.
|
|
1056
|
-
|
|
1057
|
-
**Returned handle:**
|
|
1058
|
-
|
|
1059
|
-
| Member | Type | Description |
|
|
1060
|
-
| ---------------------------------------- | ----------------------------- | ----------------------------------------------- |
|
|
1061
|
-
| `index` | `number` | Current index; keeps its last value after close |
|
|
1062
|
-
| `scale` | `number` | Current zoom factor within `[1, 4]` |
|
|
1063
|
-
| `images` | `readonly ImagePreviewItem[]` | Normalized image list |
|
|
1064
|
-
| `closed` | `Promise<void>` | Resolves after the exit transition and unmount |
|
|
1065
|
-
| `next()` / `prev()` | `() => void` | Relative navigation |
|
|
1066
|
-
| `goTo(index)` | `(index: number) => void` | Jump; clamps at the bounds when `loop` is off |
|
|
1067
|
-
| `zoomIn()` / `zoomOut()` / `resetZoom()` | `() => void` | Zoom controls |
|
|
1068
|
-
| `close()` | `() => void` | Close and play the exit animation |
|
|
1069
|
-
|
|
1070
|
-
**Interaction:** Arrow keys always navigate, independently of `nav`; `+` / `-` and the mouse wheel zoom, and `0` resets. Zooming is anchored: the wheel keeps the point under the cursor fixed and a pinch keeps the midpoint between the two fingers fixed, while the toolbar buttons, keyboard shortcuts and double-click expand around the viewport center. Holding the mouse button down or resting a single finger drags the image to pan in any direction, at any zoom level: zoomed in it reveals the cropped edges, and at 1x it moves the image around within the viewport. Either way the drag is bounded by half the size difference between the image and the stage, so the image can never be dragged outside the viewport, and it is clamped (not snapped back) when you keep dragging past the edge. Double-clicking the image toggles between 1x and 2x.
|
|
1071
|
-
|
|
1072
|
-
Pinch-to-zoom is always on and has no option: the stage already owns pointer interaction, so there is nothing for a gesture to conflict with. The first finger still drives pan/swipe as usual and the second finger starts the pinch, which aborts any in-flight pan or swipe. A pinch does not close the preview, and the compatibility `click` that mixed input may synthesize afterwards is swallowed rather than treated as a backdrop click.
|
|
1073
|
-
|
|
1074
|
-
Clicking anywhere outside the image, or pressing Escape, closes the preview. The native dialog always exposes the accessible name `图片预览`. With `swipe` on and more than one image, the images live on a single carousel track: the current image and its neighbors sit side by side in slides of equal size, the stage clips the overflow, and the whole track translates with the finger. Releasing past the distance threshold (15% of the stage width, rounded) slides the next image in, as does a fast flick whose sampled velocity is at least 320px/s even below that distance; a flick covering at least 16px whose velocity (windowed sample or whole-gesture average) reaches 160px/s also commits; otherwise it bounces back. With `loop` on the wrap-around neighbor is always present beside the current image, and at the non-looping bounds the strip bounces back instead of crossing the boundary. While it applies, the swipe gesture takes over the drag entirely: at 1x only horizontal movement changes images, and vertical movement is ignored (no pan, no vertical follow). Once zoomed in, dragging always pans on both axes and never changes images. Because panning also works at 1x, `resetZoom()` is offered whenever the image is zoomed _or_ displaced. The reset button in the toolbar is enabled for a panned 1x image too.
|
|
1075
|
-
|
|
1076
|
-
A close request does not destroy the native dialog immediately: it stays in the top layer until the exit transition completes, then `dialog.close()` runs, the host is removed from the DOM, and `closed` resolves. Page scroll is locked while open unless `noScrollLock` is `true`.
|
|
1077
|
-
|
|
1078
|
-
**CSS Custom Properties:**
|
|
1079
|
-
|
|
1080
|
-
| Property | Default | Description |
|
|
1081
|
-
| -------------------------------- | -------------------------------- | ------------------------------------------------- |
|
|
1082
|
-
| `--wui-image-preview-overlay-bg` | `rgb(0 0 0 / 0.72)` | Full-viewport backdrop background color |
|
|
1083
|
-
| `--wui-image-preview-edge-gap` | `20px` | Distance from controls to viewport edge |
|
|
1084
|
-
| `--wui-duration-swipe-settle` | `220ms` | Carousel settle (slide-in / bounce-back) duration |
|
|
1085
|
-
| `--wui-ease-swipe` | `cubic-bezier(0.32, 0.72, 0, 1)` | Carousel settle (slide-in / bounce-back) easing |
|
|
1086
|
-
|
|
1087
|
-
---
|
|
1088
|
-
|
|
1089
|
-
### In-flow Disclosure
|
|
1090
|
-
|
|
1091
|
-
#### `<web-ui-collapse>`
|
|
1092
|
-
|
|
1093
|
-
In-flow expand/collapse container with animated height (or width) transition. Single element with two slots; no portal, no scroll lock, no focus management.
|
|
1094
|
-
|
|
1095
|
-
| Attribute | Type | Default | Description |
|
|
1096
|
-
| -------------- | --------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
1097
|
-
| `open` | `boolean` | `false` | Expanded state; self-managed on interaction, `open-change` emits only for user-originated toggles |
|
|
1098
|
-
| `disabled` | `boolean` | `false` | Ignores trigger clicks and sets `aria-disabled` on the trigger element; expanded content is kept |
|
|
1099
|
-
| `horizontal` | `boolean` | `false` | Animate width instead of height |
|
|
1100
|
-
| `keep-mounted` | `boolean` | `false` | Closed state keeps the content in the 0fr track with `inert` (scroll position and layout measurable) instead of `hidden` |
|
|
1101
|
-
| `peek` | `string` | — | Closed state reveals this much of the content (CSS length, e.g. `120px`); along the animation axis; ends in an auto-sized edge fade |
|
|
1102
|
-
|
|
1103
|
-
**Events:** `open-change` (`CustomEvent<{ open: boolean }>`). Emitted only for user-originated toggles (trigger click). Programmatic writes (`open`, `show()`, `close()`, `toggle()`) never emit. Nested collapses: an inner `open-change` bubbles through the outer root (composed event); distinguish by `event.target`.
|
|
1104
|
-
|
|
1105
|
-
**Slots:** `default` (trigger) + `content` (collapsible content)
|
|
1106
|
-
|
|
1107
|
-
**Methods:** `show()`, `close()`, `toggle()`
|
|
1108
|
-
|
|
1109
|
-
```html
|
|
1110
|
-
<web-ui-collapse>
|
|
1111
|
-
<button type="button">Toggle me</button>
|
|
1112
|
-
<div slot="content">Collapsible content</div>
|
|
1113
|
-
</web-ui-collapse>
|
|
1114
|
-
```
|
|
1115
|
-
|
|
1116
|
-
The initial `open` attribute settles instantly without playing the animation. Nested collapses are supported.
|
|
1117
|
-
|
|
1118
|
-
**Trigger semantics:** interaction comes from the element you put in the default slot. A native `<button>`, `<web-ui-button>`, or any other interactive element supplies Enter/Space activation and focus natively. The collapse writes `aria-expanded` / `aria-controls` (pointing at its content track) and `aria-disabled` onto the first assigned element in the trigger slot. Use an interactive element as the trigger: a plain `<span>` is clickable but has no keyboard/focus semantics.
|
|
1119
|
-
|
|
1120
|
-
**Closed-state semantics:** the consumer's light DOM is never moved or unmounted. Default closed state applies `hidden` to the internal content container; with `keep-mounted` the inner container is marked `inert` while staying measurable inside the collapsed track.
|
|
1121
|
-
|
|
1122
|
-
**`peek` (partial reveal):** setting `peek` makes the closed state reveal the first `peek` of the content along the animation axis (`horizontal` switches it to width) instead of collapsing to zero. The track stays at the content height and the inner container is clamped. It implies `keep-mounted` semantics: the content stays mounted but is `inert`, so the clipped portion is not focusable or clickable. Content shorter than `peek` collapses to its own size rather than leaving blank space. Because a fixed length and an auto height cannot be interpolated by CSS, the two `peek` ↔ expanded directions are driven by explicit pixel lengths (measured once per toggle); every other animation path stays a zero-measurement grid `fr` transition.
|
|
1123
|
-
|
|
1124
|
-
```html
|
|
1125
|
-
<web-ui-collapse peek="120px">
|
|
1126
|
-
<button type="button">Toggle me</button>
|
|
1127
|
-
<div slot="content">Long content. Only the first 120px show while closed</div>
|
|
1128
|
-
</web-ui-collapse>
|
|
1129
|
-
```
|
|
1130
|
-
|
|
1131
|
-
**Edge fade:** the revealed area ends in an alpha-gradient fade (right edge when `horizontal`) so the clipped portion melts into the background instead of ending in a hard line. The length is derived from `peek`. There is no second attribute to set. Tune it with CSS custom properties instead:
|
|
1132
|
-
|
|
1133
|
-
| Custom property | Default | Description |
|
|
1134
|
-
| -------------------------------- | ------------- | --------------------------------------------------------------------- |
|
|
1135
|
-
| `--wui-collapse-peek-edge-ratio` | `0.4` | Fade length as a fraction of `peek` |
|
|
1136
|
-
| `--wui-collapse-peek-edge-max` | `112px` | Upper bound, so a large `peek` cannot produce an oversized fade band |
|
|
1137
|
-
| `--wui-collapse-peek-edge` | — | Explicit fade length; wins over the derived value |
|
|
1138
|
-
| `--wui-collapse-peek-edge-color` | `transparent` | Fade-stop color (must carry alpha - `mask-image` reads alpha channel) |
|
|
1139
|
-
| `--wui-collapse-peek-edge-blur` | `4px` | Blur radius of the frost layer over the same band |
|
|
1140
|
-
|
|
1141
|
-
The band carries two layers, the same pairing the layout header's glow uses: the alpha fade above, plus a real `backdrop-filter` blur over the same band, so the clipped content is blurred out rather than merely made translucent. The blur is modulated by the band's own `mask-image`, ramping from 0 at the band's start to full in its second half and back to 0 at the cut edge - a non-zero start would draw a visible seam where the blur abruptly begins, and a non-zero end would blur right up to the clip boundary. When `open`, the band length is `0`, every mask stop collapses onto 100% and the last stop is fully transparent, so the layer paints nothing.
|
|
1142
|
-
|
|
1143
|
-
Set the ratio to `0` to turn the fade off. The derivation is pure CSS `calc(peek * ratio)`, so a `rem`-based `peek` scales the fade with the root font size and nothing has to be measured in JS. Known limitation: a percentage `peek` does not derive to a length, so the fade silently falls back to "none". Set `--wui-collapse-peek-edge` explicitly in that case.
|
|
1144
|
-
|
|
1145
|
-
The fade is a four-stop ease-out gradient rather than a linear one: the body stays fully opaque to the start of the fade band, drops to 60% alpha within the first 30% of the band, then 26%, then reaches the edge color. A linear gradient hugs 1.0 alpha across the first 40% of the band, so the eye perceives a much shorter fade than declared. Front-loading the drop makes the band visibly soft from its very start, nearly doubling the perceived length at the same size.
|
|
1146
|
-
|
|
1147
|
-
The fade band is positioned in percentages, relative to the container's **current rendered height**, which is what `max-height` animates. So the band tracks the cut edge frame by frame during the collapse animation without the `mask-image` string ever needing to interpolate. Its length is driven by a registered `<length>` custom property so it can fade in and out with the toggle instead of popping in once the close animation settles.
|
|
1148
|
-
|
|
1149
|
-
```html
|
|
1150
|
-
<!-- 调强渐隐:比例 0.5 → 120px peek 配 60px 渐隐 -->
|
|
1151
|
-
<web-ui-collapse peek="120px" style="--wui-collapse-peek-edge-ratio: 0.5">
|
|
1152
|
-
<button type="button">Toggle me</button>
|
|
1153
|
-
<div slot="content">Long content with a stronger fade at the bottom of the 120px peek</div>
|
|
1154
|
-
</web-ui-collapse>
|
|
1155
|
-
```
|
|
1156
|
-
|
|
1157
|
-
**Known limitations:** the `horizontal` animation reflows content while width changes.
|
|
1158
|
-
|
|
1159
|
-
---
|
|
1160
|
-
|
|
1161
|
-
### Floating
|
|
1162
|
-
|
|
1163
|
-
#### `<web-ui-popover>`
|
|
1164
|
-
|
|
1165
|
-
Popover overlay anchored to trigger element.
|
|
1166
|
-
|
|
1167
|
-
| Attribute | Type | Default | Description |
|
|
1168
|
-
| ------------------ | ---------------------------------- | ---------- | ---------------------------------- |
|
|
1169
|
-
| `open` | `boolean` | `false` | Popover visibility |
|
|
1170
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
1171
|
-
| `placement` | `Placement` | `'bottom'` | Floating UI placement |
|
|
1172
|
-
| `trigger` | `'click' \| 'hover' \| 'manual'` | `'click'` | Open trigger |
|
|
1173
|
-
| `offset` | `number` | `8` | Offset from anchor |
|
|
1174
|
-
| `portal` | `boolean` | `false` | Render in theme-owned overlay root |
|
|
1175
|
-
| `overlayContainer` | `HTMLElement \| () => HTMLElement` | — | Explicit portal container |
|
|
1176
|
-
|
|
1177
|
-
**Events:** `open-change` (`CustomEvent<{ open: boolean }>`)
|
|
1178
|
-
|
|
1179
|
-
**Slots:** `trigger`, `default`
|
|
1180
|
-
|
|
1181
|
-
**Methods:** `show()`, `close()`, `toggle()`
|
|
1182
|
-
|
|
1183
|
-
Hover mode uses `pointerenter`/`pointerleave` with delay. Click mode toggles on trigger click. Manual mode only responds to imperative `show()`/`close()`.
|
|
1184
|
-
|
|
1185
|
-
**Trigger ARIA:** open/close state is written back to the trigger element: `aria-expanded` and `aria-controls` (pointing at the panel) are set on the first assigned element in the `trigger` slot (the wrapper retains them for backward compatibility). Put an interactive element (native button, `web-ui-button`) in the trigger slot.
|
|
1186
|
-
|
|
1187
|
-
#### `<web-ui-tooltip>`
|
|
1188
|
-
|
|
1189
|
-
Tooltip overlay using pointer/focus triggers.
|
|
1190
|
-
|
|
1191
|
-
| Attribute | Type | Default | Description |
|
|
1192
|
-
| ------------------ | ---------------------------------- | ------- | ---------------------------------- |
|
|
1193
|
-
| `placement` | `Placement` | `'top'` | Floating UI placement |
|
|
1194
|
-
| `content` | `string` | `''` | Tooltip text (alternative to slot) |
|
|
1195
|
-
| `open` | `boolean` | `false` | Tooltip visibility |
|
|
1196
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
1197
|
-
| `show-delay` | `number` | `200` | Show delay in ms |
|
|
1198
|
-
| `hide-delay` | `number` | `100` | Hide delay in ms |
|
|
1199
|
-
| `offset` | `number` | `6` | Offset from trigger |
|
|
1200
|
-
| `portal` | `boolean` | `false` | Render in theme-owned overlay root |
|
|
1201
|
-
| `overlayContainer` | `HTMLElement \| () => HTMLElement` | — | Explicit portal container |
|
|
1202
|
-
|
|
1203
|
-
**Events:** `open-change` (`CustomEvent<{ open: boolean }>`)
|
|
1204
|
-
|
|
1205
|
-
**Slots:** `default` (trigger), `content` (tooltip panel)
|
|
1206
|
-
|
|
1207
|
-
`open` is a self-managed visibility property. Pointer/focus triggers update it, and direct property writes synchronize the local or portal panel; `open-change` emits only for user-originated open/close. Adjacent tooltips open immediately after the first tooltip is visible; pointer/focus triggers otherwise use delay timers.
|
|
1208
|
-
|
|
1209
|
-
**CSS Custom Properties:**
|
|
1210
|
-
|
|
1211
|
-
| Property | Default | Description |
|
|
1212
|
-
| ------------------------- | ------- | ----------------- |
|
|
1213
|
-
| `--wui-tooltip-max-width` | `240px` | Tooltip max width |
|
|
1214
|
-
| `--wui-tooltip-font-size` | `13px` | Tooltip font size |
|
|
1215
|
-
|
|
1216
|
-
#### `<web-ui-context-menu>`
|
|
1217
|
-
|
|
1218
|
-
Right-click context menu.
|
|
1219
|
-
|
|
1220
|
-
| Attribute | Type | Default | Description |
|
|
1221
|
-
| ------------------ | --------- | ------- | ------------------------------------------------------------- |
|
|
1222
|
-
| `disabled` | `boolean` | `false` | Right-click does not open the menu; no visual dimming |
|
|
1223
|
-
| `no-scroll-lock` | `boolean` | `false` | Allow background scrolling |
|
|
1224
|
-
| `long-press` | `boolean` | `false` | Open on a touch long press (opt-in; mouse uses `contextmenu`) |
|
|
1225
|
-
| `long-press-delay` | `number` | `500` | Hold duration in ms before `long-press` opens the menu |
|
|
1226
|
-
|
|
1227
|
-
**Events:** `open-change` (`CustomEvent<{ open: boolean }>`)
|
|
1228
|
-
|
|
1229
|
-
**Slots:** `default` (menu items content)
|
|
1230
|
-
|
|
1231
|
-
**Methods:** `openAt(x: number, y: number)`, `close()`
|
|
1232
|
-
|
|
1233
|
-
**CSS Custom Properties:**
|
|
1234
|
-
|
|
1235
|
-
| Property | Default | Description |
|
|
1236
|
-
| ------------------------------------- | ---------------------------------- | -------------------------------- |
|
|
1237
|
-
| `--wui-context-menu-safe-area-bottom` | `env(safe-area-inset-bottom, 0px)` | Bottom inset a touch menu clears |
|
|
1238
|
-
|
|
1239
|
-
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`.
|
|
1240
|
-
|
|
1241
|
-
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.
|
|
1242
|
-
|
|
1243
|
-
`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.
|
|
1244
|
-
|
|
1245
|
-
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.
|
|
1246
|
-
|
|
1247
|
-
`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.
|
|
1248
|
-
|
|
1249
|
-
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.
|
|
1250
|
-
|
|
1251
|
-
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.
|
|
1252
|
-
|
|
1253
|
-
---
|
|
1254
|
-
|
|
1255
|
-
### Menu
|
|
1256
|
-
|
|
1257
|
-
#### `<web-ui-dropdown>`
|
|
1258
|
-
|
|
1259
|
-
Dropdown menu with multi-level submenu support.
|
|
1260
|
-
|
|
1261
|
-
| Attribute | Type | Default | Description |
|
|
1262
|
-
| ---------------- | ----------- | ---------------- | --------------------------------- |
|
|
1263
|
-
| `open` | `boolean` | `false` | Menu visibility |
|
|
1264
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
1265
|
-
| `placement` | `Placement` | `'bottom-start'` | Floating UI placement |
|
|
1266
|
-
| `offset` | `number` | `4` | Offset from trigger |
|
|
1267
|
-
| `match-width` | `boolean` | `false` | Match trigger width |
|
|
1268
|
-
| `no-scroll-lock` | `boolean` | `false` | Do not lock body scroll when open |
|
|
1269
|
-
|
|
1270
|
-
**Events:** `open-change` (`CustomEvent<{ open: boolean }>`)
|
|
1271
|
-
|
|
1272
|
-
**Slots:** `trigger`, `default` (menu items)
|
|
1273
|
-
|
|
1274
|
-
**Methods:** `openMenu()`, `closeAll()`
|
|
1275
|
-
|
|
1276
|
-
Items: `<web-ui-dropdown-item>`, `<web-ui-dropdown-divider>`, `<web-ui-dropdown-header>`. Submenus via `submenu` attribute on `<web-ui-dropdown-item>`. Full keyboard navigation (Arrow keys, Home, End, Enter, Space, Escape).
|
|
1277
|
-
|
|
1278
|
-
**Trigger ARIA:** `aria-haspopup="menu"`, `aria-expanded` and `aria-controls` (pointing at the root menu panel) are written back to the first assigned element in the `trigger` slot. Put an interactive element in the trigger slot.
|
|
1279
|
-
|
|
1280
|
-
#### `<web-ui-dropdown-item>`
|
|
1281
|
-
|
|
1282
|
-
Menu item for dropdown or context-menu.
|
|
1283
|
-
|
|
1284
|
-
| Attribute | Type | Default | Description |
|
|
1285
|
-
| ---------- | --------- | ------- | -------------------- |
|
|
1286
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
1287
|
-
| `submenu` | `boolean` | `false` | Has submenu children |
|
|
1288
|
-
| `value` | `string` | `''` | Item value |
|
|
1289
|
-
| `pl` | `string` | `''` | Padding-left offset |
|
|
1290
|
-
|
|
1291
|
-
**Slots:** `prefix`, `default`, `suffix`
|
|
1292
|
-
|
|
1293
|
-
**Methods:** `focusItem()`
|
|
1294
|
-
|
|
1295
|
-
Role: `menuitem`. Submenu items should be projected as child elements.
|
|
1296
|
-
|
|
1297
|
-
#### `<web-ui-dropdown-divider>`
|
|
1298
|
-
|
|
1299
|
-
Menu divider line. Role: `separator`. No attributes.
|
|
1300
|
-
|
|
1301
|
-
#### `<web-ui-dropdown-header>`
|
|
1302
|
-
|
|
1303
|
-
Menu section header. No attributes. Slots: `default` (text content).
|
|
1304
|
-
|
|
1305
|
-
---
|
|
1306
|
-
|
|
1307
|
-
### Data Display
|
|
1308
|
-
|
|
1309
|
-
#### `<web-ui-avatar>`
|
|
1310
|
-
|
|
1311
|
-
Avatar display with image fallback.
|
|
1312
|
-
|
|
1313
|
-
| Attribute | Type | Default | Description |
|
|
1314
|
-
| --------- | ---------------------- | ---------- | -------------------------------- |
|
|
1315
|
-
| `src` | `string` | `''` | Image source URL |
|
|
1316
|
-
| `alt` | `string` | `''` | Alternative text |
|
|
1317
|
-
| `name` | `string` | `''` | Display name (initials fallback) |
|
|
1318
|
-
| `size` | `number` | `40` | Avatar size in px |
|
|
1319
|
-
| `shape` | `'circle' \| 'square'` | `'circle'` | Shape variant |
|
|
1320
|
-
|
|
1321
|
-
**Slots:** `default` (fallback content when image fails)
|
|
1322
|
-
|
|
1323
|
-
#### `<web-ui-badge>`
|
|
1324
|
-
|
|
1325
|
-
Badge / notification count.
|
|
1326
|
-
|
|
1327
|
-
| Attribute | Type | Default | Description |
|
|
1328
|
-
| -------------- | -------------------------------------------------------------- | ------------- | ------------------------- |
|
|
1329
|
-
| `count` | `number` | `0` | Display count |
|
|
1330
|
-
| `max` | `number` | `99` | Maximum count (shows 99+) |
|
|
1331
|
-
| `dot` | `boolean` | `false` | Dot mode (no count) |
|
|
1332
|
-
| `show-zero` | `boolean` | `false` | Show when count is 0 |
|
|
1333
|
-
| `badge-hidden` | `boolean` | `false` | Hide completely |
|
|
1334
|
-
| `offset-x` | `number` | `0` | Horizontal offset |
|
|
1335
|
-
| `offset-y` | `number` | `0` | Vertical offset |
|
|
1336
|
-
| `placement` | `'top-right' \| 'top-left' \| 'bottom-right' \| 'bottom-left'` | `'top-right'` | Badge position |
|
|
1337
|
-
|
|
1338
|
-
#### `<web-ui-empty>`
|
|
1339
|
-
|
|
1340
|
-
Empty state placeholder.
|
|
1341
|
-
|
|
1342
|
-
| Attribute | Type | Default | Description |
|
|
1343
|
-
| ------------- | -------- | ------- | -------------------------------------------------------------- |
|
|
1344
|
-
| `title` | `string` | `''` | Title text |
|
|
1345
|
-
| `description` | `string` | `''` | Description text |
|
|
1346
|
-
| `size` | `number` | `56` | Layout scale in px; invalid or non-positive values become `56` |
|
|
1347
|
-
|
|
1348
|
-
`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:
|
|
1349
|
-
|
|
1350
|
-
| `size` | Icon box | Glyph | `min-height` | Padding block/inline | Title | Description |
|
|
1351
|
-
| ------ | -------- | ------ | ------------ | -------------------- | ------ | ----------- |
|
|
1352
|
-
| `40` | `40px` | `17px` | `171px` | `23px/17px` | `14px` | `13px` |
|
|
1353
|
-
| `56` | `56px` | `24px` | `240px` | `32px/24px` | `16px` | `14px` |
|
|
1354
|
-
| `72` | `72px` | `31px` | `309px` | `41px/31px` | `16px` | `14px` |
|
|
1355
|
-
|
|
1356
|
-
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)`.
|
|
1357
|
-
|
|
1358
|
-
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.
|
|
1359
|
-
|
|
1360
|
-
**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.
|
|
1361
|
-
|
|
1362
|
-
| Old value | New value | Default glyph |
|
|
1363
|
-
| --------- | --------- | ------------- |
|
|
1364
|
-
| `small` | `40` | `17` |
|
|
1365
|
-
| `medium` | `56` | `24` |
|
|
1366
|
-
| `large` | `72` | `31` |
|
|
1367
|
-
|
|
1368
|
-
**Slots:** `default` (title, overrides `title` prop), `icon`, `description`, `action`
|
|
1369
|
-
|
|
1370
|
-
**CSS Custom Properties:**
|
|
1371
|
-
|
|
1372
|
-
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`.
|
|
1373
|
-
|
|
1374
|
-
| Property | Default | Description |
|
|
1375
|
-
| ----------------------------------- | ----------- | ------------------------------------------- |
|
|
1376
|
-
| `--wui-empty-min-height` | `240px` | Min height |
|
|
1377
|
-
| `--wui-empty-padding` | `32px 24px` | Padding |
|
|
1378
|
-
| `--wui-empty-icon-size` | `56px` | Icon container size; overrides `size` |
|
|
1379
|
-
| `--wui-empty-content-width` | `480px` | Max width of title, description, and action |
|
|
1380
|
-
| `--wui-empty-title-margin-top` | `16px` | Space between the icon and the title |
|
|
1381
|
-
| `--wui-empty-title-font-size` | `16px` | Title font size |
|
|
1382
|
-
| `--wui-empty-description-font-size` | `14px` | Description font size |
|
|
1383
|
-
| `--wui-empty-action-margin-top` | `20px` | Space above the action slot |
|
|
1384
|
-
|
|
1385
|
-
#### `<web-ui-middle-ellipsis>`
|
|
1386
|
-
|
|
1387
|
-
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.
|
|
1388
|
-
|
|
1389
|
-
| Attribute | Type | Default | Description |
|
|
1390
|
-
| ----------------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1391
|
-
| `text` | `string` | `''` | Full text. Read-only from the component's side. |
|
|
1392
|
-
| `marker` | `string` | `'…'` | Marker inserted between the two ends. An empty string truncates without any visible signal. |
|
|
1393
|
-
| `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`. |
|
|
1394
|
-
|
|
1395
|
-
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.
|
|
1396
|
-
|
|
1397
|
-
`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:
|
|
1398
|
-
|
|
1399
|
-
| `marker-position` | In a 240px box |
|
|
1400
|
-
| ----------------- | --------------------------------- |
|
|
1401
|
-
| `0` | `very-long-file-name-abcdefghi…` |
|
|
1402
|
-
| `50` _(default)_ | `very-long-file-…-abcdefghij.txt` |
|
|
1403
|
-
| `80` | `very-l…file-name-abcdefghij.txt` |
|
|
1404
|
-
|
|
1405
|
-
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.
|
|
1406
|
-
|
|
1407
|
-
**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.
|
|
1408
|
-
|
|
1409
|
-
#### `<web-ui-icon>`
|
|
1410
|
-
|
|
1411
|
-
Icon renderer using Iconify data objects.
|
|
1412
|
-
|
|
1413
|
-
| Property | Type | Default | Description |
|
|
1414
|
-
| -------- | ------------- | ------- | -------------------------------- |
|
|
1415
|
-
| `.icon` | `IconifyIcon` | — | Icon data (Lit property binding) |
|
|
1416
|
-
| `size` | `number` | `18` | Icon size in px |
|
|
1417
|
-
| `color` | `string` | — | Icon color |
|
|
1418
|
-
| `spin` | `boolean` | `false` | Rotation animation |
|
|
1419
|
-
|
|
1420
|
-
Has `aria-hidden="true"`.
|
|
1421
|
-
|
|
1422
|
-
**Canvas:** the SVG `viewBox` is read from `icon.left`/`icon.top`/`icon.width`/`icon.height`, defaulting to `0` for the offsets and to Iconify's spec default `16` for the sizes. Generated assets always carry their canvas, so the size fallback only matters for raw `IconifyIcon` objects passed in directly.
|
|
1423
|
-
|
|
1424
|
-
```js
|
|
1425
|
-
import { lucideLoaderCircle } from '@greypan/web-ui/icons'
|
|
1426
|
-
html`<web-ui-icon .icon=${lucideLoaderCircle} spin />`
|
|
1427
|
-
```
|
|
1428
|
-
|
|
1429
|
-
**CSS Custom Properties:**
|
|
1430
|
-
|
|
1431
|
-
| Property | Default | Description |
|
|
1432
|
-
| --------------------- | --------- | ------------------------------------------------------------------------ |
|
|
1433
|
-
| `--wui-icon-color` | `inherit` | Icon color |
|
|
1434
|
-
| `--wui-duration-spin` | `600ms` | `spin` rotation period; reduced motion slows it to `1600ms`, never stops |
|
|
1435
|
-
|
|
1436
|
-
#### `<web-ui-spinner>`
|
|
1437
|
-
|
|
1438
|
-
Loading spinner.
|
|
1439
|
-
|
|
1440
|
-
| Attribute | Type | Default | Description |
|
|
1441
|
-
| ------------- | -------- | ------- | ------------------ |
|
|
1442
|
-
| `size` | `number` | `24` | Spinner size in px |
|
|
1443
|
-
| `color` | `string` | — | Spinner color |
|
|
1444
|
-
| `description` | `string` | `''` | Description text |
|
|
1445
|
-
|
|
1446
|
-
Role: `status`, `aria-label="加载中"`.
|
|
1447
|
-
|
|
1448
|
-
**Slots:** `description`
|
|
1449
|
-
|
|
1450
|
-
**Static API:**
|
|
1451
|
-
|
|
1452
|
-
```ts
|
|
1453
|
-
import { WebUiSpinner } from '@greypan/web-ui'
|
|
1454
|
-
|
|
1455
|
-
WebUiSpinner.show() // show
|
|
1456
|
-
WebUiSpinner.show({ size: 32, duration: 2000 }) // show with options
|
|
1457
|
-
WebUiSpinner.hide() // hide
|
|
1458
|
-
```
|
|
1459
|
-
|
|
1460
|
-
`WebUiSpinner.show(options?: { size?: number; duration?: number; description?: string })`
|
|
1461
|
-
|
|
1462
|
-
**CSS Custom Properties:**
|
|
1463
|
-
|
|
1464
|
-
| Property | Default | Description |
|
|
1465
|
-
| ------------------------ | ------- | -------------------------------------------------------------------- |
|
|
1466
|
-
| `--wui-spinner-size` | `24px` | Spinner size; set from the `size` property |
|
|
1467
|
-
| `--wui-duration-spinner` | `800ms` | Leaf cycle period; reduced motion slows it to `1600ms` (never stops) |
|
|
1468
|
-
|
|
1469
|
-
---
|
|
1470
|
-
|
|
1471
|
-
### Layout & Utility
|
|
1472
|
-
|
|
1473
|
-
#### `<web-ui-layout>`
|
|
1474
|
-
|
|
1475
|
-
Responsive page layout with an optional full-width banner, a collapsible desktop sidebar, and a default-drawer mobile sidebar. The page itself scrolls; the desktop sidebar and header stick to the viewport after the banner scrolls away.
|
|
1476
|
-
|
|
1477
|
-
| Attribute | Type | Default | Description |
|
|
1478
|
-
| ------------------- | --------- | --------- | -------------------------------------------------------------------------------------------------- |
|
|
1479
|
-
| `sidebar-collapsed` | `boolean` | `false` | Controlled desktop sidebar collapsed state |
|
|
1480
|
-
| `sidebar-open` | `boolean` | `false` | Controlled mobile sidebar drawer open state |
|
|
1481
|
-
| `header-glow` | `boolean` | `false` | Decorative Header background glow behind slot content |
|
|
1482
|
-
| `mobile` | `boolean` | `false` | Read-only, derived: whether the viewport is at or below the mobile breakpoint |
|
|
1483
|
-
| `sidebar-width` | `string` | `'240px'` | Expanded desktop and mobile sidebar width |
|
|
1484
|
-
| `collapsed-width` | `string` | `'72px'` | Collapsed desktop sidebar width |
|
|
1485
|
-
| `sidebar-resizable` | `boolean` | `false` | Enable drag-to-resize on the desktop sidebar edge |
|
|
1486
|
-
| `sidebar-min-width` | `string` | — | Resize lower bound (px); falls back to `collapsed-width` |
|
|
1487
|
-
| `sidebar-max-width` | `string` | — | Resize upper bound (px); clamped to half the viewport, which always wins over the configured value |
|
|
1488
|
-
|
|
1489
|
-
**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.
|
|
1490
|
-
|
|
1491
|
-
**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]`.
|
|
1492
|
-
|
|
1493
|
-
**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:
|
|
1494
|
-
|
|
1495
|
-
- **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.
|
|
1496
|
-
- **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).
|
|
1497
|
-
- **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.
|
|
1498
|
-
|
|
1499
|
-
**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.
|
|
1500
|
-
|
|
1501
|
-
**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.
|
|
1502
|
-
|
|
1503
|
-
- Dragging updates the width in real time (transition suppressed, clamped to `[min, max]` and the viewport).
|
|
1504
|
-
- Releasing emits `sidebar-width-change` with the final pixel width; the width is property-controlled again once the consumer writes it back. `pointercancel` restores the property-controlled width without emitting.
|
|
1505
|
-
- Keyboard (WAI-ARIA splitter pattern): focus the handle and use ←/→ to step by 16px (Shift for 64px), Home/End to jump to min/max, Enter to commit via the same `sidebar-width-change` request, and Escape to revert an uncommitted adjustment.
|
|
1506
|
-
- The mobile drawer always supports drag-to-close via its built-in `draggable` drawer.
|
|
1507
|
-
|
|
1508
|
-
| Slot | Description |
|
|
1509
|
-
| --------- | ---------------------------------------------------------------------------------------------------- |
|
|
1510
|
-
| `banner` | Optional full-width banner above the layout body |
|
|
1511
|
-
| `header` | Sticky content-area header |
|
|
1512
|
-
| `sidebar` | Sidebar-card content. On desktop, the consumer owns its internal fixed regions and scroll container. |
|
|
1513
|
-
| `default` | Main content |
|
|
1514
|
-
| `tabbar` | Bottom tab bar |
|
|
1515
|
-
|
|
1516
|
-
`web-ui-layout` constrains the sidebar card and owns the desktop toggle area, but does not create a desktop sidebar scrollport. To make only part of the desktop sidebar scroll, make the `sidebar` slot root a `height: 100%; min-height: 0` flex column and apply `overflow-y: auto` to the intended child. This keeps consumer-defined headers and footers fixed without adding extra public slots:
|
|
1517
|
-
|
|
1518
|
-
```html
|
|
1519
|
-
<div slot="sidebar" class="sidebar-root">
|
|
1520
|
-
<div class="sidebar-title">Components</div>
|
|
1521
|
-
<nav class="sidebar-nav">...</nav>
|
|
1522
|
-
</div>
|
|
1523
|
-
```
|
|
1524
|
-
|
|
1525
|
-
```css
|
|
1526
|
-
.sidebar-root {
|
|
1527
|
-
display: flex;
|
|
1528
|
-
flex-direction: column;
|
|
1529
|
-
height: 100%;
|
|
1530
|
-
min-height: 0;
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
.sidebar-title {
|
|
1534
|
-
flex-shrink: 0;
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
.sidebar-nav {
|
|
1538
|
-
flex: 1;
|
|
1539
|
-
min-height: 0;
|
|
1540
|
-
overflow-y: auto;
|
|
1541
|
-
}
|
|
1542
|
-
```
|
|
1543
|
-
|
|
1544
|
-
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`.
|
|
1545
|
-
|
|
1546
|
-
`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`).
|
|
1547
|
-
|
|
1548
|
-
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.
|
|
1549
|
-
|
|
1550
|
-
**CSS Custom Properties:**
|
|
1551
|
-
|
|
1552
|
-
| Property | Default | Description |
|
|
1553
|
-
| ----------------------------------- | ------- | ------------------------------------------------------------------------------------ |
|
|
1554
|
-
| `--wui-layout-sidebar-radius` | `24px` | Border radius of sidebar card (desktop & mobile) |
|
|
1555
|
-
| `--wui-layout-mobile-toggle-inset` | `8px` | Left inset of the mobile header toggle |
|
|
1556
|
-
| `--wui-layout-sidebar-toggle-width` | `44px` | Width of the desktop sidebar collapse toggle; floored at `--wui-control-size` (36px) |
|
|
1557
|
-
|
|
1558
|
-
#### `<web-ui-back-top>`
|
|
1559
|
-
|
|
1560
|
-
Scroll-to-top button.
|
|
1561
|
-
|
|
1562
|
-
| Attribute | Type | Default | Description |
|
|
1563
|
-
| ----------------- | ----------------------- | ---------- | ------------------------ |
|
|
1564
|
-
| `scroll-behavior` | `'smooth' \| 'auto'` | `'smooth'` | Scroll behavior |
|
|
1565
|
-
| `threshold` | `number` | `200` | Scroll threshold to show |
|
|
1566
|
-
| `visible` | `boolean` | `false` | Current visibility state |
|
|
1567
|
-
| `scrollTarget` | `HTMLElement \| Window` | `window` | Scroll container |
|
|
1568
|
-
|
|
1569
|
-
**Slots:** `default` (custom button content)
|
|
1570
|
-
|
|
1571
|
-
**Methods:** `toTop()`
|
|
1572
|
-
|
|
1573
|
-
**Positioning:** With `scrollTarget` as `window`, the button is fixed to the viewport corner. With `scrollTarget` as an `HTMLElement`, place the element inside that container and the button floats at the container's bottom corner via `position: sticky`. Offsets follow the `--wui-back-top-top/right/bottom/left` CSS variables.
|
|
1574
|
-
|
|
1575
|
-
Role: `button`, keyboard Enter scrolls to top.
|
|
1576
|
-
|
|
1577
|
-
**CSS Custom Properties:**
|
|
1578
|
-
|
|
1579
|
-
| Property | Default | Description |
|
|
1580
|
-
| ------------------------- | -------------------------------- | ------------- |
|
|
1581
|
-
| `--wui-back-top-position` | `fixed` | CSS position |
|
|
1582
|
-
| `--wui-back-top-z-index` | `var(--wui-layer-auxiliary, 20)` | Z-index |
|
|
1583
|
-
| `--wui-back-top-top` | `auto` | Top offset |
|
|
1584
|
-
| `--wui-back-top-right` | `20px` | Right offset |
|
|
1585
|
-
| `--wui-back-top-bottom` | `20px` | Bottom offset |
|
|
1586
|
-
| `--wui-back-top-left` | `auto` | Left offset |
|
|
1587
|
-
|
|
1588
|
-
#### `<web-ui-svg-draw-lines>`
|
|
1589
|
-
|
|
1590
|
-
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.
|
|
1591
|
-
|
|
1592
|
-
| Attribute | Type | Default | Description |
|
|
1593
|
-
| ------------- | --------- | ---------- | ------------------------------------------------------ |
|
|
1594
|
-
| `duration` | `number` | `1000` | Animation duration in ms, clamped to `[0, 30000]` |
|
|
1595
|
-
| `easing` | `string` | `'linear'` | CSS easing function passed to `element.animate()` |
|
|
1596
|
-
| `no-autoplay` | `boolean` | `false` | Skip the automatic playback when content first appears |
|
|
1597
|
-
|
|
1598
|
-
All attributes are reflected.
|
|
1599
|
-
|
|
1600
|
-
**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.
|
|
1601
|
-
|
|
1602
|
-
**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.
|
|
1603
|
-
|
|
1604
|
-
**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.
|
|
1605
|
-
|
|
1606
|
-
**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.
|
|
1607
|
-
|
|
1608
|
-
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.
|
|
1609
|
-
|
|
1610
|
-
#### `<web-ui-theme>`
|
|
1611
|
-
|
|
1612
|
-
Theme provider defining CSS custom property tokens.
|
|
1613
|
-
|
|
1614
|
-
| Attribute | Type | Default | Description |
|
|
1615
|
-
| --------------------- | --------------------------------- | ---------- | ------------------------------------------------------ |
|
|
1616
|
-
| `appearance` | `'light' \| 'dark' \| 'system'` | `'light'` | Color scheme |
|
|
1617
|
-
| `resolved-appearance` | `'light' \| 'dark'` | `'light'` | Resolved color scheme; read-only reflected host output |
|
|
1618
|
-
| `motion` | `'full' \| 'reduced' \| 'system'` | `'system'` | Motion preference for this nested theme scope |
|
|
1619
|
-
|
|
1620
|
-
**Methods:** `getOverlayRoot()`. Returns this theme-owned overlay root
|
|
1621
|
-
|
|
1622
|
-
**Portal mounting contract:** Every active `<web-ui-theme>` is also the default scoped theme-owned overlay root. Portal-based components without an explicit `overlayContainer` resolve to the nearest active theme's `getOverlayRoot()`; target-less portal calls prefer the root theme's theme-owned overlay root. When no active theme provides a root, they fall back to the global fallback overlay root.
|
|
1623
|
-
|
|
1624
|
-
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`.
|
|
1625
|
-
|
|
1626
|
-
`resolved-appearance` is a read-only derived host attribute (also exposed as the `resolvedAppearance` property): it is always `light` or `dark`, mirrors an explicit `appearance` directly, resolves `appearance="system"` from `prefers-color-scheme`, and updates live when the OS flips. A theme without `appearance` remains inactive but reports the default `light`. Because the component owns the attribute and restores external edits, consumers can bind selectors or Tailwind custom variants to `resolved-appearance` without carrying a second theme state.
|
|
1627
|
-
|
|
1628
|
-
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. The reveal direction follows the resolved appearance, including `appearance="system"` resolving from `prefers-color-scheme`; 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.
|
|
1629
|
-
|
|
1630
|
-
During an active reveal, the View Transitions rendering-suppression rules force pointer hit-testing onto the document element, so CSS `pointer-events` overrides cannot keep the underlying page interactive. The theme calls `skipTransition()` only on `pointerdown` or wheel; `pointermove` and `pointerup` never end the reveal, so follow-up movement from the initiating control or ordinary pointer movement cannot interrupt it. The event that triggers the skip is still spec-defined to target the document element, but the disabled hit-test window ends there instead of running for the full reveal duration.
|
|
1631
|
-
|
|
1632
|
-
**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.
|
|
1633
|
-
|
|
1634
|
-
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.
|
|
1635
|
-
|
|
1636
|
-
**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.
|
|
1637
|
-
|
|
1638
|
-
**Foundation tokens:**
|
|
1639
|
-
|
|
1640
|
-
| Property | Default | Description |
|
|
1641
|
-
| ------------------------- | ------- | ------------------------------------------------ |
|
|
1642
|
-
| `--wui-input-width` | `200px` | Default width for compact form controls |
|
|
1643
|
-
| `--wui-control-size` | `36px` | Default height and square min-width for controls |
|
|
1644
|
-
| `--wui-overlay-min-width` | `200px` | Minimum anchored overlay width |
|
|
1645
|
-
| `--wui-focus-ring-width` | `3px` | Focus indicator width |
|
|
1646
|
-
|
|
1647
|
-
**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.
|
|
1648
|
-
|
|
1649
|
-
| Property | Default | Description |
|
|
1650
|
-
| ---------------------------- | ------- | --------------------------------------------------------------------------- |
|
|
1651
|
-
| `--wui-font-size-caption` | `12px` | Dense chrome labels (badge, toast timestamp, menu section head) |
|
|
1652
|
-
| `--wui-font-size-readout` | `13px` | Numeric readouts (image preview count and zoom ratio) |
|
|
1653
|
-
| `--wui-font-size` | `14px` | Base font size for body copy and controls, including their placeholder text |
|
|
1654
|
-
| `--wui-font-size-title` | `18px` | Bounded card headings (dialog, drawer) |
|
|
1655
|
-
| `--wui-font-weight-medium` | `500` | Medium weight for emphasized inline text |
|
|
1656
|
-
| `--wui-font-weight-semibold` | `600` | Semibold weight for headings |
|
|
1657
|
-
| `--wui-line-height-tight` | `1.2` | Tight leading for large headings |
|
|
1658
|
-
| `--wui-line-height-snug` | `1.4` | Snug leading for mid-size headings |
|
|
1659
|
-
| `--wui-line-height-normal` | `1.5` | Normal leading for body copy |
|
|
1660
|
-
| `--wui-line-height-relaxed` | `1.6` | Relaxed leading for long-form reading |
|
|
1661
|
-
|
|
1662
|
-
`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.
|
|
1663
|
-
|
|
1664
|
-
**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.
|
|
1665
|
-
|
|
1666
|
-
| Property | Default | Description |
|
|
1667
|
-
| --------------- | ------- | ---------------------------------- |
|
|
1668
|
-
| `--wui-space-1` | `4px` | Tight inset (badge, menu) |
|
|
1669
|
-
| `--wui-space-2` | `8px` | Control gap, group gap, inline pad |
|
|
1670
|
-
| `--wui-space-3` | `12px` | Horizontal control padding |
|
|
1671
|
-
| `--wui-space-4` | `16px` | Standard section separation |
|
|
1672
|
-
| `--wui-space-5` | `20px` | Overlay content padding |
|
|
1673
|
-
| `--wui-space-6` | `24px` | Wide overlay inset |
|
|
1674
|
-
|
|
1675
|
-
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.
|
|
1676
|
-
|
|
1677
|
-
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.
|
|
1678
|
-
|
|
1679
|
-
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.
|
|
1680
|
-
|
|
1681
|
-
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.
|
|
1682
|
-
|
|
1683
|
-
**Selection control tokens (radio, checkbox):**
|
|
1684
|
-
|
|
1685
|
-
| Property | Default | Description |
|
|
1686
|
-
| ------------------------------ | -------------------- | ----------------------------------------------- |
|
|
1687
|
-
| `--wui-selection-control-size` | `18px` | Indicator (circle / box) width and height |
|
|
1688
|
-
| `--wui-checkbox-group-gap` | `var(--wui-space-2)` | Member spacing inside `<web-ui-checkbox-group>` |
|
|
1689
|
-
| `--wui-radio-group-gap` | `var(--wui-space-2)` | Member spacing inside `<web-ui-radio-group>` |
|
|
1690
|
-
|
|
1691
|
-
**Radius tokens:**
|
|
1692
|
-
|
|
1693
|
-
| Property | Default | Description |
|
|
1694
|
-
| ---------------------- | ---------------------- | --------------------------------------------------------------- |
|
|
1695
|
-
| `--wui-radius-control` | `calc(infinity * 1px)` | Pill radius for small controls (button, input, switch, option…) |
|
|
1696
|
-
| `--wui-radius-menu` | `18px` | Menu/popover floating panels, multiline textarea and toast |
|
|
1697
|
-
| `--wui-radius-overlay` | `28px` | Large overlay surfaces: dialog, drawer, layout sidebar |
|
|
1698
|
-
|
|
1699
|
-
**Layer tokens:**
|
|
1700
|
-
|
|
1701
|
-
| Property | Default | Description |
|
|
1702
|
-
| ---------------------------- | ------- | ---------------------------- |
|
|
1703
|
-
| `--wui-layer-inline-overlay` | `1` | Overlay local to a component |
|
|
1704
|
-
| `--wui-layer-header` | `10` | Page header |
|
|
1705
|
-
| `--wui-layer-auxiliary` | `20` | Floating utility controls |
|
|
1706
|
-
| `--wui-layer-banner` | `30` | Banners |
|
|
1707
|
-
| `--wui-layer-tabbar` | `40` | Tab bars |
|
|
1708
|
-
| `--wui-layer-sidebar` | `50` | Sidebars |
|
|
1709
|
-
| `--wui-layer-menu` | `100` | Menus and floating panels |
|
|
1710
|
-
| `--wui-layer-menu-nested` | `110` | Nested menus |
|
|
1711
|
-
| `--wui-layer-toast` | `200` | Toasts |
|
|
1712
|
-
| `--wui-layer-loading` | `300` | Blocking loading surfaces |
|
|
1713
|
-
|
|
1714
|
-
**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.
|
|
1715
|
-
|
|
1716
|
-
**Color tokens:**
|
|
1717
|
-
|
|
1718
|
-
| Property | Light default | Dark default | Description |
|
|
1719
|
-
| ---------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------- |
|
|
1720
|
-
| `--wui-color-page` | `#fff` | `#242628` | Page background |
|
|
1721
|
-
| `--wui-color-surface` | `#fff` | `#2c2c2e` | Plain surface |
|
|
1722
|
-
| `--wui-color-surface-raised` | `#f2f2f7` | `#2c2c2e` | Raised surface |
|
|
1723
|
-
| `--wui-color-surface-control` | `#dfdfdf` | `#3a3a3c` | Neutral control surface |
|
|
1724
|
-
| `--wui-color-surface-track` | `#e5e5ea` | `#444446` | Slider and switch track surface |
|
|
1725
|
-
| `--wui-color-surface-menu` | `rgb(250 250 250 / 0.76)` | `rgb(49 48 50 / 0.74)` | Menu and floating panel surface |
|
|
1726
|
-
| `--wui-color-surface-glass` | `rgb(250 250 250 / 0.34)` | `rgb(44 44 46 / 0.42)` | Liquid glass surface |
|
|
1727
|
-
| `--wui-color-surface-glass-hover` | `color-mix(... text 6%, surface-glass)` | `color-mix(... text 6%, surface-glass)` | Full glass hover background |
|
|
1728
|
-
| `--wui-color-surface-glass-active` | `color-mix(... text 15%, surface-glass)` | `color-mix(... text 15%, surface-glass)` | Full glass pressed background |
|
|
1729
|
-
| `--wui-color-surface-overlay` | `rgb(248 248 248 / 0.92)` | `rgb(32 34 34 / 0.92)` | Translucent overlay surface |
|
|
1730
|
-
| `--wui-color-surface-sidebar` | `rgb(233 233 233 / 0.82)` | `rgb(44 44 46 / 0.8)` | Sidebar panel surface |
|
|
1731
|
-
| `--wui-color-surface-segmented` | `#e5e5ea` | `#3a3a3c` | Segmented indicator surface |
|
|
1732
|
-
| `--wui-color-surface-selected` | `#fff` | `#5c5c5e` | Selected surface |
|
|
1733
|
-
| `--wui-color-text` | `#1b1b1b` | `#e9eaea` | Primary text |
|
|
1734
|
-
| `--wui-color-text-secondary` | `#6a6a6a` | `#a1a1a6` | Secondary text |
|
|
1735
|
-
| `--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 |
|
|
1736
|
-
| `--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 |
|
|
1737
|
-
| `--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 |
|
|
1738
|
-
| `--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 |
|
|
1739
|
-
| `--wui-color-border` | `rgb(0 0 0 / 0.1)` | `rgb(255 255 255 / 0.14)` | Normal border and divider |
|
|
1740
|
-
| `--wui-color-glass-highlight` | `rgb(255 255 255 / 0.9)` | `rgb(255 255 255 / 0.1)` | Inset highlight on the surface |
|
|
1741
|
-
| `--wui-color-glass-ring` | `rgb(0 0 0 / 0.05)` | `rgb(255 255 255 / 0.05)` | Base tint of the 1px ring |
|
|
1742
|
-
| `--wui-color-glass-ring-sheen` | `rgb(255 255 255 / 0.5)` | `rgb(255 255 255 / 0.2)` | Corner sheen on the ring |
|
|
1743
|
-
| `--wui-color-glass-ring-shade` | `rgb(0 0 0 / 0.03)` | `rgb(0 0 0 / 0.5)` | Corner shade on the ring |
|
|
1744
|
-
| `--wui-color-accent` | `#08f` | `#0a84ff` | Accent and focused input border |
|
|
1745
|
-
| `--wui-color-on-accent` | `#fff` | `#fff` | Foreground on accent |
|
|
1746
|
-
| `--wui-color-on-control` | `#fff` | `#f2f2f7` | Foreground on control inner |
|
|
1747
|
-
| `--wui-color-success` | `#16a34a` | `#30d158` | Success |
|
|
1748
|
-
| `--wui-color-warning` | `#d97706` | `#ff9f0a` | Warning |
|
|
1749
|
-
| `--wui-color-danger` | `#dc2626` | `#ff453a` | Danger |
|
|
1750
|
-
| `--wui-color-info` | `#2563eb` | `#64d2ff` | Info |
|
|
1751
|
-
| `--wui-color-backdrop` | `rgb(0 0 0 / 0.12)` | `rgb(0 0 0 / 0.48)` | Modal backdrop |
|
|
1752
|
-
| `--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 |
|
|
1753
|
-
|
|
1754
|
-
**Shadow tokens:**
|
|
1755
|
-
|
|
1756
|
-
| Property | Light default | Dark default | Description |
|
|
1757
|
-
| ---------------------- | -------------------------------- | ------------------------------- | --------------------------- |
|
|
1758
|
-
| `--wui-shadow-overlay` | `2px 16px 40px rgb(0 0 0 / 0.4)` | `0 18px 48px rgb(0 0 0 / 0.48)` | Modal and drawer shadow |
|
|
1759
|
-
| `--wui-shadow-panel` | `0 3px 9px rgb(0 0 0 / 0.27)` | `0 4px 16px rgb(0 0 0 / 0.32)` | Small floating panel shadow |
|
|
1760
|
-
| `--wui-shadow-glass` | four-layer diffuse shadow | `0 8px 24px rgb(0 0 0 / 0.08)` | Base liquid glass shadow |
|
|
1761
|
-
|
|
1762
|
-
**Glass effect tokens:** `--wui-glass-brightness` is `1.06` in light mode and `1.02` in dark mode. `--wui-glass-corner-radius` defaults to `32px`; non-pill glass surfaces set it from their own semantic radius (`--wui-radius-menu` / `--wui-radius-overlay`, or `--wui-drawer-radius` for the drawer) so the diagonal border lighting follows token overrides. Pill controls keep a finite corner radius derived from their physical size or internal size token instead, because the lighting gradient needs a physical size.
|
|
1763
|
-
|
|
1764
|
-
**Internal tokens:** variables prefixed `--wui-internal-*` are private wiring between shadow DOM parts; they are not part of the public token API and must not be overridden by consumers.
|
|
1765
|
-
|
|
1766
|
-
---
|
|
1767
|
-
|
|
1768
|
-
### Notification
|
|
1769
|
-
|
|
1770
|
-
#### `<web-ui-toast>`
|
|
1771
|
-
|
|
1772
|
-
Individual toast notification element. Managed by imperative API.
|
|
1773
|
-
|
|
1774
|
-
When using the element directly, `no-close-button` is a standard boolean attribute that hides its close button.
|
|
1775
|
-
|
|
1776
|
-
**Imperative API:**
|
|
1777
|
-
|
|
1778
|
-
```ts
|
|
1779
|
-
import { toast } from '@greypan/web-ui'
|
|
1780
|
-
|
|
1781
|
-
// Create
|
|
1782
|
-
toast.success('Operation completed')
|
|
1783
|
-
toast.error('Something went wrong', { duration: 5000 })
|
|
1784
|
-
toast.info('Heads up')
|
|
1785
|
-
toast.warning('Be careful')
|
|
1786
|
-
|
|
1787
|
-
// With options
|
|
1788
|
-
const id = toast({ message: 'Custom', type: 'info', position: 'bottom-right', duration: 4000, closable: true })
|
|
1789
|
-
|
|
1790
|
-
// Close
|
|
1791
|
-
toast.close(id)
|
|
1792
|
-
toast.clear()
|
|
1793
|
-
|
|
1794
|
-
// Upsert: calling again with the same id updates that one toast instead of adding another
|
|
1795
|
-
toast.error('Network lost, retrying…', { id: 'network' })
|
|
1796
|
-
toast.error('Network lost (retry 2)', { id: 'network' })
|
|
1797
|
-
|
|
1798
|
-
// One toast through its whole lifecycle
|
|
1799
|
-
toast.info('Uploading 0%', { id: 'upload', duration: 0 })
|
|
1800
|
-
toast.info('Uploading 60%', { id: 'upload' }) // duration omitted: timer keeps running
|
|
1801
|
-
toast.success('Upload complete', { id: 'upload', duration: 3000 }) // new type, timer restarts
|
|
1802
|
-
```
|
|
1803
|
-
|
|
1804
|
-
Every call returns the final id of that toast, so callers never branch on create vs. update.
|
|
1805
|
-
|
|
1806
|
-
**ToastOptions:**
|
|
1807
|
-
|
|
1808
|
-
| Option | Type | Default | Description |
|
|
1809
|
-
| ----------- | ------------------------------------------------------------------------------------------------- | ------------------------- | ----------------------------------------------- |
|
|
1810
|
-
| `message` | `string` | — | Notification text |
|
|
1811
|
-
| `type` | `'success' \| 'info' \| 'warning' \| 'error'` | `'info'` | Toast type |
|
|
1812
|
-
| `duration` | `number` | `3000` (`5000` for error) | Auto-close duration (0 = no auto-close) |
|
|
1813
|
-
| `closable` | `boolean` | `true` | Show close button |
|
|
1814
|
-
| `id` | `string` | auto | Merge key: calls sharing an id update one toast |
|
|
1815
|
-
| `heading` | `string` | `''` | Bold heading text |
|
|
1816
|
-
| `position` | `'top-left' \| 'top-center' \| 'top-right' \| 'bottom-left' \| 'bottom-center' \| 'bottom-right'` | `'top-right'` | Screen position |
|
|
1817
|
-
| `target` | `Element` | — | Used to find nearest theme-owned overlay root |
|
|
1818
|
-
| `container` | `HTMLElement` | — | Explicit mount container (highest priority) |
|
|
1819
|
-
|
|
1820
|
-
**Upsert semantics**. What a second call with the same `id` does:
|
|
1821
|
-
|
|
1822
|
-
| Target state | Result |
|
|
1823
|
-
| ----------------------------- | ------------------------------------------------------------------------------- |
|
|
1824
|
-
| Mounted | Same id returned; supplied fields overwrite, omitted ones keep their value |
|
|
1825
|
-
| Still queued in the same tick | The queued options are patched and applied on mount; no second toast is created |
|
|
1826
|
-
| Closed, or still exiting | A new toast is created; the one leaving finishes its exit animation on its own |
|
|
1827
|
-
|
|
1828
|
-
`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.
|
|
1829
|
-
|
|
1830
|
-
**Close semantics.** `toast.close(id)` covers every state a toast can be in, including the two before it becomes visible:
|
|
1831
|
-
|
|
1832
|
-
| Target state | Result |
|
|
1833
|
-
| ---------------------------------- | --------------------------------------------------------------- |
|
|
1834
|
-
| Visible | Plays the exit animation, then emits `toast-close` |
|
|
1835
|
-
| Mounted, `show()` has not run yet | No exit animation to play: emits `toast-close` immediately |
|
|
1836
|
-
| Still queued in the same microtask | Dropped before it mounts - nothing appears, no event is emitted |
|
|
1837
|
-
| Already exiting | No-op; that toast finishes its own exit |
|
|
1838
|
-
|
|
1839
|
-
`toast.clear()` has the same scope: it drops queued entries and closes everything mounted.
|
|
1840
|
-
|
|
1841
|
-
**Events:** `toast-close` (`CustomEvent<{ id: string; reason: 'auto' | 'manual' | 'programmatic' | 'clear' }>`)
|
|
1842
|
-
|
|
1843
|
-
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.
|
|
1844
|
-
|
|
1845
|
-
**CSS Custom Properties:**
|
|
1846
|
-
|
|
1847
|
-
| Property | Default | Description |
|
|
1848
|
-
| ------------------------------- | ------- | -------------------------------------------------- |
|
|
1849
|
-
| `--wui-toast-viewport-gap` | `16px` | Visible distance between toasts and viewport edges |
|
|
1850
|
-
| `--wui-toast-container-padding` | `40px` | Container padding reserved for diffuse shadows |
|
|
1851
|
-
|
|
1852
|
-
---
|
|
1853
|
-
|
|
1854
|
-
### Sub-items
|
|
1855
|
-
|
|
1856
|
-
#### `<web-ui-option>`
|
|
1857
|
-
|
|
1858
|
-
Selection option for `<web-ui-select>`.
|
|
1859
|
-
|
|
1860
|
-
| Attribute | Type | Default | Description |
|
|
1861
|
-
| ---------- | --------- | ------- | --------------------------------------------- |
|
|
1862
|
-
| `value` | `string` | `''` | Selection value |
|
|
1863
|
-
| `label` | `string` | `''` | Display text; falls back to default slot text |
|
|
1864
|
-
| `selected` | `boolean` | `false` | Currently selected |
|
|
1865
|
-
| `disabled` | `boolean` | `false` | Disabled state |
|
|
1866
|
-
|
|
1867
|
-
**Slots:** `default` (fallback label text), `prefix` (content before label), `suffix` (content after label)
|
|
1868
|
-
|
|
1869
|
-
Not form-associated (child of select, not independent submit).
|
|
1870
|
-
|
|
1871
|
-
**CSS Custom Properties:**
|
|
1872
|
-
|
|
1873
|
-
| Property | Default | Description |
|
|
1874
|
-
| ---------------------------- | ------- | --------------------------------------------- |
|
|
1875
|
-
| `--wui-option-check-display` | `block` | Display mode of the selection check indicator |
|
|
1876
|
-
|
|
1877
|
-
#### `<web-ui-segmented-trigger>`
|
|
1878
|
-
|
|
1879
|
-
Segment trigger for `<web-ui-segmented>`.
|
|
1880
|
-
|
|
1881
|
-
| Attribute | Type | Default | Description |
|
|
1882
|
-
| ---------- | --------- | ------- | ---------------------------------- |
|
|
1883
|
-
| `value` | `string` | `''` | Segment value |
|
|
1884
|
-
| `checked` | `boolean` | `false` | Currently selected |
|
|
1885
|
-
| `disabled` | `boolean` | `false` | Individually disables this trigger |
|
|
1886
|
-
|
|
1887
|
-
**Events:** `change`
|
|
1888
|
-
|
|
1889
|
-
Not form-associated (child of segmented, not independent submit).
|
|
1890
|
-
|
|
1891
|
-
**CSS Custom Properties:**
|
|
1892
|
-
|
|
1893
|
-
| Property | Default | Description |
|
|
1894
|
-
| ----------------------------------- | -------------------------------- | ------------------------- |
|
|
1895
|
-
| `--wui-segmented-trigger-px` | `12px` | Horizontal padding |
|
|
1896
|
-
| `--wui-segmented-trigger-bg-hover` | `--wui-color-state-layer-hover` | Trigger hover background |
|
|
1897
|
-
| `--wui-segmented-trigger-bg-active` | `--wui-color-state-layer-active` | Trigger active background |
|
|
346
|
+
| **Form Controls** | [`<web-ui-input>`](docs/input.md) |
|
|
347
|
+
| | [`<web-ui-textarea>`](docs/textarea.md) |
|
|
348
|
+
| | [`<web-ui-editable-text>`](docs/editable-text.md) |
|
|
349
|
+
| | [`<web-ui-input-number>`](docs/input-number.md) |
|
|
350
|
+
| | [`<web-ui-select>`](docs/select.md) |
|
|
351
|
+
| | [`<web-ui-autocomplete>`](docs/autocomplete.md) |
|
|
352
|
+
| | [`<web-ui-slider>`](docs/slider.md) |
|
|
353
|
+
| | [`<web-ui-checkbox>`](docs/checkbox.md) |
|
|
354
|
+
| | [`<web-ui-radio>`](docs/radio.md) |
|
|
355
|
+
| | [`<web-ui-switch>`](docs/switch.md) |
|
|
356
|
+
| | [`<web-ui-segmented>`](docs/segmented.md) |
|
|
357
|
+
| | [`<web-ui-checkbox-group>`](docs/checkbox-group.md) |
|
|
358
|
+
| | [`<web-ui-radio-group>`](docs/radio-group.md) |
|
|
359
|
+
| **Buttons** | [`<web-ui-button>`](docs/button.md) |
|
|
360
|
+
| | [`<web-ui-button-group>`](docs/button-group.md) |
|
|
361
|
+
| **Overlay / Modal** | [`<web-ui-dialog>`](docs/dialog.md) |
|
|
362
|
+
| | [`<web-ui-drawer>`](docs/drawer.md) |
|
|
363
|
+
| | [`imagePreview()`](docs/image-preview.md) |
|
|
364
|
+
| **In-flow Disclosure** | [`<web-ui-collapse>`](docs/collapse.md) |
|
|
365
|
+
| **Floating** | [`<web-ui-popover>`](docs/popover.md) |
|
|
366
|
+
| | [`<web-ui-tooltip>`](docs/tooltip.md) |
|
|
367
|
+
| | [`<web-ui-context-menu>`](docs/context-menu.md) |
|
|
368
|
+
| **Menu** | [`<web-ui-dropdown>`](docs/dropdown.md) |
|
|
369
|
+
| | [`<web-ui-dropdown-item>`](docs/dropdown-item.md) |
|
|
370
|
+
| | [`<web-ui-dropdown-divider>`](docs/dropdown-divider.md) |
|
|
371
|
+
| | [`<web-ui-dropdown-header>`](docs/dropdown-header.md) |
|
|
372
|
+
| **Data Display** | [`<web-ui-avatar>`](docs/avatar.md) |
|
|
373
|
+
| | [`<web-ui-badge>`](docs/badge.md) |
|
|
374
|
+
| | [`<web-ui-empty>`](docs/empty.md) |
|
|
375
|
+
| | [`<web-ui-middle-ellipsis>`](docs/middle-ellipsis.md) |
|
|
376
|
+
| | [`<web-ui-icon>`](docs/icon.md) |
|
|
377
|
+
| | [`<web-ui-spinner>`](docs/spinner.md) |
|
|
378
|
+
| **Layout & Utility** | [`<web-ui-layout>`](docs/layout.md) |
|
|
379
|
+
| | [`<web-ui-back-top>`](docs/back-top.md) |
|
|
380
|
+
| | [`<web-ui-svg-draw-lines>`](docs/svg-draw-lines.md) |
|
|
381
|
+
| | [`<web-ui-theme>`](docs/theme.md) |
|
|
382
|
+
| **Notification** | [`<web-ui-toast>`](docs/toast.md) |
|
|
383
|
+
| **Sub-items** | [`<web-ui-option>`](docs/option.md) |
|
|
384
|
+
| | [`<web-ui-segmented-trigger>`](docs/segmented-trigger.md) |
|