@chromvoid/uikit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +96 -0
  3. package/dist/components/cv-accordion-item.d.ts +69 -0
  4. package/dist/components/cv-accordion-item.js +176 -0
  5. package/dist/components/cv-accordion.d.ts +79 -0
  6. package/dist/components/cv-accordion.js +310 -0
  7. package/dist/components/cv-alert-dialog.d.ts +86 -0
  8. package/dist/components/cv-alert-dialog.js +393 -0
  9. package/dist/components/cv-alert.d.ts +48 -0
  10. package/dist/components/cv-alert.js +156 -0
  11. package/dist/components/cv-badge.d.ts +56 -0
  12. package/dist/components/cv-badge.js +280 -0
  13. package/dist/components/cv-breadcrumb-item.d.ts +35 -0
  14. package/dist/components/cv-breadcrumb-item.js +64 -0
  15. package/dist/components/cv-breadcrumb.d.ts +39 -0
  16. package/dist/components/cv-breadcrumb.js +160 -0
  17. package/dist/components/cv-button.d.ts +83 -0
  18. package/dist/components/cv-button.js +541 -0
  19. package/dist/components/cv-callout.d.ts +32 -0
  20. package/dist/components/cv-callout.js +221 -0
  21. package/dist/components/cv-card.d.ts +48 -0
  22. package/dist/components/cv-card.js +269 -0
  23. package/dist/components/cv-carousel-slide.d.ts +25 -0
  24. package/dist/components/cv-carousel-slide.js +51 -0
  25. package/dist/components/cv-carousel.d.ts +96 -0
  26. package/dist/components/cv-carousel.js +457 -0
  27. package/dist/components/cv-checkbox.d.ts +84 -0
  28. package/dist/components/cv-checkbox.js +274 -0
  29. package/dist/components/cv-combobox-group.d.ts +15 -0
  30. package/dist/components/cv-combobox-group.js +34 -0
  31. package/dist/components/cv-combobox-option.d.ts +30 -0
  32. package/dist/components/cv-combobox-option.js +66 -0
  33. package/dist/components/cv-combobox.d.ts +135 -0
  34. package/dist/components/cv-combobox.js +834 -0
  35. package/dist/components/cv-command-item.d.ts +30 -0
  36. package/dist/components/cv-command-item.js +68 -0
  37. package/dist/components/cv-command-palette.d.ts +105 -0
  38. package/dist/components/cv-command-palette.js +578 -0
  39. package/dist/components/cv-context-menu.d.ts +98 -0
  40. package/dist/components/cv-context-menu.js +515 -0
  41. package/dist/components/cv-copy-button.d.ts +61 -0
  42. package/dist/components/cv-copy-button.js +318 -0
  43. package/dist/components/cv-date-picker.d.ts +161 -0
  44. package/dist/components/cv-date-picker.js +803 -0
  45. package/dist/components/cv-dialog.d.ts +89 -0
  46. package/dist/components/cv-dialog.js +459 -0
  47. package/dist/components/cv-disclosure.d.ts +57 -0
  48. package/dist/components/cv-disclosure.js +241 -0
  49. package/dist/components/cv-drawer.d.ts +102 -0
  50. package/dist/components/cv-drawer.js +595 -0
  51. package/dist/components/cv-feed-article.d.ts +26 -0
  52. package/dist/components/cv-feed-article.js +52 -0
  53. package/dist/components/cv-feed.d.ts +62 -0
  54. package/dist/components/cv-feed.js +310 -0
  55. package/dist/components/cv-grid-cell.d.ts +30 -0
  56. package/dist/components/cv-grid-cell.js +57 -0
  57. package/dist/components/cv-grid-column.d.ts +30 -0
  58. package/dist/components/cv-grid-column.js +43 -0
  59. package/dist/components/cv-grid-row.d.ts +30 -0
  60. package/dist/components/cv-grid-row.js +42 -0
  61. package/dist/components/cv-grid.d.ts +119 -0
  62. package/dist/components/cv-grid.js +567 -0
  63. package/dist/components/cv-icon.d.ts +57 -0
  64. package/dist/components/cv-icon.js +352 -0
  65. package/dist/components/cv-input.d.ts +127 -0
  66. package/dist/components/cv-input.js +482 -0
  67. package/dist/components/cv-landmark.d.ts +32 -0
  68. package/dist/components/cv-landmark.js +62 -0
  69. package/dist/components/cv-link.d.ts +22 -0
  70. package/dist/components/cv-link.js +99 -0
  71. package/dist/components/cv-listbox-group.d.ts +15 -0
  72. package/dist/components/cv-listbox-group.js +42 -0
  73. package/dist/components/cv-listbox.d.ts +81 -0
  74. package/dist/components/cv-listbox.js +388 -0
  75. package/dist/components/cv-menu-button.d.ts +118 -0
  76. package/dist/components/cv-menu-button.js +822 -0
  77. package/dist/components/cv-menu-group.d.ts +20 -0
  78. package/dist/components/cv-menu-group.js +48 -0
  79. package/dist/components/cv-menu-item.d.ts +52 -0
  80. package/dist/components/cv-menu-item.js +105 -0
  81. package/dist/components/cv-menu.d.ts +62 -0
  82. package/dist/components/cv-menu.js +414 -0
  83. package/dist/components/cv-meter.d.ts +66 -0
  84. package/dist/components/cv-meter.js +154 -0
  85. package/dist/components/cv-number.d.ts +139 -0
  86. package/dist/components/cv-number.js +553 -0
  87. package/dist/components/cv-option.d.ts +30 -0
  88. package/dist/components/cv-option.js +84 -0
  89. package/dist/components/cv-popover.d.ts +87 -0
  90. package/dist/components/cv-popover.js +373 -0
  91. package/dist/components/cv-progress-ring.d.ts +45 -0
  92. package/dist/components/cv-progress-ring.js +169 -0
  93. package/dist/components/cv-progress.d.ts +45 -0
  94. package/dist/components/cv-progress.js +148 -0
  95. package/dist/components/cv-radio-group.d.ts +79 -0
  96. package/dist/components/cv-radio-group.js +398 -0
  97. package/dist/components/cv-radio.d.ts +36 -0
  98. package/dist/components/cv-radio.js +123 -0
  99. package/dist/components/cv-select-group.d.ts +15 -0
  100. package/dist/components/cv-select-group.js +44 -0
  101. package/dist/components/cv-select-option.d.ts +30 -0
  102. package/dist/components/cv-select-option.js +66 -0
  103. package/dist/components/cv-select.d.ts +128 -0
  104. package/dist/components/cv-select.js +666 -0
  105. package/dist/components/cv-sidebar-item.d.ts +26 -0
  106. package/dist/components/cv-sidebar-item.js +142 -0
  107. package/dist/components/cv-sidebar.d.ts +171 -0
  108. package/dist/components/cv-sidebar.js +767 -0
  109. package/dist/components/cv-slider-multi-thumb.d.ts +73 -0
  110. package/dist/components/cv-slider-multi-thumb.js +374 -0
  111. package/dist/components/cv-slider.d.ts +84 -0
  112. package/dist/components/cv-slider.js +328 -0
  113. package/dist/components/cv-spinbutton.d.ts +121 -0
  114. package/dist/components/cv-spinbutton.js +486 -0
  115. package/dist/components/cv-spinner.d.ts +18 -0
  116. package/dist/components/cv-spinner.js +95 -0
  117. package/dist/components/cv-switch.d.ts +81 -0
  118. package/dist/components/cv-switch.js +285 -0
  119. package/dist/components/cv-tab-panel.d.ts +20 -0
  120. package/dist/components/cv-tab-panel.js +37 -0
  121. package/dist/components/cv-tab.d.ts +40 -0
  122. package/dist/components/cv-tab.js +132 -0
  123. package/dist/components/cv-table-cell.d.ts +31 -0
  124. package/dist/components/cv-table-cell.js +49 -0
  125. package/dist/components/cv-table-column.d.ts +37 -0
  126. package/dist/components/cv-table-column.js +63 -0
  127. package/dist/components/cv-table-row.d.ts +30 -0
  128. package/dist/components/cv-table-row.js +45 -0
  129. package/dist/components/cv-table.d.ts +147 -0
  130. package/dist/components/cv-table.js +607 -0
  131. package/dist/components/cv-tabs.d.ts +70 -0
  132. package/dist/components/cv-tabs.js +524 -0
  133. package/dist/components/cv-textarea.d.ts +108 -0
  134. package/dist/components/cv-textarea.js +328 -0
  135. package/dist/components/cv-toast-region.d.ts +39 -0
  136. package/dist/components/cv-toast-region.js +162 -0
  137. package/dist/components/cv-toast.d.ts +67 -0
  138. package/dist/components/cv-toast.js +315 -0
  139. package/dist/components/cv-toolbar-item.d.ts +25 -0
  140. package/dist/components/cv-toolbar-item.js +72 -0
  141. package/dist/components/cv-toolbar-separator.d.ts +25 -0
  142. package/dist/components/cv-toolbar-separator.js +45 -0
  143. package/dist/components/cv-toolbar.d.ts +63 -0
  144. package/dist/components/cv-toolbar.js +295 -0
  145. package/dist/components/cv-tooltip.d.ts +83 -0
  146. package/dist/components/cv-tooltip.js +455 -0
  147. package/dist/components/cv-treegrid-cell.d.ts +30 -0
  148. package/dist/components/cv-treegrid-cell.js +57 -0
  149. package/dist/components/cv-treegrid-column.d.ts +37 -0
  150. package/dist/components/cv-treegrid-column.js +53 -0
  151. package/dist/components/cv-treegrid-row.d.ts +55 -0
  152. package/dist/components/cv-treegrid-row.js +90 -0
  153. package/dist/components/cv-treegrid.d.ts +96 -0
  154. package/dist/components/cv-treegrid.js +632 -0
  155. package/dist/components/cv-treeitem.d.ts +58 -0
  156. package/dist/components/cv-treeitem.js +144 -0
  157. package/dist/components/cv-treeview.d.ts +70 -0
  158. package/dist/components/cv-treeview.js +396 -0
  159. package/dist/components/cv-window-splitter.d.ts +79 -0
  160. package/dist/components/cv-window-splitter.js +316 -0
  161. package/dist/components/index.d.ts +94 -0
  162. package/dist/components/index.js +79 -0
  163. package/dist/dialog/create-dialog-controller.d.ts +31 -0
  164. package/dist/dialog/create-dialog-controller.js +320 -0
  165. package/dist/dialog/index.d.ts +2 -0
  166. package/dist/dialog/index.js +1 -0
  167. package/dist/form-associated/FormAssociatedReatomElement.d.ts +25 -0
  168. package/dist/form-associated/FormAssociatedReatomElement.js +70 -0
  169. package/dist/form-associated/withFormAssociated.d.ts +5 -0
  170. package/dist/form-associated/withFormAssociated.js +1 -0
  171. package/dist/index.d.ts +10 -0
  172. package/dist/index.js +9 -0
  173. package/dist/reatom-lit/ReatomLitElement.d.ts +27 -0
  174. package/dist/reatom-lit/ReatomLitElement.js +118 -0
  175. package/dist/reatom-lit/html.d.ts +4 -0
  176. package/dist/reatom-lit/html.js +10 -0
  177. package/dist/reatom-lit/index.d.ts +4 -0
  178. package/dist/reatom-lit/index.js +4 -0
  179. package/dist/reatom-lit/watch.d.ts +15 -0
  180. package/dist/reatom-lit/watch.js +40 -0
  181. package/dist/reatom-lit/withReatomElement.d.ts +4 -0
  182. package/dist/reatom-lit/withReatomElement.js +57 -0
  183. package/dist/register.d.ts +1 -0
  184. package/dist/register.js +84 -0
  185. package/dist/styles/component-styles.d.ts +4 -0
  186. package/dist/styles/component-styles.js +78 -0
  187. package/dist/theme/cv-theme-provider.d.ts +32 -0
  188. package/dist/theme/cv-theme-provider.js +110 -0
  189. package/dist/theme/index.d.ts +4 -0
  190. package/dist/theme/index.js +2 -0
  191. package/dist/theme/theme-engine.d.ts +4 -0
  192. package/dist/theme/theme-engine.js +67 -0
  193. package/dist/theme/tokens.css +265 -0
  194. package/dist/theme/types.d.ts +7 -0
  195. package/dist/theme/types.js +1 -0
  196. package/dist/toast/create-toast-controller.d.ts +12 -0
  197. package/dist/toast/create-toast-controller.js +12 -0
  198. package/dist/toast/index.d.ts +2 -0
  199. package/dist/toast/index.js +1 -0
  200. package/package.json +146 -0
  201. package/specs/_template.md +110 -0
  202. package/specs/components/accordion.md +207 -0
  203. package/specs/components/alert.md +83 -0
  204. package/specs/components/badge.md +183 -0
  205. package/specs/components/breadcrumb.md +152 -0
  206. package/specs/components/button.md +227 -0
  207. package/specs/components/callout.md +153 -0
  208. package/specs/components/card.md +192 -0
  209. package/specs/components/carousel.md +232 -0
  210. package/specs/components/checkbox.md +141 -0
  211. package/specs/components/combobox.md +427 -0
  212. package/specs/components/context-menu.md +375 -0
  213. package/specs/components/copy-button.md +236 -0
  214. package/specs/components/date-picker.md +290 -0
  215. package/specs/components/dialog.md +184 -0
  216. package/specs/components/disclosure.md +151 -0
  217. package/specs/components/drawer.md +216 -0
  218. package/specs/components/feed.md +266 -0
  219. package/specs/components/grid.md +423 -0
  220. package/specs/components/input.md +237 -0
  221. package/specs/components/landmark.md +92 -0
  222. package/specs/components/link.md +117 -0
  223. package/specs/components/listbox.md +327 -0
  224. package/specs/components/menu.md +508 -0
  225. package/specs/components/meter.md +148 -0
  226. package/specs/components/number.md +268 -0
  227. package/specs/components/option.md +167 -0
  228. package/specs/components/popover.md +207 -0
  229. package/specs/components/progress-ring.md +134 -0
  230. package/specs/components/progress.md +110 -0
  231. package/specs/components/radio.md +208 -0
  232. package/specs/components/select.md +305 -0
  233. package/specs/components/sidebar.md +204 -0
  234. package/specs/components/spinbutton.md +157 -0
  235. package/specs/components/spinner.md +83 -0
  236. package/specs/components/switch.md +145 -0
  237. package/specs/components/table.md +372 -0
  238. package/specs/components/tabs.md +242 -0
  239. package/specs/components/textarea.md +166 -0
  240. package/specs/components/theme.md +364 -0
  241. package/specs/components/toast.md +198 -0
  242. package/specs/components/toolbar.md +258 -0
  243. package/specs/components/tooltip.md +152 -0
  244. package/specs/components/treegrid.md +363 -0
  245. package/specs/components/treeview.md +263 -0
  246. package/specs/components/window-splitter.md +225 -0
