orio-ui 1.24.0 → 1.28.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 (102) hide show
  1. package/README.md +78 -3
  2. package/bin/orio-ui.mjs +72 -0
  3. package/dist/agents/ROUTING.md +140 -0
  4. package/dist/agents/component-finder.md +142 -0
  5. package/dist/agents/component-worker.md +152 -0
  6. package/dist/agents/snippet.md +6 -0
  7. package/dist/module.json +1 -1
  8. package/dist/runtime/components/AnimatedContainer.USAGE.md +79 -0
  9. package/dist/runtime/components/Badge.USAGE.md +75 -0
  10. package/dist/runtime/components/Banner.USAGE.md +52 -0
  11. package/dist/runtime/components/Button.USAGE.md +78 -0
  12. package/dist/runtime/components/Button.d.vue.ts +3 -2
  13. package/dist/runtime/components/Button.vue +19 -11
  14. package/dist/runtime/components/Button.vue.d.ts +3 -2
  15. package/dist/runtime/components/Calendar.USAGE.md +59 -0
  16. package/dist/runtime/components/Calendar.vue +254 -87
  17. package/dist/runtime/components/Canvas/USAGE.md +73 -0
  18. package/dist/runtime/components/CheckBox.USAGE.md +63 -0
  19. package/dist/runtime/components/CheckBox.vue +9 -3
  20. package/dist/runtime/components/CheckboxGroup.USAGE.md +95 -0
  21. package/dist/runtime/components/CheckboxGroup.vue +7 -1
  22. package/dist/runtime/components/ControlElement.USAGE.md +77 -0
  23. package/dist/runtime/components/ControlElement.d.vue.ts +42 -27
  24. package/dist/runtime/components/ControlElement.vue +28 -9
  25. package/dist/runtime/components/ControlElement.vue.d.ts +42 -27
  26. package/dist/runtime/components/DashedContainer.USAGE.md +65 -0
  27. package/dist/runtime/components/EmptyState.USAGE.md +65 -0
  28. package/dist/runtime/components/Form.USAGE.md +102 -0
  29. package/dist/runtime/components/Icon.USAGE.md +61 -0
  30. package/dist/runtime/components/Input.USAGE.md +57 -0
  31. package/dist/runtime/components/Input.vue +13 -3
  32. package/dist/runtime/components/ListItem.USAGE.md +84 -0
  33. package/dist/runtime/components/LoadingSpinner.USAGE.md +50 -0
  34. package/dist/runtime/components/LocaleSwitcher.USAGE.md +73 -0
  35. package/dist/runtime/components/Modal.USAGE.md +72 -0
  36. package/dist/runtime/components/NavButton.USAGE.md +80 -0
  37. package/dist/runtime/components/NavButton.d.vue.ts +0 -1
  38. package/dist/runtime/components/NavButton.vue +9 -5
  39. package/dist/runtime/components/NavButton.vue.d.ts +0 -1
  40. package/dist/runtime/components/NumberInput/Horizontal.USAGE.md +61 -0
  41. package/dist/runtime/components/NumberInput/Horizontal.vue +7 -2
  42. package/dist/runtime/components/NumberInput/USAGE.md +74 -0
  43. package/dist/runtime/components/NumberInput/Vertical.USAGE.md +55 -0
  44. package/dist/runtime/components/NumberInput/Vertical.vue +7 -2
  45. package/dist/runtime/components/NumberInput/index.d.vue.ts +0 -2
  46. package/dist/runtime/components/NumberInput/index.vue +9 -7
  47. package/dist/runtime/components/NumberInput/index.vue.d.ts +0 -2
  48. package/dist/runtime/components/Popover.USAGE.md +103 -0
  49. package/dist/runtime/components/RadioButton.USAGE.md +72 -0
  50. package/dist/runtime/components/RadioButton.d.vue.ts +0 -2
  51. package/dist/runtime/components/RadioButton.vue +9 -4
  52. package/dist/runtime/components/RadioButton.vue.d.ts +0 -2
  53. package/dist/runtime/components/Selector.USAGE.md +131 -0
  54. package/dist/runtime/components/Selector.d.vue.ts +1 -0
  55. package/dist/runtime/components/Selector.vue +10 -4
  56. package/dist/runtime/components/Selector.vue.d.ts +1 -0
  57. package/dist/runtime/components/SwitchButton.USAGE.md +62 -0
  58. package/dist/runtime/components/SwitchButton.d.vue.ts +1 -4
  59. package/dist/runtime/components/SwitchButton.vue +10 -7
  60. package/dist/runtime/components/SwitchButton.vue.d.ts +1 -4
  61. package/dist/runtime/components/Tag.USAGE.md +51 -0
  62. package/dist/runtime/components/TaggableSelector.USAGE.md +73 -0
  63. package/dist/runtime/components/TaggableSelector.vue +7 -1
  64. package/dist/runtime/components/Textarea.USAGE.md +72 -0
  65. package/dist/runtime/components/Textarea.vue +13 -3
  66. package/dist/runtime/components/Tooltip.USAGE.md +84 -0
  67. package/dist/runtime/components/ZoomableContainer.USAGE.md +108 -0
  68. package/dist/runtime/components/date/Picker.USAGE.md +52 -0
  69. package/dist/runtime/components/date/Picker.vue +7 -1
  70. package/dist/runtime/components/date/PickerTrigger.USAGE.md +65 -0
  71. package/dist/runtime/components/date/PickerTrigger.vue +9 -3
  72. package/dist/runtime/components/date/RangePicker.USAGE.md +97 -0
  73. package/dist/runtime/components/date/RangePicker.vue +7 -1
  74. package/dist/runtime/components/gallery/Carousel.USAGE.md +98 -0
  75. package/dist/runtime/components/gallery/CarouselPreview.USAGE.md +51 -0
  76. package/dist/runtime/components/upload/USAGE.md +91 -0
  77. package/dist/runtime/components/view/Dates.USAGE.md +67 -0
  78. package/dist/runtime/components/view/KeyBinds.USAGE.md +58 -0
  79. package/dist/runtime/components/view/Separator.USAGE.md +57 -0
  80. package/dist/runtime/components/view/Text.USAGE.md +68 -0
  81. package/dist/runtime/composables/useApi.USAGE.md +64 -0
  82. package/dist/runtime/composables/useControlSize.USAGE.md +73 -0
  83. package/dist/runtime/composables/useDecimalFormatter.USAGE.md +72 -0
  84. package/dist/runtime/composables/useFilter.USAGE.md +120 -0
  85. package/dist/runtime/composables/useFilter.d.ts +91 -0
  86. package/dist/runtime/composables/useFilter.js +111 -0
  87. package/dist/runtime/composables/useFuzzySearch.USAGE.md +68 -0
  88. package/dist/runtime/composables/useInertia.USAGE.md +80 -0
  89. package/dist/runtime/composables/useListKeyboard.USAGE.md +97 -0
  90. package/dist/runtime/composables/useModal.USAGE.md +82 -0
  91. package/dist/runtime/composables/usePinchZoom.USAGE.md +95 -0
  92. package/dist/runtime/composables/usePressAndHold.USAGE.md +70 -0
  93. package/dist/runtime/composables/useRovingGrid.USAGE.md +106 -0
  94. package/dist/runtime/composables/useRovingGrid.d.ts +35 -0
  95. package/dist/runtime/composables/useRovingGrid.js +115 -0
  96. package/dist/runtime/composables/useSound.USAGE.md +74 -0
  97. package/dist/runtime/composables/useTheme.USAGE.md +76 -0
  98. package/dist/runtime/composables/useUrlSync.USAGE.md +91 -0
  99. package/dist/runtime/composables/useValidation.USAGE.md +100 -0
  100. package/dist/runtime/i18n/en.json +4 -1
  101. package/dist/runtime/i18n/uk.json +4 -1
  102. package/package.json +12 -2
