@adia-ai/web-components 0.6.36 → 0.6.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/components/badge/badge.a2ui.json +10 -0
  3. package/components/badge/badge.css +70 -0
  4. package/components/badge/badge.yaml +20 -0
  5. package/components/blockquote/blockquote.a2ui.json +121 -0
  6. package/components/blockquote/blockquote.class.js +68 -0
  7. package/components/blockquote/blockquote.css +46 -0
  8. package/components/blockquote/blockquote.d.ts +31 -0
  9. package/components/blockquote/blockquote.js +17 -0
  10. package/components/blockquote/blockquote.yaml +124 -0
  11. package/components/button/button.css +11 -3
  12. package/components/calendar-picker/calendar-picker.a2ui.json +15 -0
  13. package/components/calendar-picker/calendar-picker.class.js +7 -1
  14. package/components/calendar-picker/calendar-picker.yaml +14 -0
  15. package/components/color-input/color-input.a2ui.json +2 -2
  16. package/components/color-input/color-input.class.js +9 -2
  17. package/components/color-input/color-input.yaml +2 -2
  18. package/components/combobox/combobox.class.js +4 -0
  19. package/components/context-menu/context-menu.a2ui.json +159 -0
  20. package/components/context-menu/context-menu.class.js +275 -0
  21. package/components/context-menu/context-menu.css +56 -0
  22. package/components/context-menu/context-menu.d.ts +70 -0
  23. package/components/context-menu/context-menu.js +17 -0
  24. package/components/context-menu/context-menu.yaml +136 -0
  25. package/components/date-range-picker/date-range-picker.a2ui.json +15 -0
  26. package/components/date-range-picker/date-range-picker.class.js +2 -0
  27. package/components/date-range-picker/date-range-picker.yaml +14 -0
  28. package/components/datetime-picker/datetime-picker.a2ui.json +15 -0
  29. package/components/datetime-picker/datetime-picker.class.js +3 -1
  30. package/components/datetime-picker/datetime-picker.d.ts +2 -0
  31. package/components/datetime-picker/datetime-picker.yaml +14 -0
  32. package/components/empty-state/empty-state.class.js +2 -0
  33. package/components/feed/feed.class.js +13 -5
  34. package/components/feed/feed.css +14 -0
  35. package/components/index.js +9 -0
  36. package/components/integration-card/integration-card.class.js +9 -0
  37. package/components/integration-card/integration-card.test.js +4 -3
  38. package/components/nav-group/nav-group.css +7 -1
  39. package/components/number-format/number-format.a2ui.json +180 -0
  40. package/components/number-format/number-format.class.js +96 -0
  41. package/components/number-format/number-format.css +18 -0
  42. package/components/number-format/number-format.d.ts +68 -0
  43. package/components/number-format/number-format.js +17 -0
  44. package/components/number-format/number-format.yaml +204 -0
  45. package/components/pagination/pagination.a2ui.json +19 -2
  46. package/components/pagination/pagination.class.js +90 -37
  47. package/components/pagination/pagination.css +32 -127
  48. package/components/pagination/pagination.d.ts +8 -2
  49. package/components/pagination/pagination.test.js +195 -0
  50. package/components/pagination/pagination.yaml +22 -1
  51. package/components/password-strength/password-strength.a2ui.json +152 -0
  52. package/components/password-strength/password-strength.class.js +157 -0
  53. package/components/password-strength/password-strength.css +80 -0
  54. package/components/password-strength/password-strength.d.ts +59 -0
  55. package/components/password-strength/password-strength.js +17 -0
  56. package/components/password-strength/password-strength.yaml +153 -0
  57. package/components/popover/popover.css +43 -23
  58. package/components/popover/popover.yaml +8 -4
  59. package/components/qr-code/QR-TEST.svg +4 -0
  60. package/components/qr-code/qr-code.a2ui.json +154 -0
  61. package/components/qr-code/qr-code.class.js +129 -0
  62. package/components/qr-code/qr-code.css +41 -0
  63. package/components/qr-code/qr-code.d.ts +83 -0
  64. package/components/qr-code/qr-code.js +17 -0
  65. package/components/qr-code/qr-code.yaml +203 -0
  66. package/components/qr-code/qr-encoder.js +633 -0
  67. package/components/relative-time/relative-time.a2ui.json +120 -0
  68. package/components/relative-time/relative-time.class.js +136 -0
  69. package/components/relative-time/relative-time.css +22 -0
  70. package/components/relative-time/relative-time.d.ts +51 -0
  71. package/components/relative-time/relative-time.js +17 -0
  72. package/components/relative-time/relative-time.yaml +133 -0
  73. package/components/segmented/segmented.class.js +5 -1
  74. package/components/select/select.class.js +4 -0
  75. package/components/skip-nav/skip-nav.a2ui.json +92 -0
  76. package/components/skip-nav/skip-nav.class.js +45 -0
  77. package/components/skip-nav/skip-nav.css +54 -0
  78. package/components/skip-nav/skip-nav.d.ts +27 -0
  79. package/components/skip-nav/skip-nav.js +12 -0
  80. package/components/skip-nav/skip-nav.yaml +68 -0
  81. package/components/slider/slider.a2ui.json +16 -1
  82. package/components/slider/slider.class.js +264 -122
  83. package/components/slider/slider.css +82 -2
  84. package/components/slider/slider.d.ts +19 -3
  85. package/components/slider/slider.test.js +55 -0
  86. package/components/slider/slider.yaml +28 -6
  87. package/components/table/table.class.js +29 -6
  88. package/components/table/table.css +31 -4
  89. package/components/table-toolbar/table-toolbar.class.js +3 -1
  90. package/components/tag/tag.a2ui.json +3 -2
  91. package/components/tag/tag.css +35 -11
  92. package/components/tag/tag.d.ts +14 -0
  93. package/components/tag/tag.test.js +35 -11
  94. package/components/tag/tag.yaml +13 -7
  95. package/components/toast/toast.class.js +12 -4
  96. package/components/toc/toc.a2ui.json +159 -0
  97. package/components/toc/toc.class.js +222 -0
  98. package/components/toc/toc.css +92 -0
  99. package/components/toc/toc.d.ts +61 -0
  100. package/components/toc/toc.js +17 -0
  101. package/components/toc/toc.yaml +180 -0
  102. package/components/toolbar/toolbar.class.js +3 -0
  103. package/components/visually-hidden/visually-hidden.a2ui.json +71 -0
  104. package/components/visually-hidden/visually-hidden.class.js +14 -0
  105. package/components/visually-hidden/visually-hidden.css +25 -0
  106. package/components/visually-hidden/visually-hidden.d.ts +26 -0
  107. package/components/visually-hidden/visually-hidden.js +12 -0
  108. package/components/visually-hidden/visually-hidden.yaml +54 -0
  109. package/core/anchor.js +19 -3
  110. package/dist/web-components.min.css +1 -1
  111. package/dist/web-components.min.js +100 -89
  112. package/package.json +1 -1
  113. package/styles/colors/semantics.css +11 -2
  114. package/styles/components.css +9 -0
  115. package/styles/resets.css +10 -0
