iryx-ui 0.11.0 → 0.12.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 +578 -498
- package/dist/component-names.d.ts +1 -1
- package/dist/component-names.js +1 -1
- package/dist/components/Alert.vue_vue_type_script_setup_true_lang.js +42 -42
- package/dist/components/Banner.vue_vue_type_script_setup_true_lang.js +40 -40
- package/dist/components/Breadcrumb.vue_vue_type_script_setup_true_lang.js +28 -28
- package/dist/components/Button.vue_vue_type_script_setup_true_lang.js +14 -14
- package/dist/components/Checkbox.vue_vue_type_script_setup_true_lang.js +37 -30
- package/dist/components/Combobox.vue_vue_type_script_setup_true_lang.js +62 -60
- package/dist/components/DatePicker.vue_vue_type_script_setup_true_lang.js +65 -65
- package/dist/components/DateRangePicker.vue_vue_type_script_setup_true_lang.js +74 -74
- package/dist/components/Dialog.vue_vue_type_script_setup_true_lang.js +34 -34
- package/dist/components/Drawer.js +5 -0
- package/dist/components/Drawer.vue.d.ts +94 -0
- package/dist/components/Drawer.vue_vue_type_script_setup_true_lang.js +144 -0
- package/dist/components/DropdownMenuItems.vue_vue_type_script_setup_true_lang.js +42 -42
- package/dist/components/FileUpload.vue_vue_type_script_setup_true_lang.js +77 -77
- package/dist/components/Icon.vue_vue_type_script_setup_true_lang.js +11 -11
- package/dist/components/Input.vue_vue_type_script_setup_true_lang.js +33 -33
- package/dist/components/NumberInput.vue_vue_type_script_setup_true_lang.js +68 -66
- package/dist/components/Pagination.vue_vue_type_script_setup_true_lang.js +37 -37
- package/dist/components/PasswordInput.vue_vue_type_script_setup_true_lang.js +37 -37
- package/dist/components/Progress.vue_vue_type_script_setup_true_lang.js +34 -31
- package/dist/components/Select.vue_vue_type_script_setup_true_lang.js +47 -45
- package/dist/components/Stat.vue_vue_type_script_setup_true_lang.js +27 -27
- package/dist/components/Stepper.vue_vue_type_script_setup_true_lang.js +38 -38
- package/dist/components/Switch.vue_vue_type_script_setup_true_lang.js +9 -2
- package/dist/components/Table.vue_vue_type_script_setup_true_lang.js +87 -87
- package/dist/components/Toaster.vue_vue_type_script_setup_true_lang.js +41 -41
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +54 -52
- package/dist/composables/date.js +12 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +55 -54
- package/dist/theme/drawer.d.ts +110 -0
- package/dist/theme/drawer.js +100 -0
- package/dist/theme/index.d.ts +1 -0
- package/package.json +3 -2
- package/theme.css +114 -0
- package/dist/node_modules/.pnpm/@hugeicons_core-free-icons@4.2.3/node_modules/@hugeicons/core-free-icons/dist/esm/index.js +0 -377
- 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
- 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
- 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
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/CalendarDate.js +0 -122
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/DateFormatter.js +0 -84
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/calendars/GregorianCalendar.js +0 -91
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/conversion.js +0 -107
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/manipulation.js +0 -159
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/queries.js +0 -29
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/string.js +0 -37
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/utils.js +0 -6
package/README.md
CHANGED
|
@@ -252,7 +252,7 @@ Tweak a single instance with `class` (conflicts are merged smartly) or per-slot
|
|
|
252
252
|
|
|
253
253
|
```vue
|
|
254
254
|
<IButton class="rounded-full">
|
|
255
|
-
Pill button
|
|
255
|
+
Pill button
|
|
256
256
|
</IButton>
|
|
257
257
|
|
|
258
258
|
<ISwitch :ui="{ thumb: 'bg-zinc-900' }" />
|
|
@@ -262,7 +262,7 @@ Or drop all built-in styles and take over completely:
|
|
|
262
262
|
|
|
263
263
|
```vue
|
|
264
264
|
<IButton unstyled class="my-own-button">
|
|
265
|
-
Headless
|
|
265
|
+
Headless
|
|
266
266
|
</IButton>
|
|
267
267
|
```
|
|
268
268
|
|
|
@@ -287,22 +287,19 @@ app.use(createIryxUi({ unstyled: true }))
|
|
|
287
287
|
| --- | --- |
|
|
288
288
|
| `IForm` | Validating form wrapper — any Standard Schema validator, or your own function |
|
|
289
289
|
| `IFormField` | Label, description, hint, help and error text around a control |
|
|
290
|
+
| `ILabel` | Field label with optional `required` asterisk |
|
|
290
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` |
|
|
291
293
|
| `INumberInput` | Decimal-safe numeric field — the model is a **string**, with `min`/`max`/`step`, `precision` and locale-aware display |
|
|
292
|
-
| `
|
|
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 |
|
|
294
|
+
| `IPasswordInput` | Masked field with a show/hide toggle and an optional strength meter |
|
|
295
295
|
| `IFileUpload` | Drag-and-drop file field with `accept` / `maxSize` / `maxFiles`, thumbnails and a remove action |
|
|
296
296
|
| `IDatePicker` | Calendar in a popover; the model is an ISO `YYYY-MM-DD` **string** |
|
|
297
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
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` |
|
|
302
301
|
| `ISelect` | Listbox with keyboard nav and typeahead, driven by an `items` array, with optional groups |
|
|
303
302
|
| `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
303
|
|
|
307
304
|
**Actions**
|
|
308
305
|
|
|
@@ -312,6 +309,15 @@ app.use(createIryxUi({ unstyled: true }))
|
|
|
312
309
|
| `IButtonGroup` | Joins any children into a segmented control — split buttons, toolbars, pagers |
|
|
313
310
|
| `IDropdownMenu` | Menu driven by an `items` array, with separators, group labels, danger items and nested submenus |
|
|
314
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
|
+
|
|
315
321
|
**Feedback**
|
|
316
322
|
|
|
317
323
|
| Component | Description |
|
|
@@ -320,14 +326,11 @@ app.use(createIryxUi({ unstyled: true }))
|
|
|
320
326
|
| `IBanner` | Page-level announcement — full-bleed, six variants, sticky top or fixed bottom |
|
|
321
327
|
| `IBadge` | Status pill — five variants × three sizes; `dot` moves the colour onto a leading dot |
|
|
322
328
|
| `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
329
|
| `IProgress` | Determinate or `indeterminate` bar, five variants, `formatValue` |
|
|
326
330
|
| `ISkeleton` | Loading placeholder — `text`/`rect`/`circle`, stackable with `lines` |
|
|
327
331
|
| `IEmptyState` | Icon, title, description and an `actions` slot for empty lists |
|
|
328
|
-
| `ITooltip` | Hover/focus tooltip with side, align, delay and optional arrow |
|
|
329
332
|
|
|
330
|
-
**Navigation
|
|
333
|
+
**Navigation**
|
|
331
334
|
|
|
332
335
|
| Component | Description |
|
|
333
336
|
| --- | --- |
|
|
@@ -335,8 +338,22 @@ app.use(createIryxUi({ unstyled: true }))
|
|
|
335
338
|
| `IBreadcrumb` | Trail from an `items` array; the last crumb is marked as the current page |
|
|
336
339
|
| `IPagination` | Page list with ellipsis, edge pages and prev/next controls; `align` places it, `size` sets the button scale |
|
|
337
340
|
| `IStepper` | Multi-step progress, horizontal or vertical, optional `linear` ordering |
|
|
338
|
-
|
|
341
|
+
|
|
342
|
+
**Data display**
|
|
343
|
+
|
|
344
|
+
| Component | Description |
|
|
345
|
+
| --- | --- |
|
|
339
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 |
|
|
340
357
|
|
|
341
358
|
Every component supports `unstyled` and a `class` override; multi-part ones take a `ui` prop for per-slot classes.
|
|
342
359
|
|
|
@@ -405,9 +422,47 @@ if (await confirm({ title: 'Delete this draft?', danger: true }))
|
|
|
405
422
|
|
|
406
423
|
`confirm()` resolves `true` on confirmation and `false` on cancel or dismissal.
|
|
407
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
|
+
|
|
408
463
|
### Internationalisation
|
|
409
464
|
|
|
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.
|
|
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.
|
|
411
466
|
|
|
412
467
|
### Forms
|
|
413
468
|
|
|
@@ -483,577 +538,290 @@ chrome; use `ui` to reach the parts (`root`, `input`, `leading`, `trailing`,
|
|
|
483
538
|
forwarded to the `<input>` itself. `ref` exposes the element as `.input` for
|
|
484
539
|
focus management.
|
|
485
540
|
|
|
486
|
-
####
|
|
487
|
-
|
|
488
|
-
Both charts take plain rows plus a `series` descriptor — the same shape
|
|
489
|
-
`ITable` uses. Omit `series` for the single-measure case.
|
|
541
|
+
#### Files
|
|
490
542
|
|
|
491
543
|
```vue
|
|
492
|
-
<
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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>
|
|
503
561
|
```
|
|
504
562
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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.
|
|
508
567
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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 |
|
|
512
577
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
alone is never a dependable identity channel, so this is not configurable.
|
|
578
|
+
Every string is a prop, including `removeLabel` and the three rejection
|
|
579
|
+
messages, so nothing bakes in English.
|
|
516
580
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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.
|
|
520
586
|
|
|
521
|
-
|
|
522
|
-
|
|
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.
|
|
523
590
|
|
|
524
|
-
####
|
|
591
|
+
#### Dates
|
|
525
592
|
|
|
526
|
-
|
|
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:
|
|
593
|
+
The model is an ISO `YYYY-MM-DD` **string**, never a `Date`.
|
|
530
594
|
|
|
531
595
|
```vue
|
|
532
|
-
<
|
|
533
|
-
|
|
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>
|
|
541
|
-
```
|
|
596
|
+
<script setup lang="ts">
|
|
597
|
+
import { ref } from 'vue'
|
|
542
598
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
599
|
+
const issuedOn = ref<string | null>('2026-08-15')
|
|
600
|
+
const period = ref({ start: '2026-08-01', end: '2026-08-31' })
|
|
601
|
+
</script>
|
|
546
602
|
|
|
547
|
-
|
|
603
|
+
<template>
|
|
604
|
+
<IDatePicker v-model="issuedOn" clearable />
|
|
605
|
+
<IDateRangePicker v-model="period" clearable />
|
|
606
|
+
</template>
|
|
607
|
+
```
|
|
548
608
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
| `ticks` | The axis values actually drawn |
|
|
556
|
-
| `orientation` | `'vertical'` or `'horizontal'` |
|
|
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.
|
|
557
615
|
|
|
558
|
-
|
|
559
|
-
|
|
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 |
|
|
560
625
|
|
|
561
|
-
|
|
562
|
-
`
|
|
563
|
-
one on the same spine rather than starting over.
|
|
626
|
+
Navigation and footer labels (`todayLabel`, `clearLabel`, `previousLabel`,
|
|
627
|
+
`nextLabel`) are all props, so nothing bakes in English.
|
|
564
628
|
|
|
565
|
-
|
|
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.
|
|
566
634
|
|
|
567
|
-
|
|
568
|
-
colours (`text-chart-3`, `fill-chart-5`). They encode **identity** — which
|
|
569
|
-
series a mark belongs to — never magnitude.
|
|
635
|
+
The helpers behind them are exported, for formatting the same values elsewhere:
|
|
570
636
|
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
<ISparkline :data="revenue" class="text-chart-1" />
|
|
637
|
+
```ts
|
|
638
|
+
import { formatIsoDate, isoToday, toCalendarDate, toIsoDate } from 'iryx-ui'
|
|
574
639
|
|
|
575
|
-
|
|
640
|
+
formatIsoDate('2026-08-15', 'en-GB', { dateStyle: 'long' }) // '15 August 2026'
|
|
641
|
+
formatIsoDate('nonsense') // '' — malformed input is "no selection", not a crash
|
|
576
642
|
```
|
|
577
643
|
|
|
578
|
-
|
|
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.
|
|
644
|
+
#### Passwords
|
|
588
645
|
|
|
589
|
-
|
|
590
|
-
|
|
646
|
+
`IPasswordInput` is `IInput` with a reveal toggle in the trailing area, plus an
|
|
647
|
+
optional four-segment strength meter.
|
|
591
648
|
|
|
592
649
|
```vue
|
|
593
|
-
|
|
594
|
-
<ISparkline :class="`text-chart-${index + 1}`" />
|
|
650
|
+
<IPasswordInput v-model="password" strength />
|
|
595
651
|
|
|
596
|
-
<!--
|
|
597
|
-
<
|
|
652
|
+
<!-- Toggle only, no meter -->
|
|
653
|
+
<IPasswordInput v-model="password" />
|
|
598
654
|
|
|
599
|
-
|
|
655
|
+
<!-- No toggle either -->
|
|
656
|
+
<IPasswordInput v-model="password" :toggle="false" />
|
|
600
657
|
```
|
|
601
658
|
|
|
602
|
-
The
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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.
|
|
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.
|
|
616
663
|
|
|
617
|
-
|
|
664
|
+
Every string is overridable, since components must not bake in English:
|
|
618
665
|
|
|
619
666
|
```vue
|
|
620
|
-
<
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
]"
|
|
626
|
-
variant="area"
|
|
627
|
-
label="Revenue by month"
|
|
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']"
|
|
628
673
|
/>
|
|
629
674
|
```
|
|
630
675
|
|
|
631
|
-
|
|
632
|
-
`
|
|
676
|
+
`class` lands on the wrapper that stacks the field above the meter; `ui` reaches
|
|
677
|
+
`root`, `input`, `toggle`, `meter`, `track`, `segment` and `label`.
|
|
633
678
|
|
|
634
|
-
|
|
635
|
-
| --- | --- |
|
|
636
|
-
| `variant` | `line` (default) or `area`, which adds a wash beneath the line |
|
|
637
|
-
| `zero` | Force zero onto the axis. **Off by default** |
|
|
679
|
+
#### Autosizing textareas
|
|
638
680
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
once the axis starts at nothing. Turn it on when the distance from zero is the
|
|
643
|
-
point.
|
|
681
|
+
```vue
|
|
682
|
+
<!-- Grows without limit -->
|
|
683
|
+
<ITextarea v-model="note" autosize />
|
|
644
684
|
|
|
645
|
-
|
|
646
|
-
|
|
685
|
+
<!-- Between 2 and 8 rows, then scrolls -->
|
|
686
|
+
<ITextarea v-model="note" :autosize="{ min: 2, max: 8 }" />
|
|
687
|
+
```
|
|
647
688
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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.
|
|
651
692
|
|
|
652
|
-
|
|
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:
|
|
653
697
|
|
|
654
698
|
```vue
|
|
655
|
-
<
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
{ label: '
|
|
659
|
-
{ label: '
|
|
699
|
+
<ISelect
|
|
700
|
+
v-model="framework"
|
|
701
|
+
:items="[
|
|
702
|
+
{ label: 'Virtual DOM', items: ['Vue', 'React'] },
|
|
703
|
+
{ label: 'Compiled', items: [{ label: 'Svelte', value: 'svelte' }] },
|
|
660
704
|
]"
|
|
661
|
-
label="Revenue by month"
|
|
662
|
-
locale="de-DE"
|
|
663
|
-
:format="{ style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }"
|
|
664
705
|
/>
|
|
665
706
|
```
|
|
666
707
|
|
|
667
|
-
|
|
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 |
|
|
675
|
-
|
|
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.
|
|
680
|
-
|
|
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.
|
|
708
|
+
### Searchable selects
|
|
692
709
|
|
|
693
|
-
|
|
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.
|
|
694
713
|
|
|
695
714
|
```vue
|
|
696
|
-
<
|
|
715
|
+
<ICombobox
|
|
716
|
+
v-model="clientId"
|
|
717
|
+
:items="clients"
|
|
718
|
+
placeholder="Search clients"
|
|
719
|
+
empty-text="No clients found."
|
|
720
|
+
/>
|
|
697
721
|
```
|
|
698
722
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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:
|
|
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:
|
|
711
726
|
|
|
712
727
|
```vue
|
|
713
|
-
<
|
|
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
|
+
/>
|
|
714
735
|
```
|
|
715
736
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
Everything else behaves the same: grouped series, the tooltip, the round-number
|
|
720
|
-
axis anchored at zero.
|
|
721
|
-
|
|
722
|
-
#### Sparklines
|
|
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.
|
|
723
740
|
|
|
724
|
-
|
|
725
|
-
|
|
741
|
+
An entry with its own `items` becomes a labelled group. A group disappears on
|
|
742
|
+
its own once nothing inside it matches:
|
|
726
743
|
|
|
727
744
|
```vue
|
|
728
|
-
<
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
+
]"
|
|
733
751
|
/>
|
|
734
752
|
```
|
|
735
753
|
|
|
736
|
-
|
|
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.
|
|
754
|
+
For lists in the thousands, `virtual` renders only the rows on screen:
|
|
763
755
|
|
|
764
|
-
|
|
756
|
+
```vue
|
|
757
|
+
<ICombobox v-model="sku" virtual :items="fiveThousandItems" :estimate-size="32" />
|
|
758
|
+
```
|
|
765
759
|
|
|
766
|
-
|
|
767
|
-
|
|
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.
|
|
768
765
|
|
|
769
|
-
|
|
770
|
-
linearScale([0, 10], [100, 0])(10) // 0 — ranges may be inverted for SVG's y-axis
|
|
771
|
-
```
|
|
766
|
+
### Numbers and money
|
|
772
767
|
|
|
773
|
-
|
|
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.
|
|
774
773
|
|
|
775
774
|
```vue
|
|
776
775
|
<script setup lang="ts">
|
|
777
776
|
import { ref } from 'vue'
|
|
778
777
|
|
|
779
|
-
|
|
778
|
+
// A string, and it stays one.
|
|
779
|
+
const amount = ref('1234.56')
|
|
780
780
|
</script>
|
|
781
781
|
|
|
782
782
|
<template>
|
|
783
|
-
<
|
|
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
|
-
/>
|
|
783
|
+
<INumberInput v-model="amount" locale="sl" :precision="2" step="0.01" min="0" />
|
|
792
784
|
</template>
|
|
793
785
|
```
|
|
794
786
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
again replaces rather than appends.
|
|
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.
|
|
799
790
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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 |
|
|
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"`.
|
|
809
794
|
|
|
810
|
-
|
|
811
|
-
messages, so nothing bakes in English.
|
|
795
|
+
The underlying helpers are exported if you need them elsewhere:
|
|
812
796
|
|
|
813
|
-
|
|
814
|
-
|
|
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.
|
|
797
|
+
```ts
|
|
798
|
+
import { addDecimals, compareDecimals, roundDecimal } from 'iryx-ui'
|
|
818
799
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
800
|
+
addDecimals('0.1', '0.2') // '0.3'
|
|
801
|
+
roundDecimal('1.005', 2) // '1.01'
|
|
802
|
+
compareDecimals('1.10', '1.1') // 0
|
|
803
|
+
```
|
|
822
804
|
|
|
823
|
-
|
|
805
|
+
### Validated forms
|
|
824
806
|
|
|
825
|
-
|
|
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.
|
|
826
808
|
|
|
827
809
|
```vue
|
|
828
810
|
<script setup lang="ts">
|
|
829
|
-
import {
|
|
811
|
+
import { reactive } from 'vue'
|
|
812
|
+
import * as z from 'zod'
|
|
830
813
|
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
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
|
+
})
|
|
834
818
|
|
|
835
|
-
|
|
836
|
-
<IDatePicker v-model="issuedOn" clearable />
|
|
837
|
-
<IDateRangePicker v-model="period" clearable />
|
|
838
|
-
</template>
|
|
839
|
-
```
|
|
819
|
+
const state = reactive({ email: '', password: '' })
|
|
840
820
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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>
|
|
821
|
+
function onSubmit(event) {
|
|
822
|
+
console.log(event.data) // only fires when valid
|
|
823
|
+
}
|
|
824
|
+
</script>
|
|
1057
825
|
|
|
1058
826
|
<template>
|
|
1059
827
|
<IForm :state="state" :schema="schema" @submit="onSubmit">
|
|
@@ -1115,6 +883,20 @@ The control inside a field automatically inherits its `id`, invalid styling and
|
|
|
1115
883
|
|
|
1116
884
|
Use the `#label` / `#description` slots instead of the props when you need markup (a link, a badge) inside the text.
|
|
1117
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
|
+
|
|
1118
900
|
### Alerts and banners
|
|
1119
901
|
|
|
1120
902
|
They look similar and do different jobs, so they're separate components.
|
|
@@ -1297,6 +1079,304 @@ const table = useDataTable({
|
|
|
1297
1079
|
// table.pageRows, table.toggleSort, table.headerSelection, …
|
|
1298
1080
|
```
|
|
1299
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
|
+
```
|
|
1357
|
+
|
|
1358
|
+
`#underlay` renders behind the marks — target bands, shaded regions. `#overlay`
|
|
1359
|
+
renders in front — reference lines, callouts. Both sit below the hit targets,
|
|
1360
|
+
so hovering keeps working through whatever you draw.
|
|
1361
|
+
|
|
1362
|
+
Both receive the `CartesianLayout`:
|
|
1363
|
+
|
|
1364
|
+
| Prop | What it gives you |
|
|
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'` |
|
|
1372
|
+
|
|
1373
|
+
That's strictly more capable than a draw hook: it's declarative, reactive, and
|
|
1374
|
+
type-checked, with no lifecycle or registration order to learn.
|
|
1375
|
+
|
|
1376
|
+
For a chart type that doesn't exist here, the same primitives are exported —
|
|
1377
|
+
`cartesianLayout`, `linearScale`, `niceTicks`, `seriesColor` — so you can build
|
|
1378
|
+
one on the same spine rather than starting over.
|
|
1379
|
+
|
|
1300
1380
|
## License
|
|
1301
1381
|
|
|
1302
1382
|
[MIT](https://github.com/therok1/iryx-ui/blob/main/LICENSE)
|