@@ -0,0 +1,216 @@
1
+ # cv-drawer
2
+
3
+ Slide-out panel dialog anchored to a viewport edge, used for navigation, forms, or supplementary content.
4
+
5
+ **Headless:** [`createDrawer`](https://github.com/chromvoid/headless-ui/blob/main/specs/components/drawer.md)
6
+
7
+ ## Anatomy
8
+
9
+ ```
10
+ <cv-drawer> (host)
11
+ ├── <button part="trigger">
12
+ │ └── <slot name="trigger">
13
+ └── <div part="overlay"> (hidden when closed)
14
+ └── <section part="panel" role="dialog|alertdialog" data-placement="...">
15
+ ├── <header part="header">
16
+ │ ├── <h2 part="title" id="...">
17
+ │ │ └── <slot name="title">
18
+ │ ├── <p part="description" id="...">
19
+ │ │ └── <slot name="description">
20
+ │ └── <button part="header-close" aria-label="Close">
21
+ │ └── <slot name="header-close">
22
+ ├── <div part="body">
23
+ │ └── <slot>
24
+ └── <footer part="footer">
25
+ └── <slot name="footer">
26
+ ```
27
+
28
+ ## Attributes
29
+
30
+ | Attribute | Type | Default | Description |
31
+ | -------------------------- | ------- | ---------- | ------------------------------------------------------------------ |
32
+ | `open` | Boolean | `false` | Whether the drawer is visible |
33
+ | `modal` | Boolean | `true` | Enables modal behavior (focus trap, scroll lock, backdrop) |
34
+ | `placement` | String | `"end"` | Edge the drawer slides from: `start` \| `end` \| `top` \| `bottom` |
35
+ | `type` | String | `"dialog"` | ARIA role type: `dialog` \| `alertdialog` |
36
+ | `close-on-escape` | Boolean | `true` | Whether Escape key closes the drawer |
37
+ | `close-on-outside-pointer` | Boolean | `true` | Whether clicking outside closes the drawer |
38
+ | `close-on-outside-focus` | Boolean | `true` | Whether focusing outside closes the drawer |
39
+ | `initial-focus-id` | String | --- | Id of element to focus when drawer opens |
40
+ | `no-header` | Boolean | `false` | Hides the header (title, description, header close button) |
41
+
42
+ ## Slots
43
+
44
+ | Slot | Description |
45
+ | -------------- | -------------------------------------------------------- |
46
+ | `(default)` | Drawer body content |
47
+ | `trigger` | Content for the trigger button |
48
+ | `title` | Drawer title text |
49
+ | `description` | Description text below the title |
50
+ | `header-close` | Icon content for the header close button (defaults to X) |
51
+ | `footer` | Footer content (action buttons, etc.) |
52
+
53
+ ## CSS Parts
54
+
55
+ | Part | Element | Description |
56
+ | -------------- | ----------- | ----------------------------------------------------------- |
57
+ | `trigger` | `<button>` | Trigger button that opens the drawer |
58
+ | `overlay` | `<div>` | Backdrop/overlay container |
59
+ | `panel` | `<section>` | Drawer panel with `role="dialog"` or `role="alertdialog"` |
60
+ | `header` | `<header>` | Header area containing title, description, and close button |
61
+ | `title` | `<h2>` | Drawer title element |
62
+ | `description` | `<p>` | Drawer description element |
63
+ | `header-close` | `<button>` | Header close icon button |
64
+ | `body` | `<div>` | Body content area |
65
+ | `footer` | `<footer>` | Footer area for user-provided action buttons |
66
+
67
+ ## CSS Custom Properties
68
+
69
+ | Property | Default | Description |
70
+ | ----------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
71
+ | `--cv-drawer-z-index` | `40` | Z-index of the overlay layer |
72
+ | `--cv-drawer-size` | `360px` | Inline size (for `start`/`end`) or block size (for `top`/`bottom`) of the panel |
73
+ | `--cv-drawer-max-size` | `calc(100dvh - 32px)` | Maximum size before internal scrolling (block axis for `top`/`bottom`, inline axis for `start`/`end`) |
74
+ | `--cv-drawer-header-spacing` | `var(--cv-space-4, 16px)` | Header padding |
75
+ | `--cv-drawer-body-spacing` | `var(--cv-space-4, 16px)` | Body padding |
76
+ | `--cv-drawer-footer-spacing` | `var(--cv-space-4, 16px)` | Footer padding |
77
+ | `--cv-drawer-overlay-color` | `color-mix(in oklab, black 56%, transparent)` | Backdrop overlay color |
78
+ | `--cv-drawer-overlay-transition-duration` | `0ms` | Overlay opacity transition duration |
79
+ | `--cv-drawer-overlay-closed-opacity` | `1` | Overlay opacity while the panel is animating out or before the panel animates in |
80
+ | `--cv-drawer-border-radius` | `var(--cv-radius-lg, 14px)` | Panel border radius (applied to the inward edge only) |
81
+ | `--cv-drawer-transition-duration` | `250ms` | Slide transition duration |
82
+
83
+ ## Visual States
84
+
85
+ | Host selector | Description |
86
+ | ----------------------------- | ------------------------------------------------------------------- |
87
+ | `:host([open])` | Drawer visible, overlay shown |
88
+ | `:host([modal])` | Modal mode active (focus trap, scroll lock, backdrop) |
89
+ | `:host([type="alertdialog"])` | Alert dialog mode with `role="alertdialog"` |
90
+ | `:host([no-header])` | Header section hidden |
91
+ | `:host([placement="start"])` | Panel anchored to the inline-start edge (left in LTR, right in RTL) |
92
+ | `:host([placement="end"])` | Panel anchored to the inline-end edge (right in LTR, left in RTL) |
93
+ | `:host([placement="top"])` | Panel anchored to the top edge |
94
+ | `:host([placement="bottom"])` | Panel anchored to the bottom edge |
95
+
96
+ ### Placement layout rules
97
+
98
+ - `start` / `end`: Panel stretches full viewport block size; inline size set by `--cv-drawer-size`. Border radius applied to the inner vertical edge.
99
+ - `top` / `bottom`: Panel stretches full viewport inline size; block size set by `--cv-drawer-size`. Border radius applied to the inner horizontal edge.
100
+ - `start` and `end` follow CSS logical directions and automatically flip in RTL layouts.
101
+
102
+ ## Events
103
+
104
+ | Event | Detail | Description |
105
+ | --------------- | ----------------- | -------------------------------------------------- |
106
+ | `cv-input` | `{open: boolean}` | Fires when open state changes via user interaction |
107
+ | `cv-change` | `{open: boolean}` | Fires when open state commits |
108
+ | `cv-show` | --- | Fires when drawer begins to open |
109
+ | `cv-after-show` | --- | Fires after drawer open animation completes |
110
+ | `cv-hide` | --- | Fires when drawer begins to close |
111
+ | `cv-after-hide` | --- | Fires after drawer close animation completes |
112
+
113
+ `cv-input` and `cv-change` fire only for user-initiated state changes (trigger click, Escape, outside pointer, outside focus, header close). Programmatic `open` attribute changes do not emit these events.
114
+
115
+ ## Reactive State Mapping
116
+
117
+ `cv-drawer` is a visual adapter over headless `createDrawer`.
118
+
119
+ | UIKit Property | Direction | Headless Binding |
120
+ | -------------------------- | -------------- | -------------------------------------------------------------------------------------- |
121
+ | `open` | attr -> action | `actions.open()` / `actions.close()` |
122
+ | `modal` | attr -> option | passed as `isModal` in `createDrawer(options)` |
123
+ | `placement` | attr -> action | `actions.setPlacement(placement)` and passed as `placement` in `createDrawer(options)` |
124
+ | `type` | attr -> option | passed as `type` in `createDrawer(options)` |
125
+ | `close-on-escape` | attr -> option | passed as `closeOnEscape` in `createDrawer(options)` |
126
+ | `close-on-outside-pointer` | attr -> option | passed as `closeOnOutsidePointer` in `createDrawer(options)` |
127
+ | `close-on-outside-focus` | attr -> option | passed as `closeOnOutsideFocus` in `createDrawer(options)` |
128
+ | `initial-focus-id` | attr -> option | passed as `initialFocusId` in `createDrawer(options)` |
129
+ | `no-header` | attr -> DOM | controls header visibility (UIKit-only, no headless binding) |
130
+
131
+ | Headless State | Direction | DOM Reflection |
132
+ | ------------------------------ | --------------- | --------------------------------------------- |
133
+ | `state.isOpen()` | state -> attr | `[open]` host attribute |
134
+ | `state.isModal()` | state -> attr | `[modal]` host attribute |
135
+ | `state.type()` | state -> attr | `[type]` host attribute |
136
+ | `state.placement()` | state -> attr | `[placement]` host attribute |
137
+ | `state.isFocusTrapped()` | state -> effect | activates focus trap within the drawer |
138
+ | `state.shouldLockScroll()` | state -> effect | applies `overflow: hidden` to `document.body` |
139
+ | `state.restoreTargetId()` | state -> effect | focuses the trigger element on close |
140
+ | `state.initialFocusTargetId()` | state -> effect | focuses the specified element on open |
141
+
142
+ - `contracts.getTriggerProps()` is spread onto `[part="trigger"]` to apply `role`, `aria-haspopup`, `aria-expanded`, `aria-controls`, `tabindex`, and click/keydown handlers.
143
+ - `contracts.getOverlayProps()` is spread onto `[part="overlay"]` to apply `hidden`, `data-open`, and outside pointer/focus handlers.
144
+ - `contracts.getPanelProps()` is spread onto `[part="panel"]` to apply `role` (`dialog` or `alertdialog`), `aria-modal`, `aria-labelledby`, `aria-describedby`, `data-placement`, `tabindex`, and keydown handler.
145
+ - `contracts.getTitleProps()` is spread onto `[part="title"]` to apply the `id` for `aria-labelledby`.
146
+ - `contracts.getDescriptionProps()` is spread onto `[part="description"]` to apply the `id` for `aria-describedby`.
147
+ - `contracts.getHeaderCloseButtonProps()` is spread onto `[part="header-close"]` to apply `role`, `tabindex`, `aria-label: 'Close'`, and click handler.
148
+ - UIKit dispatches `cv-input` and `cv-change` events by observing `isOpen` changes triggered by user interaction (not by controlled `open` attribute changes).
149
+ - UIKit dispatches `cv-show`/`cv-after-show`/`cv-hide`/`cv-after-hide` lifecycle events to bracket CSS transitions.
150
+ - UIKit owns scroll lock implementation, focus trap implementation, focus restoration, backdrop rendering, slide animations, and CSS transitions -- headless provides signals, UIKit applies side effects.
151
+
152
+ ## Usage
153
+
154
+ ```html
155
+ <!-- Basic drawer (slides from end) -->
156
+ <cv-drawer>
157
+ <span slot="trigger">Open drawer</span>
158
+ <span slot="title">Settings</span>
159
+ <p>Drawer body content here.</p>
160
+ <div slot="footer">
161
+ <cv-button variant="ghost">Cancel</cv-button>
162
+ <cv-button variant="primary">Save</cv-button>
163
+ </div>
164
+ </cv-drawer>
165
+
166
+ <!-- Left-side navigation drawer -->
167
+ <cv-drawer placement="start">
168
+ <span slot="trigger">Menu</span>
169
+ <span slot="title">Navigation</span>
170
+ <nav>
171
+ <a href="/home">Home</a>
172
+ <a href="/settings">Settings</a>
173
+ </nav>
174
+ </cv-drawer>
175
+
176
+ <!-- Bottom sheet drawer -->
177
+ <cv-drawer placement="bottom">
178
+ <span slot="trigger">Show details</span>
179
+ <span slot="title">Details</span>
180
+ <p>Content slides up from the bottom.</p>
181
+ </cv-drawer>
182
+
183
+ <!-- Top drawer -->
184
+ <cv-drawer placement="top">
185
+ <span slot="trigger">Notifications</span>
186
+ <span slot="title">Notifications</span>
187
+ <p>Notification content slides down from the top.</p>
188
+ </cv-drawer>
189
+
190
+ <!-- Non-modal drawer -->
191
+ <cv-drawer modal="false">
192
+ <span slot="trigger">Show panel</span>
193
+ <span slot="title">Side panel</span>
194
+ <p>This drawer does not block the page.</p>
195
+ </cv-drawer>
196
+
197
+ <!-- Alert drawer -->
198
+ <cv-drawer type="alertdialog">
199
+ <span slot="trigger">Delete account</span>
200
+ <span slot="title">Are you sure?</span>
201
+ <span slot="description">This action is permanent and cannot be undone.</span>
202
+ <div slot="footer">
203
+ <cv-button variant="ghost">Cancel</cv-button>
204
+ <cv-button variant="danger">Delete</cv-button>
205
+ </div>
206
+ </cv-drawer>
207
+
208
+ <!-- Without header -->
209
+ <cv-drawer no-header>
210
+ <span slot="trigger">Quick panel</span>
211
+ <p>Minimal drawer with body content only.</p>
212
+ <div slot="footer">
213
+ <cv-button variant="primary">Done</cv-button>
214
+ </div>
215
+ </cv-drawer>
216
+ ```
@@ -0,0 +1,266 @@
1
+ # cv-feed
2
+
3
+ Bidirectional infinite-scrolling feed container that dynamically loads articles as the user scrolls, with APG-compliant keyboard navigation and focus management.
4
+
5
+ **Headless:** [`createFeed`](https://github.com/chromvoid/headless-ui/blob/main/specs/components/feed.md)
6
+
7
+ ## Cross-Spec Consistency
8
+
9
+ This document is the UIKit surface contract for Feed.
10
+
11
+ - Headless `createFeed` is the source of truth for state, transitions, and invariants.
12
+ - UIKit mirrors headless contracts through DOM attributes and events.
13
+ - Any intentional divergence between UIKit and headless MUST be documented in both specs.
14
+
15
+ ## Anatomy
16
+
17
+ ```
18
+ <cv-feed> (host)
19
+ └── <div part="base" role="feed">
20
+ ├── <div part="sentinel-top"> ← IntersectionObserver target for loading newer
21
+ ├── <div part="loading-indicator" aria-hidden="true"> ← only when [loading]
22
+ │ └── <slot name="loading">
23
+ ├── <slot name="empty"> ← only when [empty]
24
+ ├── <slot name="error"> ← only when [error]
25
+ ├── <slot> ← accepts <cv-feed-article> children
26
+ └── <div part="sentinel-bottom"> ← IntersectionObserver target for loading more
27
+ ```
28
+
29
+ ## Attributes
30
+
31
+ | Attribute | Type | Default | Description |
32
+ | --------- | ------- | ------- | ------------------------------------------------------------------------ |
33
+ | `label` | String | `""` | Accessible name for the feed (`aria-label`) |
34
+ | `busy` | Boolean | `false` | Reflects `aria-busy` during load operations |
35
+ | `loading` | Boolean | `false` | Shows loading indicator |
36
+ | `empty` | Boolean | `false` | Indicates no articles are loaded (read-only, reflected from headless) |
37
+ | `error` | Boolean | `false` | Indicates an error state is present (read-only, reflected from headless) |
38
+
39
+ ## Slots
40
+
41
+ | Slot | Description |
42
+ | ----------- | ------------------------------------------------ |
43
+ | `(default)` | One or more `<cv-feed-article>` children |
44
+ | `empty` | Content shown when the feed has no articles |
45
+ | `error` | Content shown when the feed is in an error state |
46
+ | `loading` | Custom loading indicator content |
47
+
48
+ ## CSS Parts
49
+
50
+ | Part | Element | Description |
51
+ | ------------------- | -------- | ----------------------------------------------------- |
52
+ | `base` | `<div>` | Root wrapper with `role="feed"` |
53
+ | `sentinel-top` | `<div>` | Top intersection sentinel for loading newer content |
54
+ | `sentinel-bottom` | `<div>` | Bottom intersection sentinel for loading more content |
55
+ | `empty` | `<slot>` | Empty state slot wrapper |
56
+ | `error` | `<slot>` | Error state slot wrapper |
57
+ | `loading-indicator` | `<div>` | Loading indicator wrapper |
58
+
59
+ ## CSS Custom Properties
60
+
61
+ | Property | Default | Description |
62
+ | ------------------------------ | ------------------------- | --------------------------------------------------- |
63
+ | `--cv-feed-gap` | `var(--cv-space-3, 12px)` | Spacing between articles |
64
+ | `--cv-feed-padding-block` | `var(--cv-space-3, 12px)` | Vertical padding of the feed container |
65
+ | `--cv-feed-padding-inline` | `0` | Horizontal padding of the feed container |
66
+ | `--cv-feed-sentinel-height` | `1px` | Height of sentinel elements (should remain minimal) |
67
+ | `--cv-feed-loading-min-height` | `48px` | Minimum height of the loading indicator area |
68
+
69
+ ## Visual States
70
+
71
+ | Host selector | Description |
72
+ | ------------------ | ----------------------------------------------------------------- |
73
+ | `:host([busy])` | Feed is busy loading content; `aria-busy="true"` on the feed root |
74
+ | `:host([loading])` | Loading indicator is visible |
75
+ | `:host([empty])` | Feed has no articles; empty slot is rendered |
76
+ | `:host([error])` | Feed has an error; error slot is rendered |
77
+
78
+ ## ARIA Contract
79
+
80
+ - Root element has `role="feed"`
81
+ - Root element exposes `aria-label` (from `label` attribute) and `aria-busy` (from `busy` attribute)
82
+ - The feed container itself is NOT focusable
83
+ - Articles are focusable via roving tabindex managed by headless
84
+ - `Ctrl+End` and `Ctrl+Home` move focus outside the feed (adapter responsibility)
85
+
86
+ All ARIA attributes on the feed root are derived from `contracts.getFeedProps()`. UIKit does not compute ARIA state independently.
87
+
88
+ ## Events
89
+
90
+ | Event | Detail | Description |
91
+ | ---------------- | ------ | ------------------------------------------------------------------------- |
92
+ | `cv-load-more` | `{}` | Fired when the bottom sentinel enters the viewport (IntersectionObserver) |
93
+ | `cv-load-newer` | `{}` | Fired when the top sentinel enters the viewport (IntersectionObserver) |
94
+ | `cv-exit-after` | `{}` | Fired on `Ctrl+End`; consumer should move focus after the feed |
95
+ | `cv-exit-before` | `{}` | Fired on `Ctrl+Home`; consumer should move focus before the feed |
96
+
97
+ These events are output-only signals. The feed does not use `input` or `change` events because it has no user-modifiable value state.
98
+
99
+ ## Reactive State Mapping
100
+
101
+ `cv-feed` is a visual adapter over headless `createFeed`.
102
+
103
+ ### Attribute to Headless (UIKit -> Headless)
104
+
105
+ | UIKit Property | Direction | Headless Binding |
106
+ | -------------- | -------------- | ---------------------------------------------- |
107
+ | `label` | attr -> option | passed as `ariaLabel` in `createFeed(options)` |
108
+ | `busy` | attr -> action | `actions.setBusy(value)` |
109
+
110
+ ### Headless to DOM (Headless -> UIKit)
111
+
112
+ | Headless State | Direction | DOM Reflection |
113
+ | ------------------------- | --------------- | ------------------------------------------------ |
114
+ | `state.isBusy()` | state -> attr | `[busy]` host attribute |
115
+ | `state.isLoading()` | state -> attr | `[loading]` host attribute |
116
+ | `state.isEmpty()` | state -> attr | `[empty]` host attribute |
117
+ | `state.hasError()` | state -> attr | `[error]` host attribute |
118
+ | `state.error()` | state -> render | error message available for the error slot |
119
+ | `state.canLoadMore()` | state -> render | bottom sentinel visibility / observer activation |
120
+ | `state.canLoadNewer()` | state -> render | top sentinel visibility / observer activation |
121
+ | `state.articleIds()` | state -> render | ordered list for rendering articles |
122
+ | `state.activeArticleId()` | state -> render | focus management on child articles |
123
+
124
+ ### Contract Spreading
125
+
126
+ - `contracts.getFeedProps()` is spread onto `[part="base"]` -- applies `role`, `aria-label`, `aria-busy`
127
+ - `contracts.getArticleProps(articleId)` is spread onto each `cv-feed-article` child -- applies `role`, `tabindex`, `aria-posinset`, `aria-setsize`, `aria-disabled`, `data-active`, `onFocus`
128
+
129
+ ### UIKit-Only Concerns (NOT in headless)
130
+
131
+ - IntersectionObserver setup on `[part="sentinel-top"]` and `[part="sentinel-bottom"]`
132
+ - DOM focus transfer for `Ctrl+End` / `Ctrl+Home` (dispatches `cv-exit-after` / `cv-exit-before` events)
133
+ - Empty state and error state conditional slot rendering
134
+ - Loading indicator rendering
135
+ - `cv-load-more` and `cv-load-newer` event dispatch
136
+
137
+ ## Behavioral Contract
138
+
139
+ ### Bidirectional Loading
140
+
141
+ - The bottom sentinel `[part="sentinel-bottom"]` is observed via IntersectionObserver. When it intersects the viewport and `state.canLoadMore()` is `true`, `actions.loadMore()` is called and `cv-load-more` is dispatched.
142
+ - The top sentinel `[part="sentinel-top"]` is observed via IntersectionObserver. When it intersects the viewport and `state.canLoadNewer()` is `true`, `actions.loadNewer()` is called and `cv-load-newer` is dispatched.
143
+ - Concurrent load operations are guarded by headless (second call is a no-op while loading).
144
+
145
+ ### Keyboard Navigation
146
+
147
+ Per W3C APG Feed Pattern:
148
+
149
+ - `PageDown`: move focus to the next article (headless `focusNextArticle`)
150
+ - `PageUp`: move focus to the previous article (headless `focusPrevArticle`)
151
+ - `Ctrl+End`: dispatch `cv-exit-after` event; consumer moves focus after the feed
152
+ - `Ctrl+Home`: dispatch `cv-exit-before` event; consumer moves focus before the feed
153
+
154
+ Keyboard events are forwarded to `actions.handleKeyDown(event)`. The return value determines adapter behavior:
155
+
156
+ - `'next'` / `'prev'`: headless handled focus movement
157
+ - `'exit-after'`: UIKit dispatches `cv-exit-after`
158
+ - `'exit-before'`: UIKit dispatches `cv-exit-before`
159
+ - `null`: key not handled, no action
160
+
161
+ ### Conditional Rendering
162
+
163
+ - When `state.isEmpty()` is `true`, the `empty` named slot is rendered and the default slot is hidden.
164
+ - When `state.hasError()` is `true`, the `error` named slot is rendered.
165
+ - When `state.isLoading()` is `true`, the `loading` named slot / default loading indicator is rendered.
166
+ - Empty and error states are not mutually exclusive with loading; the feed may show a loading indicator alongside an error slot.
167
+
168
+ ## Usage
169
+
170
+ ```html
171
+ <!-- Basic feed -->
172
+ <cv-feed label="Latest posts">
173
+ <cv-feed-article article-id="post-1">
174
+ <h3>First Post</h3>
175
+ <p>Content of the first post.</p>
176
+ </cv-feed-article>
177
+ <cv-feed-article article-id="post-2">
178
+ <h3>Second Post</h3>
179
+ <p>Content of the second post.</p>
180
+ </cv-feed-article>
181
+ </cv-feed>
182
+
183
+ <!-- Feed with empty and error states -->
184
+ <cv-feed label="Activity feed">
185
+ <div slot="empty">No activity yet.</div>
186
+ <div slot="error">Failed to load. Please try again.</div>
187
+ <div slot="loading">Loading articles...</div>
188
+ </cv-feed>
189
+
190
+ <!-- Feed with event handling for infinite scroll -->
191
+ <cv-feed
192
+ label="News feed"
193
+ @cv-load-more="${handleLoadMore}"
194
+ @cv-load-newer="${handleLoadNewer}"
195
+ @cv-exit-after="${handleExitAfter}"
196
+ @cv-exit-before="${handleExitBefore}"
197
+ >
198
+ <cv-feed-article article-id="news-1">
199
+ <h3>Breaking News</h3>
200
+ <p>Details here.</p>
201
+ </cv-feed-article>
202
+ </cv-feed>
203
+ ```
204
+
205
+ ## Child Elements
206
+
207
+ ### cv-feed-article
208
+
209
+ Individual article within a feed. The parent `cv-feed` manages all ARIA attributes on this element via headless contracts.
210
+
211
+ #### Anatomy
212
+
213
+ ```
214
+ <cv-feed-article> (host)
215
+ └── <div part="base" role="article">
216
+ └── <slot>
217
+ ```
218
+
219
+ #### Attributes
220
+
221
+ | Attribute | Type | Default | Description |
222
+ | ------------ | ------- | ------- | ------------------------------------------------------------------------- |
223
+ | `article-id` | String | `""` | Required unique identifier for this article within the feed |
224
+ | `active` | Boolean | `false` | Whether this article is the currently focused article. Managed by parent. |
225
+ | `disabled` | Boolean | `false` | Whether this article is disabled (skipped during keyboard navigation) |
226
+
227
+ #### Slots
228
+
229
+ | Slot | Description |
230
+ | ----------- | --------------- |
231
+ | `(default)` | Article content |
232
+
233
+ #### CSS Parts
234
+
235
+ | Part | Element | Description |
236
+ | ------ | ------- | ---------------------------------- |
237
+ | `base` | `<div>` | Root wrapper with `role="article"` |
238
+
239
+ #### CSS Custom Properties
240
+
241
+ | Property | Default | Description |
242
+ | --------------------------------- | -------------------------------------------- | --------------------------------------- |
243
+ | `--cv-feed-article-padding` | `var(--cv-space-3, 12px)` | Padding inside the article |
244
+ | `--cv-feed-article-border-radius` | `var(--cv-radius-sm, 6px)` | Border radius of the article |
245
+ | `--cv-feed-article-focus-ring` | `2px solid var(--cv-color-primary, #65d7ff)` | Focus ring style for the active article |
246
+
247
+ #### Visual States
248
+
249
+ | Host selector | Description |
250
+ | ------------------- | ----------------------------------------------------------- |
251
+ | `:host([active])` | Article is the currently focused/active article in the feed |
252
+ | `:host([disabled])` | Article is disabled and skipped during keyboard navigation |
253
+
254
+ #### Reactive State Mapping
255
+
256
+ `cv-feed-article` receives its ARIA props from the parent `cv-feed` via `contracts.getArticleProps(articleId)`:
257
+
258
+ | Contract Prop | DOM Reflection |
259
+ | --------------- | ------------------------------------------------------------- |
260
+ | `role` | `role="article"` on `[part="base"]` |
261
+ | `tabindex` | `tabindex="0"` (active) or `tabindex="-1"` (inactive) on host |
262
+ | `aria-posinset` | Position within the feed (1-based) |
263
+ | `aria-setsize` | Total article count or `-1` if unknown |
264
+ | `aria-disabled` | `"true"` when article is disabled |
265
+ | `data-active` | `"true"` or `"false"` reflecting active state |
266
+ | `onFocus` | Sets this article as active in headless state |