iryx-ui 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +129 -27
  2. package/dist/component-names.d.ts +1 -1
  3. package/dist/components/App.vue.d.ts +1 -1
  4. package/dist/components/Combobox.vue.d.ts +112 -0
  5. package/dist/components/DropdownMenu.vue.d.ts +1 -1
  6. package/dist/components/FormField.vue.d.ts +0 -9
  7. package/dist/components/Label.vue.d.ts +0 -6
  8. package/dist/components/NumberInput.vue.d.ts +63 -0
  9. package/dist/components/Select.vue.d.ts +13 -2
  10. package/dist/components/index.d.ts +2 -0
  11. package/dist/composables/decimal.d.ts +53 -0
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.js +49 -44
  14. package/dist/nuxt.d.ts +1 -1
  15. package/dist/packages/iryx-ui/src/component-names.js +1 -1
  16. package/dist/packages/iryx-ui/src/components/Combobox.js +5 -0
  17. package/dist/packages/iryx-ui/src/components/Combobox.vue_vue_type_script_setup_true_lang.js +284 -0
  18. package/dist/packages/iryx-ui/src/components/FormField.vue_vue_type_script_setup_true_lang.js +3 -5
  19. package/dist/packages/iryx-ui/src/components/Label.vue_vue_type_script_setup_true_lang.js +0 -2
  20. package/dist/packages/iryx-ui/src/components/NumberInput.js +5 -0
  21. package/dist/packages/iryx-ui/src/components/NumberInput.vue_vue_type_script_setup_true_lang.js +136 -0
  22. package/dist/packages/iryx-ui/src/components/Select.vue_vue_type_script_setup_true_lang.js +72 -27
  23. package/dist/packages/iryx-ui/src/components/index.js +48 -44
  24. package/dist/packages/iryx-ui/src/composables/decimal.js +79 -0
  25. package/dist/packages/iryx-ui/src/theme/alert.js +2 -2
  26. package/dist/packages/iryx-ui/src/theme/button.js +2 -2
  27. package/dist/packages/iryx-ui/src/theme/card.js +1 -1
  28. package/dist/packages/iryx-ui/src/theme/checkbox.js +1 -1
  29. package/dist/packages/iryx-ui/src/theme/combobox.js +29 -0
  30. package/dist/packages/iryx-ui/src/theme/dialog.js +2 -2
  31. package/dist/packages/iryx-ui/src/theme/dropdown-menu.js +4 -4
  32. package/dist/packages/iryx-ui/src/theme/form.js +10 -34
  33. package/dist/packages/iryx-ui/src/theme/input.js +1 -1
  34. package/dist/packages/iryx-ui/src/theme/label.js +1 -10
  35. package/dist/packages/iryx-ui/src/theme/number-input.js +49 -0
  36. package/dist/packages/iryx-ui/src/theme/pagination.js +1 -1
  37. package/dist/packages/iryx-ui/src/theme/presets.js +0 -42
  38. package/dist/packages/iryx-ui/src/theme/radio-group.js +1 -1
  39. package/dist/packages/iryx-ui/src/theme/select.js +5 -3
  40. package/dist/packages/iryx-ui/src/theme/skeleton.js +1 -1
  41. package/dist/packages/iryx-ui/src/theme/stepper.js +1 -1
  42. package/dist/packages/iryx-ui/src/theme/switch.js +1 -1
  43. package/dist/packages/iryx-ui/src/theme/tabs.js +3 -3
  44. package/dist/packages/iryx-ui/src/theme/toast.js +3 -3
  45. package/dist/packages/iryx-ui/src/theme/tooltip.js +1 -1
  46. package/dist/plugin.d.ts +1 -1
  47. package/dist/theme/button.d.ts +2 -2
  48. package/dist/theme/combobox.d.ts +101 -0
  49. package/dist/theme/form.d.ts +35 -62
  50. package/dist/theme/index.d.ts +2 -0
  51. package/dist/theme/input.d.ts +5 -5
  52. package/dist/theme/label.d.ts +0 -18
  53. package/dist/theme/number-input.d.ts +92 -0
  54. package/dist/theme/presets.d.ts +0 -42
  55. package/dist/theme/select.d.ts +6 -0
  56. package/package.json +1 -1
  57. package/theme.css +14 -0