@@ -0,0 +1,57 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: text input, single-line input
5
+ short: text input wrapping ControlElement; supports inner-floating label layout
6
+ invariants: true
7
+ ---
8
+
9
+ # Input — agent-only invariants
10
+
11
+ `<orio-input>` is the text input wrapping `ControlElement`. Read
12
+ `ControlElement.USAGE.md` first — most of the contract lives there.
13
+
14
+ ## Invariants
15
+
16
+ - **Extends `ControlProps`** with one override: `layout?: InputLayout` where
17
+ `InputLayout = ControlLayout | "inner"`. The extra `"inner"` is an
18
+ Input-specific mode that floats the label inside the input chrome.
19
+ - **`layout="inner"` translates internally** to `layout="vertical"` on
20
+ ControlElement and adds an `.inner` class on the wrapper. The label
21
+ reposition is driven by `:deep()` styles on ControlElement internals — no
22
+ duplicate label DOM is created.
23
+ - **The `control` slot bag is spread onto the inner `<input>`** alongside
24
+ `$attrs`: `v-bind="{ ...$attrs, ...control }"`. Attrs like `type`,
25
+ `autocomplete`, `placeholder`, `inputmode` work on `<orio-input>` and land
26
+ on the underlying `<input>`.
27
+ - **v-model is `string`** (default `""`). For numeric input use
28
+ `<orio-number-input>` instead.
29
+
30
+ ## Gotchas
31
+
32
+ - The `.slot-wrapper` uses `display: flex; align-items: center;` so `before`
33
+ and `after` slots sit inline with the input. Don't add wrapping divs inside
34
+ those slots — they'll break alignment.
35
+ - `:placeholder-shown` is used internally for the inner-label "empty" state.
36
+ If you pass an empty placeholder, the inner-label trick still works because
37
+ the wrapper sets `placeholder=" "` upstream when needed.
38
+ - The default browser input border is removed; the visible border lives on
39
+ `.slot-wrapper`. Custom inputs swapped in via slots will not inherit it —
40
+ prefer `before`/`after` slots over replacing the input.
41
+
42
+ ## Quick reference
43
+
44
+ ```vue
45
+ <orio-input
46
+ v-model="email"
47
+ label="Email"
48
+ layout="inner"
49
+ type="email"
50
+ autocomplete="email"
51
+ :error="emailError"
52
+ >
53
+ <template #before>
54
+ <orio-icon name="mail" />
55
+ </template>
56
+ </orio-input>
57
+ ```
@@ -7,20 +7,30 @@ const props = defineProps({
7
7
  id: { type: String, required: false },
8
8
  label: { type: String, required: false },
9
9
  size: { type: String, required: false },
10
- fill: { type: Boolean, required: false }
10
+ fill: { type: Boolean, required: false },
11
+ tabindex: { type: [Number, String], required: false },
12
+ focusKey: { type: String, required: false },
13
+ disabled: { type: Boolean, required: false },
14
+ required: { type: Boolean, required: false },
15
+ name: { type: String, required: false },
16
+ ariaLabel: { type: String, required: false }
11
17
  });
