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