package/README.md CHANGED
@@ -104,7 +104,7 @@ and every component updates.
104
104
 
105
105
  ```vue
106
106
  <template>
107
- <IApp theme="emerald" appearance="system" dir="ltr">
107
+ <IApp theme="rose" appearance="system" dir="ltr">
108
108
  <RouterView />
109
109
  </IApp>
110
110
  </template>
@@ -165,14 +165,14 @@ utilities (shadcn-style `@custom-variant dark`).
165
165
 
166
166
  ## Theming
167
167
 
168
- Pick a built-in color preset — `violet` (default), `emerald`, `rose`,
169
- `amber`, `sky` — at startup or at runtime:
168
+ Pick a built-in color preset — `violet` (default) or `rose` — at startup or at
169
+ runtime:
170
170
 
171
171
  ```ts
172
172
  import { applyTheme } from 'iryx-ui'
173
173
 
174
- app.use(createIryxUi({ theme: 'emerald' }))
175
- // nuxt.config.ts → iryxUi: { theme: 'emerald' }
174
+ app.use(createIryxUi({ theme: 'rose' }))
175
+ // nuxt.config.ts → iryxUi: { theme: 'rose' }
176
176
 
177
177
  applyTheme('rose') // runtime, e.g. from a theme picker
178
178
  ```
@@ -202,10 +202,19 @@ Available tokens, each usable as a Tailwind color (`bg-primary`,
202
202
 
203
203
  | Group | Tokens |
204
204
  | --- | --- |
205
- | Surfaces | `background`, `foreground`, `accent`, `accent-foreground`, `muted`, `muted-foreground`, `border` |
205
+ | Surfaces | `background`, `foreground`, `accent`, `accent-foreground`, `muted`, `muted-foreground`, `border`, `input` |
206
206
  | Brand | `primary`, `primary-foreground`, `primary-from`, `primary-to` |
207
207
  | Status | `success`, `warning`, `danger`, `info` — each with `-foreground`, `-muted`, `-muted-foreground` and `-border` |
208
208
 
209
+ `input` is the fill behind the fields — `IInput`, `ITextarea`, `INumberInput`,
210
+ the `ISelect` trigger, the `ICombobox` anchor — and behind `IButton`'s
211
+ `outline` variant, so the two read as the same kind of surface. It's a token
212
+ of its own rather than a reused `muted`, so how raised a control looks can be
213
+ tuned without moving every muted surface with it. It matches the page
214
+ background in light mode and lifts off it in dark, where the difference is
215
+ legible; that split lives in the token values, so no component needs a `dark:`
216
+ class for it.
217
+
209
218
  `primary-from` / `primary-to` are the stops of the solid button's vertical
210
219
  gradient. The status tokens carry their own dark-mode values, so components
211
220
  never need a `dark:` class for them: `success` is the solid fill,
@@ -262,10 +271,12 @@ app.use(createIryxUi({ unstyled: true }))
262
271
  | `IForm` | Validating form wrapper — any Standard Schema validator, or your own function |
263
272
  | `IFormField` | Label, description, hint, help and error text around a control |
264
273
  | `IInput` | Text field with `sm`/`md`/`lg` sizes, `invalid` state, `v-model` |
274
+ | `INumberInput` | Decimal-safe numeric field — the model is a **string**, with `min`/`max`/`step`, `precision` and locale-aware display |
265
275
  | `ITextarea` | Multi-line field with matching sizes and `invalid` state |
266
- | `ILabel` | Field label with optional `required` asterisk, indented to line up with the control's text |
276
+ | `ILabel` | Field label with optional `required` asterisk |
267
277
  | `ICheckbox` | Tri-state checkbox (`true` / `false` / `'indeterminate'`), optional `label` + `description` |
268
- | `ISelect` | Listbox with keyboard nav and typeahead, driven by an `items` array |
278
+ | `ISelect` | Listbox with keyboard nav and typeahead, driven by an `items` array, with optional groups |
279
+ | `ICombobox` | Searchable select — filters as you type, with optional groups, virtualized rows and a "create from query" row |
269
280
  | `IRadioGroup` | Radio list with labels wired up automatically; items take a `description` |
270
281
  | `ISwitch` | Accessible toggle, optional `label` + `description` |
271
282
 
@@ -405,6 +416,116 @@ const framework = ref('vue')
405
416
 
406
417
  `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.
