@ecopages/radiant-ui 0.1.0-beta.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 (254) hide show
  1. package/README.md +70 -0
  2. package/dist/components/ui/alert/alert.css +27 -0
  3. package/dist/components/ui/alert/alert.d.ts +4 -0
  4. package/dist/components/ui/alert/alert.script.d.ts +25 -0
  5. package/dist/components/ui/alert/index.d.ts +2 -0
  6. package/dist/components/ui/alert/index.js +2 -0
  7. package/dist/components/ui/alert/index.js.map +7 -0
  8. package/dist/components/ui/breadcrumb/breadcrumb.css +41 -0
  9. package/dist/components/ui/breadcrumb/breadcrumb.d.ts +59 -0
  10. package/dist/components/ui/breadcrumb/breadcrumb.script.d.ts +40 -0
  11. package/dist/components/ui/breadcrumb/index.d.ts +2 -0
  12. package/dist/components/ui/breadcrumb/index.js +2 -0
  13. package/dist/components/ui/breadcrumb/index.js.map +7 -0
  14. package/dist/components/ui/button/button.d.ts +37 -0
  15. package/dist/components/ui/button/index.d.ts +1 -0
  16. package/dist/components/ui/button/index.js +2 -0
  17. package/dist/components/ui/button/index.js.map +7 -0
  18. package/dist/components/ui/carousel/carousel.css +170 -0
  19. package/dist/components/ui/carousel/carousel.d.ts +39 -0
  20. package/dist/components/ui/carousel/carousel.script.d.ts +113 -0
  21. package/dist/components/ui/carousel/index.d.ts +2 -0
  22. package/dist/components/ui/carousel/index.js +2 -0
  23. package/dist/components/ui/carousel/index.js.map +7 -0
  24. package/dist/components/ui/checkbox/checkbox.css +55 -0
  25. package/dist/components/ui/checkbox/checkbox.d.ts +4 -0
  26. package/dist/components/ui/checkbox/checkbox.script.d.ts +58 -0
  27. package/dist/components/ui/checkbox/index.d.ts +2 -0
  28. package/dist/components/ui/checkbox/index.js +2 -0
  29. package/dist/components/ui/checkbox/index.js.map +7 -0
  30. package/dist/components/ui/combobox/combobox.css +65 -0
  31. package/dist/components/ui/combobox/combobox.d.ts +53 -0
  32. package/dist/components/ui/combobox/combobox.script.d.ts +89 -0
  33. package/dist/components/ui/combobox/index.d.ts +2 -0
  34. package/dist/components/ui/combobox/index.js +2 -0
  35. package/dist/components/ui/combobox/index.js.map +7 -0
  36. package/dist/components/ui/dialog/dialog-registry.d.ts +34 -0
  37. package/dist/components/ui/dialog/dialog-registry.test.d.ts +1 -0
  38. package/dist/components/ui/dialog/dialog-types.d.ts +55 -0
  39. package/dist/components/ui/dialog/dialog.css +53 -0
  40. package/dist/components/ui/dialog/dialog.d.ts +36 -0
  41. package/dist/components/ui/dialog/dialog.script.d.ts +61 -0
  42. package/dist/components/ui/dialog/index.d.ts +4 -0
  43. package/dist/components/ui/dialog/index.js +2 -0
  44. package/dist/components/ui/dialog/index.js.map +7 -0
  45. package/dist/components/ui/disclosure/disclosure-group.script.d.ts +31 -0
  46. package/dist/components/ui/disclosure/disclosure.css +105 -0
  47. package/dist/components/ui/disclosure/disclosure.d.ts +35 -0
  48. package/dist/components/ui/disclosure/disclosure.script.d.ts +38 -0
  49. package/dist/components/ui/disclosure/index.d.ts +3 -0
  50. package/dist/components/ui/disclosure/index.js +2 -0
  51. package/dist/components/ui/disclosure/index.js.map +7 -0
  52. package/dist/components/ui/feed/feed.css +14 -0
  53. package/dist/components/ui/feed/feed.d.ts +12 -0
  54. package/dist/components/ui/feed/feed.script.d.ts +22 -0
  55. package/dist/components/ui/feed/index.d.ts +2 -0
  56. package/dist/components/ui/feed/index.js +2 -0
  57. package/dist/components/ui/feed/index.js.map +7 -0
  58. package/dist/components/ui/field/field-context.d.ts +20 -0
  59. package/dist/components/ui/field/field-description.d.ts +8 -0
  60. package/dist/components/ui/field/field-error.d.ts +9 -0
  61. package/dist/components/ui/field/field-ssr-hydration.test.d.ts +1 -0
  62. package/dist/components/ui/field/field.css +26 -0
  63. package/dist/components/ui/field/field.d.ts +4 -0
  64. package/dist/components/ui/field/field.script.d.ts +68 -0
  65. package/dist/components/ui/field/index.d.ts +5 -0
  66. package/dist/components/ui/field/index.js +2 -0
  67. package/dist/components/ui/field/index.js.map +7 -0
  68. package/dist/components/ui/form/control-protocol.d.ts +19 -0
  69. package/dist/components/ui/form/form-context.d.ts +27 -0
  70. package/dist/components/ui/form/form-field.test.d.ts +3 -0
  71. package/dist/components/ui/form/form-store.d.ts +53 -0
  72. package/dist/components/ui/form/form-store.test.d.ts +1 -0
  73. package/dist/components/ui/form/form.css +12 -0
  74. package/dist/components/ui/form/form.d.ts +5 -0
  75. package/dist/components/ui/form/form.script.d.ts +67 -0
  76. package/dist/components/ui/form/index.d.ts +7 -0
  77. package/dist/components/ui/form/index.js +2 -0
  78. package/dist/components/ui/form/index.js.map +7 -0
  79. package/dist/components/ui/form/resolvers.d.ts +3 -0
  80. package/dist/components/ui/form/types.d.ts +72 -0
  81. package/dist/components/ui/grid/grid.css +47 -0
  82. package/dist/components/ui/grid/grid.d.ts +7 -0
  83. package/dist/components/ui/grid/grid.script.d.ts +26 -0
  84. package/dist/components/ui/grid/index.d.ts +2 -0
  85. package/dist/components/ui/grid/index.js +2 -0
  86. package/dist/components/ui/grid/index.js.map +7 -0
  87. package/dist/components/ui/input/index.d.ts +1 -0
  88. package/dist/components/ui/input/index.js +2 -0
  89. package/dist/components/ui/input/index.js.map +7 -0
  90. package/dist/components/ui/input/input.css +32 -0
  91. package/dist/components/ui/input/input.d.ts +23 -0
  92. package/dist/components/ui/label/index.d.ts +1 -0
  93. package/dist/components/ui/label/index.js +2 -0
  94. package/dist/components/ui/label/index.js.map +7 -0
  95. package/dist/components/ui/label/label.css +8 -0
  96. package/dist/components/ui/label/label.d.ts +9 -0
  97. package/dist/components/ui/listbox/index.d.ts +2 -0
  98. package/dist/components/ui/listbox/index.js +2 -0
  99. package/dist/components/ui/listbox/index.js.map +7 -0
  100. package/dist/components/ui/listbox/listbox.css +23 -0
  101. package/dist/components/ui/listbox/listbox.d.ts +12 -0
  102. package/dist/components/ui/listbox/listbox.script.d.ts +40 -0
  103. package/dist/components/ui/menu-button/index.d.ts +2 -0
  104. package/dist/components/ui/menu-button/index.js +2 -0
  105. package/dist/components/ui/menu-button/index.js.map +7 -0
  106. package/dist/components/ui/menu-button/menu-button.css +43 -0
  107. package/dist/components/ui/menu-button/menu-button.d.ts +13 -0
  108. package/dist/components/ui/menu-button/menu-button.script.d.ts +64 -0
  109. package/dist/components/ui/menubar/index.d.ts +2 -0
  110. package/dist/components/ui/menubar/index.js +2 -0
  111. package/dist/components/ui/menubar/index.js.map +7 -0
  112. package/dist/components/ui/menubar/menubar.css +45 -0
  113. package/dist/components/ui/menubar/menubar.d.ts +16 -0
  114. package/dist/components/ui/menubar/menubar.script.d.ts +52 -0
  115. package/dist/components/ui/meter/index.d.ts +2 -0
  116. package/dist/components/ui/meter/index.js +2 -0
  117. package/dist/components/ui/meter/index.js.map +7 -0
  118. package/dist/components/ui/meter/meter.css +17 -0
  119. package/dist/components/ui/meter/meter.d.ts +4 -0
  120. package/dist/components/ui/meter/meter.script.d.ts +33 -0
  121. package/dist/components/ui/navigation-menu/index.d.ts +2 -0
  122. package/dist/components/ui/navigation-menu/index.js +2 -0
  123. package/dist/components/ui/navigation-menu/index.js.map +7 -0
  124. package/dist/components/ui/navigation-menu/navigation-menu.css +90 -0
  125. package/dist/components/ui/navigation-menu/navigation-menu.d.ts +28 -0
  126. package/dist/components/ui/navigation-menu/navigation-menu.script.d.ts +49 -0
  127. package/dist/components/ui/radio-group/index.d.ts +2 -0
  128. package/dist/components/ui/radio-group/index.js +2 -0
  129. package/dist/components/ui/radio-group/index.js.map +7 -0
  130. package/dist/components/ui/radio-group/radio-group.css +44 -0
  131. package/dist/components/ui/radio-group/radio-group.d.ts +12 -0
  132. package/dist/components/ui/radio-group/radio-group.script.d.ts +52 -0
  133. package/dist/components/ui/sidebar/index.d.ts +3 -0
  134. package/dist/components/ui/sidebar/index.js +2 -0
  135. package/dist/components/ui/sidebar/index.js.map +7 -0
  136. package/dist/components/ui/sidebar/sidebar-trigger.script.d.ts +53 -0
  137. package/dist/components/ui/sidebar/sidebar.css +362 -0
  138. package/dist/components/ui/sidebar/sidebar.d.ts +141 -0
  139. package/dist/components/ui/sidebar/sidebar.script.d.ts +140 -0
  140. package/dist/components/ui/sidebar/sidebar.test.d.ts +1 -0
  141. package/dist/components/ui/slider/index.d.ts +2 -0
  142. package/dist/components/ui/slider/index.js +2 -0
  143. package/dist/components/ui/slider/index.js.map +7 -0
  144. package/dist/components/ui/slider/slider.css +51 -0
  145. package/dist/components/ui/slider/slider.d.ts +7 -0
  146. package/dist/components/ui/slider/slider.script.d.ts +102 -0
  147. package/dist/components/ui/spinbutton/index.d.ts +2 -0
  148. package/dist/components/ui/spinbutton/index.js +2 -0
  149. package/dist/components/ui/spinbutton/index.js.map +7 -0
  150. package/dist/components/ui/spinbutton/spinbutton.css +19 -0
  151. package/dist/components/ui/spinbutton/spinbutton.d.ts +4 -0
  152. package/dist/components/ui/spinbutton/spinbutton.script.d.ts +64 -0
  153. package/dist/components/ui/switch/index.d.ts +2 -0
  154. package/dist/components/ui/switch/index.js +2 -0
  155. package/dist/components/ui/switch/index.js.map +7 -0
  156. package/dist/components/ui/switch/switch.css +47 -0
  157. package/dist/components/ui/switch/switch.d.ts +4 -0
  158. package/dist/components/ui/switch/switch.script.d.ts +51 -0
  159. package/dist/components/ui/tabs/index.d.ts +2 -0
  160. package/dist/components/ui/tabs/index.js +2 -0
  161. package/dist/components/ui/tabs/index.js.map +7 -0
  162. package/dist/components/ui/tabs/tabs.css +28 -0
  163. package/dist/components/ui/tabs/tabs.d.ts +12 -0
  164. package/dist/components/ui/tabs/tabs.script.d.ts +54 -0
  165. package/dist/components/ui/textarea/index.d.ts +1 -0
  166. package/dist/components/ui/textarea/index.js +2 -0
  167. package/dist/components/ui/textarea/index.js.map +7 -0
  168. package/dist/components/ui/textarea/textarea.css +33 -0
  169. package/dist/components/ui/textarea/textarea.d.ts +23 -0
  170. package/dist/components/ui/toast/index.d.ts +5 -0
  171. package/dist/components/ui/toast/index.js +2 -0
  172. package/dist/components/ui/toast/index.js.map +7 -0
  173. package/dist/components/ui/toast/toast-api.d.ts +29 -0
  174. package/dist/components/ui/toast/toast-context.d.ts +59 -0
  175. package/dist/components/ui/toast/toast-state.d.ts +25 -0
  176. package/dist/components/ui/toast/toast.css +315 -0
  177. package/dist/components/ui/toast/toast.d.ts +8 -0
  178. package/dist/components/ui/toast/toast.script.d.ts +91 -0
  179. package/dist/components/ui/toast/toast.test.d.ts +2 -0
  180. package/dist/components/ui/toast/toaster.script.d.ts +79 -0
  181. package/dist/components/ui/toolbar/index.d.ts +2 -0
  182. package/dist/components/ui/toolbar/index.js +2 -0
  183. package/dist/components/ui/toolbar/index.js.map +7 -0
  184. package/dist/components/ui/toolbar/toolbar.css +16 -0
  185. package/dist/components/ui/toolbar/toolbar.d.ts +4 -0
  186. package/dist/components/ui/toolbar/toolbar.script.d.ts +30 -0
  187. package/dist/components/ui/tooltip/index.d.ts +2 -0
  188. package/dist/components/ui/tooltip/index.js +2 -0
  189. package/dist/components/ui/tooltip/index.js.map +7 -0
  190. package/dist/components/ui/tooltip/tooltip.css +25 -0
  191. package/dist/components/ui/tooltip/tooltip.d.ts +4 -0
  192. package/dist/components/ui/tooltip/tooltip.script.d.ts +70 -0
  193. package/dist/components/ui/tree/index.d.ts +2 -0
  194. package/dist/components/ui/tree/index.js +2 -0
  195. package/dist/components/ui/tree/index.js.map +7 -0
  196. package/dist/components/ui/tree/tree.css +54 -0
  197. package/dist/components/ui/tree/tree.d.ts +13 -0
  198. package/dist/components/ui/tree/tree.script.d.ts +45 -0
  199. package/dist/components/ui/treegrid/index.d.ts +2 -0
  200. package/dist/components/ui/treegrid/index.js +2 -0
  201. package/dist/components/ui/treegrid/index.js.map +7 -0
  202. package/dist/components/ui/treegrid/treegrid.css +77 -0
  203. package/dist/components/ui/treegrid/treegrid.d.ts +14 -0
  204. package/dist/components/ui/treegrid/treegrid.script.d.ts +49 -0
  205. package/dist/components/ui/window-splitter/index.d.ts +2 -0
  206. package/dist/components/ui/window-splitter/index.js +2 -0
  207. package/dist/components/ui/window-splitter/index.js.map +7 -0
  208. package/dist/components/ui/window-splitter/window-splitter.css +35 -0
  209. package/dist/components/ui/window-splitter/window-splitter.d.ts +8 -0
  210. package/dist/components/ui/window-splitter/window-splitter.script.d.ts +49 -0
  211. package/dist/index.d.ts +36 -0
  212. package/dist/index.js +2 -0
  213. package/dist/index.js.map +7 -0
  214. package/dist/lib/focusable-elements.d.ts +2 -0
  215. package/dist/lib/radiant-view.d.ts +17 -0
  216. package/dist/lib/roving-tabindex.d.ts +27 -0
  217. package/dist/styles/components/button.css +115 -0
  218. package/dist/styles/radiant-ui-core.css +7 -0
  219. package/dist/styles/radiant-ui.css +10 -0
  220. package/dist/styles/styles.css +36 -0
  221. package/dist/styles/tailwind.css +41 -0
  222. package/dist/styles/themes/aurora.css +19 -0
  223. package/dist/styles/themes/default-compact-soft.css +19 -0
  224. package/dist/styles/themes/default.css +2 -0
  225. package/dist/styles/themes/glacier.css +19 -0
  226. package/dist/styles/tokens/colors/aurora.css +73 -0
  227. package/dist/styles/tokens/colors/basalt.css +36 -0
  228. package/dist/styles/tokens/colors/glacier.css +45 -0
  229. package/dist/styles/tokens/colors/palettes.css +3 -0
  230. package/dist/styles/tokens/elevation/default.css +22 -0
  231. package/dist/styles/tokens/elevation/flat.css +22 -0
  232. package/dist/styles/tokens/motion/default.css +19 -0
  233. package/dist/styles/tokens/motion/snappy.css +19 -0
  234. package/dist/styles/tokens/presets/colors/aurora.css +126 -0
  235. package/dist/styles/tokens/presets/colors/basalt.css +126 -0
  236. package/dist/styles/tokens/presets/colors/ember.css +134 -0
  237. package/dist/styles/tokens/presets/colors/glacier.css +130 -0
  238. package/dist/styles/tokens/presets/colors/index.css +4 -0
  239. package/dist/styles/tokens/presets/radius.css +45 -0
  240. package/dist/styles/tokens/presets/spacing.css +57 -0
  241. package/dist/styles/tokens/radius/default.css +34 -0
  242. package/dist/styles/tokens/radius/edgy.css +35 -0
  243. package/dist/styles/tokens/radius/sharp.css +2 -0
  244. package/dist/styles/tokens/radius/soft.css +34 -0
  245. package/dist/styles/tokens/semantic.css +80 -0
  246. package/dist/styles/tokens/spacing/compact.css +23 -0
  247. package/dist/styles/tokens/spacing/default.css +23 -0
  248. package/dist/styles/tokens/spacing/scale-bridge.css +17 -0
  249. package/dist/styles/tokens/spacing/wide.css +23 -0
  250. package/dist/styles/tokens/system.css +36 -0
  251. package/dist/styles/tokens/typography/default.css +37 -0
  252. package/dist/styles/utilities/sheen.css +7 -0
  253. package/dist/types.d.ts +14 -0
  254. package/package.json +371 -0