@@ -0,0 +1,180 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: UITableOfContents
3
+ tag: toc-ui
4
+ status: stable
5
+ component: TableOfContents
6
+ category: navigation
7
+ version: 1
8
+ description: |
9
+ Auto-generated in-page table of contents. Scans a target container
10
+ for headings (default `h2,h3`), ensures each has an `id` (slugifies
11
+ the text content if missing), and stamps a `<nav>` list of anchor
12
+ links. An `IntersectionObserver` tracks the active heading and
13
+ applies `[data-active]` to the matching link so consumers can style
14
+ the currently-visible section. Smooth-scroll on click is handled by
15
+ the global `scroll-behavior: smooth` set in resets.css.
16
+
17
+ Pair with a sticky container (`position: sticky; top: <offset>;`) in
18
+ an aside / right rail for the classic docs-site outline pattern.
19
+ props:
20
+ target:
21
+ description: |
22
+ CSS selector pointing to the container to scan. Empty (default)
23
+ scans the toc-ui's parent element. Set to `#article` /
24
+ `[data-toc-target]` / `main` for a specific scope.
25
+ type: string
26
+ default: ""
27
+ reflect: true
28
+ headings:
29
+ description: |
30
+ CSS selector listing the heading tags to include (e.g. `h2,h3`).
31
+ The selector is matched against the target container's
32
+ descendants. Default `h2,h3` produces the common two-level
33
+ outline.
34
+ type: string
35
+ default: "h2,h3"
36
+ reflect: true
37
+ offset:
38
+ description: |
39
+ Top offset in pixels for active-state detection. The
40
+ IntersectionObserver's `rootMargin` is set to
41
+ `-<offset>px 0px -50% 0px` so the active item becomes the
42
+ heading nearest the top of the viewport BELOW any sticky header
43
+ chrome. Tune to match the height of your sticky topbar (default
44
+ `80` is a reasonable docs-shell value).
45
+ type: number
46
+ default: 80
47
+ reflect: true
48
+ label:
49
+ description: |
50
+ `aria-label` for the nav. Defaults to `Table of contents`.
51
+ type: string
52
+ default: "Table of contents"
53
+ reflect: true
54
+ events:
55
+ section-change:
56
+ description: Fired when the active heading changes (the user scrolls into a new section). Detail carries the new active id.
57
+ detail:
58
+ activeId: string
59
+ slots:
60
+ default:
61
+ description: Stamped automatically — a `<nav>` containing a flat `<ul>` of `<a href="#id">` links. Override by authoring your own content; auto-stamp is skipped when a `<nav>` is already present.
62
+ states:
63
+ - name: idle
64
+ description: Default — no active section yet.
65
+ - name: active
66
+ description: A section is in view; the matching `<a>` carries `[data-active]`.
67
+ attribute: data-active
68
+ tokens:
69
+ --toc-fg:
70
+ description: Default link color.
71
+ default: var(--a-fg-muted)
72
+ --toc-fg-active:
73
+ description: Active-link color.
74
+ default: var(--a-fg)
75
+ --toc-fg-hover:
76
+ description: Hover color.
77
+ default: var(--a-fg)
78
+ --toc-indent:
79
+ description: Per-depth-level indent.
80
+ default: var(--a-space-3)
81
+ --toc-gap:
82
+ description: Vertical gap between links.
83
+ default: var(--a-space-1)
84
+ --toc-rule-active:
85
+ description: Active-item indicator rule color (left border).
86
+ default: var(--a-accent-bg)
87
+ --toc-padding-block:
88
+ description: Per-link block padding (top/bottom).
89
+ default: var(--a-space-1)
90
+ --toc-padding-inline:
91
+ description: Per-link inline padding.
92
+ default: var(--a-space-2)
93
+ --toc-size:
94
+ description: Link font-size.
95
+ default: var(--a-ui-sm)
96
+ requiredIcons: []
97
+ a2ui:
98
+ rules:
99
+ - rule: "Use for in-page section navigation. Pair with a sticky container in an aside / right rail for the docs-site outline pattern."
100
+ reason: "Single-use-case primitive."
101
+ - rule: "Default scans the toc-ui's parent for h2/h3 headings. Set [target] to a CSS selector for a specific container; set [headings] to a comma-separated tag list (e.g. h1,h2,h3) for different depth coverage."
102
+ reason: "Configuration contract."
103
+ - rule: "Headings missing an [id] receive an auto-generated slug from their text content. Existing ids are preserved."
104
+ reason: "Id-assignment behavior."
105
+ - rule: "Smooth-scroll on click is handled by the global `scroll-behavior: smooth` in resets.css (gated by prefers-reduced-motion). Do NOT add per-toc-ui smooth-scroll JS — the global wins."
106
+ reason: "No duplicate scroll wiring."
107
+ anti_patterns:
108
+ - wrong: |
109
+ <toc-ui></toc-ui>
110
+ <h2>...</h2><h3>...</h3>
111
+ why: |
112
+ toc-ui's default scans its PARENT for headings. If toc-ui is a
113
+ sibling of the content (not inside it), the scan finds the
114
+ headings inside the toc-ui's parent which usually means the
115
+ headings + the toc itself — works only by coincidence. Set
116
+ [target] explicitly for clarity.
117
+ fix: |
118
+ <toc-ui target="#article"></toc-ui>
119
+ <article id="article">
120
+ <h2>...</h2><h3>...</h3>
121
+ </article>
122
+ examples:
123
+ - name: default
124
+ description: TOC scanning the parent container.
125
+ a2ui: |
126
+ [
127
+ {
128
+ "id": "page",
129
+ "component": "Section",
130
+ "children": ["toc", "h1", "h2-1", "p-1", "h2-2", "p-2"]
131
+ },
132
+ {"id": "toc", "component": "TableOfContents"},
133
+ {"id": "h1", "component": "Text", "variant": "title", "textContent": "Article title"},
134
+ {"id": "h2-1", "component": "Text", "variant": "heading", "textContent": "Introduction"},
135
+ {"id": "p-1", "component": "Text", "textContent": "..."},
136
+ {"id": "h2-2", "component": "Text", "variant": "heading", "textContent": "Conclusion"},
137
+ {"id": "p-2", "component": "Text", "textContent": "..."}
138
+ ]
139
+ - name: targeted
140
+ description: TOC scanning a specific article container.
141
+ a2ui: |
142
+ [
143
+ {
144
+ "id": "shell",
145
+ "component": "Row",
146
+ "children": ["toc", "article"]
147
+ },
148
+ {
149
+ "id": "toc",
150
+ "component": "TableOfContents",
151
+ "target": "#main-article",
152
+ "headings": "h2,h3,h4"
153
+ },
154
+ {
155
+ "id": "article",
156
+ "component": "Section",
157
+ "attrs": { "id": "main-article" },
158
+ "children": []
159
+ }
160
+ ]
161
+ keywords:
162
+ - toc
163
+ - table-of-contents
164
+ - outline
165
+ - sub-nav
166
+ - page-nav
167
+ - in-page-nav
168
+ - heading-nav
169
+ synonyms:
170
+ toc:
171
+ - table-of-contents
172
+ - outline
173
+ outline:
174
+ - toc
175
+ - table-of-contents
176
+ related:
177
+ - aside
178
+ - nav
179
+ - link
180
+ - text
@@ -94,6 +94,9 @@ export class UIToolbar extends UIElement {
94
94
  gap: { type: String, default: 'sm', reflect: true },
95
95
  align: { type: String, default: 'start', reflect: true },
96
96
  overflow: { type: String, default: 'menu', reflect: true },
97
+ // yaml documented reflect:true since v1 — closing the gap so
98
+ // el.bordered = true also updates [bordered] CSS state.
99
+ bordered: { type: Boolean, default: false, reflect: true },
97
100
  };