12
18
  const modelValue = defineModel({ type: String, ...{ default: "" } });
13
19
  </script>
14
20
 
15
21
  <template>
16
22
  <orio-control-element
17
- v-slot="{ id }"
23
+ v-slot="{ control }"
18
24
  v-bind="props"
19
25
  :layout="layout === 'inner' ? 'vertical' : layout"
20
26
  :class="{ inner: layout === 'inner' }"
21
27
  >
22
28
  <slot name="before" />
23
- <input :id v-model="modelValue" type="text" v-bind="$attrs" />
29
+ <input
30
+ v-model="modelValue"
31
+ type="text"
32
+ v-bind="{ ...$attrs, ...control }"
33
+ />
24
34
  <slot name="after" />
25
35
  </orio-control-element>
26
36
  </template>
@@ -0,0 +1,84 @@
1
+ ---
2
+ kind: component
3
+ category: Media & misc
4
+ purpose: list row, list item, selectable row, list entry
5
+ short: `<li>` row with start/end slots and optional selectable checkbox-style behavior
6
+ invariants: true
7
+ ---
8
+
9
+ # ListItem — agent-only invariants
10
+
11
+ `<orio-list-item>` is a `<li>` row with three content zones (start /
12
+ center / end) and an optional selectable mode. It is also the row
13
+ primitive used internally by `<orio-selector>`.
14
+
15
+ ## Invariants
16
+
17
+ - **Renders an `<li>`.** Must be a child of `<ul>` / `<ol>` for valid
18
+ HTML. Rendering it loose works but breaks list semantics.
19
+ - **`selectable` prop** turns the row into an interactive checkbox-style
20
+ control:
21
+ - `tabindex="0"` (keyboard focusable).
22
+ - `role="checkbox"` + `aria-checked` reflect `selected`.
23
+ - Click and Enter / Space toggle the v-model.
24
+ - `cursor: pointer`.
25
+ - **v-model is `selected: boolean`.** Updates on toggle. Without
26
+ `selectable`, the model is still bound but no toggle handler runs.
27
+ - **Three slots**:
28
+ - `#start` — left zone. **Only renders** when the slot is provided
29
+ OR `selectable` is true. When selectable and no slot, defaults to
30
+ `<orio-check-box :model-value="selected">`.
31
+ - `#default` — center content. Always renders, `flex-grow: 1`.
32
+ - `#end` — right zone. Only renders when the slot is provided.
33
+ - **Selected state** uses `--color-accent` background and
34
+ `--color-accent-soft-darker` text. Hover swaps to surface bg when
35
+ not selected.
36
+ - **Used internally by `<orio-selector>`** as `role="option"` rows. In
37
+ that usage the role gets overridden via `$attrs`.
38
+
39
+ ## Gotchas
40
+
41
+ - **No multi-selection grouping.** A single ListItem holds one
42
+ selected boolean. For grouped list selection, wire each item's
43
+ `v-model:selected` to a parent array.
44
+ - **Default `<orio-check-box>` in `#start`** uses bare props — it has
45
+ no label, no accent state of its own. If the row is `selected`, the
46
+ checkbox shows checked.
47
+ - **Keyboard `Enter` and `Space` `.preventDefault()`** — Space won't
48
+ scroll the page, Enter won't submit a form. Useful, but
49
+ unconfigurable.
50
+ - **Without `selectable`**, the row is still clickable but no toggle /
51
+ focus / role is applied. To make it a button-like row without a
52
+ checkbox-style toggle, wrap content in a real `<button>` inside
53
+ `#default`.
54
+
55
+ ## Quick reference
56
+
57
+ ```vue
58
+ <template>
59
+ <ul>
60
+ <orio-list-item
61
+ v-for="item in items"
62
+ :key="item.id"
63
+ v-model:selected="item.selected"
64
+ selectable
65
+ >
66
+ <template #start>
67
+ <orio-icon :name="item.icon" />
68
+ </template>
69
+
70
+ {{ item.label }}
71
+
72
+ <template #end>
73
+ <orio-tag :text="item.badge" variant="accent" />
74
+ </template>
75
+ </orio-list-item>
76
+ </ul>
77
+ </template>
78
+ ```
79
+
80
+ ## Related
81
+
82
+ - `<orio-selector>` — uses this as listbox rows.
83
+ - `<orio-check-box>` — default `#start` content when selectable.
84
+ - Public API reference: `docs/components/list-item.md`.
@@ -0,0 +1,50 @@
1
+ ---
2
+ kind: component
3
+ category: Buttons & indicators
4
+ purpose: spinner, loading indicator, loading icon, busy indicator
5
+ short: thin wrapper that renders the bundled `loading-loop` icon; no props
6
+ invariants: false
7
+ ---
8
+
9
+ # LoadingSpinner — agent-only invariants
10
+
11
+ `<orio-loading-spinner>` renders the bundled `loading-loop` icon. That's
12
+ it. There are **no props**, no slots, no emits.
13
+
14
+ ## Invariants
15
+
16
+ - **Zero-prop wrapper.** Template is literally
17
+ `<orio-icon name="loading-loop" />`.
18
+ - **Animation lives in the icon SVG itself** (via the registry's
19
+ `loading-loop` entry). The component does not apply any CSS animation.
20
+ - **Size and color follow `<orio-icon>` defaults** — `1.5em` from
21
+ `--control-icon-size`, `currentColor`. Override via parent CSS:
22
+ `font-size`, `color`, or by passing direct CSS to a wrapper.
23
+ - **Used internally by `<orio-button :loading>`** — when wiring loading
24
+ states into buttons, prefer `:loading="..."` on the button to swapping
25
+ in this spinner manually.
26
+
27
+ ## Gotchas
28
+
29
+ - **No way to change spin direction, speed, or thickness.** The SVG is
30
+ fixed. For a custom spinner, render `<orio-icon>` with your own icon
31
+ name + CSS animation.
32
+ - **Aria semantics are absent.** No `role="status"`, no
33
+ `aria-label`. For screen-reader announcement, wrap in a `<span
34
+ role="status" aria-label="Loading">` at the consumer.
35
+
36
+ ## Quick reference
37
+
38
+ ```vue
39
+ <template>
40
+ <orio-loading-spinner v-if="loading" />
41
+ </template>
42
+ ```
43
+
44
+ ## Related
45
+
46
+ - `<orio-icon>` — under the hood; use it directly for non-spinner
47
+ glyphs.
48
+ - `<orio-button :loading>` — preferred way to show busy state on
49
+ buttons.
50
+ - Public API reference: `docs/components/loading-spinner.md`.
@@ -0,0 +1,73 @@
1
+ ---
2
+ kind: component
3
+ category: Media & misc
4
+ purpose: locale switcher, language toggle, i18n switcher
5
+ short: preconfigured Selector that mutates vue-i18n's locale; defaults to English + Ukrainian with flag emojis
6
+ invariants: true
7
+ ---
8
+
9
+ # LocaleSwitcher — agent-only invariants
10
+
11
+ `<orio-locale-switcher>` is a thin wrapper around `<orio-selector>` that
12
+ reads and writes `useI18n().locale` directly. Drop it anywhere in the app
13
+ to give users a language toggle.
14
+
15
+ ## Invariants
16
+
17
+ - **Mutates `useI18n().locale` on selection.** No model is exposed —
18
+ the side effect is the API.
19
+ - **Default `locales`**:
20
+ ```ts
21
+ [
22
+ { code: "en", flag: "🇬🇧", label: "English" },
23
+ { code: "uk", flag: "🇺🇦", label: "Українська" },
24
+ ]
25
+ ```
26
+ Override with the `locales` prop if your app supports a different set.
27
+ - **`LocaleOption` shape** (exported): `{ code, flag, label }`. All three
28
+ are strings; `flag` is rendered verbatim (emoji or any unicode).
29
+ - **Selector wiring**: `field: "code"`, `optionName: "label"`. Active
30
+ option matches by `code === currentLocale`. Falls back to the first
31
+ locale if the current i18n locale isn't in the list.
32
+ - **Custom `#trigger-label` and `#option` slots** render `flag + label`
33
+ side-by-side with 0.5rem gap.
34
+ - **Requires `useI18n` setup.** The component throws if called outside
35
+ a vue-i18n context.
36
+
37
+ ## Gotchas
38
+
39
+ - **Direct locale mutation bypasses any persistence layer.** If your
40
+ app saves locale to cookies / localStorage / API, hook into
41
+ `useI18n().locale` from elsewhere — this component does not call
42
+ any side effect beyond the i18n update.
43
+ - **Flag emojis depend on font support.** macOS / iOS render them
44
+ correctly; Windows often shows letter pairs (e.g. "GB", "UA"). For
45
+ cross-platform consistency, swap to icons via a custom `locales`
46
+ prop with icon names + a custom `#option`/`#trigger-label`.
47
+ - **No client/server hydration story.** If the locale is mutated
48
+ before vue-i18n is hydrated on the client, mismatches can occur.
49
+ Best to initialize locale in your app setup and let this switcher
50
+ only handle user-driven changes.
51
+
52
+ ## Quick reference
53
+
54
+ ```vue
55
+ <template>
56
+ <orio-locale-switcher />
57
+
58
+ <orio-locale-switcher
59
+ :locales="[
60
+ { code: 'en', flag: '🇺🇸', label: 'English' },
61
+ { code: 'es', flag: '🇪🇸', label: 'Español' },
62
+ { code: 'pt', flag: '🇧🇷', label: 'Português' },
63
+ ]"
64
+ />
65
+ </template>
66
+ ```
67
+
68
+ ## Related
69
+
70
+ - `<orio-selector>` — under the hood. Build your own switcher from
71
+ Selector if you need different side effects (e.g. routing).
72
+ - Public API reference: `docs/components/locale-switcher.md` (if
73
+ present).
@@ -0,0 +1,72 @@
1
+ ---
2
+ kind: component
3
+ category: Layout & containers
4
+ purpose: modal, dialog, popup overlay, lightbox
5
+ short: teleported overlay dialog with open-from-origin animation
6
+ invariants: true
7
+ ---
8
+
9
+ # Modal — agent-only invariants
10
+
11
+ `<orio-modal>` is the teleported overlay dialog.
12
+
13
+ ## Invariants
14
+
15
+ - **Teleported to `<body>`.** Renders outside the parent DOM subtree. Any
16
+ CSS that targets `.modal` from a parent will not apply; scope styles via
17
+ `:deep()` from a parent or write global styles.
18
+ - **`origin` prop drives the open animation.** Pass the `getBoundingClientRect`
19
+ of the element that triggered the open (e.g. the clicked button) to
20
+ animate the modal **from** that rect to centered. Pass `null` to fade in
21
+ at center with no scale-from.
22
+ - **`v-model:show`** controls visibility. Backdrop click closes (`@click.self`
23
+ on the overlay). The default close button (rendered when no `header`
24
+ slot is supplied) also toggles `show`.
25
+ - **Body scroll lock** is applied automatically while `show` is true, via
26
+ `useScrollLock` from `@vueuse/core`. SSR-safe: ref defaults to `false`
27
+ on the server.
28
+ - **Header/footer are auto-hidden** when no `title` prop and no
29
+ `#header`/`#footer` slot is present. The content section (`#default`)
30
+ always renders.
31
+
32
+ ## Gotchas
33
+
34
+ - Multiple modals stacked at once will all lock body scroll; closing one
35
+ releases the lock for all. If you nest modals, manage the lock yourself.
36
+ - `origin`'s `width` / `height` should be the trigger's rendered size, not
37
+ the modal's. The animation derives the inverse scale from
38
+ `width / modalWidth` — wrong size = visible jump.
39
+ - The component uses inline styles via direct `.style.transform`
40
+ assignment on the wrapper ref. Do not animate `transform` from outside;
41
+ the component will overwrite it on the next open.
42
+
43
+ ## Quick reference
44
+
45
+ ```vue
46
+ <script setup lang="ts">
47
+ import { ref } from "vue";
48
+ const open = ref(false);
49
+ const origin = ref<DOMRect | null>(null);
50
+
51
+ function trigger(event: MouseEvent) {
52
+ origin.value = (event.currentTarget as HTMLElement).getBoundingClientRect();
53
+ open.value = true;
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <orio-button @click="trigger">Open settings</orio-button>
59
+
60
+ <orio-modal v-model:show="open" :origin="origin" title="Settings">
61
+ <p>Modal body…</p>
62
+ <template #footer>
63
+ <orio-button @click="open = false">Done</orio-button>
64
+ </template>
65
+ </orio-modal>
66
+ </template>
67
+ ```
68
+
69
+ ## Related
70
+
71
+ - `useModal` composable — programmatic open/close API without managing
72
+ `show` yourself. See `docs/composables/use-modal.md`.
@@ -0,0 +1,80 @@
1
+ ---
2
+ kind: component
3
+ category: Buttons & indicators
4
+ purpose: nav button, link-styled button, navigation item, sidebar item
5
+ short: bare nav-styled button with `active` state and `aria-current="page"` for the current route
6
+ invariants: true
7
+ ---
8
+
9
+ # NavButton — agent-only invariants
10
+
11
+ `<orio-nav-button>` is a transparent, text-styled button for navigation
12
+ menus and tab bars. It is not a `<router-link>` — wrap it or wire
13
+ navigation in the `click` handler yourself.
14
+
15
+ ## Invariants
16
+
17
+ - **`active` prop is the "is this the current item" flag.** When true:
18
+ - Text becomes accent color, font-weight 600.
19
+ - `aria-current="page"` is set on the inner `<button>`.
20
+ - `undefined` (not removed) otherwise — so it doesn't appear in the
21
+ DOM at all when inactive.
22
+ - **`icon` prop OR `#icon` slot** — same pattern as `<orio-button>`.
23
+ - **Icon-only mode is auto-detected** (icon + no default slot) →
24
+ `border-radius: 50%`, `aspect-ratio: 1`, `padding: var(--control-py)`.
25
+ - **No `variant` prop.** One look only — transparent background, text
26
+ color, no border.
27
+ - **`disabled` blocks click** and applies 0.5 opacity + `cursor:
28
+ not-allowed`.
29
+ - **Only emits `click`.** No mousedown/mouseup like `<orio-button>`.
30
+ - **Focus ring**: `outline: 2px solid var(--color-accent)` with
31
+ `outline-offset: 2px`. Keyboard-only via `:focus-visible`.
32
+
33
+ ## Gotchas
34
+
35
+ - **Not a router link.** No `to`, no `href`. Wire navigation in
36
+ `@click`. If a real anchor is needed for a11y / right-click-to-open,
37
+ fall back to your router's link component.
38
+ - **Same `$attrs` duplication caveat as `<orio-button>`** — attrs may
39
+ land on both the wrapper and the inner `<button>`.
40
+ - **Active state is purely visual + ARIA**; the component does not
41
+ detect the current route. Compute `active` from `useRoute()` or your
42
+ router state.
43
+ - **`type` defaults to `submit`** (native default). Pass `type="button"`
44
+ if mounted inside a form to avoid accidental submits.
45
+
46
+ ## Quick reference
47
+
48
+ ```vue
49
+ <script setup lang="ts">
50
+ import { useRoute, useRouter } from "vue-router";
51
+
52
+ const route = useRoute();
53
+ const router = useRouter();
54
+ </script>
55
+
56
+ <template>
57
+ <nav>
58
+ <orio-nav-button
59
+ icon="home"
60
+ :active="route.path === '/'"
61
+ @click="router.push('/')"
62
+ >
63
+ {{ $t("nav.home") }}
64
+ </orio-nav-button>
65
+
66
+ <orio-nav-button
67
+ icon="settings"
68
+ :active="route.path === '/settings'"
69
+ @click="router.push('/settings')"
70
+ >
71
+ {{ $t("nav.settings") }}
72
+ </orio-nav-button>
73
+ </nav>
74
+ </template>
75
+ ```
76
+
77
+ ## Related
78
+
79
+ - `<orio-button>` — primary actions; use that for CTAs.
80
+ - Public API reference: `docs/components/nav-button.md`.
@@ -1,7 +1,6 @@
1
1
  import type { ControlProps } from "./ControlElement.vue.js";
