iryx-ui 0.11.0 → 0.13.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 (258) hide show
  1. package/README.md +35 -1235
  2. package/dist/component-names.d.ts +1 -1
  3. package/dist/component-names.js +1 -1
  4. package/dist/components/Accordion.js +5 -0
  5. package/dist/components/Accordion.vue.d.ts +80 -0
  6. package/dist/components/Accordion.vue_vue_type_script_setup_true_lang.js +87 -0
  7. package/dist/components/Alert.vue.d.ts +1 -1
  8. package/dist/components/Alert.vue_vue_type_script_setup_true_lang.js +43 -43
  9. package/dist/components/App.vue.d.ts +1 -1
  10. package/dist/components/AppShell.js +5 -0
  11. package/dist/components/AppShell.vue.d.ts +46 -0
  12. package/dist/components/AppShell.vue_vue_type_script_setup_true_lang.js +59 -0
  13. package/dist/components/AspectRatio.js +5 -0
  14. package/dist/components/AspectRatio.vue.d.ts +26 -0
  15. package/dist/components/AspectRatio.vue_vue_type_script_setup_true_lang.js +27 -0
  16. package/dist/components/Avatar.js +5 -0
  17. package/dist/components/Avatar.vue.d.ts +52 -0
  18. package/dist/components/Avatar.vue_vue_type_script_setup_true_lang.js +67 -0
  19. package/dist/components/AvatarGroup.js +5 -0
  20. package/dist/components/AvatarGroup.vue.d.ts +39 -0
  21. package/dist/components/AvatarGroup.vue_vue_type_script_setup_true_lang.js +45 -0
  22. package/dist/components/Badge.vue.d.ts +1 -1
  23. package/dist/components/Banner.vue.d.ts +2 -2
  24. package/dist/components/Banner.vue_vue_type_script_setup_true_lang.js +40 -40
  25. package/dist/components/BarChart.vue.d.ts +1 -1
  26. package/dist/components/BarChart.vue_vue_type_script_setup_true_lang.js +14 -14
  27. package/dist/components/Breadcrumb.vue_vue_type_script_setup_true_lang.js +28 -28
  28. package/dist/components/Button.vue.d.ts +2 -2
  29. package/dist/components/Button.vue_vue_type_script_setup_true_lang.js +14 -14
  30. package/dist/components/ButtonGroup.vue.d.ts +1 -1
  31. package/dist/components/Card.vue.d.ts +1 -1
  32. package/dist/components/Checkbox.vue_vue_type_script_setup_true_lang.js +37 -30
  33. package/dist/components/Collapsible.js +5 -0
  34. package/dist/components/Collapsible.vue.d.ts +58 -0
  35. package/dist/components/Collapsible.vue_vue_type_script_setup_true_lang.js +67 -0
  36. package/dist/components/ColorPicker.js +5 -0
  37. package/dist/components/ColorPicker.vue.d.ts +49 -0
  38. package/dist/components/ColorPicker.vue_vue_type_script_setup_true_lang.js +172 -0
  39. package/dist/components/Combobox.vue.d.ts +6 -6
  40. package/dist/components/Combobox.vue_vue_type_script_setup_true_lang.js +62 -60
  41. package/dist/components/CommandPalette.js +5 -0
  42. package/dist/components/CommandPalette.vue.d.ts +63 -0
  43. package/dist/components/CommandPalette.vue_vue_type_script_setup_true_lang.js +185 -0
  44. package/dist/components/Container.js +5 -0
  45. package/dist/components/Container.vue.d.ts +33 -0
  46. package/dist/components/Container.vue_vue_type_script_setup_true_lang.js +40 -0
  47. package/dist/components/ContextMenu.js +5 -0
  48. package/dist/components/ContextMenu.vue.d.ts +42 -0
  49. package/dist/components/ContextMenu.vue_vue_type_script_setup_true_lang.js +49 -0
  50. package/dist/components/DatePicker.vue_vue_type_script_setup_true_lang.js +65 -65
  51. package/dist/components/DateRangePicker.vue.d.ts +1 -1
  52. package/dist/components/DateRangePicker.vue_vue_type_script_setup_true_lang.js +74 -74
  53. package/dist/components/Dialog.vue.d.ts +1 -1
  54. package/dist/components/Dialog.vue_vue_type_script_setup_true_lang.js +34 -34
  55. package/dist/components/Drawer.js +5 -0
  56. package/dist/components/Drawer.vue.d.ts +94 -0
  57. package/dist/components/Drawer.vue_vue_type_script_setup_true_lang.js +144 -0
  58. package/dist/components/DropdownMenuItems.vue_vue_type_script_setup_true_lang.js +42 -42
  59. package/dist/components/EmptyState.vue.d.ts +1 -1
  60. package/dist/components/FileUpload.vue_vue_type_script_setup_true_lang.js +77 -77
  61. package/dist/components/Icon.vue_vue_type_script_setup_true_lang.js +11 -11
  62. package/dist/components/Input.vue.d.ts +1 -1
  63. package/dist/components/Input.vue_vue_type_script_setup_true_lang.js +33 -33
  64. package/dist/components/Kbd.js +5 -0
  65. package/dist/components/Kbd.vue.d.ts +46 -0
  66. package/dist/components/Kbd.vue_vue_type_script_setup_true_lang.js +41 -0
  67. package/dist/components/LineChart.vue_vue_type_script_setup_true_lang.js +19 -19
  68. package/dist/components/Menubar.js +5 -0
  69. package/dist/components/Menubar.vue.d.ts +62 -0
  70. package/dist/components/Menubar.vue_vue_type_script_setup_true_lang.js +79 -0
  71. package/dist/components/NavigationMenu.js +5 -0
  72. package/dist/components/NavigationMenu.vue.d.ts +75 -0
  73. package/dist/components/NavigationMenu.vue_vue_type_script_setup_true_lang.js +167 -0
  74. package/dist/components/NumberInput.vue_vue_type_script_setup_true_lang.js +68 -66
  75. package/dist/components/PageHeader.js +5 -0
  76. package/dist/components/PageHeader.vue.d.ts +52 -0
  77. package/dist/components/PageHeader.vue_vue_type_script_setup_true_lang.js +59 -0
  78. package/dist/components/Pagination.vue_vue_type_script_setup_true_lang.js +37 -37
  79. package/dist/components/PasswordInput.vue_vue_type_script_setup_true_lang.js +37 -37
  80. package/dist/components/PinInput.js +5 -0
  81. package/dist/components/PinInput.vue.d.ts +65 -0
  82. package/dist/components/PinInput.vue_vue_type_script_setup_true_lang.js +76 -0
  83. package/dist/components/Popover.js +5 -0
  84. package/dist/components/Popover.vue.d.ts +75 -0
  85. package/dist/components/Popover.vue_vue_type_script_setup_true_lang.js +99 -0
  86. package/dist/components/Progress.vue.d.ts +20 -1
  87. package/dist/components/Progress.vue_vue_type_script_setup_true_lang.js +85 -42
  88. package/dist/components/RadioGroup.vue_vue_type_script_setup_true_lang.js +4 -1
  89. package/dist/components/ScrollArea.js +5 -0
  90. package/dist/components/ScrollArea.vue.d.ts +41 -0
  91. package/dist/components/ScrollArea.vue_vue_type_script_setup_true_lang.js +68 -0
  92. package/dist/components/ScrollFade.js +5 -0
  93. package/dist/components/ScrollFade.vue.d.ts +36 -0
  94. package/dist/components/ScrollFade.vue_vue_type_script_setup_true_lang.js +59 -0
  95. package/dist/components/Select.vue.d.ts +2 -2
  96. package/dist/components/Select.vue_vue_type_script_setup_true_lang.js +47 -45
  97. package/dist/components/Sidebar.js +5 -0
  98. package/dist/components/Sidebar.vue.d.ts +72 -0
  99. package/dist/components/Sidebar.vue_vue_type_script_setup_true_lang.js +183 -0
  100. package/dist/components/SignaturePad.js +5 -0
  101. package/dist/components/SignaturePad.vue.d.ts +60 -0
  102. package/dist/components/SignaturePad.vue_vue_type_script_setup_true_lang.js +158 -0
  103. package/dist/components/Skeleton.vue.d.ts +1 -1
  104. package/dist/components/Skeleton.vue_vue_type_script_setup_true_lang.js +8 -8
  105. package/dist/components/Slider.js +5 -0
  106. package/dist/components/Slider.vue.d.ts +80 -0
  107. package/dist/components/Slider.vue_vue_type_script_setup_true_lang.js +115 -0
  108. package/dist/components/Splitter.js +5 -0
  109. package/dist/components/Splitter.vue.d.ts +59 -0
  110. package/dist/components/Splitter.vue_vue_type_script_setup_true_lang.js +69 -0
  111. package/dist/components/Stat.vue.d.ts +1 -1
  112. package/dist/components/Stat.vue_vue_type_script_setup_true_lang.js +27 -27
  113. package/dist/components/Stepper.vue_vue_type_script_setup_true_lang.js +38 -38
  114. package/dist/components/Switch.vue.d.ts +1 -0
  115. package/dist/components/Switch.vue_vue_type_script_setup_true_lang.js +16 -5
  116. package/dist/components/Table.vue.d.ts +7 -1
  117. package/dist/components/Table.vue_vue_type_script_setup_true_lang.js +120 -105
  118. package/dist/components/TagsInput.js +5 -0
  119. package/dist/components/TagsInput.vue.d.ts +71 -0
  120. package/dist/components/TagsInput.vue_vue_type_script_setup_true_lang.js +119 -0
  121. package/dist/components/Textarea.vue_vue_type_script_setup_true_lang.js +3 -3
  122. package/dist/components/TimeField.js +5 -0
  123. package/dist/components/TimeField.vue.d.ts +50 -0
  124. package/dist/components/TimeField.vue_vue_type_script_setup_true_lang.js +85 -0
  125. package/dist/components/Timeline.js +5 -0
  126. package/dist/components/Timeline.vue.d.ts +61 -0
  127. package/dist/components/Timeline.vue_vue_type_script_setup_true_lang.js +60 -0
  128. package/dist/components/Toaster.vue.d.ts +1 -1
  129. package/dist/components/Toaster.vue_vue_type_script_setup_true_lang.js +41 -41
  130. package/dist/components/Toggle.js +5 -0
  131. package/dist/components/Toggle.vue.d.ts +42 -0
  132. package/dist/components/Toggle.vue_vue_type_script_setup_true_lang.js +52 -0
  133. package/dist/components/ToggleGroup.js +5 -0
  134. package/dist/components/ToggleGroup.vue.d.ts +64 -0
  135. package/dist/components/ToggleGroup.vue_vue_type_script_setup_true_lang.js +78 -0
  136. package/dist/components/Toolbar.js +5 -0
  137. package/dist/components/Toolbar.vue.d.ts +52 -0
  138. package/dist/components/Toolbar.vue_vue_type_script_setup_true_lang.js +94 -0
  139. package/dist/components/Tree.js +5 -0
  140. package/dist/components/Tree.vue.d.ts +70 -0
  141. package/dist/components/Tree.vue_vue_type_script_setup_true_lang.js +112 -0
  142. package/dist/components/index.d.ts +30 -0
  143. package/dist/components/index.js +144 -84
  144. package/dist/composables/appearance.js +5 -1
  145. package/dist/composables/cartesian.d.ts +11 -0
  146. package/dist/composables/cartesian.js +18 -19
  147. package/dist/composables/command-palette.d.ts +42 -0
  148. package/dist/composables/command-palette.js +32 -0
  149. package/dist/composables/date.d.ts +18 -2
  150. package/dist/composables/date.js +23 -15
  151. package/dist/composables/kbd.d.ts +26 -0
  152. package/dist/composables/kbd.js +161 -0
  153. package/dist/composables/navigation-menu.d.ts +29 -0
  154. package/dist/composables/navigation-menu.js +6 -0
  155. package/dist/composables/scroll-fade.d.ts +35 -0
  156. package/dist/composables/scroll-fade.js +36 -0
  157. package/dist/composables/sidebar.d.ts +35 -0
  158. package/dist/composables/sidebar.js +23 -0
  159. package/dist/composables/signature.d.ts +22 -0
  160. package/dist/composables/signature.js +13 -0
  161. package/dist/index.d.ts +36 -0
  162. package/dist/index.js +153 -92
  163. package/dist/theme/accordion.d.ts +68 -0
  164. package/dist/theme/accordion.js +24 -0
  165. package/dist/theme/alert.d.ts +0 -3
  166. package/dist/theme/app-shell.d.ts +77 -0
  167. package/dist/theme/app-shell.js +35 -0
  168. package/dist/theme/avatar.d.ts +253 -0
  169. package/dist/theme/avatar.js +84 -0
  170. package/dist/theme/banner.d.ts +0 -9
  171. package/dist/theme/bar-chart.d.ts +0 -16
  172. package/dist/theme/breadcrumb.d.ts +3 -3
  173. package/dist/theme/button-group.d.ts +0 -3
  174. package/dist/theme/button.d.ts +0 -3
  175. package/dist/theme/card.js +2 -2
  176. package/dist/theme/chart-legend.d.ts +3 -11
  177. package/dist/theme/collapsible.d.ts +44 -0
  178. package/dist/theme/collapsible.js +17 -0
  179. package/dist/theme/color-picker.d.ts +98 -0
  180. package/dist/theme/color-picker.js +19 -0
  181. package/dist/theme/command-palette.d.ts +122 -0
  182. package/dist/theme/command-palette.js +22 -0
  183. package/dist/theme/container.d.ts +101 -0
  184. package/dist/theme/container.js +26 -0
  185. package/dist/theme/date-picker.d.ts +12 -0
  186. package/dist/theme/date-picker.js +1 -1
  187. package/dist/theme/drawer.d.ts +110 -0
  188. package/dist/theme/drawer.js +100 -0
  189. package/dist/theme/dropdown-menu.d.ts +0 -9
  190. package/dist/theme/file-upload.d.ts +0 -3
  191. package/dist/theme/form.d.ts +12 -12
  192. package/dist/theme/index.d.ts +26 -0
  193. package/dist/theme/kbd.d.ts +71 -0
  194. package/dist/theme/kbd.js +29 -0
  195. package/dist/theme/line-chart.d.ts +0 -9
  196. package/dist/theme/menubar.d.ts +29 -0
  197. package/dist/theme/menubar.js +11 -0
  198. package/dist/theme/navigation-menu.d.ts +161 -0
  199. package/dist/theme/navigation-menu.js +43 -0
  200. package/dist/theme/number-input.d.ts +0 -3
  201. package/dist/theme/page-header.d.ts +47 -0
  202. package/dist/theme/page-header.js +17 -0
  203. package/dist/theme/pagination.d.ts +0 -3
  204. package/dist/theme/pin-input.d.ts +86 -0
  205. package/dist/theme/pin-input.js +32 -0
  206. package/dist/theme/popover.d.ts +119 -0
  207. package/dist/theme/popover.js +31 -0
  208. package/dist/theme/presets.d.ts +0 -2
  209. package/dist/theme/progress.d.ts +93 -0
  210. package/dist/theme/progress.js +37 -7
  211. package/dist/theme/radio-group.d.ts +24 -0
  212. package/dist/theme/radio-group.js +23 -14
  213. package/dist/theme/scroll-area.d.ts +65 -0
  214. package/dist/theme/scroll-area.js +57 -0
  215. package/dist/theme/scroll-fade.d.ts +35 -0
  216. package/dist/theme/scroll-fade.js +12 -0
  217. package/dist/theme/sidebar.d.ts +167 -0
  218. package/dist/theme/sidebar.js +43 -0
  219. package/dist/theme/signature-pad.d.ts +50 -0
  220. package/dist/theme/signature-pad.js +17 -0
  221. package/dist/theme/slider.d.ts +128 -0
  222. package/dist/theme/slider.js +49 -0
  223. package/dist/theme/sparkline.d.ts +0 -6
  224. package/dist/theme/splitter.d.ts +59 -0
  225. package/dist/theme/splitter.js +25 -0
  226. package/dist/theme/stat.d.ts +0 -3
  227. package/dist/theme/stepper.js +2 -2
  228. package/dist/theme/switch.d.ts +53 -29
  229. package/dist/theme/switch.js +33 -8
  230. package/dist/theme/table.d.ts +51 -3
  231. package/dist/theme/table.js +8 -3
  232. package/dist/theme/tabs.d.ts +0 -3
  233. package/dist/theme/tags-input.d.ts +110 -0
  234. package/dist/theme/tags-input.js +42 -0
  235. package/dist/theme/time-field.d.ts +68 -0
  236. package/dist/theme/time-field.js +21 -0
  237. package/dist/theme/timeline.d.ts +182 -0
  238. package/dist/theme/timeline.js +83 -0
  239. package/dist/theme/toggle.d.ts +145 -0
  240. package/dist/theme/toggle.js +84 -0
  241. package/dist/theme/toolbar.d.ts +56 -0
  242. package/dist/theme/toolbar.js +21 -0
  243. package/dist/theme/tree.d.ts +68 -0
  244. package/dist/theme/tree.js +28 -0
  245. package/package.json +4 -3
  246. package/theme.css +335 -0
  247. package/dist/node_modules/.pnpm/@hugeicons_core-free-icons@4.2.3/node_modules/@hugeicons/core-free-icons/dist/esm/index.js +0 -377
  248. package/dist/node_modules/.pnpm/@hugeicons_vue@1.0.7_vue@3.5.35_typescript@6.0.3_/node_modules/@hugeicons/vue/dist/esm/_virtual/_plugin-vue_export-helper.js +0 -8
  249. package/dist/node_modules/.pnpm/@hugeicons_vue@1.0.7_vue@3.5.35_typescript@6.0.3_/node_modules/@hugeicons/vue/dist/esm/components/HugeiconsIcon.vue.js +0 -25
  250. package/dist/node_modules/.pnpm/@hugeicons_vue@1.0.7_vue@3.5.35_typescript@6.0.3_/node_modules/@hugeicons/vue/dist/esm/components/HugeiconsIcon.vue2.js +0 -63
  251. package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/CalendarDate.js +0 -122
  252. package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/DateFormatter.js +0 -84
  253. package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/calendars/GregorianCalendar.js +0 -91
  254. package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/conversion.js +0 -107
  255. package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/manipulation.js +0 -159
  256. package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/queries.js +0 -29
  257. package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/string.js +0 -37
  258. package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/utils.js +0 -6
