iryx-ui 0.5.0 → 0.7.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 +355 -5
- package/dist/component-names.d.ts +1 -1
- package/dist/components/Alert.vue.d.ts +19 -4
- package/dist/components/Badge.vue.d.ts +4 -3
- package/dist/components/Banner.vue.d.ts +81 -0
- package/dist/components/Combobox.vue.d.ts +3 -3
- package/dist/components/DatePicker.vue.d.ts +52 -0
- package/dist/components/DateRangePicker.vue.d.ts +60 -0
- package/dist/components/DropdownMenu.vue.d.ts +1 -1
- package/dist/components/Input.vue.d.ts +39 -1
- package/dist/components/Pagination.vue.d.ts +3 -1
- package/dist/components/PasswordInput.vue.d.ts +54 -0
- package/dist/components/Select.vue.d.ts +2 -2
- package/dist/components/Table.vue.d.ts +131 -0
- package/dist/components/Textarea.vue.d.ts +16 -1
- package/dist/components/index.d.ts +5 -0
- package/dist/composables/data-table.d.ts +93 -0
- package/dist/composables/date.d.ts +21 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +72 -60
- package/dist/node_modules/.pnpm/@hugeicons_core-free-icons@4.2.3/node_modules/@hugeicons/core-free-icons/dist/esm/index.js +104 -7
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/CalendarDate.js +122 -0
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/DateFormatter.js +84 -0
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/calendars/GregorianCalendar.js +91 -0
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/conversion.js +107 -0
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/manipulation.js +159 -0
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/queries.js +29 -0
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/string.js +37 -0
- package/dist/node_modules/.pnpm/@internationalized_date@3.12.3/node_modules/@internationalized/date/dist/private/utils.js +6 -0
- package/dist/packages/iryx-ui/src/component-names.js +1 -1
- package/dist/packages/iryx-ui/src/components/Alert.vue_vue_type_script_setup_true_lang.js +51 -35
- package/dist/packages/iryx-ui/src/components/Badge.vue_vue_type_script_setup_true_lang.js +1 -2
- package/dist/packages/iryx-ui/src/components/Banner.js +5 -0
- package/dist/packages/iryx-ui/src/components/Banner.vue_vue_type_script_setup_true_lang.js +93 -0
- package/dist/packages/iryx-ui/src/components/DatePicker.js +5 -0
- package/dist/packages/iryx-ui/src/components/DatePicker.vue_vue_type_script_setup_true_lang.js +186 -0
- package/dist/packages/iryx-ui/src/components/DateRangePicker.js +5 -0
- package/dist/packages/iryx-ui/src/components/DateRangePicker.vue_vue_type_script_setup_true_lang.js +210 -0
- package/dist/packages/iryx-ui/src/components/Input.vue_vue_type_script_setup_true_lang.js +94 -25
- package/dist/packages/iryx-ui/src/components/Pagination.vue_vue_type_script_setup_true_lang.js +5 -1
- package/dist/packages/iryx-ui/src/components/PasswordInput.js +5 -0
- package/dist/packages/iryx-ui/src/components/PasswordInput.vue_vue_type_script_setup_true_lang.js +113 -0
- package/dist/packages/iryx-ui/src/components/Table.js +5 -0
- package/dist/packages/iryx-ui/src/components/Table.vue_vue_type_script_setup_true_lang.js +217 -0
- package/dist/packages/iryx-ui/src/components/Textarea.vue_vue_type_script_setup_true_lang.js +47 -21
- package/dist/packages/iryx-ui/src/components/index.js +68 -58
- package/dist/packages/iryx-ui/src/composables/data-table.js +106 -0
- package/dist/packages/iryx-ui/src/composables/date.js +25 -0
- package/dist/packages/iryx-ui/src/theme/alert.js +10 -21
- package/dist/packages/iryx-ui/src/theme/badge.js +37 -37
- package/dist/packages/iryx-ui/src/theme/banner.js +62 -0
- package/dist/packages/iryx-ui/src/theme/date-picker.js +45 -0
- package/dist/packages/iryx-ui/src/theme/input.js +22 -6
- package/dist/packages/iryx-ui/src/theme/pagination.js +23 -13
- package/dist/packages/iryx-ui/src/theme/password-input.js +22 -0
- package/dist/packages/iryx-ui/src/theme/stat.js +4 -4
- package/dist/packages/iryx-ui/src/theme/table.js +62 -0
- package/dist/packages/iryx-ui/src/theme/toast.js +11 -26
- package/dist/theme/alert.d.ts +6 -12
- package/dist/theme/badge.d.ts +15 -12
- package/dist/theme/banner.d.ts +185 -0
- package/dist/theme/date-picker.d.ts +254 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/input.d.ts +94 -15
- package/dist/theme/pagination.d.ts +33 -0
- package/dist/theme/password-input.d.ts +79 -0
- package/dist/theme/stat.d.ts +0 -3
- package/dist/theme/table.d.ts +230 -0
- package/dist/theme/toast.d.ts +0 -15
- package/package.json +2 -1
- package/theme.css +10 -0
package/README.md
CHANGED
|
@@ -197,6 +197,24 @@ For a static re-brand, plain CSS works too — tokens are just variables:
|
|
|
197
197
|
}
|
|
198
198
|
```
|
|
199
199
|
|
|
200
|
+
### Typeface
|
|
201
|
+
|
|
202
|
+
The library ships **no webfont** — that would put font files, a licence and a
|
|
203
|
+
network request into every app whether it wanted them or not. It reads one
|
|
204
|
+
variable, defaulting to the system stack. Point it at your own family and
|
|
205
|
+
everything follows, including Tailwind's `font-sans`:
|
|
206
|
+
|
|
207
|
+
```css
|
|
208
|
+
@import "@fontsource-variable/instrument-sans"; /* or a <link>, or self-hosted */
|
|
209
|
+
|
|
210
|
+
:root {
|
|
211
|
+
--iryx-font-sans: "Instrument Sans", ui-sans-serif, system-ui, sans-serif;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Unlike the colour tokens, this one is not mode-specific — there is no `.dark`
|
|
216
|
+
counterpart to keep in sync.
|
|
217
|
+
|
|
200
218
|
Available tokens, each usable as a Tailwind color (`bg-primary`,
|
|
201
219
|
`text-muted-foreground`, …):
|
|
202
220
|
|
|
@@ -270,9 +288,12 @@ app.use(createIryxUi({ unstyled: true }))
|
|
|
270
288
|
| --- | --- |
|
|
271
289
|
| `IForm` | Validating form wrapper — any Standard Schema validator, or your own function |
|
|
272
290
|
| `IFormField` | Label, description, hint, help and error text around a control |
|
|
273
|
-
| `IInput` | Text field with `sm`/`md`/`lg` sizes, `invalid` state, `v-model` |
|
|
291
|
+
| `IInput` | Text field with `sm`/`md`/`lg` sizes, `invalid` state, `v-model`, `leading`/`trailing` slots, `clearable`, `loading`, `debounce` |
|
|
274
292
|
| `INumberInput` | Decimal-safe numeric field — the model is a **string**, with `min`/`max`/`step`, `precision` and locale-aware display |
|
|
275
|
-
| `
|
|
293
|
+
| `IDatePicker` | Calendar in a popover; the model is an ISO `YYYY-MM-DD` **string** |
|
|
294
|
+
| `IDateRangePicker` | Two-month range calendar; the model is `{ start, end }` ISO strings |
|
|
295
|
+
| `IPasswordInput` | Masked field with a show/hide toggle and an optional strength meter |
|
|
296
|
+
| `ITextarea` | Multi-line field with matching sizes, `invalid` state and optional `autosize` |
|
|
276
297
|
| `ILabel` | Field label with optional `required` asterisk |
|
|
277
298
|
| `ICheckbox` | Tri-state checkbox (`true` / `false` / `'indeterminate'`), optional `label` + `description` |
|
|
278
299
|
| `ISelect` | Listbox with keyboard nav and typeahead, driven by an `items` array, with optional groups |
|
|
@@ -292,8 +313,9 @@ app.use(createIryxUi({ unstyled: true }))
|
|
|
292
313
|
|
|
293
314
|
| Component | Description |
|
|
294
315
|
| --- | --- |
|
|
295
|
-
| `IAlert` | Inline
|
|
296
|
-
| `
|
|
316
|
+
| `IAlert` | Inline, contextual message in four variants — variant icon, `actions` slot, optional dismiss |
|
|
317
|
+
| `IBanner` | Page-level announcement — full-bleed, six variants, sticky top or fixed bottom |
|
|
318
|
+
| `IBadge` | Status pill — five variants × three sizes; `dot` moves the colour onto a leading dot |
|
|
297
319
|
| `IToaster` | Host for `useToast()`; six viewport positions, stacking, action buttons |
|
|
298
320
|
| `IDialog` | Modal with header/body/footer slots, `dismissible` and `showClose` |
|
|
299
321
|
| `IConfirmDialog` | Host for `useConfirm()` — renders the promise-based confirmation |
|
|
@@ -308,9 +330,10 @@ app.use(createIryxUi({ unstyled: true }))
|
|
|
308
330
|
| --- | --- |
|
|
309
331
|
| `ITabs` | `solid` or `line` variants with an animated indicator, horizontal or vertical |
|
|
310
332
|
| `IBreadcrumb` | Trail from an `items` array; the last crumb is marked as the current page |
|
|
311
|
-
| `IPagination` | Page list with ellipsis, edge pages and prev/next controls |
|
|
333
|
+
| `IPagination` | Page list with ellipsis, edge pages and prev/next controls; `align` places it, `size` sets the button scale |
|
|
312
334
|
| `IStepper` | Multi-step progress, horizontal or vertical, optional `linear` ordering |
|
|
313
335
|
| `IStat` | KPI tile — label, value, signed delta with trend colour, and a hint |
|
|
336
|
+
| `ITable` | Data table — sorting, selection, expansion and per-cell slots, client- or server-driven |
|
|
314
337
|
|
|
315
338
|
Every component supports `unstyled` and a `class` override; multi-part ones take a `ui` prop for per-slot classes.
|
|
316
339
|
|
|
@@ -414,6 +437,151 @@ const framework = ref('vue')
|
|
|
414
437
|
</template>
|
|
415
438
|
```
|
|
416
439
|
|
|
440
|
+
#### Input affixes, clearing, loading and debounce
|
|
441
|
+
|
|
442
|
+
`IInput` renders its chrome on a wrapper element, so `leading` and `trailing`
|
|
443
|
+
slot content sits *inside* the field and takes real space — a long value is
|
|
444
|
+
truncated by the affix rather than sliding underneath it.
|
|
445
|
+
|
|
446
|
+
```vue
|
|
447
|
+
<IInput v-model="search" clearable placeholder="Search invoices…">
|
|
448
|
+
<template #leading>
|
|
449
|
+
<HugeiconsIcon :icon="Search01Icon" />
|
|
450
|
+
</template>
|
|
451
|
+
</IInput>
|
|
452
|
+
|
|
453
|
+
<IInput v-model="team" placeholder="your-team">
|
|
454
|
+
<template #trailing>
|
|
455
|
+
<span class="text-sm">.example.com</span>
|
|
456
|
+
</template>
|
|
457
|
+
</IInput>
|
|
458
|
+
|
|
459
|
+
<!-- Spinner in the trailing area; the field stays editable. -->
|
|
460
|
+
<IInput v-model="slug" :loading="checking" />
|
|
461
|
+
|
|
462
|
+
<!-- The model updates 500ms after the last keystroke. -->
|
|
463
|
+
<IInput v-model="query" :debounce="500" clearable />
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
| Prop | Effect |
|
|
467
|
+
| --- | --- |
|
|
468
|
+
| `clearable` | Clear button in the trailing area whenever the field is non-empty |
|
|
469
|
+
| `loading` | Spinner in the trailing area. Does **not** disable the field |
|
|
470
|
+
| `debounce` | Milliseconds to wait after the last keystroke before the model updates. `0` (default) updates on every keystroke |
|
|
471
|
+
| `clearLabel` | Accessible name for the clear button, for non-English apps |
|
|
472
|
+
|
|
473
|
+
The displayed text always updates on the keystroke — only the model lags. Blur
|
|
474
|
+
and Enter flush a pending update immediately, so a submit never reads a stale
|
|
475
|
+
value, and an external write (a reset or prefill) cancels whatever is queued.
|
|
476
|
+
|
|
477
|
+
`class` lands on the wrapper, since that is the element carrying the field
|
|
478
|
+
chrome; use `ui` to reach the parts (`root`, `input`, `leading`, `trailing`,
|
|
479
|
+
`clear`). Stray attributes like `name`, `autocomplete` and `maxlength` are
|
|
480
|
+
forwarded to the `<input>` itself. `ref` exposes the element as `.input` for
|
|
481
|
+
focus management.
|
|
482
|
+
|
|
483
|
+
#### Dates
|
|
484
|
+
|
|
485
|
+
The model is an ISO `YYYY-MM-DD` **string**, never a `Date`.
|
|
486
|
+
|
|
487
|
+
```vue
|
|
488
|
+
<script setup lang="ts">
|
|
489
|
+
import { ref } from 'vue'
|
|
490
|
+
|
|
491
|
+
const issuedOn = ref<string | null>('2026-08-15')
|
|
492
|
+
const period = ref({ start: '2026-08-01', end: '2026-08-31' })
|
|
493
|
+
</script>
|
|
494
|
+
|
|
495
|
+
<template>
|
|
496
|
+
<IDatePicker v-model="issuedOn" clearable />
|
|
497
|
+
<IDateRangePicker v-model="period" clearable />
|
|
498
|
+
</template>
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
A `Date` is a timestamp, so it always carries a time zone. `new Date('2026-08-15')`
|
|
502
|
+
parses as UTC midnight, and a user west of Greenwich formatting it locally sees
|
|
503
|
+
the 14th — which silently moves a record into the wrong reporting period. A
|
|
504
|
+
calendar date has no zone, so it stays the day you picked. Internally the
|
|
505
|
+
components use `@internationalized/date`; that never reaches your model, so
|
|
506
|
+
formatting the string with `dayjs` or anything else on the way out is fine.
|
|
507
|
+
|
|
508
|
+
| Prop | Effect |
|
|
509
|
+
| --- | --- |
|
|
510
|
+
| `min` / `max` | Selectable bounds, as ISO strings |
|
|
511
|
+
| `locale` | Month names, weekday initials, and the trigger's text |
|
|
512
|
+
| `format` | `Intl.DateTimeFormatOptions` for the trigger, e.g. `{ dateStyle: 'full' }` |
|
|
513
|
+
| `weekStartsOn` | `0` is Sunday. Defaults to the locale's convention |
|
|
514
|
+
| `clearable` | Adds a clear action to the footer |
|
|
515
|
+
| `months` | Range picker only — months side by side, default `2` |
|
|
516
|
+
| `separator` | Range picker only — text between the two dates |
|
|
517
|
+
|
|
518
|
+
Navigation and footer labels (`todayLabel`, `clearLabel`, `previousLabel`,
|
|
519
|
+
`nextLabel`) are all props, so nothing bakes in English.
|
|
520
|
+
|
|
521
|
+
Both render their calendar at a fixed six weeks, so a short month cannot stretch
|
|
522
|
+
its rows to match a taller neighbour and the popover does not resize as you page
|
|
523
|
+
through it. The range picker draws only the committed range — the days between
|
|
524
|
+
the endpoints take a flat tint while the two ends take the solid fill, so a long
|
|
525
|
+
span still shows where it begins and ends.
|
|
526
|
+
|
|
527
|
+
The helpers behind them are exported, for formatting the same values elsewhere:
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
import { formatIsoDate, isoToday, toCalendarDate, toIsoDate } from 'iryx-ui'
|
|
531
|
+
|
|
532
|
+
formatIsoDate('2026-08-15', 'en-GB', { dateStyle: 'long' }) // '15 August 2026'
|
|
533
|
+
formatIsoDate('nonsense') // '' — malformed input is "no selection", not a crash
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
#### Passwords
|
|
537
|
+
|
|
538
|
+
`IPasswordInput` is `IInput` with a reveal toggle in the trailing area, plus an
|
|
539
|
+
optional four-segment strength meter.
|
|
540
|
+
|
|
541
|
+
```vue
|
|
542
|
+
<IPasswordInput v-model="password" strength />
|
|
543
|
+
|
|
544
|
+
<!-- Toggle only, no meter -->
|
|
545
|
+
<IPasswordInput v-model="password" />
|
|
546
|
+
|
|
547
|
+
<!-- No toggle either -->
|
|
548
|
+
<IPasswordInput v-model="password" :toggle="false" />
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
The score counts length (8 and 12 characters), mixed case, a digit and a
|
|
552
|
+
symbol, capped at four. It is a deliberately transparent nudge toward better
|
|
553
|
+
passwords, **not** a security control — enforce real policy in the `IForm`
|
|
554
|
+
validator, where it can actually reject a value.
|
|
555
|
+
|
|
556
|
+
Every string is overridable, since components must not bake in English:
|
|
557
|
+
|
|
558
|
+
```vue
|
|
559
|
+
<IPasswordInput
|
|
560
|
+
v-model="password"
|
|
561
|
+
strength
|
|
562
|
+
show-label="Afficher le mot de passe"
|
|
563
|
+
hide-label="Masquer le mot de passe"
|
|
564
|
+
:strength-labels="['Faible', 'Moyen', 'Bon', 'Fort']"
|
|
565
|
+
/>
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
`class` lands on the wrapper that stacks the field above the meter; `ui` reaches
|
|
569
|
+
`root`, `input`, `toggle`, `meter`, `track`, `segment` and `label`.
|
|
570
|
+
|
|
571
|
+
#### Autosizing textareas
|
|
572
|
+
|
|
573
|
+
```vue
|
|
574
|
+
<!-- Grows without limit -->
|
|
575
|
+
<ITextarea v-model="note" autosize />
|
|
576
|
+
|
|
577
|
+
<!-- Between 2 and 8 rows, then scrolls -->
|
|
578
|
+
<ITextarea v-model="note" :autosize="{ min: 2, max: 8 }" />
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
`autosize` overrides `rows` and drops the drag handle, since the measured
|
|
582
|
+
height is the point. The field shrinks as well as grows, and re-measures when
|
|
583
|
+
the model changes from outside — a reset or a prefill resizes correctly.
|
|
584
|
+
|
|
417
585
|
`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.
|
|
418
586
|
|
|
419
587
|
`ISelect` also takes groups — an entry with its own `items` becomes a labelled
|
|
@@ -607,6 +775,188 @@ The control inside a field automatically inherits its `id`, invalid styling and
|
|
|
607
775
|
|
|
608
776
|
Use the `#label` / `#description` slots instead of the props when you need markup (a link, a badge) inside the text.
|
|
609
777
|
|
|
778
|
+
### Alerts and banners
|
|
779
|
+
|
|
780
|
+
They look similar and do different jobs, so they're separate components.
|
|
781
|
+
|
|
782
|
+
**`IAlert` is contextual.** It sits in the flow next to the thing it's about —
|
|
783
|
+
a failed upload, a form that won't submit — boxed, with an icon and a title.
|
|
784
|
+
Danger and warning variants take `role="alert"` so a screen reader interrupts,
|
|
785
|
+
because the user caused it and needs to know now.
|
|
786
|
+
|
|
787
|
+
**`IBanner` is page-level.** It spans the full width and announces something
|
|
788
|
+
that isn't about any one element: a trial ending, scheduled maintenance. It's a
|
|
789
|
+
labelled `role="region"`, never an alert — it's ambient, so interrupting
|
|
790
|
+
someone mid-task would be wrong.
|
|
791
|
+
|
|
792
|
+
```vue
|
|
793
|
+
<IBanner
|
|
794
|
+
v-model:open="showTrial"
|
|
795
|
+
variant="primary"
|
|
796
|
+
position="top"
|
|
797
|
+
title="Trial ends in 3 days."
|
|
798
|
+
description="Upgrade to keep your data."
|
|
799
|
+
closable
|
|
800
|
+
label="Trial notice"
|
|
801
|
+
>
|
|
802
|
+
<template #actions>
|
|
803
|
+
<IButton size="sm" variant="outline">Upgrade</IButton>
|
|
804
|
+
</template>
|
|
805
|
+
</IBanner>
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
`position` is `static` (in flow), `top` (sticky) or `bottom` (fixed to the
|
|
809
|
+
viewport, with the rule moved to its top edge). `contained` keeps the text at a
|
|
810
|
+
readable measure while the fill still spans the window.
|
|
811
|
+
|
|
812
|
+
Both dismiss through `v-model:open`, so the usual case is one binding instead
|
|
813
|
+
of a `close` handler plus a `v-if`. `close` still fires if you need to persist
|
|
814
|
+
or confirm first:
|
|
815
|
+
|
|
816
|
+
```vue
|
|
817
|
+
<IAlert v-model:open="visible" variant="danger" title="Upload failed" closable>
|
|
818
|
+
The file was larger than 10 MB.
|
|
819
|
+
<template #actions>
|
|
820
|
+
<IButton size="sm" @click="retry()">Retry</IButton>
|
|
821
|
+
</template>
|
|
822
|
+
</IAlert>
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
### Tables
|
|
826
|
+
|
|
827
|
+
`ITable` takes `rows` and `columns` and renders a real `<table>`. Columns are
|
|
828
|
+
plain objects — no render functions — and anything beyond the raw value is a
|
|
829
|
+
slot:
|
|
830
|
+
|
|
831
|
+
```vue
|
|
832
|
+
<script setup>
|
|
833
|
+
const columns = [
|
|
834
|
+
{ key: 'number', label: 'Invoice', sortable: true },
|
|
835
|
+
{ key: 'customer.name', label: 'Customer', sortable: true, sortKey: 'customer_name' },
|
|
836
|
+
{ key: 'total', label: 'Total', sortable: true, numeric: true },
|
|
837
|
+
{ key: 'status', label: 'Status' },
|
|
838
|
+
]
|
|
839
|
+
</script>
|
|
840
|
+
|
|
841
|
+
<template>
|
|
842
|
+
<ITable :rows="invoices" :columns="columns" label="Invoices">
|
|
843
|
+
<template #cell-total="{ value }">
|
|
844
|
+
{{ formatMoney(value) }}
|
|
845
|
+
</template>
|
|
846
|
+
<template #cell-status="{ row }">
|
|
847
|
+
<IBadge :variant="row.paid ? 'success' : 'warning'">
|
|
848
|
+
{{ row.paid ? 'Paid' : 'Due' }}
|
|
849
|
+
</IBadge>
|
|
850
|
+
</template>
|
|
851
|
+
</ITable>
|
|
852
|
+
</template>
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
`key` doubles as the accessor (dot-notation reaches nested values) and the slot
|
|
856
|
+
suffix, so `#cell-customer.name` targets that column. `#header-<key>` replaces a
|
|
857
|
+
header the same way.
|
|
858
|
+
|
|
859
|
+
`numeric` gives a column tabular figures and end alignment, so amounts line up
|
|
860
|
+
digit-for-digit down the column instead of wandering with each glyph's width.
|
|
861
|
+
Set `align` alongside it to keep the figures but place the column differently.
|
|
862
|
+
|
|
863
|
+
#### Client or server
|
|
864
|
+
|
|
865
|
+
**The table never fetches.** It renders what you give it and emits what the
|
|
866
|
+
user did, so caching, cancellation and auth stay in your data layer.
|
|
867
|
+
|
|
868
|
+
Which mode it runs in is decided by one prop. Omit `total` and it sorts and
|
|
869
|
+
paginates `rows` itself. Provide `total` and it does neither — the rows you
|
|
870
|
+
passed are already the page the server returned, so it only reflects state and
|
|
871
|
+
emits changes:
|
|
872
|
+
|
|
873
|
+
```vue
|
|
874
|
+
<ITable
|
|
875
|
+
v-model:sort="sort"
|
|
876
|
+
v-model:page="page"
|
|
877
|
+
:rows="data.rows"
|
|
878
|
+
:columns="columns"
|
|
879
|
+
:total="data.total"
|
|
880
|
+
:loading="pending"
|
|
881
|
+
/>
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
Watch those models and refetch. Sorting a column resets `page` to 1, since the
|
|
885
|
+
old page number means nothing against a reordered list.
|
|
886
|
+
|
|
887
|
+
#### State ownership
|
|
888
|
+
|
|
889
|
+
Every model is optional. Bind one and you own that state — put it in the URL, a
|
|
890
|
+
store, or `useState`. Leave it unbound and the table keeps it internally, so the
|
|
891
|
+
simple case needs no wiring:
|
|
892
|
+
|
|
893
|
+
| Model | Type |
|
|
894
|
+
| --- | --- |
|
|
895
|
+
| `v-model:sort` | `{ key, order } \| null` — `null` is "unsorted", distinct from never-sorted |
|
|
896
|
+
| `v-model:page` | `number`, 1-indexed |
|
|
897
|
+
| `v-model:perPage` | `number` |
|
|
898
|
+
| `v-model:selection` | `(string \| number)[]` of row keys |
|
|
899
|
+
| `v-model:expanded` | `(string \| number)[]` of row keys |
|
|
900
|
+
|
|
901
|
+
#### Selection and expansion
|
|
902
|
+
|
|
903
|
+
`selectable` adds a checkbox column whose header is tri-state over the rows on
|
|
904
|
+
screen. Selection is held as **row keys** (`rowKey`, default `'id'`), and
|
|
905
|
+
select-all only touches the current page, so selections made on other pages
|
|
906
|
+
survive. `isRowSelectable` vetoes a row — its checkbox is hidden and the header
|
|
907
|
+
skips it.
|
|
908
|
+
|
|
909
|
+
`expandable` adds a disclosure column and renders the `#expanded` slot beneath
|
|
910
|
+
an open row; `canExpandRow` vetoes it per row.
|
|
911
|
+
|
|
912
|
+
```vue
|
|
913
|
+
<ITable
|
|
914
|
+
v-model:selection="selected"
|
|
915
|
+
:rows="rows"
|
|
916
|
+
:columns="columns"
|
|
917
|
+
selectable
|
|
918
|
+
expandable
|
|
919
|
+
:is-row-selectable="row => !row.locked"
|
|
920
|
+
>
|
|
921
|
+
<template #expanded="{ row }">
|
|
922
|
+
<div class="p-4">{{ row.notes }}</div>
|
|
923
|
+
</template>
|
|
924
|
+
</ITable>
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
#### States and props
|
|
928
|
+
|
|
929
|
+
`loading` sets `aria-busy` and, when there is nothing to show yet, renders
|
|
930
|
+
`loadingRows` skeleton rows instead of the empty message. With rows already on
|
|
931
|
+
screen it leaves them in place, so a refetch doesn't blank the table. Otherwise
|
|
932
|
+
`emptyText` — or the `#empty` slot — takes over.
|
|
933
|
+
|
|
934
|
+
| Prop | What it does |
|
|
935
|
+
| --- | --- |
|
|
936
|
+
| `rowKey` | Field identifying a row. Default `'id'` |
|
|
937
|
+
| `clickableRows` | Emits `rowClick` and shows a pointer cursor |
|
|
938
|
+
| `striped` / `hoverable` / `stickyHeader` | Row and header treatment |
|
|
939
|
+
| `size` | `sm` / `md` / `lg` row density |
|
|
940
|
+
| `label` / `caption` | Accessible name, and an optional visible caption |
|
|
941
|
+
|
|
942
|
+
Headers are real `<button>`s, sorted columns carry `aria-sort`, and the table is
|
|
943
|
+
a plain `<table>`, so screen readers and keyboard users get the semantics for
|
|
944
|
+
free. Give it a `label` (or a `caption`) — a table with no accessible name is
|
|
945
|
+
hard to place when tabbing through a page.
|
|
946
|
+
|
|
947
|
+
#### Without the markup
|
|
948
|
+
|
|
949
|
+
`useDataTable()` holds the whole state machine — sorting, paging, selection,
|
|
950
|
+
expansion — and renders nothing, if you want the logic under your own markup:
|
|
951
|
+
|
|
952
|
+
```ts
|
|
953
|
+
const table = useDataTable({
|
|
954
|
+
rows: () => rows.value,
|
|
955
|
+
columns: () => columns,
|
|
956
|
+
})
|
|
957
|
+
// table.pageRows, table.toggleSort, table.headerSelection, …
|
|
958
|
+
```
|
|
959
|
+
|
|
610
960
|
## License
|
|
611
961
|
|
|
612
962
|
[MIT](https://github.com/therok1/iryx-ui/blob/main/LICENSE)
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* Consumed by the Vue plugin (global registration) and the Nuxt module
|
|
4
4
|
* (auto-imports). Keep in sync with `src/components/index.ts`.
|
|
5
5
|
*/
|
|
6
|
-
export declare const componentNames: readonly ["Alert", "App", "Badge", "Breadcrumb", "Button", "ButtonGroup", "Card", "Checkbox", "Combobox", "ConfirmDialog", "Dialog", "DropdownMenu", "EmptyState", "Form", "FormField", "Input", "Label", "NumberInput", "Pagination", "Progress", "RadioGroup", "Select", "Separator", "Skeleton", "Stat", "Stepper", "Switch", "Tabs", "Textarea", "Toaster", "Tooltip"];
|
|
6
|
+
export declare const componentNames: readonly ["Alert", "App", "Badge", "Banner", "Breadcrumb", "Button", "ButtonGroup", "Card", "Checkbox", "Combobox", "ConfirmDialog", "DatePicker", "DateRangePicker", "Dialog", "DropdownMenu", "EmptyState", "Form", "FormField", "Input", "Label", "NumberInput", "Pagination", "PasswordInput", "Progress", "RadioGroup", "Select", "Separator", "Skeleton", "Stat", "Stepper", "Switch", "Table", "Tabs", "Textarea", "Toaster", "Tooltip"];
|
|
7
7
|
export type ComponentName = (typeof componentNames)[number];
|
|
@@ -26,10 +26,21 @@ export interface AlertProps {
|
|
|
26
26
|
content?: string;
|
|
27
27
|
title?: string;
|
|
28
28
|
description?: string;
|
|
29
|
+
actions?: string;
|
|
29
30
|
close?: string;
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
type __VLS_Props = AlertProps;
|
|
34
|
+
type __VLS_ModelProps = {
|
|
35
|
+
/**
|
|
36
|
+
* Dismissal as a model, so the common case is one binding rather than a
|
|
37
|
+
* `close` handler plus a `v-if` in every consumer. `close` still fires, for
|
|
38
|
+
* callers that want to confirm or persist before it goes.
|
|
39
|
+
*/
|
|
40
|
+
'open'?: boolean;
|
|
41
|
+
};
|
|
42
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
43
|
+
declare var __VLS_8: {}, __VLS_15: {}, __VLS_17: {}, __VLS_19: {}, __VLS_21: {};
|
|
33
44
|
type __VLS_Slots = {} & {
|
|
34
45
|
icon?: (props: typeof __VLS_8) => any;
|
|
35
46
|
} & {
|
|
@@ -37,12 +48,16 @@ type __VLS_Slots = {} & {
|
|
|
37
48
|
} & {
|
|
38
49
|
default?: (props: typeof __VLS_17) => any;
|
|
39
50
|
} & {
|
|
40
|
-
|
|
51
|
+
actions?: (props: typeof __VLS_19) => any;
|
|
52
|
+
} & {
|
|
53
|
+
close?: (props: typeof __VLS_21) => any;
|
|
41
54
|
};
|
|
42
|
-
declare const __VLS_base: import("vue").DefineComponent<
|
|
55
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
43
56
|
close: () => any;
|
|
44
|
-
|
|
57
|
+
"update:open": (value: boolean) => any;
|
|
58
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
45
59
|
onClose?: (() => any) | undefined;
|
|
60
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
46
61
|
}>, {
|
|
47
62
|
icon: IconLike | false;
|
|
48
63
|
as: string;
|
|
@@ -4,10 +4,11 @@ export interface BadgeProps {
|
|
|
4
4
|
/** Merge props onto the immediate child instead of rendering an element. */
|
|
5
5
|
asChild?: boolean;
|
|
6
6
|
variant?: 'neutral' | 'success' | 'warning' | 'danger' | 'info';
|
|
7
|
-
/** `soft` is a tinted background; `solid` is a filled block of colour. */
|
|
8
|
-
tone?: 'soft' | 'solid';
|
|
9
7
|
size?: 'sm' | 'md' | 'lg';
|
|
10
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Show a leading status dot. This also switches the look: the badge goes
|
|
10
|
+
* neutral and the dot alone carries the variant's colour.
|
|
11
|
+
*/
|
|
11
12
|
dot?: boolean;
|
|
12
13
|
/** Text content. Ignored when the default slot is used. */
|
|
13
14
|
label?: string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { IconLike } from '../composables/icon';
|
|
2
|
+
export interface BannerProps {
|
|
3
|
+
/** Render as a different element or component. */
|
|
4
|
+
as?: string;
|
|
5
|
+
variant?: 'neutral' | 'info' | 'success' | 'warning' | 'danger' | 'primary';
|
|
6
|
+
/**
|
|
7
|
+
* `static` sits in the flow, `top` sticks to the top of the scroll
|
|
8
|
+
* container, `bottom` pins to the bottom of the viewport.
|
|
9
|
+
*/
|
|
10
|
+
position?: 'static' | 'top' | 'bottom';
|
|
11
|
+
/** Bold lead-in before the message. */
|
|
12
|
+
title?: string;
|
|
13
|
+
/** Message text. Ignored when the default slot is used. */
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Leading icon. None by default — a banner is ambient, not a status report. */
|
|
16
|
+
icon?: IconLike;
|
|
17
|
+
/** Constrain the text to a readable measure while the fill still spans full width. */
|
|
18
|
+
contained?: boolean;
|
|
19
|
+
align?: 'start' | 'center';
|
|
20
|
+
/** Render a dismiss button. */
|
|
21
|
+
closable?: boolean;
|
|
22
|
+
/** Accessible name for the dismiss button — override for non-English apps. */
|
|
23
|
+
closeLabel?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Accessible name for the landmark. A banner is a region rather than an
|
|
26
|
+
* alert: it is ambient, so it must not interrupt a screen reader mid-task.
|
|
27
|
+
*/
|
|
28
|
+
label?: string;
|
|
29
|
+
/** Skip built-in classes; you take over styling entirely. */
|
|
30
|
+
unstyled?: boolean;
|
|
31
|
+
class?: string;
|
|
32
|
+
/** Override classes per slot, e.g. `{ container: 'max-w-3xl' }`. */
|
|
33
|
+
ui?: {
|
|
34
|
+
root?: string;
|
|
35
|
+
container?: string;
|
|
36
|
+
icon?: string;
|
|
37
|
+
content?: string;
|
|
38
|
+
title?: string;
|
|
39
|
+
actions?: string;
|
|
40
|
+
close?: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
type __VLS_Props = BannerProps;
|
|
44
|
+
type __VLS_ModelProps = {
|
|
45
|
+
/** Dismissal as a model, matching IAlert — bind it, or let the banner hide itself. */
|
|
46
|
+
'open'?: boolean;
|
|
47
|
+
};
|
|
48
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
49
|
+
declare var __VLS_8: {}, __VLS_15: {}, __VLS_17: {}, __VLS_19: {}, __VLS_21: {};
|
|
50
|
+
type __VLS_Slots = {} & {
|
|
51
|
+
icon?: (props: typeof __VLS_8) => any;
|
|
52
|
+
} & {
|
|
53
|
+
title?: (props: typeof __VLS_15) => any;
|
|
54
|
+
} & {
|
|
55
|
+
default?: (props: typeof __VLS_17) => any;
|
|
56
|
+
} & {
|
|
57
|
+
actions?: (props: typeof __VLS_19) => any;
|
|
58
|
+
} & {
|
|
59
|
+
close?: (props: typeof __VLS_21) => any;
|
|
60
|
+
};
|
|
61
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
62
|
+
close: () => any;
|
|
63
|
+
"update:open": (value: boolean) => any;
|
|
64
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
65
|
+
onClose?: (() => any) | undefined;
|
|
66
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
67
|
+
}>, {
|
|
68
|
+
as: string;
|
|
69
|
+
closeLabel: string;
|
|
70
|
+
unstyled: boolean;
|
|
71
|
+
label: string;
|
|
72
|
+
contained: boolean;
|
|
73
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
74
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
75
|
+
declare const _default: typeof __VLS_export;
|
|
76
|
+
export default _default;
|
|
77
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
78
|
+
new (): {
|
|
79
|
+
$slots: S;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
@@ -74,23 +74,24 @@ type __VLS_Slots = {} & {
|
|
|
74
74
|
create?: (props: typeof __VLS_139) => any;
|
|
75
75
|
};
|
|
76
76
|
declare const __VLS_base: import("vue").DefineComponent<ComboboxProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
77
|
+
"update:open": (value: boolean) => any;
|
|
77
78
|
"update:modelValue": (value: AcceptableValue) => any;
|
|
78
79
|
create: (query: string) => any;
|
|
79
80
|
highlight: (payload: {
|
|
80
81
|
ref: HTMLElement;
|
|
81
82
|
value: AcceptableValue;
|
|
82
83
|
} | undefined) => any;
|
|
83
|
-
"update:open": (value: boolean) => any;
|
|
84
84
|
}, string, import("vue").PublicProps, Readonly<ComboboxProps> & Readonly<{
|
|
85
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
85
86
|
"onUpdate:modelValue"?: ((value: AcceptableValue) => any) | undefined;
|
|
86
87
|
onCreate?: ((query: string) => any) | undefined;
|
|
87
88
|
onHighlight?: ((payload: {
|
|
88
89
|
ref: HTMLElement;
|
|
89
90
|
value: AcceptableValue;
|
|
90
91
|
} | undefined) => any) | undefined;
|
|
91
|
-
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
92
92
|
}>, {
|
|
93
93
|
unstyled: boolean;
|
|
94
|
+
open: boolean;
|
|
94
95
|
disabled: boolean;
|
|
95
96
|
invalid: boolean;
|
|
96
97
|
emptyText: string;
|
|
@@ -99,7 +100,6 @@ declare const __VLS_base: import("vue").DefineComponent<ComboboxProps, {}, {}, {
|
|
|
99
100
|
virtual: boolean;
|
|
100
101
|
estimateSize: number;
|
|
101
102
|
overscan: number;
|
|
102
|
-
open: boolean;
|
|
103
103
|
defaultOpen: boolean;
|
|
104
104
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
105
105
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export interface DatePickerProps {
|
|
2
|
+
size?: 'sm' | 'md' | 'lg';
|
|
3
|
+
placeholder?: string;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
/** Mark the field as invalid — styles the border and ring red. */
|
|
6
|
+
invalid?: boolean;
|
|
7
|
+
id?: string;
|
|
8
|
+
/** Earliest selectable date, as an ISO `YYYY-MM-DD` string. */
|
|
9
|
+
min?: string;
|
|
10
|
+
/** Latest selectable date, as an ISO `YYYY-MM-DD` string. */
|
|
11
|
+
max?: string;
|
|
12
|
+
/** Locale for the month names, weekday initials and the trigger's text. */
|
|
13
|
+
locale?: string;
|
|
14
|
+
/** How the selected date reads on the trigger. */
|
|
15
|
+
format?: Intl.DateTimeFormatOptions;
|
|
16
|
+
/** 0 is Sunday. Defaults to the locale's own convention. */
|
|
17
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
18
|
+
/** Offer a "clear" action in the footer. */
|
|
19
|
+
clearable?: boolean;
|
|
20
|
+
/** Footer and navigation labels — override for non-English apps. */
|
|
21
|
+
todayLabel?: string;
|
|
22
|
+
clearLabel?: string;
|
|
23
|
+
previousLabel?: string;
|
|
24
|
+
nextLabel?: string;
|
|
25
|
+
/** Skip built-in classes; you take over styling entirely. */
|
|
26
|
+
unstyled?: boolean;
|
|
27
|
+
/** Applied to the trigger, which is the element carrying the field chrome. */
|
|
28
|
+
class?: string;
|
|
29
|
+
/** Override classes per slot, e.g. `{ content: 'p-4' }`. */
|
|
30
|
+
ui?: Partial<Record<'trigger' | 'placeholder' | 'content' | 'header' | 'heading' | 'nav' | 'months' | 'grid' | 'headCell' | 'cell' | 'cellTrigger' | 'footer' | 'action', string>>;
|
|
31
|
+
}
|
|
32
|
+
type __VLS_Props = DatePickerProps;
|
|
33
|
+
type __VLS_ModelProps = {
|
|
34
|
+
/** ISO `YYYY-MM-DD`, never a `Date`. See `composables/date.ts` for why. */
|
|
35
|
+
modelValue?: string | null;
|
|
36
|
+
};
|
|
37
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
38
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
39
|
+
"update:modelValue": (value: string | null) => any;
|
|
40
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
41
|
+
"onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
|
|
42
|
+
}>, {
|
|
43
|
+
unstyled: boolean;
|
|
44
|
+
placeholder: string;
|
|
45
|
+
invalid: boolean;
|
|
46
|
+
todayLabel: string;
|
|
47
|
+
clearLabel: string;
|
|
48
|
+
previousLabel: string;
|
|
49
|
+
nextLabel: string;
|
|
50
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
51
|
+
declare const _default: typeof __VLS_export;
|
|
52
|
+
export default _default;
|