2
2
  interface Props extends ControlProps {
3
3
  icon?: string;
4
- disabled?: boolean;
5
4
  active?: boolean;
6
5
  }
7
6
  declare var __VLS_8: {}, __VLS_15: {};
@@ -2,7 +2,6 @@
2
2
  import { computed, toRefs, useSlots } from "vue";
3
3
  const props = defineProps({
4
4
  icon: { type: String, required: false },
5
- disabled: { type: Boolean, required: false },
6
5
  active: { type: Boolean, required: false, default: false },
7
6
  appearance: { type: String, required: false },
8
7
  error: { type: [String, null], required: false },
@@ -11,7 +10,13 @@ const props = defineProps({
11
10
  label: { type: String, required: false },
12
11
  layout: { type: String, required: false },
13
12
  size: { type: String, required: false },
14
- fill: { type: Boolean, required: false }
13
+ fill: { type: Boolean, required: false },
14
+ tabindex: { type: [Number, String], required: false },
15
+ focusKey: { type: String, required: false },
16
+ disabled: { type: Boolean, required: false },
17
+ required: { type: Boolean, required: false },
18
+ name: { type: String, required: false },
19
+ ariaLabel: { type: String, required: false }
15
20
  });
16
21
  const { disabled, active } = toRefs(props);
17
22
  const slots = useSlots();
@@ -28,11 +33,10 @@ function click(event) {
28
33
  </script>
29
34
 
30
35
  <template>
31
- <orio-control-element v-bind="props">
36
+ <orio-control-element v-slot="{ control }" v-bind="props">
32
37
  <button
33
- v-bind="$attrs"
38
+ v-bind="{ ...$attrs, ...control }"
34
39
  :class="{ 'icon-only': isIconOnly, active }"
35
- :disabled
36
40
  :aria-current="active ? 'page' : void 0"
37
41
  @click="click"
38
42
  >
@@ -1,7 +1,6 @@
1
1
  import type { ControlProps } from "./ControlElement.vue.js";
2
2
  interface Props extends ControlProps {
3
3
  icon?: string;
4
- disabled?: boolean;
5
4
  active?: boolean;
6
5
  }
7
6
  declare var __VLS_8: {}, __VLS_15: {};
@@ -0,0 +1,61 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: number input horizontal, minus-plus stepper, quantity stepper
5
+ short: number input variant with minus/plus buttons flanking the field and press-and-hold repeat
6
+ invariants: true
7
+ ---
8
+
9
+ # NumberInput/Horizontal — agent-only invariants
10
+
11
+ `<orio-number-input-horizontal>` is a pre-styled wrapper around
12
+ `<orio-number-input>` that renders minus/plus buttons on either side of the
13
+ field. Read `NumberInput/USAGE.md` first — this variant inherits all of its
14
+ contract.
15
+
16
+ ## Invariants
17
+
18
+ - **Accepts the full `NumberInputProps` interface** plus a `disabled` prop
19
+ for the buttons. All props are forwarded via `v-bind="$props"`.
20
+ - **Buttons use `usePressAndHold`** — `@mousedown` starts the auto-repeat,
21
+ `@mouseup`/`@mouseleave` stops it. Hold to ramp through a range.
22
+ - **`disabled` and the per-button bound state both apply.** A minus button
23
+ is disabled when `disabled || isAtMin`; a plus button when
24
+ `disabled || isAtMax`.
25
+ - **Input text is centered** (`text-align: center` via `:deep(.number-input)`).
26
+ - **Controls are full-width inside the wrapper**: `justify-content: space-between`
27
+ with 3px horizontal padding. Buttons sit at the edges.
28
+ - **`layout="inner"` is supported** — the label centers between the two
29
+ buttons (`left: 0; right: 0; text-align: center`).
30
+ - **No keyboard auto-repeat.** Press-and-hold listens to mouse events
31
+ only; holding Enter on a focused button does not ramp.
32
+
33
+ ## Gotchas
34
+
35
+ - **Buttons are `<orio-button appearance="minimal" variant="subdued">`** —
36
+ they take theme tokens but are not slotted. To swap iconography, fall
37
+ back to the base `<orio-number-input>` with a custom `#controls` slot.
38
+ - **Touch behavior**: press-and-hold uses `@mousedown`/`@mouseup`. On touch
39
+ devices these may not fire reliably across all browsers — confirm on
40
+ iOS Safari if mobile is a target.
41
+
42
+ ## Quick reference
43
+
44
+ ```vue
45
+ <template>
46
+ <orio-number-input-horizontal
47
+ v-model="quantity"
48
+ :min="0"
49
+ :max="10"
50
+ :step="1"
51
+ :label="$t('cart.quantity')"
52
+ />
53
+ </template>
54
+ ```
55
+
56
+ ## Related
57
+
58
+ - `<orio-number-input>` — the base; use it when you need custom button
59
+ iconography or layout.
60
+ - `<orio-number-input-vertical>` — chevron-stack variant.
61
+ - `usePressAndHold` — composable behind the auto-repeat.
@@ -6,14 +6,19 @@ defineProps({
6
6
  max: { type: Number, required: false, default: void 0 },
7
7
  step: { type: Number, required: false, default: 1 },
8
8
  decimalPlaces: { type: Number, required: false, default: 0 },
9
- disabled: { type: Boolean, required: false, default: false },
10
9
  appearance: { type: String, required: false },
11
10
  error: { type: [String, null], required: false },
12
11
  group: { type: Boolean, required: false },
13
12
  id: { type: String, required: false },
14
13
  label: { type: String, required: false },
15
14
  size: { type: String, required: false },
16
- fill: { type: Boolean, required: false }
15
+ fill: { type: Boolean, required: false },
16
+ tabindex: { type: [Number, String], required: false },
17
+ focusKey: { type: String, required: false },
18
+ disabled: { type: Boolean, required: false, default: false },
19
+ required: { type: Boolean, required: false },
20
+ name: { type: String, required: false },
21
+ ariaLabel: { type: String, required: false }
17
22
  });
18
23
  const modelValue = defineModel({ type: Number, ...{ default: 0 } });
19
24
  const { pressAndHold, stop } = usePressAndHold();
@@ -0,0 +1,74 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: number input, numeric input, custom-control numeric stepper
5
+ short: numeric input base with overlay controls slot; pair with Horizontal/Vertical variants for ready-made spinners
6
+ invariants: true
7
+ ---
8
+
9
+ # NumberInput — agent-only invariants
10
+
11
+ `<orio-number-input>` is the **base** numeric input. By itself it renders a
12
+ `<input type="number">` with no spinner. Custom step controls go in the
13
+ `#controls` slot. For ready-made stepper UIs, use
14
+ `<orio-number-input-horizontal>` or `<orio-number-input-vertical>`.
15
+
16
+ ## Invariants
17
+
18
+ - **Extends `NumberInputProps`** (exported from this file): `ControlProps`
19
+ minus `layout`, plus `layout?: InputLayout`, `min`, `max`, `step`,
20
+ `decimalPlaces`. Same `"inner"` layout trick as Input/Textarea.
21
+ - **v-model is `number`** (default `0`). Native input value is coerced via
22
+ Vue's `v-model.number` semantics.
23
+ - **Validation runs on blur and on every increase/decrease.** Value is
24
+ clamped to `[min, max]` (if finite) and then rounded to `decimalPlaces`
25
+ via `toFixed`. Typing an out-of-range value is allowed *during* edit;
26
+ blur snaps it back.
27
+ - **`decimalPlaces` defaults to `0`.** Decimal input requires both
28
+ `decimalPlaces` and a matching `step` (e.g. `:decimalPlaces="2" :step="0.01"`).
29
+ - **Native webkit/firefox spin buttons are hidden** via CSS. Always
30
+ `appearance: textfield`.
31
+ - **`#controls` slot overlays the input absolutely** with `pointer-events:
32
+ none` on the container and `:deep(button) { pointer-events: auto }`.
33
+ Only buttons receive clicks; the rest of the overlay passes through to
34
+ the input.
35
+ - **`#controls` slot props**: `{ increase, decrease, isAtMax, isAtMin }`.
36
+ `increase`/`decrease` apply `step` and run validation; `isAtMax`/`isAtMin`
37
+ are `false` when `min`/`max` are undefined.
38
+ - **`min`, `max`, `step`, `decimalPlaces` are stripped from `controlProps`**
39
+ before forwarding to ControlElement — they do not pollute the wrapper's
40
+ prop bag.
41
+ - **`$attrs` is spread before `control`** on the inner `<input>`, same as
42
+ Input.
43
+
44
+ ## Gotchas
45
+
46
+ - **No spinner UI without the slot or a variant.** A bare
47
+ `<orio-number-input v-model="n" />` renders nothing in the controls area.
48
+ - **`min`/`max` of `0` are honored** because the check uses `Number.isFinite`,
49
+ not truthiness.
50
+ - **Blur normalization rewrites the model.** Even if the user types a value
51
+ that is already valid, it gets re-`toFixed`d on blur — `"3"` becomes
52
+ `"3.00"` displayed when `decimalPlaces: 2`.
53
+ - **Negative `step` is not blocked.** Passing `step: -1` makes increase
54
+ decrement.
55
+
56
+ ## Quick reference — custom controls slot
57
+
58
+ ```vue
59
+ <template>
60
+ <orio-number-input v-model="quantity" :min="1" :max="99" :label="$t('cart.qty')">
61
+ <template #controls="{ increase, decrease, isAtMax, isAtMin }">
62
+ <orio-button :disabled="isAtMin" @click="decrease">−</orio-button>
63
+ <orio-button :disabled="isAtMax" @click="increase">+</orio-button>
64
+ </template>
65
+ </orio-number-input>
66
+ </template>
67
+ ```
68
+
69
+ ## Related
70
+
71
+ - `<orio-number-input-horizontal>` — pre-styled minus/plus on either side.
72
+ - `<orio-number-input-vertical>` — pre-styled chevron stack on the right.
73
+ - `<orio-input>` — when you want raw text, not a number.
74
+ - Public API reference: `docs/components/number-input.md`.