package/README.md CHANGED
@@ -1,21 +1,27 @@
1
1
  # iryx-ui
2
2
 
3
- A Vue 3 component library built on [Reka UI](https://reka-ui.com) and [Tailwind CSS v4](https://tailwindcss.com). Styled by default, headless when you need it, with a Vue plugin and a Nuxt module in one package.
3
+ A Vue 3 component library built on [Reka UI](https://reka-ui.com) and [Tailwind CSS v4](https://tailwindcss.com). Accessible by default, yours to restyle.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/iryx-ui.svg)](https://www.npmjs.com/package/iryx-ui)
6
6
  [![license](https://img.shields.io/npm/l/iryx-ui.svg)](https://github.com/therok1/iryx-ui/blob/main/LICENSE)
7
7
 
8
+ **[Documentation →](https://iryx-ui.com)**
9
+
10
+ Every component arrives with keyboard navigation, focus management and ARIA already working, then hands you a `class`, a per-slot `ui`, or `unstyled` when the defaults are not what you want.
11
+
8
12
  ## Installation
9
13
 
10
14
  ```bash
11
15
  pnpm add iryx-ui
12
16
  ```
13
17
 
14
- ### Vue 3 (Vite)
18
+ `npm install`, `yarn add` and `bun add` work the same — it is one package on npm.
19
+
20
+ ### Vue 3
15
21
 
16
22
  ```ts
17
- import { IryxUi } from 'iryx-ui'
18
23
  // main.ts
24
+ import { IryxUi } from 'iryx-ui'
19
25
  import { createApp } from 'vue'
20
26
  import App from './App.vue'
21
27
 
@@ -43,1259 +49,53 @@ export default defineNuxtConfig({
43
49
  @import "iryx-ui/theme.css";
44
50
  ```
45
51
 
46
- Components are auto-imported with the `I` prefix (configurable via the `iryxUi.prefix` option).
52
+ Components are auto-imported with the `I` prefix.
47
53
 
48
54
  ## Usage
49
55
 
50
56
  ```vue
51
57
  <template>
52
- <IButton variant="outline" size="lg">
53
- Click me
54
- </IButton>
55
- <ISwitch v-model="enabled" />
56
- </template>
57
- ```
58
-
59
- ### Icons
60
-
61
- Put an icon in the default slot, leading or trailing. Icons are sized to match the button, and any SVG icon set works. Iryx's own icons come from [Hugeicons](https://hugeicons.com), which ships icons as data rather than components:
62
-
63
- ```vue
64
- <script setup lang="ts">
65
- import { ArrowRight01Icon, Search01Icon } from '@hugeicons/core-free-icons'
66
- import { HugeiconsIcon } from '@hugeicons/vue'
67
- </script>
68
-
69
- <template>
70
- <IButton>
71
- <HugeiconsIcon :icon="Search01Icon" data-icon="inline-start" /> Search
72
- </IButton>
73
- <IButton variant="outline">
74
- Next <HugeiconsIcon :icon="ArrowRight01Icon" data-icon="inline-end" />
75
- </IButton>
76
- <IButton square aria-label="Search">
77
- <HugeiconsIcon :icon="Search01Icon" />
78
- </IButton>
79
- </template>
80
- ```
81
-
82
- Mark the icon with `data-icon="inline-start"` or `"inline-end"` and the padding tightens on the side it sits on. For an icon with no label, add `square`.
83
-
84
- Props that take an icon — `IAlert`'s `icon`, and the `icon` on `IDropdownMenu`, `IBreadcrumb`, `ITabs` and `IEmptyState` items — accept **either** a Hugeicons icon or any component that renders an SVG, so an icon set like Lucide still works:
85
-
86
- ```ts
87
- import { Alert02Icon } from '@hugeicons/core-free-icons'
88
- import { Bell } from 'lucide-vue-next'
89
-
90
- // Both are valid.
91
- const a = { label: 'Alerts', icon: Alert02Icon }
92
- const b = { label: 'Alerts', icon: Bell }
93
- ```
94
-
95
- The marker is needed because a label is a bare text node: CSS's `:first-child` and `:last-child` count element children, so an icon beside text matches both. An unmarked icon still renders, it just keeps the full padding.
96
-
97
- `IBadge` follows the same convention. When `loading` is set on a button, a spinner takes the leading position automatically.
98
-
99
- ## The `IApp` wrapper
100
-
101
- Wrap your app once to configure everything below it. Unlike the plugin options
102
- (which are read at install time), `IApp`'s props are **reactive** — change them
103
- and every component updates.
104
-
105
- ```vue
106
- <template>
107
- <IApp theme="rose" appearance="system" dir="ltr">
108
- <RouterView />
109
- </IApp>
110
- </template>
111
- ```
112
-
113
- It renders **no wrapper element** by default. Pass `as` (plus `class`) if you'd
114
- rather it own your page shell:
115
-
116
- ```vue
117
- <IApp as="div" class="min-h-screen bg-background text-foreground">
118
- <RouterView />
119
- </IApp>
120
- ```
121
-
122
- | Prop | What it does |
123
- | --- | --- |
124
- | `theme` | Applies a preset or custom theme, reactively. Removing it restores the defaults. |
125
- | `appearance` | Startup `light` / `dark` / `system`. A stored user preference wins. Omit it and `IApp` won't touch dark mode at all. |
126
- | `unstyled` | Strips built-in classes from every descendant. |
127
- | `dir` / `locale` / `scrollBody` / `nonce` | Forwarded to Reka UI's `ConfigProvider`, so RTL and locale-aware primitives work. |
128
-
129
- Per-component props still win over the app config, so `<IButton :unstyled="false">`
130
- stays styled inside an `<IApp unstyled>`.
131
-
132
- `IApp` is optional — the plugin options and `applyTheme()` / `useAppearance()`
133
- still work on their own.
134
-
135
- ## Appearance (light / dark)
136
-
137
- Dark mode is class-based: the `.dark` class on `<html>` flips every token.
138
- The `useAppearance()` composable manages it for you — it persists the choice
139
- and follows the OS preference in `system` mode:
140
-
141
- ```vue
142
- <script setup>
143
- import { useAppearance } from 'iryx-ui'
144
-
145
- const { appearance, isDark, setAppearance, toggleAppearance } = useAppearance()
146
- </script>
147
-
148
- <template>
149
- <IButton variant="ghost" @click="toggleAppearance()">
150
- {{ isDark ? '🌙' : '☀️' }}
151
- </IButton>
152
- </template>
153
- ```
154
-
155
- You can set the startup default via the plugin or Nuxt module (a stored user
156
- preference always wins):
157
-
158
- ```ts
159
- app.use(createIryxUi({ appearance: 'system' }))
160
- // nuxt.config.ts → iryxUi: { appearance: 'system' }
161
- ```
162
-
163
- `theme.css` also registers the class-based `dark:` variant for your own
164
- utilities (shadcn-style `@custom-variant dark`).
165
-
166
- ## Theming
167
-
168
- Pick a built-in color preset — `violet` (default) or `rose` — at startup or at
169
- runtime:
170
-
171
- ```ts
172
- import { applyTheme } from 'iryx-ui'
173
-
174
- app.use(createIryxUi({ theme: 'rose' }))
175
- // nuxt.config.ts → iryxUi: { theme: 'rose' }
176
-
177
- applyTheme('rose') // runtime, e.g. from a theme picker
178
- ```
179
-
180
- Or bring your own theme — every token can differ between light and dark:
181
-
182
- ```ts
183
- applyTheme({
184
- light: { primary: 'oklch(0.55 0.2 250)', primaryForeground: 'white' },
185
- dark: { primary: 'oklch(0.68 0.17 250)', primaryForeground: 'oklch(0.15 0.04 250)' },
186
- })
187
- ```
188
-
189
- For a static re-brand, plain CSS works too — tokens are just variables:
190
-
191
- ```css
192
- :root {
193
- --iryx-primary: oklch(0.65 0.2 145); /* make it green */
194
- }
195
- .dark {
196
- --iryx-primary: oklch(0.75 0.18 145);
197
- }
198
- ```
199
-
200
- ### Typeface
201
-
202
- Set `--iryx-font-sans` and everything follows, including Tailwind's
203
- `font-sans`. Load the family however you like — a package, a `<link>`, or
204
- self-hosted:
205
-
206
- ```css
207
- @import "@fontsource-variable/instrument-sans";
208
-
209
- :root {
210
- --iryx-font-sans: "Instrument Sans", ui-sans-serif, system-ui, sans-serif;
211
- }
212
- ```
213
-
214
- Leave it unset to use the system stack. There is no `.dark` counterpart for
215
- this one.
216
-
217
- Available tokens, each usable as a Tailwind color (`bg-primary`,
218
- `text-muted-foreground`, …):
219
-
220
- | Group | Tokens |
221
- | --- | --- |
222
- | Surfaces | `background`, `foreground`, `accent`, `accent-foreground`, `muted`, `muted-foreground`, `border`, `input` |
223
- | Brand | `primary`, `primary-foreground`, `primary-from`, `primary-to` |
224
- | Status | `success`, `warning`, `danger`, `info` — each with `-foreground`, `-muted`, `-muted-foreground` and `-border` |
225
-
226
- `input` is the fill behind the fields — `IInput`, `ITextarea`, `INumberInput`,
227
- the `ISelect` trigger, the `ICombobox` anchor — and behind `IButton`'s
228
- `outline` variant, so the two read as the same kind of surface. It's a token
229
- of its own rather than a reused `muted`, so how raised a control looks can be
230
- tuned without moving every muted surface with it. It matches the page
231
- background in light mode and lifts off it in dark, where the difference is
232
- legible; that split lives in the token values, so no component needs a `dark:`
233
- class for it.
234
-
235
- `primary-from` / `primary-to` are the stops of the solid button's vertical
236
- gradient. The status tokens carry their own dark-mode values, so components
237
- never need a `dark:` class for them: `success` is the solid fill,
238
- `success-foreground` the text on it, `success-muted` a tinted surface,
239
- `success-muted-foreground` the text on that, and `success-border` the edge.
240
-
241
- The built-in presets swap only the brand colors — red should mean danger in
242
- every theme — but `applyTheme()` accepts the status tokens too:
243
-
244
- ```ts
245
- applyTheme({
246
- light: { success: 'oklch(0.6 0.15 150)', successMuted: 'oklch(0.97 0.02 150)' },
247
- dark: { success: 'oklch(0.7 0.15 150)', successMuted: 'oklch(0.26 0.05 150)' },
248
- })
249
- ```
250
-
251
- Tweak a single instance with `class` (conflicts are merged smartly) or per-slot with `ui`:
252
-
253
- ```vue
254
- <IButton class="rounded-full">
255
- Pill button
256
- </IButton>
257
-
258
- <ISwitch :ui="{ thumb: 'bg-zinc-900' }" />
259
- ```
260
-
261
- Or drop all built-in styles and take over completely:
262
-
263
- ```vue
264
- <IButton unstyled class="my-own-button">
265
- Headless
266
- </IButton>
267
- ```
268
-
269
- ```ts
270
- // …or globally:
271
- app.use(createIryxUi({ unstyled: true }))
272
- ```
273
-
274
- ## Components
275
-
276
- **Layout & structure**
277
-
278
- | Component | Description |
279
- | --- | --- |
280
- | `IApp` | Root wrapper — reactive global config, theme, appearance, RTL/locale |
281
- | `ICard` | Panel with `outline`/`soft` variants, four paddings, header and footer slots |
282
- | `ISeparator` | Horizontal or vertical rule, optionally with a centred label |
283
-
284
- **Forms**
285
-
286
- | Component | Description |
287
- | --- | --- |
288
- | `IForm` | Validating form wrapper — any Standard Schema validator, or your own function |
289
- | `IFormField` | Label, description, hint, help and error text around a control |
290
- | `IInput` | Text field with `sm`/`md`/`lg` sizes, `invalid` state, `v-model`, `leading`/`trailing` slots, `clearable`, `loading`, `debounce` |
291
- | `INumberInput` | Decimal-safe numeric field — the model is a **string**, with `min`/`max`/`step`, `precision` and locale-aware display |
292
- | `ISparkline` | Tiny inline trend chart — pure SVG, no charting dependency |
293
- | `IBarChart` | Categorical bar chart with a round-number axis, hover tooltip and a table view |
294
- | `ILineChart` | Line or area chart with a crosshair, hover marker and a table view |
295
- | `IFileUpload` | Drag-and-drop file field with `accept` / `maxSize` / `maxFiles`, thumbnails and a remove action |
296
- | `IDatePicker` | Calendar in a popover; the model is an ISO `YYYY-MM-DD` **string** |
297
- | `IDateRangePicker` | Two-month range calendar; the model is `{ start, end }` ISO strings |
298
- | `IPasswordInput` | Masked field with a show/hide toggle and an optional strength meter |
299
- | `ITextarea` | Multi-line field with matching sizes, `invalid` state and optional `autosize` |
300
- | `ILabel` | Field label with optional `required` asterisk |
301
- | `ICheckbox` | Tri-state checkbox (`true` / `false` / `'indeterminate'`), optional `label` + `description` |
302
- | `ISelect` | Listbox with keyboard nav and typeahead, driven by an `items` array, with optional groups |
303
- | `ICombobox` | Searchable select — filters as you type, with optional groups, virtualized rows and a "create from query" row |
304
- | `IRadioGroup` | Radio list with labels wired up automatically; items take a `description` |
305
- | `ISwitch` | Accessible toggle, optional `label` + `description` |
306
-
307
- **Actions**
308
-
309
- | Component | Description |
310
- | --- | --- |
311
- | `IButton` | Variants (`solid`, `outline`, `ghost`, `link`), five sizes, `loading`, `block` and `square` states, polymorphic via `as` / `asChild` |
312
- | `IButtonGroup` | Joins any children into a segmented control — split buttons, toolbars, pagers |
313
- | `IDropdownMenu` | Menu driven by an `items` array, with separators, group labels, danger items and nested submenus |
314
-
315
- **Feedback**
316
-
317
- | Component | Description |
318
- | --- | --- |
319
- | `IAlert` | Inline, contextual message in four variants — variant icon, `actions` slot, optional dismiss |
320
- | `IBanner` | Page-level announcement — full-bleed, six variants, sticky top or fixed bottom |
321
- | `IBadge` | Status pill — five variants × three sizes; `dot` moves the colour onto a leading dot |
322
- | `IToaster` | Host for `useToast()`; six viewport positions, stacking, action buttons |
323
- | `IDialog` | Modal with header/body/footer slots, `dismissible` and `showClose` |
324
- | `IConfirmDialog` | Host for `useConfirm()` — renders the promise-based confirmation |
325
- | `IProgress` | Determinate or `indeterminate` bar, five variants, `formatValue` |
326
- | `ISkeleton` | Loading placeholder — `text`/`rect`/`circle`, stackable with `lines` |
327
- | `IEmptyState` | Icon, title, description and an `actions` slot for empty lists |
328
- | `ITooltip` | Hover/focus tooltip with side, align, delay and optional arrow |
329
-
330
- **Navigation & data display**
331
-
332
- | Component | Description |
333
- | --- | --- |
334
- | `ITabs` | `solid` or `line` variants with an animated indicator, horizontal or vertical |
335
- | `IBreadcrumb` | Trail from an `items` array; the last crumb is marked as the current page |
336
- | `IPagination` | Page list with ellipsis, edge pages and prev/next controls; `align` places it, `size` sets the button scale |
337
- | `IStepper` | Multi-step progress, horizontal or vertical, optional `linear` ordering |
338
- | `IStat` | KPI tile — label, value, signed delta with trend colour, and a hint |
339
- | `ITable` | Data table — sorting, selection, expansion and per-cell slots, client- or server-driven |
340
-
341
- Every component supports `unstyled` and a `class` override; multi-part ones take a `ui` prop for per-slot classes.
342
-
343
- ### Button groups
344
-
345
- `IButtonGroup` joins whatever you put inside it — buttons, a menu trigger, a link — squaring the inner edges and collapsing the shared borders. Set `size` once on the group and the buttons inherit it:
346
-
347
- ```vue
348
- <template>
349
- <IButtonGroup>
350
- <IButton @click="save">
351
- Save
352
- </IButton>
353
- <IDropdownMenu :items="saveActions" align="end">
354
- <template #trigger>
355
- <IButton square aria-label="More options">
356
- <ChevronDown />
357
- </IButton>
58
+ <IApp appearance="system">
59
+ <ICard>
60
+ <IFormField label="Email" description="We'll never share it.">
61
+ <IInput v-model="email" type="email" placeholder="you@example.com" />
62
+ </IFormField>
63
+
64
+ <template #footer>
65
+ <IButton @click="save">Save</IButton>
358
66
  </template>
359
- </IDropdownMenu>
360
- </IButtonGroup>
361
- </template>
362
- ```
363
-
364
- ### Menus
365
-
366
- `IDropdownMenu` takes entries as data. A `'-'` is a separator, an entry without `onSelect` is a group label, and one with its own `items` opens a submenu — to any depth:
367
-
368
- ```ts
369
- const items = [
370
- { label: 'Invoice' },
371
- { label: 'Open', icon: Search, onSelect: () => open() },
372
- { label: 'Export as', icon: Download, items: [
373
- { label: 'PDF', onSelect: () => exportPdf() },
374
- { label: 'CSV', onSelect: () => exportCsv() },
375
- ] },
376
- '-',
377
- { label: 'Delete', icon: Trash2, danger: true, onSelect: () => remove() },
378
- ]
379
- ```
380
-
381
- ### Toasts and confirmations
382
-
383
- Both are imperative, so they can be called from anywhere — including plain functions outside a component. Mount each host once, typically just inside `<IApp>`:
384
-
385
- ```vue
386
- <template>
387
- <IApp>
388
- <!-- your app -->
389
- <IConfirmDialog />
390
- <IToaster />
67
+ </ICard>
391
68
  </IApp>
392
69
  </template>
393
70
  ```
394
71
 
395
- ```ts
396
- const toast = useToast()
397
- toast.success('Saved')
398
- toast.danger({ title: 'Failed to send', description: 'Check the address.' })
399
- toast.toast({ title: 'Note deleted', action: { label: 'Undo', onClick: restore } })
400
-
401
- const { confirm } = useConfirm()
402
- if (await confirm({ title: 'Delete this draft?', danger: true }))
403
- await remove()
404
- ```
405
-
406
- `confirm()` resolves `true` on confirmation and `false` on cancel or dismissal.
407
-
408
- ### Internationalisation
409
-
410
- No English string is baked in without an escape hatch. `IAlert`, `IDialog` and `IToaster` take a `closeLabel`, `IPagination` takes `prevLabel` / `nextLabel` / `label`, `IBreadcrumb` and `ISkeleton` take a `label`, and `IProgress` and `IStat` take `formatValue` / `formatDelta` for locale-aware numbers.
411
-
412
- ### Forms
413
-
414
- ```vue
415
- <script setup lang="ts">
416
- import { ref } from 'vue'
417
-
418
- const email = ref('')
419
- const plan = ref('pro')
420
- const framework = ref('vue')
421
- </script>
422
-
423
- <template>
424
- <ILabel for="email" required>
425
- Email
426
- </ILabel>
427
- <IInput id="email" v-model="email" type="email" placeholder="you@example.com" />
428
-
429
- <ILabel class="gap-2">
430
- <ICheckbox v-model="accepted" /> Accept terms
431
- </ILabel>
432
-
433
- <ISelect
434
- v-model="framework"
435
- placeholder="Pick one"
436
- :items="['Vue', 'React', { label: 'Angular', value: 'ng', disabled: true }]"
437
- />
438
-
439
- <IRadioGroup v-model="plan" :items="['free', 'pro']" />
440
- </template>
441
- ```
442
-
443
- #### Input affixes, clearing, loading and debounce
444
-
445
- `IInput` renders its chrome on a wrapper element, so `leading` and `trailing`
446
- slot content sits *inside* the field and takes real space — a long value is
447
- truncated by the affix rather than sliding underneath it.
448
-
449
- ```vue
450
- <IInput v-model="search" clearable placeholder="Search invoices…">
451
- <template #leading>
452
- <HugeiconsIcon :icon="Search01Icon" />
453
- </template>
454
- </IInput>
455
-
456
- <IInput v-model="team" placeholder="your-team">
457
- <template #trailing>
458
- <span class="text-sm">.example.com</span>
459
- </template>
460
- </IInput>
461
-
462
- <!-- Spinner in the trailing area; the field stays editable. -->
463
- <IInput v-model="slug" :loading="checking" />
464
-
465
- <!-- The model updates 500ms after the last keystroke. -->
466
- <IInput v-model="query" :debounce="500" clearable />
467
- ```
468
-
469
- | Prop | Effect |
470
- | --- | --- |
471
- | `clearable` | Clear button in the trailing area whenever the field is non-empty |
472
- | `loading` | Spinner in the trailing area. Does **not** disable the field |
473
- | `debounce` | Milliseconds to wait after the last keystroke before the model updates. `0` (default) updates on every keystroke |
474
- | `clearLabel` | Accessible name for the clear button, for non-English apps |
475
-
476
- The displayed text always updates on the keystroke — only the model lags. Blur
477
- and Enter flush a pending update immediately, so a submit never reads a stale
478
- value, and an external write (a reset or prefill) cancels whatever is queued.
479
-
480
- `class` lands on the wrapper, since that is the element carrying the field
481
- chrome; use `ui` to reach the parts (`root`, `input`, `leading`, `trailing`,
482
- `clear`). Stray attributes like `name`, `autocomplete` and `maxlength` are
483
- forwarded to the `<input>` itself. `ref` exposes the element as `.input` for
484
- focus management.
485
-
486
- #### Multiple series
487
-
488
- Both charts take plain rows plus a `series` descriptor — the same shape
489
- `ITable` uses. Omit `series` for the single-measure case.
72
+ ## Three ways to take the styling back
490
73
 
491
74
  ```vue
492
- <IBarChart
493
- :data="[
494
- { label: 'Jan', revenue: 4200, expenses: 3100 },
495
- { label: 'Feb', revenue: 5600, expenses: 3400 },
496
- ]"
497
- :series="[
498
- { key: 'revenue', name: 'Revenue', slot: 0 },
499
- { key: 'expenses', name: 'Expenses', slot: 1 },
500
- ]"
501
- label="Cashflow by month"
502
- />
503
- ```
504
-
505
- Bars group inside their category; lines draw one path each. One hover reports
506
- **every** series for that category in a single tooltip, so the reader compares
507
- in one place instead of chasing marks.
508
-
509
- **`slot` pins a series to a palette colour.** Without it, colour follows array
510
- position — so filtering a series out repaints the survivors and the reader has
511
- to relearn the chart. Pin the slots whenever series can be toggled.
512
-
513
- **The legend is mandatory from two series up.** `legend: false` only silences
514
- the single-series case, where the title already names what is plotted. Colour
515
- alone is never a dependable identity channel, so this is not configurable.
516
-
517
- Past eight series the colours stop identifying anything; the chart warns in
518
- development and you should fold the tail into "Other" or switch to small
519
- multiples.
75
+ <!-- merge with the built-in classes -->
76
+ <IButton class="rounded-full" />
520
77
 
521
- `variant="area"` is ignored for multiple series — overlapping washes muddy into
522
- a colour that belongs to neither.
78
+ <!-- reach a single slot -->
79
+ <ISelect :ui="{ content: 'w-72' }" />
523
80
 
524
- #### Annotations, and why there is no plugin API
525
-
526
- Chart.js has plugins because canvas is opaque — once painted you cannot select
527
- or style anything, so the only way in is an imperative draw hook. SVG has no
528
- such problem, so these charts hand you the layout and let you write ordinary
529
- markup into it:
530
-
531
- ```vue
532
- <ILineChart :data="revenue" label="Revenue against target">
533
- <template #overlay="{ plot, value }">
534
- <line
535
- :x1="plot.left" :y1="value(7000)"
536
- :x2="plot.left + plot.width" :y2="value(7000)"
537
- stroke="var(--iryx-warning)" stroke-width="2" stroke-dasharray="4 4"
538
- />
539
- </template>
540
- </ILineChart>
81
+ <!-- keep the primitive, drop the paint -->
82
+ <IDialog unstyled />
541
83
  ```
542
84
 
543
- `#underlay` renders behind the marks — target bands, shaded regions. `#overlay`
544
- renders in front — reference lines, callouts. Both sit below the hit targets,
545
- so hovering keeps working through whatever you draw.
546
-
547
- Both receive the `CartesianLayout`:
548
-
549
- | Prop | What it gives you |
550
- | --- | --- |
551
- | `plot` | `{ left, top, width, height }` of the plot rectangle, in px |
552
- | `value(n)` | A data value to its pixel on the value axis |
553
- | `bandCentre(i)` | The centre of category `i`, in px |
554
- | `bandWidth` | Size of one category slot |
555
- | `ticks` | The axis values actually drawn |
556
- | `orientation` | `'vertical'` or `'horizontal'` |
557
-
558
- That's strictly more capable than a draw hook: it's declarative, reactive, and
559
- type-checked, with no lifecycle or registration order to learn.
85
+ ## Requirements
560
86
 
561
- For a chart type that doesn't exist here, the same primitives are exported —
562
- `cartesianLayout`, `linearScale`, `niceTicks`, `seriesColor` — so you can build
563
- one on the same spine rather than starting over.
564
-
565
- #### Chart colours
566
-
567
- Eight categorical slots, `--iryx-chart-1` … `--iryx-chart-8`, usable as Tailwind
568
- colours (`text-chart-3`, `fill-chart-5`). They encode **identity** — which
569
- series a mark belongs to — never magnitude.
570
-
571
- ```vue
572
- <!-- One series per slot, assigned in order. -->
573
- <ISparkline :data="revenue" class="text-chart-1" />
574
-
575
- <ISparkline :data="expenses" class="text-chart-2" />
576
- ```
87
+ - Vue 3.5 or newer
88
+ - Tailwind CSS v4
577
89
 
578
- Three rules, and they are not stylistic:
579
-
580
- - **Assign in order, never cycle.** A ninth series is not a generated ninth
581
- hue — fold it into "Other", or switch to small multiples. A generated colour
582
- hasn't been checked for separation against its neighbours.
583
- - **Status colours are never series colours.** A series that happens to land in
584
- slot 4 must not read as a warning. `success`/`warning`/`danger`/`info` stay
585
- reserved.
586
- - **Colour follows the entity, not its rank.** If a filter removes a series,
587
- the survivors keep their slots rather than shifting up.
588
-
589
- Tailwind scans source text, so a class name assembled at runtime is never
590
- generated — write the slots out, or reach for the variable:
591
-
592
- ```vue
593
- <!-- Silently unstyled: Tailwind never sees this string -->
594
- <ISparkline :class="`text-chart-${index + 1}`" />
595
-
596
- <!-- Either of these works -->
597
- <ISparkline :class="['text-chart-1', 'text-chart-2'][index]" />
598
-
599
- <ISparkline :style="{ color: `var(--iryx-chart-${index + 1})` }" />
600
- ```
601
-
602
- The steps are not eyeballed. Each clears a lightness band, a chroma floor, and
603
- protanopia/deuteranopia separation against its own surface, checked with a
604
- validator rather than by eye. **Dark has its own steps**, validated against the
605
- dark background — not an automatic flip of the light ones.
606
-
607
- Two caps worth knowing before you design around them:
608
-
609
- | Chart form | Max series |
610
- | --- | --- |
611
- | Bars, lines, stacks — only neighbours touch | **8** |
612
- | Scatter, bubble, small multiples — any two marks can sit side by side | **3** |
613
-
614
- Past those, the answer is fewer series or facets, not more colours. If you
615
- re-step any slot, re-run the validator for **both** modes.
616
-
617
- #### Line charts
618
-
619
- ```vue
620
- <ILineChart
621
- :data="[
622
- { label: 'Jan', value: 4200 },
623
- { label: 'Feb', value: null },
624
- { label: 'Mar', value: 5600 },
625
- ]"
626
- variant="area"
627
- label="Revenue by month"
628
- />
629
- ```
630
-
631
- Same `data`, `height`, `ticks`, `axis`, `locale`, `format` and `label` props as
632
- `IBarChart`, plus:
633
-
634
- | Prop | Effect |
635
- | --- | --- |
636
- | `variant` | `line` (default) or `area`, which adds a wash beneath the line |
637
- | `zero` | Force zero onto the axis. **Off by default** |
638
-
639
- **`zero` is off here and always on for bars, deliberately.** A bar is read by
640
- length, so a truncated baseline lies about the comparison. A line is read by
641
- its *shape*, and a series hovering around 8,000 flattens into a straight edge
642
- once the axis starts at nothing. Turn it on when the distance from zero is the
643
- point.
644
-
645
- `null` breaks the line rather than bridging it, so a missing reading never
646
- draws a slope that didn't happen.
647
-
648
- Hovering shows a crosshair and a single ringed marker on the reading under the
649
- cursor — not a dot on every point, which is noise the axis and tooltip already
650
- cover.
651
-
652
- #### Bar charts
653
-
654
- ```vue
655
- <IBarChart
656
- :data="[
657
- { label: 'Jan', value: 4200 },
658
- { label: 'Feb', value: 5600 },
659
- { label: 'Mar', value: null },
660
- ]"
661
- label="Revenue by month"
662
- locale="de-DE"
663
- :format="{ style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }"
664
- />
665
- ```
90
+ Ships as ESM with full type definitions. Every component renders on the server.
666
91
 
667
- | Prop | Effect |
668
- | --- | --- |
669
- | `data` | `{ label, value }[]`. `value: null` is a **missing reading** — no bar, which is not a zero |
670
- | `height` | Rendered height in px (default 240). Width fills the container |
671
- | `ticks` | Target tick count. A hint — the axis lands on round numbers first |
672
- | `axis` | Set `false` to drop the value axis and gridlines |
673
- | `locale` / `format` | `Intl.NumberFormat` settings, applied to ticks and tooltip alike |
674
- | `label` | Accessible name for the figure |
92
+ ## Documentation
675
93
 
676
- **The axis picks the domain, not the data.** Values are snapped outwards to a
677
- 1/2/5 step, so an axis reads `0 / 2,000 / 4,000` rather than `0 / 1,726.8`.
678
- Zero is always included, because bars are compared by length and a truncated
679
- baseline makes that comparison a lie.
94
+ Every component and composable has its own page, with the props in full and demos you can operate:
680
95
 
681
- Bars are capped at 24px and never fill their slot — the gap between them is
682
- what separates them. They're rounded at the data end and square at the
683
- baseline, so the rounding reads as the tip of the value.
684
-
685
- Hovering a bar dims the rest and shows a tooltip; hit targets span the full
686
- band and plot height, so a short bar is no harder to hit than a tall one.
687
-
688
- **Accessibility:** the SVG is `aria-hidden` and the data is exposed as a
689
- visually-hidden table instead, so a screen reader gets the actual numbers
690
- rather than a blank graphic. That table renders even before the container has
691
- been measured — the data is never gated behind layout.
692
-
693
- **`stacked`** turns grouped series into one bar per category:
694
-
695
- ```vue
696
- <IBarChart :data="cashflow" :series="series" stacked label="Cashflow" />
697
- ```
698
-
699
- Stacking answers *"what makes up the total"*; grouping answers *"how do these
700
- compare"*. Only the bottom segment shares a baseline, so segments above it are
701
- hard to compare across categories — stay with grouped bars when the comparison
702
- matters more than the total.
703
-
704
- The axis is sized against the running totals, only the outermost segment is
705
- rounded, and the tooltip adds a **Total** row (`totalLabel` to rename it).
706
- Negative values stack downward from zero rather than cancelling positives out,
707
- so a mixed stack shows both sides at full length. Stacking is ignored for a
708
- single series, and works horizontally too.
709
-
710
- **`orientation="horizontal"`** runs the categories down the side:
711
-
712
- ```vue
713
- <IBarChart :data="spend" orientation="horizontal" label="Spend by category" />
714
- ```
715
-
716
- Vertical charts thin colliding labels to every *n*th, which is fine for `Jan`
717
- / `Feb` and lossy for `Travel and accommodation`. Turn the chart and the names
718
- get real width, so nothing is dropped — that's the case horizontal is for.
719
- Everything else behaves the same: grouped series, the tooltip, the round-number
720
- axis anchored at zero.
721
-
722
- #### Sparklines
723
-
724
- A trend at a glance, for a stat tile or a table cell. Plain SVG — no charting
725
- library, no canvas, nothing to install.
726
-
727
- ```vue
728
- <ISparkline
729
- :data="[4200, 4600, 4100, 5200, 5800, 6300]"
730
- variant="area"
731
- end-dot
732
- label="Revenue over six months, trending up"
733
- />
734
- ```
735
-
736
- Because it is SVG, colour comes from `currentColor`: recolour it with a text
737
- utility, and it follows your theme preset and light/dark automatically with no
738
- JavaScript. A canvas chart cannot read CSS variables, so it would need a
739
- re-render on every theme change.
740
-
741
- | Prop | Effect |
742
- | --- | --- |
743
- | `data` | Values, oldest first. `null` is a **gap**, not a zero — the line breaks |
744
- | `variant` | `line` (default) or `area`, which adds a wash beneath the line |
745
- | `endDot` | Marks the most recent point |
746
- | `baseline` | Lower edge of the `area` wash: `min` (default) or `zero` |
747
- | `min` / `max` | Pin the domain — set both to put several sparklines on one scale |
748
- | `muted` | Draw in muted ink, for a de-emphasised trend |
749
- | `height` | Rendered height in px (default 32). Width always fills the container |
750
-
751
- Width is fluid and the stroke never distorts: the drawing stretches via
752
- `preserveAspectRatio="none"`, while every stroke carries
753
- `vector-effect="non-scaling-stroke"`, so a 2px line stays 2px and the end dot
754
- stays circular at any aspect ratio.
755
-
756
- `label` sets an accessible description. **Without one the sparkline is hidden
757
- from assistive tech as decorative** — which is correct when it sits beside a
758
- value that already states the number, and wrong if it is the only thing
759
- carrying the information.
760
-
761
- Edge cases behave: an empty series draws nothing, a flat series draws through
762
- the middle rather than collapsing to an edge, and a single reading is a dot.
763
-
764
- The scale helpers are exported for building your own marks:
765
-
766
- ```ts
767
- import { extent, linearScale } from 'iryx-ui'
768
-
769
- extent([3, null, 9, 1]) // [1, 9] — gaps ignored
770
- linearScale([0, 10], [100, 0])(10) // 0 — ranges may be inverted for SVG's y-axis
771
- ```
772
-
773
- #### Files
774
-
775
- ```vue
776
- <script setup lang="ts">
777
- import { ref } from 'vue'
778
-
779
- const logo = ref<File[]>([])
780
- </script>
781
-
782
- <template>
783
- <IFileUpload
784
- v-model="logo"
785
- accept="image/*"
786
- :max-size="2 * 1024 * 1024"
787
- label="Drag your logo here"
788
- browse-label="Browse images"
789
- hint="PNG, JPG or SVG up to 2 MB"
790
- @reject="onReject"
791
- />
792
- </template>
793
- ```
794
-
795
- The model is **always a `File[]`**, even without `multiple` — a `File | File[]`
796
- union would make every caller narrow the type before touching it, and the
797
- single case is just an array holding at most one. Without `multiple`, picking
798
- again replaces rather than appends.
799
-
800
- | Prop | Effect |
801
- | --- | --- |
802
- | `multiple` | Accept more than one file |
803
- | `accept` | Native syntax: `image/*`, `.pdf`, `image/png` |
804
- | `maxSize` | Largest accepted size, in bytes |
805
- | `maxFiles` | Cap on how many files may be held at once |
806
- | `label` | Prompt in the zone, above the hint |
807
- | `browseLabel` | Text on the browse button |
808
- | `hint` | Fine print under the prompt — the accepted types belong here |
809
-
810
- Every string is a prop, including `removeLabel` and the three rejection
811
- messages, so nothing bakes in English.
812
-
813
- `accept` is enforced in the component as well as on the input, because a
814
- dragged-in file bypasses the native filter entirely. Refused files raise
815
- `@reject` with `{ file, reason }` — `'type'`, `'size'` or `'count'` — so you
816
- can word your own message; the built-in text is available through the
817
- `tooLargeText`, `wrongTypeText` and `tooManyText` props.
818
-
819
- Image files get a thumbnail, anything else a placeholder of the same size so
820
- rows stay aligned. The object URLs behind those thumbnails are revoked as soon
821
- as a file leaves the list or the component unmounts.
822
-
823
- #### Dates
824
-
825
- The model is an ISO `YYYY-MM-DD` **string**, never a `Date`.
826
-
827
- ```vue
828
- <script setup lang="ts">
829
- import { ref } from 'vue'
830
-
831
- const issuedOn = ref<string | null>('2026-08-15')
832
- const period = ref({ start: '2026-08-01', end: '2026-08-31' })
833
- </script>
834
-
835
- <template>
836
- <IDatePicker v-model="issuedOn" clearable />
837
- <IDateRangePicker v-model="period" clearable />
838
- </template>
839
- ```
840
-
841
- A `Date` is a timestamp, so it always carries a time zone. `new Date('2026-08-15')`
842
- parses as UTC midnight, and a user west of Greenwich formatting it locally sees
843
- the 14th — which silently moves a record into the wrong reporting period. A
844
- calendar date has no zone, so it stays the day you picked. Internally the
845
- components use `@internationalized/date`; that never reaches your model, so
846
- formatting the string with `dayjs` or anything else on the way out is fine.
847
-
848
- | Prop | Effect |
849
- | --- | --- |
850
- | `min` / `max` | Selectable bounds, as ISO strings |
851
- | `locale` | Month names, weekday initials, and the trigger's text |
852
- | `format` | `Intl.DateTimeFormatOptions` for the trigger, e.g. `{ dateStyle: 'full' }` |
853
- | `weekStartsOn` | `0` is Sunday. Defaults to the locale's convention |
854
- | `clearable` | Adds a clear action to the footer |
855
- | `months` | Range picker only — months side by side, default `2` |
856
- | `separator` | Range picker only — text between the two dates |
857
-
858
- Navigation and footer labels (`todayLabel`, `clearLabel`, `previousLabel`,
859
- `nextLabel`) are all props, so nothing bakes in English.
860
-
861
- Both render their calendar at a fixed six weeks, so a short month cannot stretch
862
- its rows to match a taller neighbour and the popover does not resize as you page
863
- through it. The range picker draws only the committed range — the days between
864
- the endpoints take a flat tint while the two ends take the solid fill, so a long
865
- span still shows where it begins and ends.
866
-
867
- The helpers behind them are exported, for formatting the same values elsewhere:
868
-
869
- ```ts
870
- import { formatIsoDate, isoToday, toCalendarDate, toIsoDate } from 'iryx-ui'
871
-
872
- formatIsoDate('2026-08-15', 'en-GB', { dateStyle: 'long' }) // '15 August 2026'
873
- formatIsoDate('nonsense') // '' — malformed input is "no selection", not a crash
874
- ```
875
-
876
- #### Passwords
877
-
878
- `IPasswordInput` is `IInput` with a reveal toggle in the trailing area, plus an
879
- optional four-segment strength meter.
880
-
881
- ```vue
882
- <IPasswordInput v-model="password" strength />
883
-
884
- <!-- Toggle only, no meter -->
885
- <IPasswordInput v-model="password" />
886
-
887
- <!-- No toggle either -->
888
- <IPasswordInput v-model="password" :toggle="false" />
889
- ```
890
-
891
- The score counts length (8 and 12 characters), mixed case, a digit and a
892
- symbol, capped at four. It is a deliberately transparent nudge toward better
893
- passwords, **not** a security control — enforce real policy in the `IForm`
894
- validator, where it can actually reject a value.
895
-
896
- Every string is overridable, since components must not bake in English:
897
-
898
- ```vue
899
- <IPasswordInput
900
- v-model="password"
901
- strength
902
- show-label="Afficher le mot de passe"
903
- hide-label="Masquer le mot de passe"
904
- :strength-labels="['Faible', 'Moyen', 'Bon', 'Fort']"
905
- />
906
- ```
907
-
908
- `class` lands on the wrapper that stacks the field above the meter; `ui` reaches
909
- `root`, `input`, `toggle`, `meter`, `track`, `segment` and `label`.
910
-
911
- #### Autosizing textareas
912
-
913
- ```vue
914
- <!-- Grows without limit -->
915
- <ITextarea v-model="note" autosize />
916
-
917
- <!-- Between 2 and 8 rows, then scrolls -->
918
- <ITextarea v-model="note" :autosize="{ min: 2, max: 8 }" />
919
- ```
920
-
921
- `autosize` overrides `rows` and drops the drag handle, since the measured
922
- height is the point. The field shrinks as well as grows, and re-measures when
923
- the model changes from outside — a reset or a prefill resizes correctly.
924
-
925
- `ISelect` and `IRadioGroup` accept plain strings or `{ label, value, disabled }` objects. Both also take a default slot if you'd rather compose the Reka primitives yourself.
926
-
927
- `ISelect` also takes groups — an entry with its own `items` becomes a labelled
928
- heading, the same shape `ICombobox` uses:
929
-
930
- ```vue
931
- <ISelect
932
- v-model="framework"
933
- :items="[
934
- { label: 'Virtual DOM', items: ['Vue', 'React'] },
935
- { label: 'Compiled', items: [{ label: 'Svelte', value: 'svelte' }] },
936
- ]"
937
- />
938
- ```
939
-
940
- ### Searchable selects
941
-
942
- `ICombobox` takes the same `items` as `ISelect` and filters them against what
943
- the user types, which is what you want once a list runs to hundreds of entries.
944
- The field shows the selected option's **label** while the model holds its value.
945
-
946
- ```vue
947
- <ICombobox
948
- v-model="clientId"
949
- :items="clients"
950
- placeholder="Search clients"
951
- empty-text="No clients found."
952
- />
953
- ```
954
-
955
- Set `create` to offer a row for whatever the user typed when nothing matches.
956
- Choosing it emits `create` with the query and does **not** change the model —
957
- the option doesn't exist yet, so you add it and select it yourself:
958
-
959
- ```vue
960
- <ICombobox
961
- v-model="clientId"
962
- :items="clients"
963
- create
964
- :create-label="query => `Add ${query}`"
965
- @create="query => clients.push({ label: query, value: addClient(query) })"
966
- />
967
- ```
968
-
969
- Both `empty-text` and `create-label` are props precisely so a non-English app
970
- never inherits an English string; `empty` and `create` slots take over the
971
- markup entirely if you need more than text.
972
-
973
- An entry with its own `items` becomes a labelled group. A group disappears on
974
- its own once nothing inside it matches:
975
-
976
- ```vue
977
- <ICombobox
978
- v-model="clientId"
979
- :items="[
980
- { label: 'Recent', items: ['Acme Industries', 'Bolt Logistics'] },
981
- { label: 'Archived', items: [{ label: 'Cirrus Systems', value: 'cirrus' }] },
982
- ]"
983
- />
984
- ```
985
-
986
- For lists in the thousands, `virtual` renders only the rows on screen:
987
-
988
- ```vue
989
- <ICombobox v-model="sku" virtual :items="fiveThousandItems" :estimate-size="32" />
990
- ```
991
-
992
- `virtual` and grouped items are **mutually exclusive** — the underlying
993
- virtualizer is a flat window with no notion of group headings, so groups are
994
- flattened and their labels dropped (with a warning in dev). `estimate-size` is
995
- the assumed row height in px, used to size the scrollbar before rows are
996
- measured; set it if you have restyled rows to a different height.
997
-
998
- ### Numbers and money
999
-
1000
- `INumberInput` never turns your value into a `number`. The model is a decimal
1001
- **string**, because binary floating point cannot represent decimal money —
1002
- `0.1 + 0.2` is `0.30000000000000004`, and `10.00` becomes `10`. Values are
1003
- added, compared and rounded with `BigInt` internally, so precision survives
1004
- regardless of magnitude.
1005
-
1006
- ```vue
1007
- <script setup lang="ts">
1008
- import { ref } from 'vue'
1009
-
1010
- // A string, and it stays one.
1011
- const amount = ref('1234.56')
1012
- </script>
1013
-
1014
- <template>
1015
- <INumberInput v-model="amount" locale="sl" :precision="2" step="0.01" min="0" />
1016
- </template>
1017
- ```
1018
-
1019
- `locale` affects the **display only** — `sl` shows `1.234,56` while the model
1020
- stays `"1234.56"`. Typing in the locale's own format works too. While the field
1021
- is focused it shows the canonical value so separators can't fight your typing.
1022
-
1023
- `precision` fixes the number of decimal places, rounding half-up, and preserves
1024
- trailing zeros (`"10.00"` stays `"10.00"`). `min`, `max` and `step` are decimal
1025
- strings as well, and stepping is exact: `0.1 + 0.2` gives `"0.3"`.
1026
-
1027
- The underlying helpers are exported if you need them elsewhere:
1028
-
1029
- ```ts
1030
- import { addDecimals, compareDecimals, roundDecimal } from 'iryx-ui'
1031
-
1032
- addDecimals('0.1', '0.2') // '0.3'
1033
- roundDecimal('1.005', 2) // '1.01'
1034
- compareDecimals('1.10', '1.1') // 0
1035
- ```
1036
-
1037
- ### Validated forms
1038
-
1039
- `IForm` handles client-side validation. It accepts any [Standard Schema](https://standardschema.dev) validator — Zod 3.24+, Valibot, ArkType — so Iryx doesn't depend on a validation library. Wrap each control in an `IFormField` with a `name` matching the schema path and errors wire themselves up.
1040
-
1041
- ```vue
1042
- <script setup lang="ts">
1043
- import { reactive } from 'vue'
1044
- import * as z from 'zod'
1045
-
1046
- const schema = z.object({
1047
- email: z.string().email('That doesn\'t look like an email'),
1048
- password: z.string().min(8, 'Use at least 8 characters'),
1049
- })
1050
-
1051
- const state = reactive({ email: '', password: '' })
1052
-
1053
- function onSubmit(event) {
1054
- console.log(event.data) // only fires when valid
1055
- }
1056
- </script>
1057
-
1058
- <template>
1059
- <IForm :state="state" :schema="schema" @submit="onSubmit">
1060
- <IFormField name="email" label="Email" required description="We'll never share it.">
1061
- <IInput v-model="state.email" type="email" />
1062
- </IFormField>
1063
- <IFormField name="password" label="Password" help="At least 8 characters.">
1064
- <IInput v-model="state.password" type="password" />
1065
- </IFormField>
1066
- <IButton type="submit">
1067
- Create account
1068
- </IButton>
1069
- </IForm>
1070
- </template>
1071
- ```
1072
-
1073
- The control inside a field automatically inherits its `id`, invalid styling and `aria-describedby` — no wiring needed. On a failed submit, focus moves to the first invalid control.
1074
-
1075
- **Validation timing** — `validate-on` defaults to `['blur', 'change']`; submit always validates everything.
1076
-
1077
- **Custom rules** — pass `validate` for anything a schema can't express (it runs alongside the schema, and works without one):
1078
-
1079
- ```vue
1080
- <IForm
1081
- :state="state"
1082
- :validate="s => s.email.endsWith('@corp.com') ? [] : [{ name: 'email', message: 'Must be a work email' }]"
1083
- />
1084
- ```
1085
-
1086
- **Server errors and manual control** — grab a template ref to the form and call `validate()`, `clear(name?)` or `setErrors()`. `IFormField` also takes a plain `error` prop that bypasses validation entirely.
1087
-
1088
- ### Labels and descriptions
1089
-
1090
- `ICheckbox` and `ISwitch` render bare by default. Give them a `label` and/or `description` and they render a wired-up layout instead — the text is clickable, and the description is linked with `aria-describedby`. `IRadioGroup` items take a `description` too.
1091
-
1092
- ```vue
1093
- <template>
1094
- <ICheckbox
1095
- v-model="accepted"
1096
- label="Accept terms"
1097
- description="You agree to the terms of service and privacy policy."
1098
- />
1099
-
1100
- <ISwitch
1101
- v-model="notify"
1102
- label="Push notifications"
1103
- description="Send alerts to this device."
1104
- />
1105
-
1106
- <IRadioGroup
1107
- v-model="plan"
1108
- :items="[
1109
- { label: 'Free', value: 'free', description: 'Up to 3 projects.' },
1110
- { label: 'Pro', value: 'pro', description: 'Unlimited projects.' },
1111
- ]"
1112
- />
1113
- </template>
1114
- ```
1115
-
1116
- Use the `#label` / `#description` slots instead of the props when you need markup (a link, a badge) inside the text.
1117
-
1118
- ### Alerts and banners
1119
-
1120
- They look similar and do different jobs, so they're separate components.
1121
-
1122
- **`IAlert` is contextual.** It sits in the flow next to the thing it's about —
1123
- a failed upload, a form that won't submit — boxed, with an icon and a title.
1124
- Danger and warning variants take `role="alert"` so a screen reader interrupts,
1125
- because the user caused it and needs to know now.
1126
-
1127
- **`IBanner` is page-level.** It spans the full width and announces something
1128
- that isn't about any one element: a trial ending, scheduled maintenance. It's a
1129
- labelled `role="region"`, never an alert — it's ambient, so interrupting
1130
- someone mid-task would be wrong.
1131
-
1132
- ```vue
1133
- <IBanner
1134
- v-model:open="showTrial"
1135
- variant="primary"
1136
- position="top"
1137
- title="Trial ends in 3 days."
1138
- description="Upgrade to keep your data."
1139
- closable
1140
- label="Trial notice"
1141
- >
1142
- <template #actions>
1143
- <IButton size="sm" variant="outline">Upgrade</IButton>
1144
- </template>
1145
- </IBanner>
1146
- ```
1147
-
1148
- `position` is `static` (in flow), `top` (sticky) or `bottom` (fixed to the
1149
- viewport, with the rule moved to its top edge). `contained` keeps the text at a
1150
- readable measure while the fill still spans the window.
1151
-
1152
- Both dismiss through `v-model:open`, so the usual case is one binding instead
1153
- of a `close` handler plus a `v-if`. `close` still fires if you need to persist
1154
- or confirm first:
1155
-
1156
- ```vue
1157
- <IAlert v-model:open="visible" variant="danger" title="Upload failed" closable>
1158
- The file was larger than 10 MB.
1159
- <template #actions>
1160
- <IButton size="sm" @click="retry()">Retry</IButton>
1161
- </template>
1162
- </IAlert>
1163
- ```
1164
-
1165
- ### Tables
1166
-
1167
- `ITable` takes `rows` and `columns` and renders a real `<table>`. Columns are
1168
- plain objects — no render functions — and anything beyond the raw value is a
1169
- slot:
1170
-
1171
- ```vue
1172
- <script setup>
1173
- const columns = [
1174
- { key: 'number', label: 'Invoice', sortable: true },
1175
- { key: 'customer.name', label: 'Customer', sortable: true, sortKey: 'customer_name' },
1176
- { key: 'total', label: 'Total', sortable: true, numeric: true },
1177
- { key: 'status', label: 'Status' },
1178
- ]
1179
- </script>
1180
-
1181
- <template>
1182
- <ITable :rows="invoices" :columns="columns" label="Invoices">
1183
- <template #cell-total="{ value }">
1184
- {{ formatMoney(value) }}
1185
- </template>
1186
- <template #cell-status="{ row }">
1187
- <IBadge :variant="row.paid ? 'success' : 'warning'">
1188
- {{ row.paid ? 'Paid' : 'Due' }}
1189
- </IBadge>
1190
- </template>
1191
- </ITable>
1192
- </template>
1193
- ```
1194
-
1195
- `key` doubles as the accessor (dot-notation reaches nested values) and the slot
1196
- suffix, so `#cell-customer.name` targets that column. `#header-<key>` replaces a
1197
- header the same way.
1198
-
1199
- `numeric` gives a column tabular figures and end alignment, so amounts line up
1200
- digit-for-digit down the column instead of wandering with each glyph's width.
1201
- Set `align` alongside it to keep the figures but place the column differently.
1202
-
1203
- #### Client or server
1204
-
1205
- **The table never fetches.** It renders what you give it and emits what the
1206
- user did, so caching, cancellation and auth stay in your data layer.
1207
-
1208
- Which mode it runs in is decided by one prop. Omit `total` and it sorts and
1209
- paginates `rows` itself. Provide `total` and it does neither — the rows you
1210
- passed are already the page the server returned, so it only reflects state and
1211
- emits changes:
1212
-
1213
- ```vue
1214
- <ITable
1215
- v-model:sort="sort"
1216
- v-model:page="page"
1217
- :rows="data.rows"
1218
- :columns="columns"
1219
- :total="data.total"
1220
- :loading="pending"
1221
- />
1222
- ```
1223
-
1224
- Watch those models and refetch. Sorting a column resets `page` to 1, since the
1225
- old page number means nothing against a reordered list.
1226
-
1227
- #### State ownership
1228
-
1229
- Every model is optional. Bind one and you own that state — put it in the URL, a
1230
- store, or `useState`. Leave it unbound and the table keeps it internally, so the
1231
- simple case needs no wiring:
1232
-
1233
- | Model | Type |
1234
- | --- | --- |
1235
- | `v-model:sort` | `{ key, order } \| null` — `null` is "unsorted", distinct from never-sorted |
1236
- | `v-model:page` | `number`, 1-indexed |
1237
- | `v-model:perPage` | `number` |
1238
- | `v-model:selection` | `(string \| number)[]` of row keys |
1239
- | `v-model:expanded` | `(string \| number)[]` of row keys |
1240
-
1241
- #### Selection and expansion
1242
-
1243
- `selectable` adds a checkbox column whose header is tri-state over the rows on
1244
- screen. Selection is held as **row keys** (`rowKey`, default `'id'`), and
1245
- select-all only touches the current page, so selections made on other pages
1246
- survive. `isRowSelectable` vetoes a row — its checkbox is hidden and the header
1247
- skips it.
1248
-
1249
- `expandable` adds a disclosure column and renders the `#expanded` slot beneath
1250
- an open row; `canExpandRow` vetoes it per row.
1251
-
1252
- ```vue
1253
- <ITable
1254
- v-model:selection="selected"
1255
- :rows="rows"
1256
- :columns="columns"
1257
- selectable
1258
- expandable
1259
- :is-row-selectable="row => !row.locked"
1260
- >
1261
- <template #expanded="{ row }">
1262
- <div class="p-4">{{ row.notes }}</div>
1263
- </template>
1264
- </ITable>
1265
- ```
1266
-
1267
- #### States and props
1268
-
1269
- `loading` sets `aria-busy` and, when there is nothing to show yet, renders
1270
- `loadingRows` skeleton rows instead of the empty message. With rows already on
1271
- screen it leaves them in place, so a refetch doesn't blank the table. Otherwise
1272
- `emptyText` — or the `#empty` slot — takes over.
1273
-
1274
- | Prop | What it does |
1275
- | --- | --- |
1276
- | `rowKey` | Field identifying a row. Default `'id'` |
1277
- | `clickableRows` | Emits `rowClick` and shows a pointer cursor |
1278
- | `striped` / `hoverable` / `stickyHeader` | Row and header treatment |
1279
- | `size` | `sm` / `md` / `lg` row density |
1280
- | `label` / `caption` | Accessible name, and an optional visible caption |
1281
-
1282
- Headers are real `<button>`s, sorted columns carry `aria-sort`, and the table is
1283
- a plain `<table>`, so screen readers and keyboard users get the semantics for
1284
- free. Give it a `label` (or a `caption`) — a table with no accessible name is
1285
- hard to place when tabbing through a page.
1286
-
1287
- #### Without the markup
1288
-
1289
- `useDataTable()` holds the whole state machine — sorting, paging, selection,
1290
- expansion — and renders nothing, if you want the logic under your own markup:
1291
-
1292
- ```ts
1293
- const table = useDataTable({
1294
- rows: () => rows.value,
1295
- columns: () => columns,
1296
- })
1297
- // table.pageRows, table.toggleSort, table.headerSelection, …
1298
- ```
96
+ - [Components](https://iryx-ui.com/components/)
97
+ - [Composables](https://iryx-ui.com/composables/)
98
+ - [Theming](https://iryx-ui.com/guide/theming)
1299
99
 
1300
100
  ## License
1301
101