98
101
 
99
102
  static template = () => null;
@@ -0,0 +1,71 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/VisuallyHidden.json",
4
+ "title": "VisuallyHidden",
5
+ "description": "Accessibility utility — content visible to screen readers but hidden\nvisually (the canonical \"sr-only\" pattern). Use for icon-only buttons,\ncontextual labels on duplicate controls, status announcements via\nlive regions, and any text the AT needs but sighted users don't.\n\nDistinct from `[hidden]` / `display:none` (hidden from EVERYONE\nincluding AT) and from `aria-label` (which replaces visible text but\ndoesn't add invisible text). When you need both visible and invisible\ntext on the same element, `<visually-hidden-ui>` is the wrapper for\nthe invisible part.\n",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "component": {
17
+ "const": "VisuallyHidden"
18
+ }
19
+ },
20
+ "required": [
21
+ "component"
22
+ ],
23
+ "unevaluatedProperties": false,
24
+ "x-adiaui": {
25
+ "anti_patterns": [
26
+ {
27
+ "fix": "<button-ui icon=\"trash\"><visually-hidden-ui>Delete</visually-hidden-ui></button-ui> OR set aria-label=\"Delete\" — but visually-hidden-ui scales to longer context (\"Delete row 42 in the orders table\") without overloading the label attribute.",
28
+ "why": "Icon-only buttons with no accessible name are unreadable for screen-reader users.",
29
+ "wrong": "<button-ui icon=\"trash\"></button-ui>"
30
+ }
31
+ ],
32
+ "category": "utility",
33
+ "composes": [],
34
+ "events": {},
35
+ "examples": [
36
+ {
37
+ "description": "Sr-only label for an icon-only button.",
38
+ "a2ui": "[\n { \"id\": \"btn\", \"component\": \"Button\", \"icon\": \"trash\", \"children\": [\"lbl\"] },\n { \"id\": \"lbl\", \"component\": \"VisuallyHidden\", \"textContent\": \"Delete row\" }\n]\n",
39
+ "name": "icon-button-label"
40
+ }
41
+ ],
42
+ "keywords": [
43
+ "visually-hidden",
44
+ "sr-only",
45
+ "screen-reader",
46
+ "a11y",
47
+ "accessibility"
48
+ ],
49
+ "name": "UIVisuallyHidden",
50
+ "related": [
51
+ "text"
52
+ ],
53
+ "slots": {
54
+ "default": {
55
+ "description": "The text or content to hide visually while keeping accessible."
56
+ }
57
+ },
58
+ "states": [
59
+ {
60
+ "description": "Default — content is visually hidden but in the accessibility tree.",
61
+ "name": "idle"
62
+ }
63
+ ],
64
+ "status": "stable",
65
+ "synonyms": {},
66
+ "tag": "visually-hidden-ui",
67
+ "tokens": {},
68
+ "traits": [],
69
+ "version": 1
70
+ }
71
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `<visually-hidden-ui>` — sr-only utility.
3
+ *
4
+ * Pure CSS atom — no template, no JS behavior. Stamps the canonical
5
+ * sr-only pattern (see visually-hidden.css) so consumers don't need to
6
+ * memorize the spell.
7
+ */
8
+
9
+ import { UIElement } from '../../core/element.js';
10
+
11
+ export class UIVisuallyHidden extends UIElement {
12
+ static properties = {};
13
+ static template = () => null;
14
+ }
@@ -0,0 +1,25 @@
1
+ @scope (visually-hidden-ui) {
2
+ /* Canonical sr-only pattern — content available to assistive tech but
3
+ not painted. Avoid `display: none` (removes from a11y tree) and
4
+ `visibility: hidden` (removes too).
5
+
6
+ - position:absolute + clip-path:inset(100%) — hides without affecting
7
+ layout, with the clip-path doing the real visual removal
8
+ - width/height/padding/margin:0 — prevents layout impact
9
+ - white-space:nowrap — keeps multi-word strings on one line so the
10
+ clip works (a wrapping span can break the technique on some UAs)
11
+ - overflow:hidden + border:0 — belt-and-suspenders for cross-UA
12
+ */
13
+ :scope {
14
+ position: absolute !important;
15
+ width: 1px !important;
16
+ height: 1px !important;
17
+ padding: 0 !important;
18
+ margin: -1px !important;
19
+ overflow: hidden !important;
20
+ clip: rect(0, 0, 0, 0) !important;
21
+ clip-path: inset(100%) !important;
22
+ white-space: nowrap !important;
23
+ border: 0 !important;
24
+ }
25
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `<visually-hidden-ui>` — Accessibility utility — content visible to screen readers but hidden
3
+ visually (the canonical "sr-only" pattern). Use for icon-only buttons,
4
+ contextual labels on duplicate controls, status announcements via
5
+ live regions, and any text the AT needs but sighted users don't.
6
+
7
+ Distinct from `[hidden]` / `display:none` (hidden from EVERYONE
8
+ including AT) and from `aria-label` (which replaces visible text but
9
+ doesn't add invisible text). When you need both visible and invisible
10
+ text on the same element, `<visually-hidden-ui>` is the wrapper for
11
+ the invisible part.
12
+
13
+ *
14
+ * @see https://ui-kit.exe.xyz/site/components/visually-hidden
15
+ *
16
+ * Type declarations generated by scripts/build/dts-codegen.mjs from
17
+ * the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
18
+ * run `npm run build:components`, then `npm run codegen:dts` to
19
+ * regenerate; or hand-author this file fully if rich event types are
20
+ * needed beyond what the yaml `events:` block can express.
21
+ */
22
+
23
+ import { UIElement } from '../../core/element.js';
24
+
25
+ export class UIVisuallyHidden extends UIElement {
26
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `<visually-hidden-ui>` — auto-registers the tag on import.
3
+ *
4
+ * @see ../../USAGE.md#registration--auto-vs-explicit
5
+ */
6
+
7
+ import { defineIfFree } from '../../core/register.js';
8
+ import { UIVisuallyHidden } from './visually-hidden.class.js';
9
+
10
+ defineIfFree('visually-hidden-ui', UIVisuallyHidden);
11
+
12
+ export { UIVisuallyHidden };
@@ -0,0 +1,54 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: UIVisuallyHidden
3
+ tag: visually-hidden-ui
4
+ status: stable
5
+ component: VisuallyHidden
6
+ category: utility
7
+ version: 1
8
+ description: |
9
+ Accessibility utility — content visible to screen readers but hidden
10
+ visually (the canonical "sr-only" pattern). Use for icon-only buttons,
11
+ contextual labels on duplicate controls, status announcements via
12
+ live regions, and any text the AT needs but sighted users don't.
13
+
14
+ Distinct from `[hidden]` / `display:none` (hidden from EVERYONE
15
+ including AT) and from `aria-label` (which replaces visible text but
16
+ doesn't add invisible text). When you need both visible and invisible
17
+ text on the same element, `<visually-hidden-ui>` is the wrapper for
18
+ the invisible part.
19
+ props: {}
20
+ events: {}
21
+ slots:
22
+ default:
23
+ description: The text or content to hide visually while keeping accessible.
24
+ states:
25
+ - name: idle
26
+ description: Default — content is visually hidden but in the accessibility tree.
27
+ traits: []
28
+ tokens: {}
29
+ a2ui:
30
+ rules:
31
+ - rule: 'Wrap text that screen readers need but sighted users do not — icon-only-button labels, contextual disambiguation ("Edit profile" inside a row whose visible button just says "Edit").'
32
+ reason: 'Accessibility — AT users need the missing context.'
33
+ - rule: 'Distinct from [hidden] / display:none (hides from EVERYONE) and from aria-label (replaces visible text). Use <visually-hidden-ui> when both visible AND invisible text need to coexist on the same element.'
34
+ reason: 'Three different hide-mechanisms with different semantics.'
35
+ anti_patterns:
36
+ - wrong: '<button-ui icon="trash"></button-ui>'
37
+ why: 'Icon-only buttons with no accessible name are unreadable for screen-reader users.'
38
+ fix: '<button-ui icon="trash"><visually-hidden-ui>Delete</visually-hidden-ui></button-ui> OR set aria-label="Delete" — but visually-hidden-ui scales to longer context ("Delete row 42 in the orders table") without overloading the label attribute.'
39
+ examples:
40
+ - name: icon-button-label
41
+ description: Sr-only label for an icon-only button.
42
+ a2ui: |
43
+ [
44
+ { "id": "btn", "component": "Button", "icon": "trash", "children": ["lbl"] },
45
+ { "id": "lbl", "component": "VisuallyHidden", "textContent": "Delete row" }
46
+ ]
47
+ keywords:
48
+ - visually-hidden
49
+ - sr-only
50
+ - screen-reader
51
+ - a11y
52
+ - accessibility
53
+ related:
54
+ - text
package/core/anchor.js CHANGED
@@ -45,6 +45,9 @@ function anchorNative(anchor, popover, { placement, gap, matchWidth }) {
45
45
  positionTryFallbacks: popover.style.positionTryFallbacks,
46
46
  margin: popover.style.margin,
47
47
  width: popover.style.width,
48
+ maxWidth: popover.style.maxWidth,
49
+ maxHeight: popover.style.maxHeight,
50
+ overflowY: popover.style.overflowY,
48
51
  top: popover.style.top,
49
52
  left: popover.style.left,
50
53
  };
@@ -59,9 +62,22 @@ function anchorNative(anchor, popover, { placement, gap, matchWidth }) {
59
62
  // Let the browser flip across the opposite side + both perpendicular edges.
60
63
  popover.style.positionTryFallbacks = 'flip-block, flip-inline, flip-block flip-inline';
61
64
 
62
- if (matchWidth) {
63
- popover.style.width = `anchor-size(${name} width)`;
64
- }
65
+ // Sizing concerns that follow from anchor positioning — kept here (not in
66
+ // popover.css) because they're the anchor module's contract:
67
+ // • width: max-content — position-area defines a bounding span, NOT a
68
+ // content width; without a width hint the browser stretches the popover
69
+ // to fill the entire span (e.g. span-left can yield a 1300 px-wide
70
+ // popover on a wide viewport). max-content sizes to content; consumer
71
+ // min-width still clamps the lower bound.
72
+ // • max-width / max-height — viewport safety so popovers don't escape
73
+ // the visible area when the anchored content overflows.
74
+ // • overflow-y: auto — required when max-height clamps (otherwise content
75
+ // past the cap is silently hidden).
76
+ // Consumers can override per-instance via --popover-* tokens / inline style.
77
+ popover.style.width = matchWidth ? `anchor-size(${name} width)` : 'max-content';
78
+ popover.style.maxWidth = 'min(calc(100vw - 1rem), 32rem)';
79
+ popover.style.maxHeight = 'calc(100vh - 3rem)';
80
+ popover.style.overflowY = 'auto';
65
81
 
66
82
  // top/left are controlled by position-area; make sure no stale values fight it.
67
83
  popover.style.top = '';