407
418
 
419
+ `ISelect` also takes groups — an entry with its own `items` becomes a labelled
420
+ heading, the same shape `ICombobox` uses:
421
+
422
+ ```vue
423
+ <ISelect
424
+ v-model="framework"
425
+ :items="[
426
+ { label: 'Virtual DOM', items: ['Vue', 'React'] },
427
+ { label: 'Compiled', items: [{ label: 'Svelte', value: 'svelte' }] },
428
+ ]"
429
+ />
430
+ ```
431
+
432
+ ### Searchable selects
433
+
434
+ `ICombobox` takes the same `items` as `ISelect` and filters them against what
435
+ the user types, which is what you want once a list runs to hundreds of entries.
436
+ The field shows the selected option's **label** while the model holds its value.
437
+
438
+ ```vue
439
+ <ICombobox
440
+ v-model="clientId"
441
+ :items="clients"
442
+ placeholder="Search clients"
443
+ empty-text="No clients found."
444
+ />
445
+ ```
446
+
447
+ Set `create` to offer a row for whatever the user typed when nothing matches.
448
+ Choosing it emits `create` with the query and does **not** change the model —
449
+ the option doesn't exist yet, so you add it and select it yourself:
450
+
451
+ ```vue
452
+ <ICombobox
453
+ v-model="clientId"
454
+ :items="clients"
455
+ create
456
+ :create-label="query => `Add ${query}`"
457
+ @create="query => clients.push({ label: query, value: addClient(query) })"
458
+ />
459
+ ```
460
+
461
+ Both `empty-text` and `create-label` are props precisely so a non-English app
462
+ never inherits an English string; `empty` and `create` slots take over the
463
+ markup entirely if you need more than text.
464
+
465
+ An entry with its own `items` becomes a labelled group. A group disappears on
466
+ its own once nothing inside it matches:
467
+
468
+ ```vue
469
+ <ICombobox
470
+ v-model="clientId"
471
+ :items="[
472
+ { label: 'Recent', items: ['Acme Industries', 'Bolt Logistics'] },
473
+ { label: 'Archived', items: [{ label: 'Cirrus Systems', value: 'cirrus' }] },
474
+ ]"
475
+ />
476
+ ```
477
+
478
+ For lists in the thousands, `virtual` renders only the rows on screen:
479
+
480
+ ```vue
481
+ <ICombobox v-model="sku" virtual :items="fiveThousandItems" :estimate-size="32" />
482
+ ```
483
+
484
+ `virtual` and grouped items are **mutually exclusive** — the underlying
485
+ virtualizer is a flat window with no notion of group headings, so groups are
486
+ flattened and their labels dropped (with a warning in dev). `estimate-size` is
487
+ the assumed row height in px, used to size the scrollbar before rows are
488
+ measured; set it if you have restyled rows to a different height.
489
+
490
+ ### Numbers and money
491
+
492
+ `INumberInput` never turns your value into a `number`. The model is a decimal
493
+ **string**, because binary floating point cannot represent decimal money —
494
+ `0.1 + 0.2` is `0.30000000000000004`, and `10.00` becomes `10`. Values are
495
+ added, compared and rounded with `BigInt` internally, so precision survives
496
+ regardless of magnitude.
497
+
498
+ ```vue
499
+ <script setup lang="ts">
500
+ import { ref } from 'vue'
501
+
502
+ // A string, and it stays one.
503
+ const amount = ref('1234.56')
504
+ </script>
505
+
506
+ <template>
507
+ <INumberInput v-model="amount" locale="sl" :precision="2" step="0.01" min="0" />
508
+ </template>
509
+ ```
510
+
511
+ `locale` affects the **display only** — `sl` shows `1.234,56` while the model
512
+ stays `"1234.56"`. Typing in the locale's own format works too. While the field
513
+ is focused it shows the canonical value so separators can't fight your typing.
514
+
515
+ `precision` fixes the number of decimal places, rounding half-up, and preserves
516
+ trailing zeros (`"10.00"` stays `"10.00"`). `min`, `max` and `step` are decimal
517
+ strings as well, and stepping is exact: `0.1 + 0.2` gives `"0.3"`.
518
+
519
+ The underlying helpers are exported if you need them elsewhere:
520
+
521
+ ```ts
522
+ import { addDecimals, compareDecimals, roundDecimal } from 'iryx-ui'
523
+
524
+ addDecimals('0.1', '0.2') // '0.3'
525
+ roundDecimal('1.005', 2) // '1.01'
526
+ compareDecimals('1.10', '1.1') // 0
527
+ ```
528
+
408
529
  ### Validated forms
