@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,375 @@
1
+ # cv-context-menu
2
+
3
+ Contextual menu triggered by right-click, long-press on touch, or keyboard invocation, supporting action items, checkable items (checkbox/radio), sub-menus, separators, and group labels.
4
+
5
+ **Headless:** [`createContextMenu`](https://github.com/chromvoid/headless-ui/blob/main/specs/components/context-menu.md)
6
+
7
+ ## Anatomy
8
+
9
+ ```
10
+ <cv-context-menu> (host)
11
+ ├── <div part="target" tabindex="0">
12
+ │ └── <slot name="target">
13
+ └── <div part="menu" role="menu" tabindex="-1">
14
+ └── <slot> ← cv-context-menu-item / cv-context-menu-separator / cv-context-menu-group children
15
+ ```
16
+
17
+ ## Attributes
18
+
19
+ | Attribute | Type | Default | Description |
20
+ | -------------------------- | ------- | ------- | --------------------------------------------- |
21
+ | `value` | String | `""` | Last selected item value |
22
+ | `open` | Boolean | `false` | Whether the menu is currently visible |
23
+ | `anchor-x` | Number | `0` | X coordinate of the menu anchor point |
24
+ | `anchor-y` | Number | `0` | Y coordinate of the menu anchor point |
25
+ | `aria-label` | String | `""` | Accessible label for the menu |
26
+ | `close-on-select` | Boolean | `true` | Close the menu after an item is selected |
27
+ | `close-on-outside-pointer` | Boolean | `true` | Close the menu on pointer interaction outside |
28
+
29
+ ## Slots
30
+
31
+ | Slot | Description |
32
+ | ----------- | ----------------------------------------------------------------------------------------- |
33
+ | `target` | Content that acts as the right-click/long-press target zone |
34
+ | `(default)` | `cv-context-menu-item`, `cv-context-menu-separator`, and `cv-context-menu-group` children |
35
+
36
+ ## CSS Parts
37
+
38
+ | Part | Element | Description |
39
+ | -------- | ------- | ----------------------------------------------------- |
40
+ | `target` | `<div>` | Wrapper for the trigger/target zone |
41
+ | `menu` | `<div>` | Menu popup container positioned at anchor coordinates |
42
+
43
+ ## CSS Custom Properties
44
+
45
+ | Property | Default | Description |
46
+ | ----------------------------------- | --------------------------- | ------------------------------------------------------------------------------ |
47
+ | `--cv-context-menu-x` | `0px` | Inline-start position of the menu popup (set programmatically from `anchor-x`) |
48
+ | `--cv-context-menu-y` | `0px` | Block-start position of the menu popup (set programmatically from `anchor-y`) |
49
+ | `--cv-context-menu-min-inline-size` | `180px` | Minimum inline size of the menu popup |
50
+ | `--cv-context-menu-padding` | `var(--cv-space-1, 4px)` | Padding inside the menu popup |
51
+ | `--cv-context-menu-gap` | `var(--cv-space-1, 4px)` | Gap between menu items |
52
+ | `--cv-context-menu-border-radius` | `var(--cv-radius-md, 10px)` | Border radius of the menu popup |
53
+ | `--cv-context-menu-z-index` | `80` | Z-index of the menu popup |
54
+
55
+ ## Visual States
56
+
57
+ | Host selector | Description |
58
+ | --------------------- | --------------------- |
59
+ | `:host([open])` | Menu popup is visible |
60
+ | `:host(:not([open]))` | Menu popup is hidden |
61
+
62
+ ## Events
63
+
64
+ | Event | Detail | Description |
65
+ | ----------- | ----------------------------------------------------- | ----------------------------------------------------------- |
66
+ | `cv-input` | `{value, activeId, open, anchorX, anchorY, openedBy}` | Fires on any state change (selection, active, open, anchor) |
67
+ | `cv-change` | `{value, activeId, open, anchorX, anchorY, openedBy}` | Fires only when the selected `value` changes |
68
+
69
+ Event detail type:
70
+
71
+ ```ts
72
+ interface CVContextMenuEventDetail {
73
+ value: string | null
74
+ activeId: string | null
75
+ open: boolean
76
+ anchorX: number
77
+ anchorY: number
78
+ openedBy: string | null // 'pointer' | 'keyboard' | 'programmatic' | null
79
+ }
80
+ ```
81
+
82
+ ## Imperative API
83
+
84
+ | Method | Signature | Description |
85
+ | ---------- | -------------------------------- | --------------------------------------- |
86
+ | `openAt` | `(x: number, y: number) => void` | Opens the menu at the given coordinates |
87
+ | `cv-close` | `() => void` | Closes the menu |
88
+
89
+ ## Keyboard Interaction
90
+
91
+ ### Target element
92
+
93
+ | Key | Action |
94
+ | ------------- | --------------------------------------- |
95
+ | `ContextMenu` | Open menu at current anchor coordinates |
96
+ | `Shift+F10` | Open menu at current anchor coordinates |
97
+
98
+ ### Menu (when open, no sub-menu)
99
+
100
+ | Key | Action |
101
+ | ------------------- | --------------------------------------------------------- |
102
+ | `Escape` | Close menu, restore focus to target |
103
+ | `Tab` | Close menu, restore focus to target |
104
+ | `ArrowDown` | Move active to next enabled item (wrapping) |
105
+ | `ArrowUp` | Move active to previous enabled item (wrapping) |
106
+ | `ArrowRight` | If active item has a sub-menu: open it, focus first child |
107
+ | `Home` | Move active to first enabled item |
108
+ | `End` | Move active to last enabled item |
109
+ | `Enter` / `Space` | Select active item |
110
+ | Printable character | Type-ahead: move active to matching item by label prefix |
111
+
112
+ ### Sub-menu (when open)
113
+
114
+ | Key | Action |
115
+ | ----------------- | ------------------------------------------------- |
116
+ | `Escape` | Close sub-menu, return to parent menu |
117
+ | `ArrowLeft` | Close sub-menu, return to parent menu |
118
+ | `ArrowDown` | Move to next enabled sub-menu item (wrapping) |
119
+ | `ArrowUp` | Move to previous enabled sub-menu item (wrapping) |
120
+ | `Home` | Move to first enabled sub-menu item |
121
+ | `End` | Move to last enabled sub-menu item |
122
+ | `Enter` / `Space` | Select active sub-menu item |
123
+
124
+ ## Touch Interaction
125
+
126
+ Long-press on the target zone opens the menu at the touch coordinates after the long-press threshold (default 500ms). Touch move or touch end before the threshold cancels the long-press.
127
+
128
+ ## ARIA Contract
129
+
130
+ | Element | Attribute | Value |
131
+ | ------- | --------------- | ------------------- |
132
+ | menu | `role` | `menu` |
133
+ | menu | `tabindex` | `-1` |
134
+ | menu | `aria-label` | optional label text |
135
+ | menu | `hidden` | reflects `!open` |
136
+ | menu | `data-anchor-x` | string of `anchorX` |
137
+ | menu | `data-anchor-y` | string of `anchorY` |
138
+ | target | `id` | `{idBase}-target` |
139
+
140
+ ## Reactive State Mapping
141
+
142
+ `cv-context-menu` is a visual adapter over headless `createContextMenu`.
143
+
144
+ | UIKit Property | Direction | Headless Binding |
145
+ | -------------------------- | -------------- | ------------------------------------------------------------------------------ |
146
+ | `value` | attr -> action | `actions.select(value)` when value changes |
147
+ | `open` | attr -> action | `actions.openAt(anchorX, anchorY)` when `true`; `actions.close()` when `false` |
148
+ | `anchor-x` / `anchor-y` | attr -> action | passed to `actions.openAt(x, y)` |
149
+ | `aria-label` | attr -> option | passed as `ariaLabel` in `createContextMenu(options)` |
150
+ | `close-on-select` | attr -> option | passed as `closeOnSelect` in `createContextMenu(options)` |
151
+ | `close-on-outside-pointer` | attr -> option | passed as `closeOnOutsidePointer` in `createContextMenu(options)` |
152
+
153
+ | Headless State | Direction | DOM Reflection |
154
+ | ------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------- |
155
+ | `state.isOpen()` | state -> attr | `[open]` host attribute, menu `[hidden]` |
156
+ | `state.activeId()` | state -> DOM | `[data-active]` on item elements, focus management |
157
+ | `state.anchorX()` / `state.anchorY()` | state -> attr | `[anchor-x]` / `[anchor-y]` host attributes, `--cv-context-menu-x` / `--cv-context-menu-y` CSS custom properties |
158
+ | `state.openedBy()` | state -> event | included in `cv-input`/`cv-change` event detail |
159
+ | `state.restoreTargetId()` | state -> DOM | focus restored to target element on close |
160
+ | `state.checkedIds()` | state -> DOM | `[aria-checked]` on checkbox/radio item elements |
161
+ | `state.openSubmenuId()` | state -> DOM | sub-menu container `[hidden]` state |
162
+ | `state.submenuActiveId()` | state -> DOM | `[data-active]` on sub-menu child items |
163
+
164
+ Contracts applied to DOM elements:
165
+
166
+ - `contracts.getTargetProps()` -> target wrapper (`[part="target"]`): provides `id`, `onContextMenu`, `onKeyDown`
167
+ - `contracts.getMenuProps()` -> menu container (`[part="menu"]`): provides `id`, `role`, `tabindex`, `hidden`, `aria-label`, `data-anchor-x`, `data-anchor-y`, `onKeyDown`
168
+ - `contracts.getItemProps(id)` -> each item element: provides `id`, `role`, `tabindex`, `aria-disabled`, `data-active`, `aria-checked`, `aria-haspopup`, `aria-expanded`, `onClick`
169
+ - `contracts.getSeparatorProps(id)` -> separator elements: provides `id`, `role`
170
+ - `contracts.getGroupLabelProps(id)` -> group label elements: provides `id`, `role`, `aria-label`
171
+ - `contracts.getSubmenuProps(id)` -> sub-menu containers: provides `id`, `role`, `tabindex`, `hidden`
172
+
173
+ UIKit does not own activation, navigation, or toggle logic; headless state is the source of truth.
174
+
175
+ ## Usage
176
+
177
+ ```html
178
+ <!-- Basic context menu -->
179
+ <cv-context-menu aria-label="File actions">
180
+ <div slot="target">Right-click here</div>
181
+ <cv-context-menu-item value="copy">Copy</cv-context-menu-item>
182
+ <cv-context-menu-item value="paste">Paste</cv-context-menu-item>
183
+ <cv-context-menu-item value="delete" disabled>Delete</cv-context-menu-item>
184
+ </cv-context-menu>
185
+
186
+ <!-- With separators and groups -->
187
+ <cv-context-menu aria-label="Edit actions">
188
+ <div slot="target">Right-click here</div>
189
+ <cv-context-menu-item value="cut">Cut</cv-context-menu-item>
190
+ <cv-context-menu-item value="copy">Copy</cv-context-menu-item>
191
+ <cv-context-menu-item value="paste">Paste</cv-context-menu-item>
192
+ <cv-context-menu-separator></cv-context-menu-separator>
193
+ <cv-context-menu-item value="select-all">Select All</cv-context-menu-item>
194
+ </cv-context-menu>
195
+
196
+ <!-- With checkbox items -->
197
+ <cv-context-menu aria-label="View options">
198
+ <div slot="target">Right-click here</div>
199
+ <cv-context-menu-item value="toolbar" type="checkbox" checked>Toolbar</cv-context-menu-item>
200
+ <cv-context-menu-item value="sidebar" type="checkbox">Sidebar</cv-context-menu-item>
201
+ <cv-context-menu-item value="statusbar" type="checkbox" checked>Status Bar</cv-context-menu-item>
202
+ </cv-context-menu>
203
+
204
+ <!-- With radio items -->
205
+ <cv-context-menu aria-label="Sort order">
206
+ <div slot="target">Right-click here</div>
207
+ <cv-context-menu-item value="name" type="radio" group="sort" checked>By Name</cv-context-menu-item>
208
+ <cv-context-menu-item value="date" type="radio" group="sort">By Date</cv-context-menu-item>
209
+ <cv-context-menu-item value="size" type="radio" group="sort">By Size</cv-context-menu-item>
210
+ </cv-context-menu>
211
+
212
+ <!-- With sub-menu -->
213
+ <cv-context-menu aria-label="Actions">
214
+ <div slot="target">Right-click here</div>
215
+ <cv-context-menu-item value="open">Open</cv-context-menu-item>
216
+ <cv-context-menu-item value="share" type="submenu">
217
+ Share
218
+ <cv-context-menu-item slot="submenu" value="email">Email</cv-context-menu-item>
219
+ <cv-context-menu-item slot="submenu" value="link">Copy Link</cv-context-menu-item>
220
+ </cv-context-menu-item>
221
+ </cv-context-menu>
222
+
223
+ <!-- Imperative positioning -->
224
+ <cv-context-menu id="my-menu" aria-label="Custom menu">
225
+ <div slot="target">Content area</div>
226
+ <cv-context-menu-item value="action1">Action 1</cv-context-menu-item>
227
+ </cv-context-menu>
228
+ <script>
229
+ document.getElementById('my-menu').openAt(200, 150)
230
+ </script>
231
+ ```
232
+
233
+ ## Child Elements
234
+
235
+ ### cv-context-menu-item
236
+
237
+ Actionable item within a context menu. Supports standard, checkbox, radio, and submenu types.
238
+
239
+ #### Anatomy
240
+
241
+ ```
242
+ <cv-context-menu-item> (host)
243
+ └── <div part="base" class="item">
244
+ └── <slot>
245
+ ```
246
+
247
+ #### Attributes
248
+
249
+ | Attribute | Type | Default | Description |
250
+ | ---------- | ------- | -------- | ------------------------------------------------------------------------- |
251
+ | `value` | String | `""` | Identifier for the item (used as selection value) |
252
+ | `disabled` | Boolean | `false` | Prevents selection and skips during navigation |
253
+ | `active` | Boolean | `false` | Reflects keyboard-active (highlighted) state (managed by parent) |
254
+ | `selected` | Boolean | `false` | Reflects whether this item is the last selected value (managed by parent) |
255
+ | `type` | String | `"item"` | Item type: `item` \| `checkbox` \| `radio` \| `submenu` |
256
+ | `checked` | Boolean | `false` | Initial checked state for checkbox/radio items |
257
+ | `group` | String | `""` | Radio group name for radio items |
258
+
259
+ #### Slots
260
+
261
+ | Slot | Description |
262
+ | ----------- | ------------------------------------------------------------------ |
263
+ | `(default)` | Item label text |
264
+ | `submenu` | Nested `cv-context-menu-item` children (only for `type="submenu"`) |
265
+
266
+ #### CSS Parts
267
+
268
+ | Part | Element | Description |
269
+ | ------ | ------- | ----------------- |
270
+ | `base` | `<div>` | Item root wrapper |
271
+
272
+ #### CSS Custom Properties
273
+
274
+ | Property | Default | Description |
275
+ | --------------------------------------- | -------------------------- | ------------------------------ |
276
+ | `--cv-context-menu-item-padding-inline` | `var(--cv-space-3, 12px)` | Horizontal padding of the item |
277
+ | `--cv-context-menu-item-padding-block` | `var(--cv-space-2, 8px)` | Vertical padding of the item |
278
+ | `--cv-context-menu-item-border-radius` | `var(--cv-radius-sm, 6px)` | Border radius of the item |
279
+
280
+ #### Visual States
281
+
282
+ | Host selector | Description |
283
+ | ----------------------------------------------- | ----------------------------------------------------- |
284
+ | `:host([active])` | Item has keyboard focus (primary tint at 24%) |
285
+ | `:host([selected])` | Item is the last selected value (primary tint at 32%) |
286
+ | `:host([disabled])` | Item is non-selectable (opacity 0.5) |
287
+ | `:host([hidden])` | Item is hidden when menu is closed |
288
+ | `:host([type="checkbox"][aria-checked="true"])` | Checkbox item is checked |
289
+ | `:host([type="radio"][aria-checked="true"])` | Radio item is checked |
290
+
291
+ #### ARIA Contract
292
+
293
+ | Item type | `role` | Additional attributes |
294
+ | ---------------- | ------------------ | ---------------------------------------------------------------------------------------- |
295
+ | `item` (default) | `menuitem` | `tabindex="-1"`, `aria-disabled` (when disabled), `data-active` |
296
+ | `checkbox` | `menuitemcheckbox` | `tabindex="-1"`, `aria-disabled`, `data-active`, `aria-checked` |
297
+ | `radio` | `menuitemradio` | `tabindex="-1"`, `aria-disabled`, `data-active`, `aria-checked` |
298
+ | `submenu` | `menuitem` | `tabindex="-1"`, `aria-disabled`, `data-active`, `aria-haspopup="menu"`, `aria-expanded` |
299
+
300
+ ---
301
+
302
+ ### cv-context-menu-separator
303
+
304
+ Visual divider between groups of menu items. Not actionable, skipped during keyboard navigation.
305
+
306
+ #### Anatomy
307
+
308
+ ```
309
+ <cv-context-menu-separator> (host)
310
+ └── <div part="base" role="separator">
311
+ ```
312
+
313
+ #### CSS Parts
314
+
315
+ | Part | Element | Description |
316
+ | ------ | ------- | ---------------------- |
317
+ | `base` | `<div>` | Separator line element |
318
+
319
+ #### CSS Custom Properties
320
+
321
+ | Property | Default | Description |
322
+ | ----------------------------------- | --------------------------------- | --------------------------- |
323
+ | `--cv-context-menu-separator-color` | `var(--cv-color-border, #2a3245)` | Color of the separator line |
324
+
325
+ #### ARIA Contract
326
+
327
+ | Attribute | Value |
328
+ | --------- | ----------- |
329
+ | `role` | `separator` |
330
+
331
+ ---
332
+
333
+ ### cv-context-menu-group
334
+
335
+ Labels a group of related menu items. Not actionable, skipped during keyboard navigation.
336
+
337
+ #### Anatomy
338
+
339
+ ```
340
+ <cv-context-menu-group> (host)
341
+ ├── <div part="label" role="presentation" aria-label="...">
342
+ └── <slot> ← cv-context-menu-item children
343
+ ```
344
+
345
+ #### Attributes
346
+
347
+ | Attribute | Type | Default | Description |
348
+ | --------- | ------ | ------- | ----------------------------------------------------------- |
349
+ | `label` | String | `""` | Group label text (set as `aria-label` on the label element) |
350
+
351
+ #### Slots
352
+
353
+ | Slot | Description |
354
+ | ----------- | --------------------------------------------- |
355
+ | `(default)` | `cv-context-menu-item` children in this group |
356
+
357
+ #### CSS Parts
358
+
359
+ | Part | Element | Description |
360
+ | ------- | ------- | ------------------------ |
361
+ | `label` | `<div>` | Group label text element |
362
+
363
+ #### CSS Custom Properties
364
+
365
+ | Property | Default | Description |
366
+ | ---------------------------------------------- | ------------------------- | ------------------------------------- |
367
+ | `--cv-context-menu-group-label-padding-inline` | `var(--cv-space-3, 12px)` | Horizontal padding of the group label |
368
+ | `--cv-context-menu-group-label-font-size` | `0.75em` | Font size of the group label |
369
+
370
+ #### ARIA Contract
371
+
372
+ | Attribute | Value |
373
+ | ------------ | ---------------- |
374
+ | `role` | `presentation` |
375
+ | `aria-label` | group label text |
@@ -0,0 +1,236 @@
1
+ # cv-copy-button
2
+
3
+ Button that copies a value to the system clipboard with three-state visual feedback (idle, success, error).
4
+
5
+ **Headless:** [`createCopyButton`](https://github.com/chromvoid/headless-ui/blob/main/specs/components/copy-button.md)
6
+
7
+ ## Anatomy
8
+
9
+ ```
10
+ <cv-copy-button> (host)
11
+ └── <div part="base" role="button">
12
+ ├── <span part="copy-icon">
13
+ │ └── <slot name="copy-icon"> (default: clipboard icon)
14
+ ├── <span part="success-icon">
15
+ │ └── <slot name="success-icon"> (default: check icon)
16
+ ├── <span part="error-icon">
17
+ │ └── <slot name="error-icon"> (default: x icon)
18
+ └── <span part="status" role="status" aria-live="polite">
19
+ ```
20
+
21
+ ## Attributes
22
+
23
+ | Attribute | Type | Default | Description |
24
+ | ------------------- | ------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
25
+ | `value` | String | `''` | Text to copy. Property also accepts `(() => Promise<string>)` for lazy/sensitive values (property-only, not reflected as attribute for security) |
26
+ | `disabled` | Boolean | `false` | Prevents interaction |
27
+ | `feedback-duration` | Number | `1500` | Milliseconds to show success/error feedback before reverting to idle |
28
+ | `size` | String | `"medium"` | Size: `small` \| `medium` \| `large` |
29
+
30
+ ## Sizes
31
+
32
+ | Size | `--cv-copy-button-size` |
33
+ | -------- | ----------------------- |
34
+ | `small` | `30px` |
35
+ | `medium` | `36px` |
36
+ | `large` | `42px` |
37
+
38
+ ## Slots
39
+
40
+ | Slot | Description |
41
+ | -------------- | ------------------------------------------------------ |
42
+ | `copy-icon` | Icon shown in idle state (default: clipboard icon) |
43
+ | `success-icon` | Icon shown after successful copy (default: check icon) |
44
+ | `error-icon` | Icon shown after copy failure (default: x icon) |
45
+
46
+ ## CSS Parts
47
+
48
+ | Part | Element | Description |
49
+ | -------------- | -------- | -------------------------------------------------- |
50
+ | `base` | `<div>` | Root interactive element with `role="button"` |
51
+ | `copy-icon` | `<span>` | Wrapper around the `copy-icon` slot |
52
+ | `success-icon` | `<span>` | Wrapper around the `success-icon` slot |
53
+ | `error-icon` | `<span>` | Wrapper around the `error-icon` slot |
54
+ | `status` | `<span>` | Live region for assistive technology announcements |
55
+
56
+ ## CSS Custom Properties
57
+
58
+ | Property | Default | Description |
59
+ | -------------------------------- | ---------------------------------- | -------------------------------------- |
60
+ | `--cv-copy-button-size` | `36px` | Overall button size (width and height) |
61
+ | `--cv-copy-button-border-radius` | `var(--cv-radius-sm, 6px)` | Border radius for button shape |
62
+ | `--cv-copy-button-success-color` | `var(--cv-color-success, #4ade80)` | Color applied during success state |
63
+ | `--cv-copy-button-error-color` | `var(--cv-color-danger, #ff7d86)` | Color applied during error state |
64
+
65
+ Additionally, component styles depend on theme tokens through fallback values:
66
+
67
+ | Theme Property | Default | Description |
68
+ | ---------------------- | --------- | -------------------------- |
69
+ | `--cv-color-border` | `#2a3245` | Base border color |
70
+ | `--cv-color-surface` | `#141923` | Surface background color |
71
+ | `--cv-color-text` | `#e8ecf6` | Default text/icon color |
72
+ | `--cv-color-success` | `#4ade80` | Success accent color |
73
+ | `--cv-color-danger` | `#ff7d86` | Danger accent color |
74
+ | `--cv-duration-fast` | `120ms` | Transition duration |
75
+ | `--cv-easing-standard` | `ease` | Transition timing function |
76
+ | `--cv-radius-sm` | `6px` | Base radius fallback |
77
+
78
+ ## Visual States
79
+
80
+ | Host selector | Description |
81
+ | --------------------------- | -------------------------------------------------------------------------------- |
82
+ | `:host([disabled])` | Reduced opacity (`0.55`), `cursor: not-allowed` |
83
+ | `:host([status="idle"])` | Default state; copy icon visible, success/error icons hidden |
84
+ | `:host([status="success"])` | Success color applied via `--cv-copy-button-success-color`; success icon visible |
85
+ | `:host([status="error"])` | Error color applied via `--cv-copy-button-error-color`; error icon visible |
86
+ | `:host([copying])` | Shown while async copy is in-flight; `cursor: progress` |
87
+ | `:host([size="small"])` | Small size overrides |
88
+ | `:host([size="large"])` | Large size overrides |
89
+
90
+ ## Reactive State Mapping
91
+
92
+ `cv-copy-button` is a visual adapter over headless `createCopyButton`.
93
+
94
+ ### UIKit properties to headless actions
95
+
96
+ | UIKit Property | Direction | Headless Binding |
97
+ | ------------------- | -------------- | ------------------------------------ |
98
+ | `disabled` | attr -> action | `actions.setDisabled(value)` |
99
+ | `feedback-duration` | attr -> action | `actions.setFeedbackDuration(value)` |
100
+ | `value` | prop -> action | `actions.setValue(value)` |
101
+
102
+ ### Headless state to DOM reflection
103
+
104
+ | Headless State | Direction | DOM Reflection |
105
+ | -------------------- | ------------- | ---------------------------------------------------------------- |
106
+ | `state.status()` | state -> attr | `[status]` host attribute (`"idle"` \| `"success"` \| `"error"`) |
107
+ | `state.isDisabled()` | state -> attr | `[disabled]` host attribute |
108
+ | `state.isCopying()` | state -> attr | `[copying]` host attribute |
109
+
110
+ ### Headless contracts to DOM elements
111
+
112
+ | Contract | Target Element | Notes |
113
+ | -------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- |
114
+ | `contracts.getButtonProps()` | Inner `[part="base"]` | Spread as attributes; provides `role`, `aria-disabled`, `tabindex`, `aria-label`, `onClick`, `onKeyDown`, `onKeyUp` |
115
+ | `contracts.getStatusProps()` | Inner `[part="status"]` | Spread as attributes; provides `role="status"`, `aria-live="polite"`, `aria-atomic="true"` |
116
+ | `contracts.getIconContainerProps('copy')` | Inner `[part="copy-icon"]` | Spread as attributes; provides `aria-hidden`, `hidden` |
117
+ | `contracts.getIconContainerProps('success')` | Inner `[part="success-icon"]` | Spread as attributes; provides `aria-hidden`, `hidden` |
118
+ | `contracts.getIconContainerProps('error')` | Inner `[part="error-icon"]` | Spread as attributes; provides `aria-hidden`, `hidden` |
119
+
120
+ ### Headless options passed from UIKit attributes
121
+
122
+ | UIKit Attribute | Headless Option | Notes |
123
+ | ------------------- | ------------------ | ---------------------------------------------------------- |
124
+ | `value` | `value` | Property-only; accepts `string \| (() => Promise<string>)` |
125
+ | `feedback-duration` | `feedbackDuration` | Numeric attribute, defaults to `1500` |
126
+ | `disabled` | `isDisabled` | Boolean attribute |
127
+ | `aria-label` | `ariaLabel` | Standard ARIA labeling |
128
+
129
+ ### UIKit-only concerns (not in headless)
130
+
131
+ - Icon rendering via slotted content (`copy-icon`, `success-icon`, `error-icon` slots with default SVG icons)
132
+ - CSS custom properties for sizing and colors (`--cv-copy-button-*`)
133
+ - `size` attribute controlling icon/button dimensions
134
+ - `cv-copy` and `cv-error` custom events dispatched on the host element
135
+ - Pulse/scale animation on copy activation
136
+
137
+ ### Headless-owned concerns (UIKit does NOT reimplement)
138
+
139
+ - Copy cycle logic (resolve value, write to clipboard, transition status, schedule revert)
140
+ - Keyboard interaction (Enter on keydown, Space on keyup)
141
+ - Click handling
142
+ - ARIA attribute computation (`aria-disabled`, `tabindex`, `aria-label`)
143
+ - Timer management (revert timer, cancellation)
144
+ - `isCopying` re-entrant guard
145
+
146
+ ## Events
147
+
148
+ | Event | Detail | Description |
149
+ | ---------- | -------------------- | -------------------------------------------------------------- |
150
+ | `cv-copy` | `{ value: string }` | Fired on successful clipboard write |
151
+ | `cv-error` | `{ error: unknown }` | Fired on clipboard write failure or async value getter failure |
152
+
153
+ Events are dispatched by the UIKit adapter by providing `onCopy` and `onError` callbacks to `createCopyButton`:
154
+
155
+ - `onCopy(value)` -> dispatches `cv-copy` with `{ detail: { value } }`
156
+ - `onError(error)` -> dispatches `cv-error` with `{ detail: { error } }`
157
+
158
+ ## Usage
159
+
160
+ ```html
161
+ <!-- Basic usage -->
162
+ <cv-copy-button value="text to copy"></cv-copy-button>
163
+
164
+ <!-- With aria-label for accessible context -->
165
+ <cv-copy-button value="secret123" aria-label="Copy password"></cv-copy-button>
166
+
167
+ <!-- Small size -->
168
+ <cv-copy-button value="hello" size="small"></cv-copy-button>
169
+
170
+ <!-- Custom feedback duration (3 seconds) -->
171
+ <cv-copy-button value="hello" feedback-duration="3000"></cv-copy-button>
172
+
173
+ <!-- Custom icons via slots -->
174
+ <cv-copy-button value="hello">
175
+ <svg
176
+ slot="copy-icon"
177
+ xmlns="http://www.w3.org/2000/svg"
178
+ viewBox="0 0 24 24"
179
+ fill="none"
180
+ stroke="currentColor"
181
+ stroke-width="2"
182
+ stroke-linecap="round"
183
+ stroke-linejoin="round"
184
+ >
185
+ <path d="M12 5v14" />
186
+ <path d="M5 12h14" />
187
+ </svg>
188
+ <svg
189
+ slot="success-icon"
190
+ xmlns="http://www.w3.org/2000/svg"
191
+ viewBox="0 0 24 24"
192
+ fill="none"
193
+ stroke="currentColor"
194
+ stroke-width="2"
195
+ stroke-linecap="round"
196
+ stroke-linejoin="round"
197
+ >
198
+ <circle cx="12" cy="12" r="9" />
199
+ <path d="m8.5 12 2.5 2.5 4.5-5" />
200
+ </svg>
201
+ <svg
202
+ slot="error-icon"
203
+ xmlns="http://www.w3.org/2000/svg"
204
+ viewBox="0 0 24 24"
205
+ fill="none"
206
+ stroke="currentColor"
207
+ stroke-width="2"
208
+ stroke-linecap="round"
209
+ stroke-linejoin="round"
210
+ >
211
+ <path d="M12 3 3 20h18L12 3Z" />
212
+ <path d="M12 9v4" />
213
+ <path d="M12 17h.01" />
214
+ </svg>
215
+ </cv-copy-button>
216
+
217
+ <!-- Disabled -->
218
+ <cv-copy-button value="hello" disabled></cv-copy-button>
219
+
220
+ <!-- Async value (property-only, set via JS) -->
221
+ <cv-copy-button id="lazy-copy"></cv-copy-button>
222
+ <script>
223
+ document.querySelector('#lazy-copy').value = async () => {
224
+ const res = await fetch('/api/secret')
225
+ return res.text()
226
+ }
227
+ </script>
228
+
229
+ <!-- Listening for events -->
230
+ <cv-copy-button
231
+ value="hello"
232
+ @cv-copy="${(e) => console.log('Copied:', e.detail.value)}"
233
+ @cv-error="${(e) => console.error('Failed:', e.detail.error)}"
234
+ >
235
+ </cv-copy-button>
236
+ ```