@@ -0,0 +1,32 @@
1
+ @reference '../../../styles/radiant-ui.css';
2
+
3
+ @layer components {
4
+ .rui-input {
5
+ @apply w-full border border-border bg-background text-on-background outline-none;
6
+ border-radius: var(--radius-control);
7
+ padding-inline: var(--space-control-x);
8
+ padding-block: var(--space-control-y);
9
+ font-size: var(--text-control, 0.875rem);
10
+ transition-duration: var(--duration-normal);
11
+ }
12
+
13
+ .rui-input:focus-visible {
14
+ @apply ring-2 ring-focus-ring;
15
+ }
16
+
17
+ .rui-input:disabled {
18
+ @apply cursor-not-allowed opacity-disabled;
19
+ }
20
+
21
+ .rui-input[aria-invalid='true'] {
22
+ @apply border-error;
23
+ }
24
+
25
+ .rui-input--sm {
26
+ padding-block: calc(var(--space-control-y) * 0.75);
27
+ }
28
+
29
+ .rui-input--lg {
30
+ padding-block: calc(var(--space-control-y) * 1.25);
31
+ }
32
+ }
@@ -0,0 +1,23 @@
1
+ import './input.css';
2
+ export type RuiInputSize = 'sm' | 'md' | 'lg';
3
+ export type RuiInputProps = {
4
+ value?: string;
5
+ type?: string;
6
+ placeholder?: string;
7
+ disabled?: boolean;
8
+ name?: string;
9
+ size?: RuiInputSize;
10
+ class?: string;
11
+ id?: string;
12
+ 'aria-label'?: string;
13
+ 'on:input'?: (event: Event) => void;
14
+ 'on:change'?: (event: Event) => void;
15
+ 'on:blur'?: (event: Event) => void;
16
+ };
17
+ /**
18
+ * Presentational wrapper around a native `<input>`.
19
+ *
20
+ * No custom element — Field owns labeling, `aria-*`, and validation wiring.
21
+ * Marked with `data-rui-control` so `<rui-field>` can discover it.
22
+ */
23
+ export declare function RuiInput({ value, type, placeholder, disabled, name, size, class: className, id, 'aria-label': ariaLabel, 'on:input': onInput, 'on:change': onChange, 'on:blur': onBlur, ...rest }: RuiInputProps): import("@ecopages/jsx").JsxRenderable;
@@ -0,0 +1 @@
1
+ export { RuiLabel, type RuiLabelProps } from './label';
@@ -0,0 +1,2 @@
1
+ var l="data-rui-control";var e="data-rui-field-label";var i=new Set(["rui-combobox","rui-checkbox","rui-switch","rui-radio-group","rui-slider","rui-spinbutton","rui-listbox"]),o=`[${l}], ${Array.from(i).join(", ")}, input, textarea`;import"./label.css";function u({htmlFor:t,class:n,children:r}){return React.createElement("label",{[e]:"",class:["rui-label",n].filter(Boolean).join(" "),htmlFor:t},r)}export{u as RuiLabel};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/components/ui/form/control-protocol.ts", "../../../../src/components/ui/label/label.tsx"],
4
+ "sourcesContent": ["export const RUI_CONTROL_ATTR = 'data-rui-control';\nexport const RUI_FIELD_MANAGED_ATTR = 'data-rui-field-managed';\nexport const RUI_FIELD_LABEL_ATTR = 'data-rui-field-label';\nexport const RUI_FIELD_DESCRIPTION_ATTR = 'data-rui-field-description';\nexport const RUI_FIELD_ERROR_ATTR = 'data-rui-field-error';\nexport const RUI_FIELD_DEFAULT_VALUE_ATTR = 'data-default-value';\nexport const RUI_FORM_DEFAULT_VALUES_ATTR = 'data-default-values';\n\nconst HOST_CONTROL_TAGS = new Set([\n\t'rui-combobox',\n\t'rui-checkbox',\n\t'rui-switch',\n\t'rui-radio-group',\n\t'rui-slider',\n\t'rui-spinbutton',\n\t'rui-listbox',\n]);\n\nconst CONTROL_SELECTOR = `[${RUI_CONTROL_ATTR}], ${Array.from(HOST_CONTROL_TAGS).join(', ')}, input, textarea`;\n\ntype FieldSlotHost = HTMLElement & { getSlotElements?: (name?: string) => Element[] };\n\n/** Radiant slot projection moves authored children into the render tree; search projected nodes first. */\nfunction forEachFieldContentNode(root: HTMLElement, visit: (node: Element) => void): void {\n\tconst slotHost = root as FieldSlotHost;\n\tconst visited = new Set<Element>();\n\tconst safeVisit = (node: Element) => {\n\t\tif (visited.has(node)) {\n\t\t\treturn;\n\t\t}\n\t\tvisited.add(node);\n\t\tvisit(node);\n\t};\n\n\tif (typeof slotHost.getSlotElements === 'function') {\n\t\tfor (const node of slotHost.getSlotElements()) {\n\t\t\tsafeVisit(node);\n\t\t}\n\t}\n\n\tconst renderRoot = root.querySelector('.rui-field') ?? root;\n\tfor (const child of Array.from(renderRoot.children)) {\n\t\tsafeVisit(child);\n\t}\n\tfor (const node of renderRoot.querySelectorAll('*')) {\n\t\tsafeVisit(node);\n\t}\n\n\tfor (const child of Array.from(root.children)) {\n\t\tsafeVisit(child);\n\t}\n\n\tconst shadow = root.shadowRoot;\n\tif (shadow) {\n\t\tfor (const child of Array.from(shadow.children)) {\n\t\t\tsafeVisit(child);\n\t\t}\n\t}\n}\n\nfunction queryFieldContent(root: HTMLElement, selector: string): HTMLElement | null {\n\tlet found: HTMLElement | null = null;\n\tforEachFieldContentNode(root, (node) => {\n\t\tif (found || !(node instanceof HTMLElement)) {\n\t\t\treturn;\n\t\t}\n\t\tif (node.matches(selector)) {\n\t\t\tfound = node;\n\t\t\treturn;\n\t\t}\n\t\tfound = node.querySelector<HTMLElement>(selector);\n\t});\n\treturn found;\n}\n\nfunction pickPrimaryFieldControl(candidates: HTMLElement[]): HTMLElement | null {\n\tif (candidates.length === 0) {\n\t\treturn null;\n\t}\n\tif (candidates.length === 1) {\n\t\treturn candidates[0];\n\t}\n\n\tconst active = document.activeElement;\n\tif (active instanceof HTMLElement) {\n\t\tfor (const candidate of candidates) {\n\t\t\tif (candidate === active || candidate.contains(active)) {\n\t\t\t\treturn candidate;\n\t\t\t}\n\t\t}\n\t}\n\n\tconst visible = candidates.filter(\n\t\t(el) => el.isConnected && el.getClientRects().length > 0 && !el.closest('[hidden],[aria-hidden=\"true\"]'),\n\t);\n\tconst pool = visible.length > 0 ? visible : candidates.filter((el) => el.isConnected);\n\treturn pool[pool.length - 1] ?? candidates[candidates.length - 1];\n}\n\nfunction listFieldControlsInRenderTree(root: HTMLElement): HTMLElement[] {\n\tconst renderRoot = root.querySelector('.rui-field');\n\tif (!(renderRoot instanceof HTMLElement)) {\n\t\treturn [];\n\t}\n\treturn Array.from(renderRoot.querySelectorAll<HTMLElement>(CONTROL_SELECTOR)).filter((el) => root.contains(el));\n}\n\nexport function findFieldControl(root: HTMLElement): HTMLElement | null {\n\tconst fromRenderTree = pickPrimaryFieldControl(listFieldControlsInRenderTree(root));\n\tif (fromRenderTree) {\n\t\treturn fromRenderTree;\n\t}\n\n\tlet found: HTMLElement | null = null;\n\tforEachFieldContentNode(root, (node) => {\n\t\tif (found) {\n\t\t\treturn;\n\t\t}\n\t\tconst match = findControlInSubtree(node);\n\t\tif (match) {\n\t\t\tfound = match;\n\t\t}\n\t});\n\tif (found) {\n\t\treturn found;\n\t}\n\tconst renderRoot = root.querySelector('.rui-field') ?? root;\n\treturn findControlInSubtree(renderRoot);\n}\n\nfunction findControlInSubtree(node: Element): HTMLElement | null {\n\tif (!(node instanceof HTMLElement)) {\n\t\treturn null;\n\t}\n\tif (node.matches(`[${RUI_CONTROL_ATTR}]`)) {\n\t\treturn node;\n\t}\n\tif (HOST_CONTROL_TAGS.has(node.localName)) {\n\t\treturn node;\n\t}\n\tif (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {\n\t\treturn node;\n\t}\n\tconst nested = node.querySelector<HTMLElement>(CONTROL_SELECTOR);\n\treturn nested;\n}\n\nexport function readControlValue(control: HTMLElement): unknown {\n\tif (control instanceof HTMLInputElement) {\n\t\tif (control.type === 'checkbox') {\n\t\t\treturn control.checked;\n\t\t}\n\t\tif (control.type === 'number' || control.type === 'range') {\n\t\t\treturn control.value === '' ? '' : Number(control.value);\n\t\t}\n\t\treturn control.value;\n\t}\n\n\tif (control instanceof HTMLTextAreaElement) {\n\t\treturn control.value;\n\t}\n\n\tconst host = resolveControlHost(control);\n\n\tif (host.localName === 'rui-checkbox' || host.localName === 'rui-switch') {\n\t\tconst input = host.querySelector<HTMLInputElement>('input[type=\"checkbox\"]');\n\t\tif (input) {\n\t\t\treturn input.checked;\n\t\t}\n\t\treturn host.hasAttribute('checked');\n\t}\n\n\tif (host.localName === 'rui-combobox' || host.localName === 'rui-radio-group' || host.localName === 'rui-listbox') {\n\t\treturn (host as HTMLElement & { value?: string }).value ?? host.getAttribute('value') ?? '';\n\t}\n\n\tif (host.localName === 'rui-slider' || host.localName === 'rui-spinbutton') {\n\t\tconst propValue = (host as HTMLElement & { value?: number }).value;\n\t\tif (typeof propValue === 'number') {\n\t\t\treturn propValue;\n\t\t}\n\t\tconst raw = host.getAttribute('value');\n\t\treturn raw == null || raw === '' ? '' : Number(raw);\n\t}\n\n\treturn host.getAttribute('value') ?? '';\n}\n\nexport function writeControlValue(control: HTMLElement, value: unknown): void {\n\tif (control instanceof HTMLInputElement) {\n\t\tif (control.type === 'checkbox') {\n\t\t\tcontrol.checked = Boolean(value);\n\t\t\tconst ce = control.closest('rui-checkbox, rui-switch');\n\t\t\tif (ce) {\n\t\t\t\tif (value) {\n\t\t\t\t\tce.setAttribute('checked', '');\n\t\t\t\t} else {\n\t\t\t\t\tce.removeAttribute('checked');\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tcontrol.value = value == null ? '' : String(value);\n\t\treturn;\n\t}\n\n\tif (control instanceof HTMLTextAreaElement) {\n\t\tcontrol.value = value == null ? '' : String(value);\n\t\treturn;\n\t}\n\n\tconst host = resolveControlHost(control);\n\n\tif (host.localName === 'rui-checkbox' || host.localName === 'rui-switch') {\n\t\tif (value) {\n\t\t\thost.setAttribute('checked', '');\n\t\t} else {\n\t\t\thost.removeAttribute('checked');\n\t\t}\n\t\treturn;\n\t}\n\n\thost.setAttribute('value', value == null ? '' : String(value));\n}\n\nfunction resolveControlHost(control: HTMLElement): HTMLElement {\n\tif (control.hasAttribute(RUI_CONTROL_ATTR)) {\n\t\tconst host = control.closest(Array.from(HOST_CONTROL_TAGS).join(',')) as HTMLElement | null;\n\t\treturn host ?? control;\n\t}\n\treturn control;\n}\n\nexport function findFieldLabel(root: HTMLElement): HTMLLabelElement | null {\n\treturn queryFieldContent(root, `[${RUI_FIELD_LABEL_ATTR}], label.rui-label`) as HTMLLabelElement | null;\n}\n\nexport function findFieldDescription(root: HTMLElement): HTMLElement | null {\n\treturn queryFieldContent(root, `[${RUI_FIELD_DESCRIPTION_ATTR}]`);\n}\n\nexport function findFieldError(root: HTMLElement): HTMLElement | null {\n\tconst all = findFieldErrorElements(root);\n\treturn all[0] ?? null;\n}\n\n/** Every error region in the field (handles duplicate slot-projection copies). */\nexport function findFieldErrorElements(root: HTMLElement): HTMLElement[] {\n\tconst renderRoot = root.querySelector('.rui-field');\n\tif (renderRoot instanceof HTMLElement) {\n\t\tconst inRender = Array.from(renderRoot.querySelectorAll<HTMLElement>(`[${RUI_FIELD_ERROR_ATTR}]`));\n\t\tconst visible = inRender.filter((el) => el.getClientRects().length > 0);\n\t\tif (visible.length > 0) {\n\t\t\treturn visible;\n\t\t}\n\t\tif (inRender.length > 0) {\n\t\t\treturn inRender;\n\t\t}\n\t}\n\tconst fromDom = Array.from(root.querySelectorAll<HTMLElement>(`[${RUI_FIELD_ERROR_ATTR}]`));\n\tif (fromDom.length > 0) {\n\t\treturn fromDom;\n\t}\n\tconst single = queryFieldContent(root, `[${RUI_FIELD_ERROR_ATTR}]`);\n\treturn single ? [single] : [];\n}\n\n/** Syncs the field name onto the native control and custom-element hosts. */\nexport function wireFieldControlName(\n\tcontrolHost: HTMLElement | null,\n\tariaTarget: HTMLElement | null,\n\tname: string,\n): void {\n\tif (!name) {\n\t\treturn;\n\t}\n\n\tif (ariaTarget instanceof HTMLInputElement || ariaTarget instanceof HTMLTextAreaElement) {\n\t\tariaTarget.name = name;\n\t}\n\n\tif (!controlHost) {\n\t\treturn;\n\t}\n\n\tif (controlHost === ariaTarget) {\n\t\treturn;\n\t}\n\n\tif (HOST_CONTROL_TAGS.has(controlHost.localName)) {\n\t\tcontrolHost.setAttribute('name', name);\n\t}\n}\n\n/** Element that receives `id`, `aria-invalid`, and `aria-describedby` from Field. */\nexport function getAriaControlTarget(control: HTMLElement): HTMLElement {\n\tif (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) {\n\t\treturn control;\n\t}\n\n\tif (control.hasAttribute(RUI_CONTROL_ATTR)) {\n\t\treturn control;\n\t}\n\n\tconst marked = control.querySelector<HTMLElement>(`[${RUI_CONTROL_ATTR}]`);\n\tif (marked) {\n\t\treturn marked;\n\t}\n\n\tif (control.localName === 'rui-combobox') {\n\t\treturn control.querySelector<HTMLElement>('[data-combobox-input]') ?? control;\n\t}\n\n\tif (control.localName === 'rui-spinbutton') {\n\t\treturn control.querySelector<HTMLElement>('[data-spinbutton-input], input') ?? control;\n\t}\n\n\tif (control.localName === 'rui-slider') {\n\t\treturn control.querySelector<HTMLElement>('input[type=\"range\"]') ?? control;\n\t}\n\n\tif (control.localName === 'rui-checkbox' || control.localName === 'rui-switch') {\n\t\treturn control.querySelector<HTMLElement>('input') ?? control;\n\t}\n\n\tif (control.localName === 'rui-radio-group') {\n\t\tconst checked = control.querySelector<HTMLInputElement>('input[type=\"radio\"]:checked');\n\t\tconst first = control.querySelector<HTMLInputElement>('input[type=\"radio\"]');\n\t\treturn checked ?? first ?? control;\n\t}\n\n\treturn control;\n}\n", "import type { JsxRenderable } from '@ecopages/jsx';\nimport { RUI_FIELD_LABEL_ATTR } from '../form/control-protocol';\nimport './label.css';\n\nexport type RuiLabelProps = {\n\thtmlFor?: string;\n\tclass?: string;\n\tchildren: JsxRenderable;\n};\n\n/** Shared label styles for form fields. */\nexport function RuiLabel({ htmlFor, class: className, children }: RuiLabelProps) {\n\treturn (\n\t\t<label\n\t\t\t{...{ [RUI_FIELD_LABEL_ATTR]: '' }}\n\t\t\tclass={['rui-label', className].filter(Boolean).join(' ')}\n\t\t\thtmlFor={htmlFor}\n\t\t>\n\t\t\t{children}\n\t\t</label>\n\t);\n}\n"],
5
+ "mappings": "AAAO,IAAMA,EAAmB,mBAEzB,IAAMC,EAAuB,uBAMpC,IAAMC,EAAoB,IAAI,IAAI,CACjC,eACA,eACA,aACA,kBACA,aACA,iBACA,aACD,CAAC,EAEKC,EAAmB,IAAIC,CAAgB,MAAM,MAAM,KAAKF,CAAiB,EAAE,KAAK,IAAI,CAAC,oBChB3F,MAAO,cASA,SAASG,EAAS,CAAE,QAAAC,EAAS,MAAOC,EAAW,SAAAC,CAAS,EAAkB,CAChF,OACC,oBAAC,SACM,CAACC,CAAoB,EAAG,GAC9B,MAAO,CAAC,YAAaF,CAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,EACxD,QAASD,GAERE,CACF,CAEF",
6
+ "names": ["RUI_CONTROL_ATTR", "RUI_FIELD_LABEL_ATTR", "HOST_CONTROL_TAGS", "CONTROL_SELECTOR", "RUI_CONTROL_ATTR", "RuiLabel", "htmlFor", "className", "children", "RUI_FIELD_LABEL_ATTR"]
7
+ }
@@ -0,0 +1,8 @@
1
+ @reference '../../../styles/radiant-ui.css';
2
+
3
+ @layer components {
4
+ .rui-label {
5
+ @apply block font-medium text-on-background;
6
+ font-size: var(--text-control, 0.875rem);
7
+ }
8
+ }
@@ -0,0 +1,9 @@
1
+ import type { JsxRenderable } from '@ecopages/jsx';
2
+ import './label.css';
3
+ export type RuiLabelProps = {
4
+ htmlFor?: string;
5
+ class?: string;
6
+ children: JsxRenderable;
7
+ };
8
+ /** Shared label styles for form fields. */
9
+ export declare function RuiLabel({ htmlFor, class: className, children }: RuiLabelProps): JsxRenderable;
@@ -0,0 +1,2 @@
1
+ export { RuiListbox as RuiListboxElement, type RuiListboxProps, type RuiListboxChangeDetail } from './listbox.script';
2
+ export { RuiListbox, type RuiListboxOption } from './listbox';
@@ -0,0 +1,2 @@
1
+ import{RadiantElement as h,customElement as E,event as T,onEvent as b,onUpdated as y,prop as u}from"@ecopages/radiant";function x(n,e){n.forEach((t,i)=>{t.tabIndex=i===e?0:-1})}function R(n,e){if(!n.length)return null;let t=Math.max(0,Math.min(n.length-1,e));return x(n,t),n[t]?.focus(),n[t]??null}function v(n,e){return n==="Home"?"first":n==="End"?"last":e==="horizontal"?n==="ArrowRight"?"next":n==="ArrowLeft"?"prev":null:n==="ArrowDown"?"next":n==="ArrowUp"?"prev":null}function c(n){let{items:e,current:t,key:i,orientation:o="horizontal",wrap:r=!0}=n;if(!e.length||!t)return{handled:!1};let a=e.indexOf(t);if(a<0)return{handled:!1};let d=v(i,o);if(!d)return{handled:!1};let s=a;d==="first"?s=0:d==="last"?s=e.length-1:d==="next"?s=r?(a+1)%e.length:Math.min(e.length-1,a+1):s=r?(a-1+e.length)%e.length:Math.max(0,a-1);let p=R(e,s);return p?{handled:!0,index:s,item:p}:{handled:!1}}var l=@E("rui-listbox")class extends h{@u({type:String,reflect:!0,defaultValue:""})value;@u({type:String,defaultValue:""})label;@u({type:Boolean,reflect:!0,defaultValue:!1})disabled;@T({name:"rui-change",bubbles:!0,composed:!0})changeEvent;resolvedAriaLabel=this.$.label.map(e=>e||void 0);resolvedAriaDisabled=this.$.disabled.map(e=>e?"true":void 0);connectedCallback(){super.connectedCallback(),queueMicrotask(()=>this.sync())}@y(["value","disabled"])onPropsUpdated(){this.sync()}getOptions(){return Array.from(this.querySelectorAll('[role="option"]'))}sync(){let e=this.getOptions();for(let t of e){let i=t.getAttribute("data-value")===this.value;t.setAttribute("aria-selected",String(i)),t.tabIndex=i||!this.value&&t===e[0]?0:-1}}select(e){this.disabled||e.getAttribute("aria-disabled")==="true"||(this.value=e.getAttribute("data-value")||e.textContent?.trim()||"",this.sync(),this.changeEvent.emit({value:this.value}),e.focus())}@b({selector:'[role="option"]',type:"click"})onOptionClick(e){let t=e.target.closest('[role="option"]');t&&this.contains(t)&&this.select(t)}@b({selector:'[role="option"]',type:"keydown"})onOptionKeydown(e){let t=this.getOptions(),i=e.target.closest('[role="option"]');if(!i)return;if(e.key==="Enter"||e.key===" "){e.preventDefault(),this.select(i);return}let o=c({items:t,current:i,key:e.key,orientation:"vertical"});o.handled&&(e.preventDefault(),this.select(o.item))}render(){return React.createElement("div",{class:"rui-listbox",role:"listbox","data-rui-control":!0,"data-rui-control-type":"text","aria-label":this.resolvedAriaLabel,"aria-disabled":this.resolvedAriaDisabled},React.createElement("slot",null))}};var A=Symbol.for("@ecopages/storybook-radiant.viewElement"),m=Symbol.for("@ecopages/storybook-radiant.scriptModule"),f=Symbol.for("@ecopages/storybook-radiant.scriptExport");function L(n,e){n[A]=e;let t=e[m];t&&(n[m]=t,n[f]=e[f]??e.name)}function g(n,e){let t=(i=>e(i));return L(t,n),t}import"./listbox.css";var M=g(l,({slot:n,value:e,label:t,disabled:i,options:o})=>React.createElement("rui-listbox",{slot:n,value:e,label:t,disabled:i},o.map(r=>React.createElement("div",{class:"rui-listbox__option",role:"option","data-value":r.value,"aria-disabled":r.disabled?"true":void 0,tabindex:-1},r.label))));export{M as RuiListbox,l as RuiListboxElement};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/components/ui/listbox/listbox.script.tsx", "../../../../src/lib/roving-tabindex.ts", "../../../../src/lib/radiant-view.ts", "../../../../src/components/ui/listbox/listbox.tsx"],
4
+ "sourcesContent": ["import { RadiantElement, customElement, event, onEvent, onUpdated, prop } from '@ecopages/radiant';\nimport type { EventEmitter } from '@ecopages/radiant/tools/event-emitter';\nimport { navigateRovingTabindex } from '../../../lib/roving-tabindex';\n\nexport type RuiListboxProps = {\n\tvalue?: string;\n\tlabel?: string;\n\tdisabled?: boolean;\n};\n\nexport type RuiListboxChangeDetail = { value: string };\n\ntype RuiListboxBindings = {\n\tlabel: string;\n\tdisabled: boolean;\n};\n\n/**\n * `<rui-listbox>` \u2014 a list of options where one may be selected.\n *\n * Implements the APG Listbox pattern with roving tabindex on `[role=\"option\"]`.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/listbox/\n * @element rui-listbox\n * @fires rui-change\n */\n@customElement('rui-listbox')\nexport class RuiListbox extends RadiantElement<RuiListboxBindings> {\n\t@prop({ type: String, reflect: true, defaultValue: '' }) value: string;\n\t@prop({ type: String, defaultValue: '' }) label: string;\n\t@prop({ type: Boolean, reflect: true, defaultValue: false }) disabled: boolean;\n\n\t@event({ name: 'rui-change', bubbles: true, composed: true })\n\tchangeEvent: EventEmitter<RuiListboxChangeDetail>;\n\n\tprivate readonly resolvedAriaLabel = this.$.label.map((label) => label || undefined);\n\tprivate readonly resolvedAriaDisabled = this.$.disabled.map((disabled) => (disabled ? 'true' : undefined));\n\n\toverride connectedCallback(): void {\n\t\tsuper.connectedCallback();\n\t\tqueueMicrotask(() => this.sync());\n\t}\n\n\t@onUpdated(['value', 'disabled'])\n\tonPropsUpdated(): void {\n\t\tthis.sync();\n\t}\n\n\tprivate getOptions(): HTMLElement[] {\n\t\treturn Array.from(this.querySelectorAll<HTMLElement>('[role=\"option\"]'));\n\t}\n\n\tprivate sync(): void {\n\t\tconst options = this.getOptions();\n\t\tfor (const option of options) {\n\t\t\tconst selected = option.getAttribute('data-value') === this.value;\n\t\t\toption.setAttribute('aria-selected', String(selected));\n\t\t\toption.tabIndex = selected || (!this.value && option === options[0]) ? 0 : -1;\n\t\t}\n\t}\n\n\tprivate select(option: HTMLElement): void {\n\t\tif (this.disabled || option.getAttribute('aria-disabled') === 'true') return;\n\t\tthis.value = option.getAttribute('data-value') || option.textContent?.trim() || '';\n\t\tthis.sync();\n\t\tthis.changeEvent.emit({ value: this.value });\n\t\toption.focus();\n\t}\n\n\t@onEvent({ selector: '[role=\"option\"]', type: 'click' })\n\tonOptionClick(event: Event): void {\n\t\tconst option = (event.target as HTMLElement).closest('[role=\"option\"]') as HTMLElement | null;\n\t\tif (option && this.contains(option)) this.select(option);\n\t}\n\n\t@onEvent({ selector: '[role=\"option\"]', type: 'keydown' })\n\tonOptionKeydown(event: KeyboardEvent): void {\n\t\tconst options = this.getOptions();\n\t\tconst current = (event.target as HTMLElement).closest('[role=\"option\"]') as HTMLElement | null;\n\t\tif (!current) return;\n\n\t\tif (event.key === 'Enter' || event.key === ' ') {\n\t\t\tevent.preventDefault();\n\t\t\tthis.select(current);\n\t\t\treturn;\n\t\t}\n\n\t\tconst result = navigateRovingTabindex({\n\t\t\titems: options,\n\t\t\tcurrent,\n\t\t\tkey: event.key,\n\t\t\torientation: 'vertical',\n\t\t});\n\t\tif (!result.handled) return;\n\n\t\tevent.preventDefault();\n\t\tthis.select(result.item);\n\t}\n\n\toverride render() {\n\t\treturn (\n\t\t\t<div\n\t\t\t\tclass=\"rui-listbox\"\n\t\t\t\trole=\"listbox\"\n\t\t\t\tdata-rui-control\n\t\t\t\tdata-rui-control-type=\"text\"\n\t\t\t\taria-label={this.resolvedAriaLabel}\n\t\t\t\taria-disabled={this.resolvedAriaDisabled}\n\t\t\t>\n\t\t\t\t<slot></slot>\n\t\t\t</div>\n\t\t);\n\t}\n}\n", "export type RovingOrientation = 'horizontal' | 'vertical';\n\nexport type RovingNavResult = { handled: false } | { handled: true; index: number; item: HTMLElement };\n\n/**\n * Apply the APG roving-tabindex pattern: one item is tabbable, the rest are `-1`.\n */\nexport function applyRovingTabindex(items: HTMLElement[], activeIndex: number): void {\n\titems.forEach((item, index) => {\n\t\titem.tabIndex = index === activeIndex ? 0 : -1;\n\t});\n}\n\n/**\n * Focus `items[index]` and update tabindexes. Returns the focused item, or null.\n */\nexport function focusRovingItem(items: HTMLElement[], index: number): HTMLElement | null {\n\tif (!items.length) return null;\n\tconst next = Math.max(0, Math.min(items.length - 1, index));\n\tapplyRovingTabindex(items, next);\n\titems[next]?.focus();\n\treturn items[next] ?? null;\n}\n\nfunction deltaForKey(key: string, orientation: RovingOrientation): 'prev' | 'next' | 'first' | 'last' | null {\n\tif (key === 'Home') return 'first';\n\tif (key === 'End') return 'last';\n\n\tif (orientation === 'horizontal') {\n\t\tif (key === 'ArrowRight') return 'next';\n\t\tif (key === 'ArrowLeft') return 'prev';\n\t\treturn null;\n\t}\n\n\tif (key === 'ArrowDown') return 'next';\n\tif (key === 'ArrowUp') return 'prev';\n\treturn null;\n}\n\n/**\n * Handle arrow / Home / End navigation for a flat roving-tabindex collection.\n * Does not activate the item \u2014 callers decide whether to select on move.\n */\nexport function navigateRovingTabindex(options: {\n\titems: HTMLElement[];\n\tcurrent: HTMLElement | null;\n\tkey: string;\n\torientation?: RovingOrientation;\n\twrap?: boolean;\n}): RovingNavResult {\n\tconst { items, current, key, orientation = 'horizontal', wrap = true } = options;\n\tif (!items.length || !current) return { handled: false };\n\n\tconst index = items.indexOf(current);\n\tif (index < 0) return { handled: false };\n\n\tconst delta = deltaForKey(key, orientation);\n\tif (!delta) return { handled: false };\n\n\tlet next = index;\n\tif (delta === 'first') next = 0;\n\telse if (delta === 'last') next = items.length - 1;\n\telse if (delta === 'next') next = wrap ? (index + 1) % items.length : Math.min(items.length - 1, index + 1);\n\telse next = wrap ? (index - 1 + items.length) % items.length : Math.max(0, index - 1);\n\n\tconst item = focusRovingItem(items, next);\n\tif (!item) return { handled: false };\n\treturn { handled: true, index: next, item };\n}\n", "import type { JsxRenderable } from '@ecopages/jsx';\n\nconst RADIANT_VIEW_ELEMENT = Symbol.for('@ecopages/storybook-radiant.viewElement');\nconst RADIANT_SCRIPT_MODULE = Symbol.for('@ecopages/storybook-radiant.scriptModule');\nconst RADIANT_SCRIPT_EXPORT = Symbol.for('@ecopages/storybook-radiant.scriptExport');\n\nexport type RadiantViewComponent<TArgs = unknown> = ((args: TArgs) => JsxRenderable) & {\n\t[RADIANT_VIEW_ELEMENT]?: CustomElementConstructor;\n\t[RADIANT_SCRIPT_MODULE]?: string;\n\t[RADIANT_SCRIPT_EXPORT]?: string;\n};\n\nfunction linkViewToElement<TArgs>(\n\tview: RadiantViewComponent<TArgs>,\n\telement: CustomElementConstructor & {\n\t\t[RADIANT_SCRIPT_MODULE]?: string;\n\t\t[RADIANT_SCRIPT_EXPORT]?: string;\n\t},\n): void {\n\tview[RADIANT_VIEW_ELEMENT] = element;\n\n\tconst ssrModule = element[RADIANT_SCRIPT_MODULE];\n\tif (ssrModule) {\n\t\tview[RADIANT_SCRIPT_MODULE] = ssrModule;\n\t\tview[RADIANT_SCRIPT_EXPORT] = element[RADIANT_SCRIPT_EXPORT] ?? element.name;\n\t}\n}\n\n/** Link a JSX view export to its RadiantElement class (same symbols as the Storybook framework). */\nexport function defineRadiantView<TArgs>(\n\telement: CustomElementConstructor & {\n\t\t[RADIANT_SCRIPT_MODULE]?: string;\n\t\t[RADIANT_SCRIPT_EXPORT]?: string;\n\t},\n\trender: (args: TArgs) => JsxRenderable,\n): RadiantViewComponent<TArgs> {\n\tconst view = ((args: TArgs) => render(args)) as RadiantViewComponent<TArgs>;\n\tlinkViewToElement(view, element);\n\treturn view;\n}\n\n/** Re-copy SSR metadata from the linked element (e.g. after HMR). */\nexport function syncRadiantViewMetadata(view: RadiantViewComponent): void {\n\tconst element = view[RADIANT_VIEW_ELEMENT];\n\tif (element) {\n\t\tlinkViewToElement(view, element);\n\t}\n}\n", "import type { JsxRenderable } from '@ecopages/jsx';\nimport type { RadiantSlotProps } from '../../../types';\nimport { defineRadiantView } from '../../../lib/radiant-view';\nimport type { RuiListboxProps } from './listbox.script';\nimport { RuiListbox as RuiListboxElement } from './listbox.script';\nimport './listbox.css';\n\nexport type RuiListboxOption = { value: string; label: JsxRenderable; disabled?: boolean };\n\nexport const RuiListbox = defineRadiantView(\n\tRuiListboxElement,\n\t({\n\t\tslot,\n\t\tvalue,\n\t\tlabel,\n\t\tdisabled,\n\t\toptions,\n\t}: RuiListboxProps & RadiantSlotProps & { options: RuiListboxOption[] }) => (\n\t\t<rui-listbox slot={slot} value={value} label={label} disabled={disabled}>\n\t\t\t{options.map((option) => (\n\t\t\t\t<div\n\t\t\t\t\tclass=\"rui-listbox__option\"\n\t\t\t\t\trole=\"option\"\n\t\t\t\t\tdata-value={option.value}\n\t\t\t\t\taria-disabled={option.disabled ? 'true' : undefined}\n\t\t\t\t\ttabindex={-1}\n\t\t\t\t>\n\t\t\t\t\t{option.label}\n\t\t\t\t</div>\n\t\t\t))}\n\t\t</rui-listbox>\n\t),\n);\n"],
5
+ "mappings": "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",
6
+ "names": ["RadiantElement", "customElement", "event", "onEvent", "onUpdated", "prop", "applyRovingTabindex", "items", "activeIndex", "item", "index", "focusRovingItem", "next", "deltaForKey", "key", "orientation", "navigateRovingTabindex", "options", "current", "wrap", "delta", "RuiListbox", "customElement", "RadiantElement", "prop", "event", "label", "disabled", "onUpdated", "options", "option", "selected", "onEvent", "current", "result", "navigateRovingTabindex", "RADIANT_VIEW_ELEMENT", "RADIANT_SCRIPT_MODULE", "RADIANT_SCRIPT_EXPORT", "linkViewToElement", "view", "element", "ssrModule", "defineRadiantView", "render", "args", "RuiListbox", "defineRadiantView", "slot", "value", "label", "disabled", "options", "option"]
7
+ }
@@ -0,0 +1,23 @@
1
+ @reference '../../../styles/radiant-ui.css';
2
+
3
+ rui-listbox {
4
+ @apply block;
5
+ }
6
+
7
+ @layer components {
8
+ .rui-listbox {
9
+ @apply max-h-60 overflow-auto rounded-container border border-border bg-background p-1 rui-sheen;
10
+ }
11
+
12
+ .rui-listbox__option {
13
+ @apply cursor-pointer rounded-control px-3 py-2 text-sm;
14
+ }
15
+
16
+ .rui-listbox__option[aria-selected='true'] {
17
+ @apply bg-primary text-on-primary rui-sheen;
18
+ }
19
+
20
+ .rui-listbox__option:focus-visible {
21
+ @apply outline-none ring-2 ring-focus-ring;
22
+ }
23
+ }
@@ -0,0 +1,12 @@
1
+ import type { JsxRenderable } from '@ecopages/jsx';
2
+ import type { RadiantSlotProps } from '../../../types';
3
+ import type { RuiListboxProps } from './listbox.script';
4
+ import './listbox.css';
5
+ export type RuiListboxOption = {
6
+ value: string;
7
+ label: JsxRenderable;
8
+ disabled?: boolean;
9
+ };
10
+ export declare const RuiListbox: import("../../../lib/radiant-view").RadiantViewComponent<RuiListboxProps & RadiantSlotProps & {
11
+ options: RuiListboxOption[];
12
+ }>;
@@ -0,0 +1,40 @@
1
+ import { RadiantElement } from '@ecopages/radiant';
2
+ import type { EventEmitter } from '@ecopages/radiant/tools/event-emitter';
3
+ export type RuiListboxProps = {
4
+ value?: string;
5
+ label?: string;
6
+ disabled?: boolean;
7
+ };
8
+ export type RuiListboxChangeDetail = {
9
+ value: string;
10
+ };
11
+ type RuiListboxBindings = {
12
+ label: string;
13
+ disabled: boolean;
14
+ };
15
+ /**
16
+ * `<rui-listbox>` — a list of options where one may be selected.
17
+ *
18
+ * Implements the APG Listbox pattern with roving tabindex on `[role="option"]`.
19
+ *
20
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/listbox/
21
+ * @element rui-listbox
22
+ * @fires rui-change
23
+ */
24
+ export declare class RuiListbox extends RadiantElement<RuiListboxBindings> {
25
+ value: string;
26
+ label: string;
27
+ disabled: boolean;
28
+ changeEvent: EventEmitter<RuiListboxChangeDetail>;
29
+ private readonly resolvedAriaLabel;
30
+ private readonly resolvedAriaDisabled;
31
+ connectedCallback(): void;
32
+ onPropsUpdated(): void;
33
+ private getOptions;
34
+ private sync;
35
+ private select;
36
+ onOptionClick(event: Event): void;
37
+ onOptionKeydown(event: KeyboardEvent): void;
38
+ render(): import("@ecopages/jsx").JsxRenderable;
39
+ }
40
+ export {};
@@ -0,0 +1,2 @@
1
+ export { RuiMenuButton as RuiMenuButtonElement, type RuiMenuButtonProps, type RuiMenuButtonSelectDetail, } from './menu-button.script';
2
+ export { RuiMenuButton, type RuiMenuItem } from './menu-button';
@@ -0,0 +1,2 @@
1
+ import{RadiantElement as f,bound as u,customElement as h,event as l,onEvent as o,onUpdated as b,prop as p,query as c}from"@ecopages/radiant";import{autoUpdate as T,computePosition as v,flip as R,offset as E,shift as y}from"@floating-ui/dom";var s=@h("rui-menu-button")class extends f{@p({type:Boolean,reflect:!0,defaultValue:!1})open;@p({type:String,defaultValue:"bottom-start"})placement;@c({ref:"trigger"})triggerTarget;@c({ref:"menu"})menuTarget;@l({name:"rui-change",bubbles:!0,composed:!0})changeEvent;@l({name:"rui-close",bubbles:!0,composed:!0})closeEvent;cleanup=null;menuId=`rui-menu-${Math.random().toString(36).slice(2,9)}`;connectedCallback(){super.connectedCallback(),queueMicrotask(()=>this.syncOpenState())}disconnectedCallback(){this.teardownFloating(),super.disconnectedCallback()}getItems(){return Array.from(this.menuTarget?.querySelectorAll('[role="menuitem"]')??[]).filter(e=>e.getAttribute("aria-disabled")!=="true")}teardownFloating(){this.cleanup?.(),this.cleanup=null}@u updatePosition(){!this.triggerTarget||!this.menuTarget||!this.open||v(this.triggerTarget,this.menuTarget,{placement:this.placement,middleware:[E(6),R(),y({padding:8})]}).then(({x:e,y:t})=>{Object.assign(this.menuTarget.style,{left:`${e}px`,top:`${t}px`})})}pendingFocus=null;@u@b(["open","placement"])syncOpenState(){if(!this.triggerTarget||!this.menuTarget)return;this.menuTarget.id=this.menuId,this.triggerTarget.setAttribute("aria-controls",this.menuId),this.triggerTarget.setAttribute("aria-haspopup","menu"),this.triggerTarget.setAttribute("aria-expanded",String(this.open)),this.menuTarget.hidden=!this.open,this.open?(this.teardownFloating(),this.cleanup=T(this.triggerTarget,this.menuTarget,this.updatePosition),this.updatePosition()):this.teardownFloating();let e=this.pendingFocus;if(this.pendingFocus=null,!!e){if(e==="first"&&this.getItems()[0]?.focus(),e==="last"){let t=this.getItems();t[t.length-1]?.focus()}e==="trigger"&&this.triggerTarget?.focus()}}setOpen(e,t=null){let n=this.open;this.pendingFocus=t,this.open=e,n&&!e&&this.closeEvent.emit(),n===e&&this.syncOpenState()}@o({document:!0,type:"click"})onDocumentClick(e){if(!this.open)return;let t=e.target;this.triggerTarget?.contains(t)||this.menuTarget?.contains(t)||this.setOpen(!1)}@o({ref:"trigger",type:"click"})onTriggerClick(){this.setOpen(!this.open,this.open?null:"first")}@o({ref:"trigger",type:"keydown"})onTriggerKeydown(e){switch(e.key){case"ArrowDown":case"Enter":case" ":e.preventDefault(),this.setOpen(!0,"first");break;case"ArrowUp":e.preventDefault(),this.setOpen(!0,"last");break;default:break}}@o({ref:"menu",type:"keydown"})onMenuKeydown(e){let t=this.getItems(),n=document.activeElement,a=n?t.indexOf(n):-1;switch(e.key){case"ArrowDown":{e.preventDefault(),t[(a+1)%t.length]?.focus();break}case"ArrowUp":{e.preventDefault(),t[(a-1+t.length)%t.length]?.focus();break}case"Home":e.preventDefault(),t[0]?.focus();break;case"End":e.preventDefault(),t[t.length-1]?.focus();break;case"Escape":e.preventDefault(),this.setOpen(!1,"trigger");break;case"Tab":this.setOpen(!1);break;case"Enter":case" ":n&&t.includes(n)&&(e.preventDefault(),this.activateItem(n));break;default:break}}@o({selector:'[role="menuitem"]',type:"click"})onItemClick(e){let t=e.target.closest('[role="menuitem"]');!t||!this.contains(t)||this.activateItem(t)}activateItem(e){let t=e.getAttribute("data-value")||e.textContent?.trim()||"";this.changeEvent.emit({value:t}),this.setOpen(!1,"trigger")}render(){return React.createElement("div",{class:"rui-menu-button"},React.createElement("button",{type:"button","data-ref":"trigger",class:"rui-button rui-button--primary rui-button--md rui-menu-button__trigger","aria-haspopup":"menu","aria-expanded":"false"},React.createElement("slot",{name:"trigger"}),React.createElement("span",{class:"rui-menu-button__chevron","aria-hidden":"true"})),React.createElement("div",{"data-ref":"menu",class:"rui-menu-button__menu",role:"menu",hidden:!0},React.createElement("slot",null)))}};var A=Symbol.for("@ecopages/storybook-radiant.viewElement"),m=Symbol.for("@ecopages/storybook-radiant.scriptModule"),d=Symbol.for("@ecopages/storybook-radiant.scriptExport");function M(r,e){r[A]=e;let t=e[m];t&&(r[m]=t,r[d]=e[d]??e.name)}function g(r,e){let t=(n=>e(n));return M(t,r),t}import"./menu-button.css";var I=g(s,({slot:r,open:e,placement:t,trigger:n,items:a})=>React.createElement("rui-menu-button",{slot:r,open:e,placement:t},React.createElement("span",{slot:"trigger"},n),a.map(i=>React.createElement("button",{type:"button",class:"rui-menu-button__item",role:"menuitem","data-value":i.value,"aria-disabled":i.disabled?"true":void 0,tabindex:-1},i.label))));export{I as RuiMenuButton,s as RuiMenuButtonElement};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/components/ui/menu-button/menu-button.script.tsx", "../../../../src/lib/radiant-view.ts", "../../../../src/components/ui/menu-button/menu-button.tsx"],
4
+ "sourcesContent": ["import { RadiantElement, bound, customElement, event, onEvent, onUpdated, prop, query } from '@ecopages/radiant';\nimport type { EventEmitter } from '@ecopages/radiant/tools/event-emitter';\nimport { type Placement, autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';\n\nexport type RuiMenuButtonProps = {\n\t/** Whether the menu starts open. Default: `false`. */\n\topen?: boolean;\n\t/** Floating-ui placement for the menu. Default: `bottom-start`. */\n\tplacement?: Placement;\n};\n\nexport type RuiMenuButtonSelectDetail = {\n\tvalue: string;\n};\n\n/**\n * `<rui-menu-button>` \u2014 a button that opens a menu of actions.\n *\n * Implements the WAI-ARIA APG Menu Button pattern together with the Menu\n * keyboard model: the trigger exposes `aria-haspopup=\"menu\"` and\n * `aria-expanded`, and the popup has `role=\"menu\"` with `role=\"menuitem\"`\n * children.\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menubar/\n *\n * Keyboard interaction (trigger):\n * - `Enter` / `Space` / `ArrowDown`: open and focus the first item\n * - `ArrowUp`: open and focus the last item\n *\n * Keyboard interaction (open menu):\n * - `ArrowDown` / `ArrowUp`: move between items (wraps)\n * - `Home` / `End`: first / last item\n * - `Enter` / `Space`: activate the focused item and close\n * - `Escape`: close and return focus to the trigger\n *\n * @element rui-menu-button\n * @slot trigger - Label for the menu button.\n * @slot - Menu items (`role=\"menuitem\"`), typically buttons or anchors.\n * @fires rui-change - Emitted when a menu item is activated; `detail.value` is the item's `data-value` or text.\n * @fires rui-close - Emitted when the menu closes.\n */\n@customElement('rui-menu-button')\nexport class RuiMenuButton extends RadiantElement {\n\t@prop({ type: Boolean, reflect: true, defaultValue: false }) open: boolean;\n\t@prop({ type: String, defaultValue: 'bottom-start' }) placement: Placement;\n\n\t@query({ ref: 'trigger' }) triggerTarget: HTMLButtonElement;\n\t@query({ ref: 'menu' }) menuTarget: HTMLElement;\n\n\t@event({ name: 'rui-change', bubbles: true, composed: true })\n\tchangeEvent: EventEmitter<RuiMenuButtonSelectDetail>;\n\n\t@event({ name: 'rui-close', bubbles: true, composed: true })\n\tcloseEvent: EventEmitter<void>;\n\n\tprivate cleanup: ReturnType<typeof autoUpdate> | null = null;\n\tprivate menuId = `rui-menu-${Math.random().toString(36).slice(2, 9)}`;\n\n\toverride connectedCallback(): void {\n\t\tsuper.connectedCallback();\n\t\tqueueMicrotask(() => this.syncOpenState());\n\t}\n\n\toverride disconnectedCallback(): void {\n\t\tthis.teardownFloating();\n\t\tsuper.disconnectedCallback();\n\t}\n\n\tprivate getItems(): HTMLElement[] {\n\t\treturn Array.from(this.menuTarget?.querySelectorAll<HTMLElement>('[role=\"menuitem\"]') ?? []).filter(\n\t\t\t(item) => item.getAttribute('aria-disabled') !== 'true',\n\t\t);\n\t}\n\n\tprivate teardownFloating(): void {\n\t\tthis.cleanup?.();\n\t\tthis.cleanup = null;\n\t}\n\n\t@bound\n\tupdatePosition(): void {\n\t\tif (!this.triggerTarget || !this.menuTarget || !this.open) return;\n\t\tcomputePosition(this.triggerTarget, this.menuTarget, {\n\t\t\tplacement: this.placement,\n\t\t\tmiddleware: [offset(6), flip(), shift({ padding: 8 })],\n\t\t}).then(({ x, y }) => {\n\t\t\tObject.assign(this.menuTarget.style, {\n\t\t\t\tleft: `${x}px`,\n\t\t\t\ttop: `${y}px`,\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate pendingFocus: 'first' | 'last' | 'trigger' | null = null;\n\n\t@bound\n\t@onUpdated(['open', 'placement'])\n\tsyncOpenState(): void {\n\t\tif (!this.triggerTarget || !this.menuTarget) return;\n\n\t\tthis.menuTarget.id = this.menuId;\n\t\tthis.triggerTarget.setAttribute('aria-controls', this.menuId);\n\t\tthis.triggerTarget.setAttribute('aria-haspopup', 'menu');\n\t\tthis.triggerTarget.setAttribute('aria-expanded', String(this.open));\n\t\tthis.menuTarget.hidden = !this.open;\n\n\t\tif (this.open) {\n\t\t\tthis.teardownFloating();\n\t\t\tthis.cleanup = autoUpdate(this.triggerTarget, this.menuTarget, this.updatePosition);\n\t\t\tthis.updatePosition();\n\t\t} else {\n\t\t\tthis.teardownFloating();\n\t\t}\n\n\t\tconst focus = this.pendingFocus;\n\t\tthis.pendingFocus = null;\n\t\tif (!focus) return;\n\n\t\tif (focus === 'first') this.getItems()[0]?.focus();\n\t\tif (focus === 'last') {\n\t\t\tconst items = this.getItems();\n\t\t\titems[items.length - 1]?.focus();\n\t\t}\n\t\tif (focus === 'trigger') this.triggerTarget?.focus();\n\t}\n\n\tprivate setOpen(next: boolean, focus: 'first' | 'last' | 'trigger' | null = null): void {\n\t\tconst wasOpen = this.open;\n\t\tthis.pendingFocus = focus;\n\t\tthis.open = next;\n\t\tif (wasOpen && !next) this.closeEvent.emit();\n\t\tif (wasOpen === next) this.syncOpenState();\n\t}\n\n\t@onEvent({ document: true, type: 'click' })\n\tonDocumentClick(event: MouseEvent): void {\n\t\tif (!this.open) return;\n\t\tconst target = event.target as Node;\n\t\tif (this.triggerTarget?.contains(target) || this.menuTarget?.contains(target)) return;\n\t\tthis.setOpen(false);\n\t}\n\n\t@onEvent({ ref: 'trigger', type: 'click' })\n\tonTriggerClick(): void {\n\t\tthis.setOpen(!this.open, this.open ? null : 'first');\n\t}\n\n\t@onEvent({ ref: 'trigger', type: 'keydown' })\n\tonTriggerKeydown(event: KeyboardEvent): void {\n\t\tswitch (event.key) {\n\t\t\tcase 'ArrowDown':\n\t\t\tcase 'Enter':\n\t\t\tcase ' ':\n\t\t\t\tevent.preventDefault();\n\t\t\t\tthis.setOpen(true, 'first');\n\t\t\t\tbreak;\n\t\t\tcase 'ArrowUp':\n\t\t\t\tevent.preventDefault();\n\t\t\t\tthis.setOpen(true, 'last');\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t@onEvent({ ref: 'menu', type: 'keydown' })\n\tonMenuKeydown(event: KeyboardEvent): void {\n\t\tconst items = this.getItems();\n\t\tconst current = document.activeElement as HTMLElement | null;\n\t\tconst index = current ? items.indexOf(current) : -1;\n\n\t\tswitch (event.key) {\n\t\t\tcase 'ArrowDown': {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tconst next = items[(index + 1) % items.length];\n\t\t\t\tnext?.focus();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'ArrowUp': {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tconst prev = items[(index - 1 + items.length) % items.length];\n\t\t\t\tprev?.focus();\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase 'Home':\n\t\t\t\tevent.preventDefault();\n\t\t\t\titems[0]?.focus();\n\t\t\t\tbreak;\n\t\t\tcase 'End':\n\t\t\t\tevent.preventDefault();\n\t\t\t\titems[items.length - 1]?.focus();\n\t\t\t\tbreak;\n\t\t\tcase 'Escape':\n\t\t\t\tevent.preventDefault();\n\t\t\t\tthis.setOpen(false, 'trigger');\n\t\t\t\tbreak;\n\t\t\tcase 'Tab':\n\t\t\t\tthis.setOpen(false);\n\t\t\t\tbreak;\n\t\t\tcase 'Enter':\n\t\t\tcase ' ':\n\t\t\t\tif (current && items.includes(current)) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tthis.activateItem(current);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t@onEvent({ selector: '[role=\"menuitem\"]', type: 'click' })\n\tonItemClick(event: Event): void {\n\t\tconst item = (event.target as HTMLElement).closest('[role=\"menuitem\"]') as HTMLElement | null;\n\t\tif (!item || !this.contains(item)) return;\n\t\tthis.activateItem(item);\n\t}\n\n\tprivate activateItem(item: HTMLElement): void {\n\t\tconst value = item.getAttribute('data-value') || item.textContent?.trim() || '';\n\t\tthis.changeEvent.emit({ value });\n\t\tthis.setOpen(false, 'trigger');\n\t}\n\n\toverride render() {\n\t\treturn (\n\t\t\t<div class=\"rui-menu-button\">\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tdata-ref=\"trigger\"\n\t\t\t\t\tclass=\"rui-button rui-button--primary rui-button--md rui-menu-button__trigger\"\n\t\t\t\t\taria-haspopup=\"menu\"\n\t\t\t\t\taria-expanded=\"false\"\n\t\t\t\t>\n\t\t\t\t\t<slot name=\"trigger\"></slot>\n\t\t\t\t\t<span class=\"rui-menu-button__chevron\" aria-hidden=\"true\"></span>\n\t\t\t\t</button>\n\t\t\t\t<div data-ref=\"menu\" class=\"rui-menu-button__menu\" role=\"menu\" hidden>\n\t\t\t\t\t<slot></slot>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t}\n}\n", "import type { JsxRenderable } from '@ecopages/jsx';\n\nconst RADIANT_VIEW_ELEMENT = Symbol.for('@ecopages/storybook-radiant.viewElement');\nconst RADIANT_SCRIPT_MODULE = Symbol.for('@ecopages/storybook-radiant.scriptModule');\nconst RADIANT_SCRIPT_EXPORT = Symbol.for('@ecopages/storybook-radiant.scriptExport');\n\nexport type RadiantViewComponent<TArgs = unknown> = ((args: TArgs) => JsxRenderable) & {\n\t[RADIANT_VIEW_ELEMENT]?: CustomElementConstructor;\n\t[RADIANT_SCRIPT_MODULE]?: string;\n\t[RADIANT_SCRIPT_EXPORT]?: string;\n};\n\nfunction linkViewToElement<TArgs>(\n\tview: RadiantViewComponent<TArgs>,\n\telement: CustomElementConstructor & {\n\t\t[RADIANT_SCRIPT_MODULE]?: string;\n\t\t[RADIANT_SCRIPT_EXPORT]?: string;\n\t},\n): void {\n\tview[RADIANT_VIEW_ELEMENT] = element;\n\n\tconst ssrModule = element[RADIANT_SCRIPT_MODULE];\n\tif (ssrModule) {\n\t\tview[RADIANT_SCRIPT_MODULE] = ssrModule;\n\t\tview[RADIANT_SCRIPT_EXPORT] = element[RADIANT_SCRIPT_EXPORT] ?? element.name;\n\t}\n}\n\n/** Link a JSX view export to its RadiantElement class (same symbols as the Storybook framework). */\nexport function defineRadiantView<TArgs>(\n\telement: CustomElementConstructor & {\n\t\t[RADIANT_SCRIPT_MODULE]?: string;\n\t\t[RADIANT_SCRIPT_EXPORT]?: string;\n\t},\n\trender: (args: TArgs) => JsxRenderable,\n): RadiantViewComponent<TArgs> {\n\tconst view = ((args: TArgs) => render(args)) as RadiantViewComponent<TArgs>;\n\tlinkViewToElement(view, element);\n\treturn view;\n}\n\n/** Re-copy SSR metadata from the linked element (e.g. after HMR). */\nexport function syncRadiantViewMetadata(view: RadiantViewComponent): void {\n\tconst element = view[RADIANT_VIEW_ELEMENT];\n\tif (element) {\n\t\tlinkViewToElement(view, element);\n\t}\n}\n", "import type { JsxRenderable } from '@ecopages/jsx';\nimport type { RadiantSlotProps } from '../../../types';\nimport { defineRadiantView } from '../../../lib/radiant-view';\nimport type { RuiMenuButtonProps } from './menu-button.script';\nimport { RuiMenuButton as RuiMenuButtonElement } from './menu-button.script';\nimport './menu-button.css';\n\nexport type RuiMenuItem = {\n\tvalue: string;\n\tlabel: JsxRenderable;\n\tdisabled?: boolean;\n};\n\nexport const RuiMenuButton = defineRadiantView(\n\tRuiMenuButtonElement,\n\t({\n\t\tslot,\n\t\topen,\n\t\tplacement,\n\t\ttrigger,\n\t\titems,\n\t}: RuiMenuButtonProps &\n\t\tRadiantSlotProps & {\n\t\t\ttrigger: JsxRenderable;\n\t\t\titems: RuiMenuItem[];\n\t\t}) => (\n\t\t<rui-menu-button slot={slot} open={open} placement={placement}>\n\t\t\t<span slot=\"trigger\">{trigger}</span>\n\t\t\t{items.map((item) => (\n\t\t\t\t<button\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"rui-menu-button__item\"\n\t\t\t\t\trole=\"menuitem\"\n\t\t\t\t\tdata-value={item.value}\n\t\t\t\t\taria-disabled={item.disabled ? 'true' : undefined}\n\t\t\t\t\ttabindex={-1}\n\t\t\t\t>\n\t\t\t\t\t{item.label}\n\t\t\t\t</button>\n\t\t\t))}\n\t\t</rui-menu-button>\n\t),\n);\n"],
5
+ "mappings": "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",
6
+ "names": ["RadiantElement", "bound", "customElement", "event", "onEvent", "onUpdated", "prop", "query", "autoUpdate", "computePosition", "flip", "offset", "shift", "RuiMenuButton", "item", "x", "y", "focus", "items", "next", "wasOpen", "target", "current", "index", "value", "RADIANT_VIEW_ELEMENT", "RADIANT_SCRIPT_MODULE", "RADIANT_SCRIPT_EXPORT", "linkViewToElement", "view", "element", "ssrModule", "defineRadiantView", "render", "args", "RuiMenuButton", "defineRadiantView", "slot", "open", "placement", "trigger", "items", "item"]
7
+ }
@@ -0,0 +1,43 @@
1
+ @reference '../../../styles/radiant-ui.css';
2
+
3
+ rui-menu-button {
4
+ @apply inline-flex;
5
+ }
6
+
7
+ @layer components {
8
+ .rui-menu-button {
9
+ @apply relative inline-flex;
10
+ }
11
+
12
+ .rui-menu-button__trigger {
13
+ @apply gap-inline;
14
+ }
15
+
16
+ .rui-menu-button__chevron {
17
+ @apply inline-block border-x-4 border-t-[5px] border-x-transparent border-t-current;
18
+ width: 0;
19
+ height: 0;
20
+ }
21
+
22
+ .rui-menu-button__menu {
23
+ @apply absolute z-popover min-w-40 rounded-container border border-border bg-background p-1 shadow-md;
24
+ }
25
+
26
+ .rui-menu-button__menu[hidden] {
27
+ @apply hidden;
28
+ }
29
+
30
+ .rui-menu-button__item {
31
+ @apply flex w-full cursor-pointer items-center rounded-control border-0 bg-transparent px-3 py-2 text-left text-sm;
32
+ @apply text-on-background;
33
+ }
34
+
35
+ .rui-menu-button__item:hover,
36
+ .rui-menu-button__item:focus-visible {
37
+ @apply bg-surface outline-none;
38
+ }
39
+
40
+ .rui-menu-button__item[aria-disabled='true'] {
41
+ @apply cursor-not-allowed opacity-disabled;
42
+ }
43
+ }
@@ -0,0 +1,13 @@
1
+ import type { JsxRenderable } from '@ecopages/jsx';
2
+ import type { RadiantSlotProps } from '../../../types';
3
+ import type { RuiMenuButtonProps } from './menu-button.script';
4
+ import './menu-button.css';
5
+ export type RuiMenuItem = {
6
+ value: string;
7
+ label: JsxRenderable;
8
+ disabled?: boolean;
9
+ };
10
+ export declare const RuiMenuButton: import("../../../lib/radiant-view").RadiantViewComponent<RuiMenuButtonProps & RadiantSlotProps & {
11
+ trigger: JsxRenderable;
12
+ items: RuiMenuItem[];
13
+ }>;
@@ -0,0 +1,64 @@
1
+ import { RadiantElement } from '@ecopages/radiant';
2
+ import type { EventEmitter } from '@ecopages/radiant/tools/event-emitter';
3
+ import { type Placement } from '@floating-ui/dom';
4
+ export type RuiMenuButtonProps = {
5
+ /** Whether the menu starts open. Default: `false`. */
6
+ open?: boolean;
7
+ /** Floating-ui placement for the menu. Default: `bottom-start`. */
8
+ placement?: Placement;
9
+ };
10
+ export type RuiMenuButtonSelectDetail = {
11
+ value: string;
12
+ };
13
+ /**
14
+ * `<rui-menu-button>` — a button that opens a menu of actions.
15
+ *
16
+ * Implements the WAI-ARIA APG Menu Button pattern together with the Menu
17
+ * keyboard model: the trigger exposes `aria-haspopup="menu"` and
18
+ * `aria-expanded`, and the popup has `role="menu"` with `role="menuitem"`
19
+ * children.
20
+ *
21
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/
22
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/menubar/
23
+ *
24
+ * Keyboard interaction (trigger):
25
+ * - `Enter` / `Space` / `ArrowDown`: open and focus the first item
26
+ * - `ArrowUp`: open and focus the last item
27
+ *
28
+ * Keyboard interaction (open menu):
29
+ * - `ArrowDown` / `ArrowUp`: move between items (wraps)
30
+ * - `Home` / `End`: first / last item
31
+ * - `Enter` / `Space`: activate the focused item and close
32
+ * - `Escape`: close and return focus to the trigger
33
+ *
34
+ * @element rui-menu-button
35
+ * @slot trigger - Label for the menu button.
36
+ * @slot - Menu items (`role="menuitem"`), typically buttons or anchors.
37
+ * @fires rui-change - Emitted when a menu item is activated; `detail.value` is the item's `data-value` or text.
38
+ * @fires rui-close - Emitted when the menu closes.
39
+ */
40
+ export declare class RuiMenuButton extends RadiantElement {
41
+ open: boolean;
42
+ placement: Placement;
43
+ triggerTarget: HTMLButtonElement;
44
+ menuTarget: HTMLElement;
45
+ changeEvent: EventEmitter<RuiMenuButtonSelectDetail>;
46
+ closeEvent: EventEmitter<void>;
47
+ private cleanup;
48
+ private menuId;
49
+ connectedCallback(): void;
50
+ disconnectedCallback(): void;
51
+ private getItems;
52
+ private teardownFloating;
53
+ updatePosition(): void;
54
+ private pendingFocus;
55
+ syncOpenState(): void;
56
+ private setOpen;
57
+ onDocumentClick(event: MouseEvent): void;
58
+ onTriggerClick(): void;
59
+ onTriggerKeydown(event: KeyboardEvent): void;
60
+ onMenuKeydown(event: KeyboardEvent): void;
61
+ onItemClick(event: Event): void;
62
+ private activateItem;
63
+ render(): import("@ecopages/jsx").JsxRenderable;
64
+ }
@@ -0,0 +1,2 @@
1
+ export { RuiMenubar as RuiMenubarElement, type RuiMenubarProps, type RuiMenubarChangeDetail } from './menubar.script';
2
+ export { RuiMenubar, type RuiMenubarItem } from './menubar';
@@ -0,0 +1,2 @@
1
+ import{RadiantElement as R,customElement as E,event as T,onEvent as u,prop as y}from"@ecopages/radiant";function p(r,e){r.forEach((t,n)=>{t.tabIndex=n===e?0:-1})}function M(r,e){if(!r.length)return null;let t=Math.max(0,Math.min(r.length-1,e));return p(r,t),r[t]?.focus(),r[t]??null}function h(r,e){return r==="Home"?"first":r==="End"?"last":e==="horizontal"?r==="ArrowRight"?"next":r==="ArrowLeft"?"prev":null:r==="ArrowDown"?"next":r==="ArrowUp"?"prev":null}function d(r){let{items:e,current:t,key:n,orientation:o="horizontal",wrap:l=!0}=r;if(!e.length||!t)return{handled:!1};let a=e.indexOf(t);if(a<0)return{handled:!1};let s=h(n,o);if(!s)return{handled:!1};let i=a;s==="first"?i=0:s==="last"?i=e.length-1:s==="next"?i=l?(a+1)%e.length:Math.min(e.length-1,a+1):i=l?(a-1+e.length)%e.length:Math.max(0,a-1);let c=M(e,i);return c?{handled:!0,index:i,item:c}:{handled:!1}}var m=@E("rui-menubar")class extends R{@y({type:String,defaultValue:""})label;@T({name:"rui-change",bubbles:!0,composed:!0})changeEvent;resolvedAriaLabel=this.$.label.map(e=>e||void 0);openRoot=null;getTopItems(){return Array.from(this.querySelectorAll('[data-ref="menubar"] > [data-ref="menubar-root"] > [role="menuitem"]'))}getMenuFor(e){return e.closest('[data-ref="menubar-root"]')?.querySelector(':scope > [role="menu"]')??null}getMenuItems(e){return Array.from(e.querySelectorAll(':scope > [role="menuitem"]')).filter(t=>t.getAttribute("aria-disabled")!=="true")}connectedCallback(){super.connectedCallback(),queueMicrotask(()=>p(this.getTopItems(),0))}closeOpenMenu(e=!1){if(!this.openRoot)return;let t=this.openRoot.querySelector(':scope > [role="menuitem"]'),n=this.openRoot.querySelector(':scope > [role="menu"]');t&&t.setAttribute("aria-expanded","false"),n&&(n.hidden=!0),this.openRoot=null,e&&t&&t.focus()}openMenu(e,t="first"){let n=e.closest('[data-ref="menubar-root"]'),o=this.getMenuFor(e);if(!(!n||!o)&&(this.openRoot&&this.openRoot!==n&&this.closeOpenMenu(),e.setAttribute("aria-expanded","true"),o.hidden=!1,this.openRoot=n,t==="first")){let l=this.getMenuItems(o);p(l,0),l[0]?.focus()}}@u({document:!0,type:"click"})onDocumentClick(e){this.openRoot&&(this.contains(e.target)||this.closeOpenMenu())}@u({selector:'[data-ref="menubar"] > [data-ref="menubar-root"] > [role="menuitem"]',type:"keydown"})onTopKeydown(e){let t=this.getTopItems(),n=e.target.closest('[role="menuitem"]');if(!n||!t.includes(n))return;if(e.key==="ArrowDown"||e.key==="Enter"||e.key===" "){this.getMenuFor(n)&&(e.preventDefault(),this.openMenu(n,"first"));return}if(e.key==="Escape"){e.preventDefault(),this.closeOpenMenu(!0);return}let o=d({items:t,current:n,key:e.key,orientation:"horizontal"});o.handled&&(e.preventDefault(),this.openRoot&&(this.getMenuFor(o.item)?this.openMenu(o.item,"first"):this.closeOpenMenu()))}@u({selector:'[data-ref="menubar"] > [data-ref="menubar-root"] > [role="menuitem"]',type:"click"})onTopClick(e){let t=e.target.closest('[role="menuitem"]');if(!t||!this.getTopItems().includes(t)||!this.getMenuFor(t))return;t.getAttribute("aria-expanded")==="true"?this.closeOpenMenu():this.openMenu(t,"first")}@u({selector:'[role="menu"] > [role="menuitem"]',type:"keydown"})onMenuKeydown(e){let t=e.target.closest('[role="menu"]'),n=e.target.closest('[role="menuitem"]');if(!t||!n||!this.contains(t))return;let o=this.getMenuItems(t);if(e.key==="Escape"){e.preventDefault(),this.closeOpenMenu(!0);return}if(e.key==="Enter"||e.key===" "){e.preventDefault(),this.activateItem(n);return}if(e.key==="ArrowLeft"||e.key==="ArrowRight"){let a=this.getTopItems(),s=t.parentElement?.querySelector(':scope > [role="menuitem"]');if(!s)return;e.preventDefault();let i=d({items:a,current:s,key:e.key,orientation:"horizontal"});i.handled&&this.getMenuFor(i.item)&&this.openMenu(i.item,"first");return}d({items:o,current:n,key:e.key,orientation:"vertical"}).handled&&e.preventDefault()}@u({selector:'[role="menu"] > [role="menuitem"]',type:"click"})onMenuItemClick(e){let t=e.target.closest('[role="menuitem"]');!t||!this.contains(t)||t.parentElement?.getAttribute("role")==="menu"&&this.activateItem(t)}activateItem(e){let t=e.getAttribute("data-value")||e.textContent?.trim()||"";this.changeEvent.emit({value:t}),this.closeOpenMenu(!0)}render(){return React.createElement("div",{class:"rui-menubar","data-ref":"menubar",role:"menubar","aria-label":this.resolvedAriaLabel},React.createElement("slot",null))}};var v=Symbol.for("@ecopages/storybook-radiant.viewElement"),f=Symbol.for("@ecopages/storybook-radiant.scriptModule"),b=Symbol.for("@ecopages/storybook-radiant.scriptExport");function x(r,e){r[v]=e;let t=e[f];t&&(r[f]=t,r[b]=e[b]??e.name)}function g(r,e){let t=(n=>e(n));return x(t,r),t}import"./menubar.css";var A=g(m,({slot:r,label:e,items:t})=>React.createElement("rui-menubar",{slot:r,label:e},t.map(n=>React.createElement("div",{class:"rui-menubar__root","data-ref":"menubar-root"},React.createElement("button",{type:"button",class:"rui-menubar__item",role:"menuitem",tabindex:-1,"aria-haspopup":n.items?.length?"true":void 0,"aria-expanded":n.items?.length?"false":void 0},n.label),n.items?.length?React.createElement("div",{class:"rui-menubar__menu",role:"menu",hidden:!0},n.items.map(o=>React.createElement("button",{type:"button",class:"rui-menubar__menu-item",role:"menuitem","data-value":o.id,tabindex:-1},o.label))):null))));export{A as RuiMenubar,m as RuiMenubarElement};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/components/ui/menubar/menubar.script.tsx", "../../../../src/lib/roving-tabindex.ts", "../../../../src/lib/radiant-view.ts", "../../../../src/components/ui/menubar/menubar.tsx"],
4
+ "sourcesContent": ["import { RadiantElement, customElement, event, onEvent, prop } from '@ecopages/radiant';\nimport type { EventEmitter } from '@ecopages/radiant/tools/event-emitter';\nimport { applyRovingTabindex, navigateRovingTabindex } from '../../../lib/roving-tabindex';\n\nexport type RuiMenubarProps = {\n\tlabel?: string;\n};\n\nexport type RuiMenubarChangeDetail = { value: string };\n\ntype RuiMenubarBindings = {\n\tlabel: string;\n};\n\n/**\n * `<rui-menubar>` \u2014 a horizontal menubar of menus.\n *\n * Implements the APG Menubar pattern: top-level items use `role=\"menuitem\"` with\n * `aria-haspopup` / `aria-expanded` when they own a nested `[role=\"menu\"]`. Arrow\n * keys move across the bar; Enter/Space/ArrowDown open the popup; Escape closes it.\n *\n * Expected markup (also produced by the JSX view helper):\n * ```html\n * <div class=\"rui-menubar__root\" data-ref=\"menubar-root\">\n * <button role=\"menuitem\" aria-haspopup=\"true\" aria-expanded=\"false\">File</button>\n * <div role=\"menu\" hidden>\n * <button role=\"menuitem\" data-value=\"new\">New</button>\n * </div>\n * </div>\n * ```\n *\n * @see https://www.w3.org/WAI/ARIA/apg/patterns/menubar/\n * @element rui-menubar\n * @fires rui-change\n */\n@customElement('rui-menubar')\nexport class RuiMenubar extends RadiantElement<RuiMenubarBindings> {\n\t@prop({ type: String, defaultValue: '' }) label: string;\n\n\t@event({ name: 'rui-change', bubbles: true, composed: true })\n\tchangeEvent: EventEmitter<RuiMenubarChangeDetail>;\n\n\tprivate readonly resolvedAriaLabel = this.$.label.map((label) => label || undefined);\n\n\tprivate openRoot: HTMLElement | null = null;\n\n\tprivate getTopItems(): HTMLElement[] {\n\t\treturn Array.from(\n\t\t\tthis.querySelectorAll<HTMLElement>('[data-ref=\"menubar\"] > [data-ref=\"menubar-root\"] > [role=\"menuitem\"]'),\n\t\t);\n\t}\n\n\tprivate getMenuFor(topItem: HTMLElement): HTMLElement | null {\n\t\tconst root = topItem.closest('[data-ref=\"menubar-root\"]');\n\t\treturn root?.querySelector<HTMLElement>(':scope > [role=\"menu\"]') ?? null;\n\t}\n\n\tprivate getMenuItems(menu: HTMLElement): HTMLElement[] {\n\t\treturn Array.from(menu.querySelectorAll<HTMLElement>(':scope > [role=\"menuitem\"]')).filter(\n\t\t\t(item) => item.getAttribute('aria-disabled') !== 'true',\n\t\t);\n\t}\n\n\toverride connectedCallback(): void {\n\t\tsuper.connectedCallback();\n\t\tqueueMicrotask(() => applyRovingTabindex(this.getTopItems(), 0));\n\t}\n\n\tprivate closeOpenMenu(returnFocus = false): void {\n\t\tif (!this.openRoot) return;\n\t\tconst top = this.openRoot.querySelector<HTMLElement>(':scope > [role=\"menuitem\"]');\n\t\tconst menu = this.openRoot.querySelector<HTMLElement>(':scope > [role=\"menu\"]');\n\t\tif (top) top.setAttribute('aria-expanded', 'false');\n\t\tif (menu) menu.hidden = true;\n\t\tthis.openRoot = null;\n\t\tif (returnFocus && top) top.focus();\n\t}\n\n\tprivate openMenu(topItem: HTMLElement, focus: 'first' | null = 'first'): void {\n\t\tconst root = topItem.closest('[data-ref=\"menubar-root\"]') as HTMLElement | null;\n\t\tconst menu = this.getMenuFor(topItem);\n\t\tif (!root || !menu) return;\n\n\t\tif (this.openRoot && this.openRoot !== root) this.closeOpenMenu();\n\n\t\ttopItem.setAttribute('aria-expanded', 'true');\n\t\tmenu.hidden = false;\n\t\tthis.openRoot = root;\n\n\t\tif (focus === 'first') {\n\t\t\tconst items = this.getMenuItems(menu);\n\t\t\tapplyRovingTabindex(items, 0);\n\t\t\titems[0]?.focus();\n\t\t}\n\t}\n\n\t@onEvent({ document: true, type: 'click' })\n\tonDocumentClick(event: MouseEvent): void {\n\t\tif (!this.openRoot) return;\n\t\tif (!this.contains(event.target as Node)) this.closeOpenMenu();\n\t}\n\n\t@onEvent({ selector: '[data-ref=\"menubar\"] > [data-ref=\"menubar-root\"] > [role=\"menuitem\"]', type: 'keydown' })\n\tonTopKeydown(event: KeyboardEvent): void {\n\t\tconst items = this.getTopItems();\n\t\tconst current = (event.target as HTMLElement).closest('[role=\"menuitem\"]') as HTMLElement | null;\n\t\tif (!current || !items.includes(current)) return;\n\n\t\tif (event.key === 'ArrowDown' || event.key === 'Enter' || event.key === ' ') {\n\t\t\tif (this.getMenuFor(current)) {\n\t\t\t\tevent.preventDefault();\n\t\t\t\tthis.openMenu(current, 'first');\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === 'Escape') {\n\t\t\tevent.preventDefault();\n\t\t\tthis.closeOpenMenu(true);\n\t\t\treturn;\n\t\t}\n\n\t\tconst result = navigateRovingTabindex({\n\t\t\titems,\n\t\t\tcurrent,\n\t\t\tkey: event.key,\n\t\t\torientation: 'horizontal',\n\t\t});\n\t\tif (!result.handled) return;\n\t\tevent.preventDefault();\n\n\t\t// APG: when a menu is open, moving across the bar opens the newly focused menu.\n\t\tif (this.openRoot) {\n\t\t\tif (this.getMenuFor(result.item)) this.openMenu(result.item, 'first');\n\t\t\telse this.closeOpenMenu();\n\t\t}\n\t}\n\n\t@onEvent({ selector: '[data-ref=\"menubar\"] > [data-ref=\"menubar-root\"] > [role=\"menuitem\"]', type: 'click' })\n\tonTopClick(event: Event): void {\n\t\tconst current = (event.target as HTMLElement).closest('[role=\"menuitem\"]') as HTMLElement | null;\n\t\tif (!current || !this.getTopItems().includes(current)) return;\n\t\tif (!this.getMenuFor(current)) return;\n\n\t\tconst expanded = current.getAttribute('aria-expanded') === 'true';\n\t\tif (expanded) this.closeOpenMenu();\n\t\telse this.openMenu(current, 'first');\n\t}\n\n\t@onEvent({ selector: '[role=\"menu\"] > [role=\"menuitem\"]', type: 'keydown' })\n\tonMenuKeydown(event: KeyboardEvent): void {\n\t\tconst menu = (event.target as HTMLElement).closest('[role=\"menu\"]') as HTMLElement | null;\n\t\tconst current = (event.target as HTMLElement).closest('[role=\"menuitem\"]') as HTMLElement | null;\n\t\tif (!menu || !current || !this.contains(menu)) return;\n\n\t\tconst items = this.getMenuItems(menu);\n\n\t\tif (event.key === 'Escape') {\n\t\t\tevent.preventDefault();\n\t\t\tthis.closeOpenMenu(true);\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === 'Enter' || event.key === ' ') {\n\t\t\tevent.preventDefault();\n\t\t\tthis.activateItem(current);\n\t\t\treturn;\n\t\t}\n\n\t\tif (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {\n\t\t\tconst tops = this.getTopItems();\n\t\t\tconst top = menu.parentElement?.querySelector<HTMLElement>(':scope > [role=\"menuitem\"]');\n\t\t\tif (!top) return;\n\t\t\tevent.preventDefault();\n\t\t\tconst result = navigateRovingTabindex({\n\t\t\t\titems: tops,\n\t\t\t\tcurrent: top,\n\t\t\t\tkey: event.key,\n\t\t\t\torientation: 'horizontal',\n\t\t\t});\n\t\t\tif (result.handled && this.getMenuFor(result.item)) this.openMenu(result.item, 'first');\n\t\t\treturn;\n\t\t}\n\n\t\tconst result = navigateRovingTabindex({\n\t\t\titems,\n\t\t\tcurrent,\n\t\t\tkey: event.key,\n\t\t\torientation: 'vertical',\n\t\t});\n\t\tif (result.handled) event.preventDefault();\n\t}\n\n\t@onEvent({ selector: '[role=\"menu\"] > [role=\"menuitem\"]', type: 'click' })\n\tonMenuItemClick(event: Event): void {\n\t\tconst item = (event.target as HTMLElement).closest('[role=\"menuitem\"]') as HTMLElement | null;\n\t\tif (!item || !this.contains(item)) return;\n\t\t// Ignore top-level items (they are not direct children of role=menu in our markup).\n\t\tif (item.parentElement?.getAttribute('role') !== 'menu') return;\n\t\tthis.activateItem(item);\n\t}\n\n\tprivate activateItem(item: HTMLElement): void {\n\t\tconst value = item.getAttribute('data-value') || item.textContent?.trim() || '';\n\t\tthis.changeEvent.emit({ value });\n\t\tthis.closeOpenMenu(true);\n\t}\n\n\toverride render() {\n\t\treturn (\n\t\t\t<div class=\"rui-menubar\" data-ref=\"menubar\" role=\"menubar\" aria-label={this.resolvedAriaLabel}>\n\t\t\t\t<slot></slot>\n\t\t\t</div>\n\t\t);\n\t}\n}\n", "export type RovingOrientation = 'horizontal' | 'vertical';\n\nexport type RovingNavResult = { handled: false } | { handled: true; index: number; item: HTMLElement };\n\n/**\n * Apply the APG roving-tabindex pattern: one item is tabbable, the rest are `-1`.\n */\nexport function applyRovingTabindex(items: HTMLElement[], activeIndex: number): void {\n\titems.forEach((item, index) => {\n\t\titem.tabIndex = index === activeIndex ? 0 : -1;\n\t});\n}\n\n/**\n * Focus `items[index]` and update tabindexes. Returns the focused item, or null.\n */\nexport function focusRovingItem(items: HTMLElement[], index: number): HTMLElement | null {\n\tif (!items.length) return null;\n\tconst next = Math.max(0, Math.min(items.length - 1, index));\n\tapplyRovingTabindex(items, next);\n\titems[next]?.focus();\n\treturn items[next] ?? null;\n}\n\nfunction deltaForKey(key: string, orientation: RovingOrientation): 'prev' | 'next' | 'first' | 'last' | null {\n\tif (key === 'Home') return 'first';\n\tif (key === 'End') return 'last';\n\n\tif (orientation === 'horizontal') {\n\t\tif (key === 'ArrowRight') return 'next';\n\t\tif (key === 'ArrowLeft') return 'prev';\n\t\treturn null;\n\t}\n\n\tif (key === 'ArrowDown') return 'next';\n\tif (key === 'ArrowUp') return 'prev';\n\treturn null;\n}\n\n/**\n * Handle arrow / Home / End navigation for a flat roving-tabindex collection.\n * Does not activate the item \u2014 callers decide whether to select on move.\n */\nexport function navigateRovingTabindex(options: {\n\titems: HTMLElement[];\n\tcurrent: HTMLElement | null;\n\tkey: string;\n\torientation?: RovingOrientation;\n\twrap?: boolean;\n}): RovingNavResult {\n\tconst { items, current, key, orientation = 'horizontal', wrap = true } = options;\n\tif (!items.length || !current) return { handled: false };\n\n\tconst index = items.indexOf(current);\n\tif (index < 0) return { handled: false };\n\n\tconst delta = deltaForKey(key, orientation);\n\tif (!delta) return { handled: false };\n\n\tlet next = index;\n\tif (delta === 'first') next = 0;\n\telse if (delta === 'last') next = items.length - 1;\n\telse if (delta === 'next') next = wrap ? (index + 1) % items.length : Math.min(items.length - 1, index + 1);\n\telse next = wrap ? (index - 1 + items.length) % items.length : Math.max(0, index - 1);\n\n\tconst item = focusRovingItem(items, next);\n\tif (!item) return { handled: false };\n\treturn { handled: true, index: next, item };\n}\n", "import type { JsxRenderable } from '@ecopages/jsx';\n\nconst RADIANT_VIEW_ELEMENT = Symbol.for('@ecopages/storybook-radiant.viewElement');\nconst RADIANT_SCRIPT_MODULE = Symbol.for('@ecopages/storybook-radiant.scriptModule');\nconst RADIANT_SCRIPT_EXPORT = Symbol.for('@ecopages/storybook-radiant.scriptExport');\n\nexport type RadiantViewComponent<TArgs = unknown> = ((args: TArgs) => JsxRenderable) & {\n\t[RADIANT_VIEW_ELEMENT]?: CustomElementConstructor;\n\t[RADIANT_SCRIPT_MODULE]?: string;\n\t[RADIANT_SCRIPT_EXPORT]?: string;\n};\n\nfunction linkViewToElement<TArgs>(\n\tview: RadiantViewComponent<TArgs>,\n\telement: CustomElementConstructor & {\n\t\t[RADIANT_SCRIPT_MODULE]?: string;\n\t\t[RADIANT_SCRIPT_EXPORT]?: string;\n\t},\n): void {\n\tview[RADIANT_VIEW_ELEMENT] = element;\n\n\tconst ssrModule = element[RADIANT_SCRIPT_MODULE];\n\tif (ssrModule) {\n\t\tview[RADIANT_SCRIPT_MODULE] = ssrModule;\n\t\tview[RADIANT_SCRIPT_EXPORT] = element[RADIANT_SCRIPT_EXPORT] ?? element.name;\n\t}\n}\n\n/** Link a JSX view export to its RadiantElement class (same symbols as the Storybook framework). */\nexport function defineRadiantView<TArgs>(\n\telement: CustomElementConstructor & {\n\t\t[RADIANT_SCRIPT_MODULE]?: string;\n\t\t[RADIANT_SCRIPT_EXPORT]?: string;\n\t},\n\trender: (args: TArgs) => JsxRenderable,\n): RadiantViewComponent<TArgs> {\n\tconst view = ((args: TArgs) => render(args)) as RadiantViewComponent<TArgs>;\n\tlinkViewToElement(view, element);\n\treturn view;\n}\n\n/** Re-copy SSR metadata from the linked element (e.g. after HMR). */\nexport function syncRadiantViewMetadata(view: RadiantViewComponent): void {\n\tconst element = view[RADIANT_VIEW_ELEMENT];\n\tif (element) {\n\t\tlinkViewToElement(view, element);\n\t}\n}\n", "import type { JsxRenderable } from '@ecopages/jsx';\nimport type { RadiantSlotProps } from '../../../types';\nimport { defineRadiantView } from '../../../lib/radiant-view';\nimport type { RuiMenubarProps } from './menubar.script';\nimport { RuiMenubar as RuiMenubarElement } from './menubar.script';\nimport './menubar.css';\n\nexport type RuiMenubarItem = {\n\tid: string;\n\tlabel: JsxRenderable;\n\t/** Popup actions. When present, the top item opens a `role=\"menu\"`. */\n\titems?: Array<{ id: string; label: JsxRenderable }>;\n};\n\nexport const RuiMenubar = defineRadiantView(\n\tRuiMenubarElement,\n\t({ slot, label, items }: RuiMenubarProps & RadiantSlotProps & { items: RuiMenubarItem[] }) => (\n\t\t<rui-menubar slot={slot} label={label}>\n\t\t\t{items.map((item) => (\n\t\t\t\t<div class=\"rui-menubar__root\" data-ref=\"menubar-root\">\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"rui-menubar__item\"\n\t\t\t\t\t\trole=\"menuitem\"\n\t\t\t\t\t\ttabindex={-1}\n\t\t\t\t\t\taria-haspopup={item.items?.length ? 'true' : undefined}\n\t\t\t\t\t\taria-expanded={item.items?.length ? 'false' : undefined}\n\t\t\t\t\t>\n\t\t\t\t\t\t{item.label}\n\t\t\t\t\t</button>\n\t\t\t\t\t{item.items?.length ? (\n\t\t\t\t\t\t<div class=\"rui-menubar__menu\" role=\"menu\" hidden>\n\t\t\t\t\t\t\t{item.items.map((child) => (\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tclass=\"rui-menubar__menu-item\"\n\t\t\t\t\t\t\t\t\trole=\"menuitem\"\n\t\t\t\t\t\t\t\t\tdata-value={child.id}\n\t\t\t\t\t\t\t\t\ttabindex={-1}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{child.label}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : null}\n\t\t\t\t</div>\n\t\t\t))}\n\t\t</rui-menubar>\n\t),\n);\n"],
5
+ "mappings": "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",
6
+ "names": ["RadiantElement", "customElement", "event", "onEvent", "prop", "applyRovingTabindex", "items", "activeIndex", "item", "index", "focusRovingItem", "next", "deltaForKey", "key", "orientation", "navigateRovingTabindex", "options", "current", "wrap", "delta", "RuiMenubar", "customElement", "RadiantElement", "prop", "event", "label", "topItem", "menu", "item", "applyRovingTabindex", "returnFocus", "top", "focus", "root", "items", "onEvent", "current", "result", "navigateRovingTabindex", "tops", "value", "RADIANT_VIEW_ELEMENT", "RADIANT_SCRIPT_MODULE", "RADIANT_SCRIPT_EXPORT", "linkViewToElement", "view", "element", "ssrModule", "defineRadiantView", "render", "args", "RuiMenubar", "defineRadiantView", "slot", "label", "items", "item", "child"]
7
+ }
@@ -0,0 +1,45 @@
1
+ @reference '../../../styles/radiant-ui.css';
2
+
3
+ rui-menubar {
4
+ @apply block;
5
+ }
6
+
7
+ @layer components {
8
+ .rui-menubar {
9
+ @apply relative inline-flex gap-1 rounded-container border border-border bg-surface p-1;
10
+ }
11
+ .rui-menubar__root {
12
+ @apply relative;
13
+ }
14
+ .rui-menubar__item {
15
+ @apply cursor-pointer rounded-control border-0 bg-transparent px-3 py-1.5 text-sm text-on-surface;
16
+ @apply transition-colors duration-fast;
17
+ }
18
+ .rui-menubar__item:hover {
19
+ @apply bg-background;
20
+ }
21
+ .rui-menubar__item:active {
22
+ @apply bg-background/80;
23
+ }
24
+ .rui-menubar__item[aria-expanded='true'] {
25
+ @apply bg-background font-medium text-on-background shadow-sm;
26
+ }
27
+ .rui-menubar__item:focus-visible,
28
+ .rui-menubar__menu-item:focus-visible {
29
+ @apply bg-background outline-none ring-2 ring-focus-ring;
30
+ }
31
+ .rui-menubar__menu {
32
+ @apply absolute left-0 top-full z-dropdown mt-1 min-w-40 rounded-container border border-border bg-surface p-1 shadow-md;
33
+ }
34
+ .rui-menubar__menu-item {
35
+ @apply block w-full cursor-pointer rounded-control border-0 bg-transparent px-3 py-1.5 text-left text-sm text-on-surface;
36
+ @apply transition-colors duration-fast;
37
+ }
38
+ .rui-menubar__menu-item:hover,
39
+ .rui-menubar__menu-item:focus-visible {
40
+ @apply bg-background outline-none;
41
+ }
42
+ .rui-menubar__menu-item:active {
43
+ @apply bg-background/80;
44
+ }
45
+ }
@@ -0,0 +1,16 @@
1
+ import type { JsxRenderable } from '@ecopages/jsx';
2
+ import type { RadiantSlotProps } from '../../../types';
3
+ import type { RuiMenubarProps } from './menubar.script';
4
+ import './menubar.css';
5
+ export type RuiMenubarItem = {
6
+ id: string;
7
+ label: JsxRenderable;
8
+ /** Popup actions. When present, the top item opens a `role="menu"`. */
9
+ items?: Array<{
10
+ id: string;
11
+ label: JsxRenderable;
12
+ }>;
13
+ };
14
+ export declare const RuiMenubar: import("../../../lib/radiant-view").RadiantViewComponent<RuiMenubarProps & RadiantSlotProps & {
15
+ items: RuiMenubarItem[];
16
+ }>;