409
530
 
410
531
  `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.
@@ -456,25 +577,6 @@ The control inside a field automatically inherits its `id`, invalid styling and
456
577
 
457
578
  **Server errors and manual control** — grab a template ref to the form and call `validate()`, `clear(name?)` or `setErrors()`. `IFormField` also takes a plain `error` prop that bypasses validation entirely.
458
579
 
459
- ### Label alignment
460
-
461
- `ILabel` and `IFormField` indent their text so it lines up with the control's
462
- text rather than its outer edge, putting the label directly above the
463
- placeholder. The offset matches the input's horizontal padding — `sm` 10px,
464
- `md` 12px, `lg` 16px — so pass the same size you gave the control:
465
-
466
- ```vue
467
- <template>
468
- <ILabel for="amount" indent="lg">
469
- Amount
470
- </ILabel>
471
- <IInput id="amount" size="lg" />
472
- </template>
473
- ```
474
-
475
- Use `indent="none"` when the label wraps its control, as with a checkbox, since
476
- there is no text to line up with.
477
-
478
580
  ### Labels and descriptions
479
581
 
480
582
  `ICheckbox` and `ISwitch` render bare by default. Give them a `label` and/or `description` and they render a wired-up layout instead — the text is clickable, and the description is linked with `aria-describedby`. `IRadioGroup` items take a `description` too.
@@ -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", "ConfirmDialog", "Dialog", "DropdownMenu", "EmptyState", "Form", "FormField", "Input", "Label", "Pagination", "Progress", "RadioGroup", "Select", "Separator", "Skeleton", "Stat", "Stepper", "Switch", "Tabs", "Textarea", "Toaster", "Tooltip"];
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"];
7
7
  export type ComponentName = (typeof componentNames)[number];
@@ -7,7 +7,7 @@ export interface AppProps extends /* @vue-ignore */ ConfigProviderProps {
7
7
  * below. Reactive, unlike the install-time plugin option.
8
8
  */
9
9
  unstyled?: boolean;
10
- /** Colour theme: a preset name (`'emerald'`…) or a custom theme. */
10
+ /** Colour theme: a preset name (`'violet'`, `'rose'`) or a custom theme. */
11
11
  theme?: Theme | ThemePresetName;
12
12
  /** Startup appearance. A preference the user already stored wins over this. */
13
13
  appearance?: Appearance;
@@ -0,0 +1,112 @@
1
+ import type { AcceptableValue, ComboboxRootProps } from 'reka-ui';
2
+ export interface ComboboxItemOption {
3
+ label: string;
4
+ value: string;
5
+ disabled?: boolean;
6
+ }
7
+ /** A labelled run of options. Reka hides the whole group once nothing in it matches. */
8
+ export interface ComboboxItemGroup {
9
+ label: string;
10
+ items: (ComboboxItemOption | string)[];
11
+ }
12
+ export type ComboboxItems = (ComboboxItemOption | ComboboxItemGroup | string)[];
13
+ export interface ComboboxProps extends ComboboxRootProps {
14
+ /**
15
+ * Options to render. Strings are expanded to `{ label, value }`, and an
16
+ * entry with its own `items` becomes a labelled group.
17
+ */
18
+ items?: ComboboxItems;
19
+ placeholder?: string;
20
+ size?: 'sm' | 'md' | 'lg';
21
+ /** Mark the field as invalid — styles the border and ring red. */
22
+ invalid?: boolean;
23
+ id?: string;
24
+ /** Shown when the query matches nothing. Override for non-English apps. */
25
+ emptyText?: string;
26
+ /**
27
+ * Offer a "create" row when the query matches no option's label, for
28
+ * adding a client or item without leaving the field. Selecting it emits
29
+ * `create` with the query — the value is *not* set, since the option
30
+ * doesn't exist yet; add it to `items` and set the model yourself.
31
+ */
32
+ create?: boolean;
33
+ /** Label for that row. Receives the current query. */
34
+ createLabel?: (query: string) => string;
35
+ /**
36
+ * Render only the rows in view. Worth it in the thousands, not the hundreds.
37
+ * Groups are flattened — Reka's virtualizer is a flat window — so `virtual`
38
+ * and grouped `items` are mutually exclusive.
39
+ */
40
+ virtual?: boolean;
41
+ /** Row height in px, used to size the scrollbar before rows are measured. */
42
+ estimateSize?: number;
43
+ /** Rows rendered beyond the viewport on each side. */
44
+ overscan?: number;
45
+ /** Skip built-in classes; you take over styling entirely. */
46
+ unstyled?: boolean;
47
+ class?: string;
48
+ ui?: {
49
+ anchor?: string;
50
+ input?: string;
51
+ trigger?: string;
52
+ content?: string;
53
+ viewport?: string;
54
+ item?: string;
55
+ empty?: string;
56
+ group?: string;
57
+ groupLabel?: string;
58
+ };
59
+ }
60
+ declare var __VLS_50: {
61
+ query: string;
62
+ }, __VLS_58: {
63
+ query: string;
64
+ }, __VLS_60: {}, __VLS_139: {
65
+ query: string;
66
+ };
67
+ type __VLS_Slots = {} & {
68
+ empty?: (props: typeof __VLS_50) => any;
69
+ } & {
70
+ empty?: (props: typeof __VLS_58) => any;
71
+ } & {
72
+ default?: (props: typeof __VLS_60) => any;
73
+ } & {
74
+ create?: (props: typeof __VLS_139) => any;
75
+ };
76
+ declare const __VLS_base: import("vue").DefineComponent<ComboboxProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
77
+ "update:modelValue": (value: AcceptableValue) => any;
78
+ create: (query: string) => any;
79
+ highlight: (payload: {
80
+ ref: HTMLElement;
81
+ value: AcceptableValue;
82
+ } | undefined) => any;
83
+ "update:open": (value: boolean) => any;
84
+ }, string, import("vue").PublicProps, Readonly<ComboboxProps> & Readonly<{
85
+ "onUpdate:modelValue"?: ((value: AcceptableValue) => any) | undefined;
86
+ onCreate?: ((query: string) => any) | undefined;
87
+ onHighlight?: ((payload: {
88
+ ref: HTMLElement;
89
+ value: AcceptableValue;
90
+ } | undefined) => any) | undefined;
91
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
92
+ }>, {
93
+ unstyled: boolean;
94
+ disabled: boolean;
95
+ invalid: boolean;
96
+ emptyText: string;
97
+ create: boolean;
98
+ createLabel: (query: string) => string;
99
+ virtual: boolean;
100
+ estimateSize: number;
101
+ overscan: number;
102
+ open: boolean;
103
+ defaultOpen: boolean;
104
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
105
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
106
+ declare const _default: typeof __VLS_export;
107
+ export default _default;
108
+ type __VLS_WithSlots<T, S> = T & {
109
+ new (): {
110
+ $slots: S;
111
+ };
112
+ };
@@ -36,8 +36,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
36
36
  "onUpdate:open"?: ((value: boolean) => any) | undefined;
37
37
  }>, {
38
38
  unstyled: boolean;
39
- align: "start" | "center" | "end";
40
39
  sideOffset: number;
40
+ align: "start" | "center" | "end";
41
41
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
42
42
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
43
43
  declare const _default: typeof __VLS_export;
@@ -11,15 +11,6 @@ export interface FormFieldProps {
11
11
  required?: boolean;
12
12
  /** Force an error message, bypassing the form's validation. */
13
13
  error?: string;
14
- /**
15
- * Indent the label, description, error and help text so they line up with
16
- * the control's own text rather than its outer edge.
17
- *
18
- * The values match the input's horizontal padding, so pass the same size you
19
- * gave the control. Use `none` for controls that draw their own label, like
20
- * a checkbox or switch.
21
- */
22
- indent?: 'none' | 'sm' | 'md' | 'lg';
23
14
  /** Skip built-in classes; you take over styling entirely. */
24
15
  unstyled?: boolean;
25
16
  class?: string;
@@ -2,12 +2,6 @@ import type { LabelProps as RekaLabelProps } from 'reka-ui';
2
2
  export interface LabelProps extends RekaLabelProps {
3
3
  /** Append a red asterisk to mark the associated field as required. */
4
4
  required?: boolean;
5
- /**
6
- * Indent the label to line up with the control's text rather than its outer
7
- * edge. Match it to the control's size; use `none` when the label wraps its
8
- * control, as with a checkbox.
9
- */
10
- indent?: 'none' | 'sm' | 'md' | 'lg';
11
5
  /** Skip built-in classes; you take over styling entirely. */
12
6
  unstyled?: boolean;
13
7
  class?: string;
@@ -0,0 +1,63 @@
1
+ export interface NumberInputProps {
2
+ /** Smallest allowed value, as a decimal string. */
3
+ min?: string;
4
+ /** Largest allowed value, as a decimal string. */
5
+ max?: string;
6
+ /** Amount the stepper adds or subtracts, as a decimal string. */
7
+ step?: string;
8
+ /** Fixed decimal places. Values are rounded half-up to this scale. */
9
+ precision?: number;
10
+ /**
11
+ * Locale used for the *display* only — `sl` shows `1.234,56`. The model
12
+ * stays canonical (`"1234.56"`) whatever the locale.
13
+ */
14
+ locale?: string;
15
+ /** Show the stacked increment/decrement controls. */
16
+ stepper?: boolean;
17
+ size?: 'sm' | 'md' | 'lg';
18
+ placeholder?: string;
19
+ disabled?: boolean;
20
+ required?: boolean;
21
+ /** Mark the field as invalid — styles the border and ring red. */
22
+ invalid?: boolean;
23
+ id?: string;
24
+ /** Accessible names for the stepper controls — override for non-English apps. */
25
+ incrementLabel?: string;
26
+ decrementLabel?: string;
27
+ /** Skip built-in classes; you take over styling entirely. */
28
+ unstyled?: boolean;
29
+ class?: string;
30
+ /** Override classes per slot, e.g. `{ step: 'px-2' }`. */
31
+ ui?: {
32
+ root?: string;
33
+ input?: string;
34
+ stepper?: string;
35
+ step?: string;
36
+ };
37
+ }
38
+ type __VLS_Props = NumberInputProps;
39
+ type __VLS_ModelProps = {
40
+ /**
41
+ * The model is a decimal **string**, never a number.
42
+ *
43
+ * Money in a finance app is computed with decimal arithmetic; coercing to a
44
+ * float here would silently lose precision before the value ever reached the
45
+ * caller. An empty string means "no value".
46
+ */
47
+ modelValue?: string;
48
+ };
49
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
50
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
51
+ "update:modelValue": (value: string) => any;
52
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
53
+ "onUpdate:modelValue"?: ((value: string) => any) | undefined;
54
+ }>, {
55
+ unstyled: boolean;
56
+ step: string;
57
+ invalid: boolean;
58
+ stepper: boolean;
59
+ incrementLabel: string;
60
+ decrementLabel: string;
61
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
62
+ declare const _default: typeof __VLS_export;
63
+ export default _default;
@@ -4,9 +4,18 @@ export interface SelectItemOption {
4
4
  value: string;
5
5
  disabled?: boolean;
6
6
  }
7
+ /** A labelled run of options, rendered under a heading. */
8
+ export interface SelectItemGroup {
9
+ label: string;
10
+ items: (SelectItemOption | string)[];
11
+ }
12
+ export type SelectItems = (SelectItemOption | SelectItemGroup | string)[];
7
13
  export interface SelectProps extends SelectRootProps {
8
- /** Options to render. Strings are expanded to `{ label, value }`. */
9
- items?: (SelectItemOption | string)[];
14
+ /**
15
+ * Options to render. Strings are expanded to `{ label, value }`, and an
16
+ * entry with its own `items` becomes a labelled group.
17
+ */
18
+ items?: SelectItems;
10
19
  placeholder?: string;
11
20
  size?: 'sm' | 'md' | 'lg';
12
21
  /** Skip built-in classes; you take over styling entirely. */
@@ -17,6 +26,8 @@ export interface SelectProps extends SelectRootProps {
17
26
  content?: string;
18
27
  viewport?: string;
19
28
  item?: string;
29
+ group?: string;
30
+ groupLabel?: string;
20
31
  };
21
32
  }
22
33
  declare var __VLS_48: {};
@@ -6,6 +6,7 @@ export { default as Button } from './Button.vue';
6
6
  export { default as ButtonGroup } from './ButtonGroup.vue';
7
7
  export { default as Card } from './Card.vue';
8
8
  export { default as Checkbox } from './Checkbox.vue';
9
+ export { default as Combobox } from './Combobox.vue';
9
10
  export { default as ConfirmDialog } from './ConfirmDialog.vue';
10
11
  export { default as Dialog } from './Dialog.vue';
11
12
  export { default as DropdownMenu } from './DropdownMenu.vue';
@@ -14,6 +15,7 @@ export { default as Form } from './Form.vue';
14
15
  export { default as FormField } from './FormField.vue';
15
16
  export { default as Input } from './Input.vue';
16
17
  export { default as Label } from './Label.vue';
18
+ export { default as NumberInput } from './NumberInput.vue';
17
19
  export { default as Pagination } from './Pagination.vue';
18
20
  export { default as Progress } from './Progress.vue';
19
21
  export { default as RadioGroup } from './RadioGroup.vue';
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Decimal-string arithmetic, exact by construction.
3
+ *
4
+ * Money is stored and computed as decimal strings — `0.1 + 0.2` in binary
5
+ * floating point is `0.30000000000000004`, which silently corrupts a total.
6
+ * Everything here scales to integers and uses `BigInt`, so a value only ever
7
+ * round-trips through `Number` if the caller asks for it.
8
+ */
9
+ interface Decimal {
10
+ /** Unscaled value: `1234.56` at scale 2 is `123456n`. */
11
+ units: bigint;
12
+ /** Number of decimal places. */
13
+ scale: number;
14
+ }
15
+ /** Parse a canonical decimal string (`-1234.56`). Returns undefined if invalid. */
16
+ export declare function parseDecimal(value: string): Decimal | undefined;
17
+ /** Render a Decimal back to a canonical string. */
18
+ export declare function formatDecimal({ units, scale }: Decimal): string;
19
+ /** `-1` if a < b, `0` if equal, `1` if a > b. */
20
+ export declare function compareDecimals(a: string, b: string): number;
21
+ /** Exact addition. `addDecimals('0.1', '0.2')` is `'0.3'`, not `'0.30000000000000004'`. */
22
+ export declare function addDecimals(a: string, b: string): string | undefined;
23
+ /** Round half-up to a fixed number of decimal places. */
24
+ export declare function roundDecimal(value: string, precision: number): string | undefined;
25
+ /** Constrain a value to a range, comparing exactly. */
26
+ export declare function clampDecimal(value: string, min?: string, max?: string): string;
27
+ /** Whether a string is a well-formed decimal. */
28
+ export declare function isDecimal(value: string): boolean;
29
+ /** The separators a locale uses, e.g. `.`/`,` for `en`, `,`/`.` for `sl`. */
30
+ export declare function localeSeparators(locale: string): {
31
+ decimal: string;
32
+ group: string;
33
+ };
34
+ /**
35
+ * Render a canonical decimal for display, e.g. `"1234.56"` as `1.234,56` in
36
+ * `sl`. Formatting goes through the digits, never `Number`, so a value too
37
+ * large for a float still displays exactly.
38
+ */
39
+ export declare function formatForLocale(value: string, locale: string, precision?: number): string;
40
+ /**
41
+ * Render a canonical decimal for *editing*: the locale's decimal separator,
42
+ * but no grouping.
43
+ *
44
+ * Editing must not show the canonical `1234.56` in a locale where `.` groups
45
+ * digits — parsing that back would read it as `123456`.
46
+ */
47
+ export declare function toEditable(value: string, locale?: string): string;
48
+ /**
49
+ * Turn typed input into a canonical decimal, accepting the locale's
50
+ * separators. `1.234,56` in `sl` becomes `1234.56`.
51
+ */
52
+ export declare function parseFromLocale(input: string, locale: string): string | undefined;
53
+ export {};
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export type { ButtonProps } from './components/Button.vue';
9
9
  export type { ButtonGroupProps } from './components/ButtonGroup.vue';
10
10
  export type { CardProps } from './components/Card.vue';
11
11
  export type { CheckboxProps } from './components/Checkbox.vue';
12
+ export type { ComboboxItemGroup, ComboboxItemOption, ComboboxItems, ComboboxProps } from './components/Combobox.vue';
12
13
  export type { ConfirmDialogProps } from './components/ConfirmDialog.vue';
13
14
  export type { DialogProps } from './components/Dialog.vue';
14
15
  export type { DropdownMenuProps } from './components/DropdownMenu.vue';
@@ -17,10 +18,11 @@ export type { FormProps } from './components/Form.vue';
17
18
  export type { FormFieldProps } from './components/FormField.vue';
18
19
  export type { InputProps } from './components/Input.vue';
19
20
  export type { LabelProps } from './components/Label.vue';
21
+ export type { NumberInputProps } from './components/NumberInput.vue';
20
22
  export type { PaginationProps } from './components/Pagination.vue';
21
23
  export type { ProgressProps } from './components/Progress.vue';
22
24
  export type { RadioGroupItemOption, RadioGroupProps } from './components/RadioGroup.vue';
23
- export type { SelectItemOption, SelectProps } from './components/Select.vue';
25
+ export type { SelectItemGroup, SelectItemOption, SelectItems, SelectProps } from './components/Select.vue';
24
26
  export type { SeparatorProps } from './components/Separator.vue';
25
27
  export type { SkeletonProps } from './components/Skeleton.vue';
26
28
  export type { StatProps } from './components/Stat.vue';
@@ -33,6 +35,7 @@ export type { TooltipProps } from './components/Tooltip.vue';
33
35
  export * from './composables/appearance';
34
36
  export * from './composables/button-group';
35
37
  export * from './composables/confirm';
38
+ export * from './composables/decimal';
36
39
  export * from './composables/dropdown-menu';
37
40
  export * from './composables/form';
38
41
  export * from './composables/toast';