orio-ui 1.27.0 → 1.28.1

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 (72) hide show
  1. package/README.md +76 -1
  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 +83 -0
  12. package/dist/runtime/components/Button.d.vue.ts +1 -0
  13. package/dist/runtime/components/Button.vue +5 -1
  14. package/dist/runtime/components/Button.vue.d.ts +1 -0
  15. package/dist/runtime/components/Calendar.USAGE.md +8 -0
  16. package/dist/runtime/components/Canvas/USAGE.md +8 -0
  17. package/dist/runtime/components/CheckBox.USAGE.md +63 -0
  18. package/dist/runtime/components/CheckboxGroup.USAGE.md +95 -0
  19. package/dist/runtime/components/ControlElement.USAGE.md +8 -0
  20. package/dist/runtime/components/ControlElement.d.vue.ts +1 -1
  21. package/dist/runtime/components/ControlElement.vue.d.ts +1 -1
  22. package/dist/runtime/components/DashedContainer.USAGE.md +65 -0
  23. package/dist/runtime/components/EmptyState.USAGE.md +65 -0
  24. package/dist/runtime/components/Form.USAGE.md +102 -0
  25. package/dist/runtime/components/Icon.USAGE.md +61 -0
  26. package/dist/runtime/components/Input.USAGE.md +8 -0
  27. package/dist/runtime/components/ListItem.USAGE.md +84 -0
  28. package/dist/runtime/components/LoadingSpinner.USAGE.md +50 -0
  29. package/dist/runtime/components/LocaleSwitcher.USAGE.md +73 -0
  30. package/dist/runtime/components/Modal.USAGE.md +8 -0
  31. package/dist/runtime/components/NavButton.USAGE.md +80 -0
  32. package/dist/runtime/components/NumberInput/Horizontal.USAGE.md +61 -0
  33. package/dist/runtime/components/NumberInput/Horizontal.vue +5 -0
  34. package/dist/runtime/components/NumberInput/USAGE.md +74 -0
  35. package/dist/runtime/components/NumberInput/Vertical.USAGE.md +55 -0
  36. package/dist/runtime/components/NumberInput/Vertical.vue +9 -1
  37. package/dist/runtime/components/Popover.USAGE.md +103 -0
  38. package/dist/runtime/components/RadioButton.USAGE.md +72 -0
  39. package/dist/runtime/components/Selector.USAGE.md +131 -0
  40. package/dist/runtime/components/SwitchButton.USAGE.md +62 -0
  41. package/dist/runtime/components/Tag.USAGE.md +51 -0
  42. package/dist/runtime/components/TaggableSelector.USAGE.md +73 -0
  43. package/dist/runtime/components/Textarea.USAGE.md +72 -0
  44. package/dist/runtime/components/Tooltip.USAGE.md +84 -0
  45. package/dist/runtime/components/ZoomableContainer.USAGE.md +108 -0
  46. package/dist/runtime/components/date/Picker.USAGE.md +8 -0
  47. package/dist/runtime/components/date/PickerTrigger.USAGE.md +65 -0
  48. package/dist/runtime/components/date/RangePicker.USAGE.md +97 -0
  49. package/dist/runtime/components/gallery/Carousel.USAGE.md +98 -0
  50. package/dist/runtime/components/gallery/CarouselPreview.USAGE.md +51 -0
  51. package/dist/runtime/components/upload/USAGE.md +91 -0
  52. package/dist/runtime/components/view/Dates.USAGE.md +67 -0
  53. package/dist/runtime/components/view/KeyBinds.USAGE.md +58 -0
  54. package/dist/runtime/components/view/Separator.USAGE.md +57 -0
  55. package/dist/runtime/components/view/Text.USAGE.md +68 -0
  56. package/dist/runtime/composables/useApi.USAGE.md +64 -0
  57. package/dist/runtime/composables/useControlSize.USAGE.md +73 -0
  58. package/dist/runtime/composables/useControlSize.js +12 -0
  59. package/dist/runtime/composables/useDecimalFormatter.USAGE.md +72 -0
  60. package/dist/runtime/composables/useFilter.USAGE.md +120 -0
  61. package/dist/runtime/composables/useFuzzySearch.USAGE.md +68 -0
  62. package/dist/runtime/composables/useInertia.USAGE.md +80 -0
  63. package/dist/runtime/composables/useListKeyboard.USAGE.md +97 -0
  64. package/dist/runtime/composables/useModal.USAGE.md +82 -0
  65. package/dist/runtime/composables/usePinchZoom.USAGE.md +95 -0
  66. package/dist/runtime/composables/usePressAndHold.USAGE.md +70 -0
  67. package/dist/runtime/composables/useRovingGrid.USAGE.md +106 -0
  68. package/dist/runtime/composables/useSound.USAGE.md +74 -0
  69. package/dist/runtime/composables/useTheme.USAGE.md +76 -0
  70. package/dist/runtime/composables/useUrlSync.USAGE.md +91 -0
  71. package/dist/runtime/composables/useValidation.USAGE.md +100 -0
  72. package/package.json +12 -2
@@ -0,0 +1,65 @@
1
+ ---
2
+ kind: component
3
+ category: Buttons & indicators
4
+ purpose: empty state, no-results placeholder, blank slate, empty list
5
+ short: centered empty-list placeholder with optional icon, title, description, and action slot
6
+ invariants: true
7
+ ---
8
+
9
+ # EmptyState — agent-only invariants
10
+
11
+ `<orio-empty-state>` is a centered placeholder for empty lists, no-search-
12
+ results screens, and similar blank slates.
13
+
14
+ ## Invariants
15
+
16
+ - **`title` is required.** Renders as `<orio-view-text type="title">` —
17
+ size scales with the empty-state `size` (large → medium title, others →
18
+ small title).
19
+ - **`description` is optional**, max-width `30ch`. Wraps after that.
20
+ - **`icon` is optional.** Rendered as `<orio-icon>` above the title with
21
+ reduced opacity (`0.5`). Icon size scales with `size`:
22
+ `small` → 2rem, `medium` → 3rem, `large` → 4rem.
23
+ - **Default slot is rendered after the description** — typically a CTA
24
+ button.
25
+ - **Always vertically stacked, centered, text-centered.** No prop to
26
+ switch to horizontal.
27
+ - **`size`**: `"small"` / `"medium"` (default) / `"large"`. Affects
28
+ padding, gap, and icon size — not title color or weight.
29
+
30
+ ## Gotchas
31
+
32
+ - **`title` and `description` are plain strings.** Pass i18n keys
33
+ through `$t()` from the parent; no `#title` / `#description` slot.
34
+ For inline `<strong>` or links inside copy, build a custom empty
35
+ state from view primitives.
36
+ - **Slot CTA spacing**: the default slot inherits the column gap
37
+ (`0.25` / `0.5` / `1rem` per size). Buttons sit tight against the
38
+ description — add margin if you need separation.
39
+ - **Used internally by `<orio-selector>` as the `no-options` default.**
40
+ When overriding via the `#no-options` slot, keep the layout similar
41
+ for visual consistency.
42
+
43
+ ## Quick reference
44
+
45
+ ```vue
46
+ <template>
47
+ <orio-empty-state
48
+ icon="inbox"
49
+ :title="$t('inbox.empty.title')"
50
+ :description="$t('inbox.empty.description')"
51
+ size="large"
52
+ >
53
+ <orio-button @click="createMessage">
54
+ {{ $t("inbox.empty.compose") }}
55
+ </orio-button>
56
+ </orio-empty-state>
57
+ </template>
58
+ ```
59
+
60
+ ## Related
61
+
62
+ - `<orio-view-text>` — used internally for title/description.
63
+ - `<orio-banner>` — for inline notice strips instead of full-block
64
+ empty states.
65
+ - Public API reference: `docs/components/empty-state.md`.
@@ -0,0 +1,102 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: form wrapper, submission, validation surface, auto-bind form model
5
+ short: form wrapper that auto-binds child controls by `name` prop to a single object v-model
6
+ invariants: true
7
+ ---
8
+
9
+ # Form — agent-only invariants
10
+
11
+ `<orio-form>` is a form wrapper that walks its slot vnodes and **auto-binds
12
+ child controls to a single object v-model** by matching each child's
13
+ `name` prop to a key (or dot path) in the model. Generic over `T extends
14
+ object`.
15
+
16
+ ## Invariants
17
+
18
+ - **v-model is an object `T`.** Each child with a `name="..."` prop
19
+ receives `modelValue` and `onUpdate:modelValue` cloned in via
20
+ `cloneVNode`. The user's hand-written v-model on that child is
21
+ overridden if `name` matches a field.
22
+ - **Auto-bind requires both `name` and a matching field path.** Children
23
+ without `name`, or with a `name` that doesn't resolve via `getByPath`,
24
+ render unchanged. Auto-bind is silent — no warning when a name doesn't
25
+ match.
26
+ - **Dot-paths supported**: `name="user.email"` writes to `model.user.email`.
27
+ The intermediate path must already exist (`setByPath` no-ops if the
28
+ parent is missing).
29
+ - **Children must follow the `modelValue` / `update:modelValue` contract.**
30
+ Auto-bind injects these props directly; v-model sugar is not used. Any
31
+ child that doesn't honor that pair is silently un-bound. All orio
32
+ form components do; custom children need to follow the contract.
33
+ - **Slot walking is recursive.** Vnode children, slot functions, and slot
34
+ objects are all descended. Wrappers (`<div>` groupers, layout columns)
35
+ are transparent.
36
+ - **`disabled` wraps the children in `<fieldset disabled>`** with
37
+ `display: contents` so nothing visual changes. The native `disabled`
38
+ attribute on the fieldset gates every form control inside (browser-
39
+ native behavior).
40
+ - **`loading` adds `pointer-events: none`** plus 0.6 opacity on the form.
41
+ Visual + interaction lock without disabling form values.
42
+ - **`novalidate` is set on the `<form>`** — HTML5 native validation
43
+ bubbles are off. Pair with `useValidation` for declarative checks.
44
+ - **`@submit` emits a single `submit` event** after `preventDefault`. The
45
+ emit is gated by `disabled || loading`. There is **no** built-in submit
46
+ button — render `<orio-button type="submit">` (or similar) inside.
47
+
48
+ ## Gotchas
49
+
50
+ - **No per-field error wiring.** `error` props on children are still
51
+ user-managed. The Form is only a binder, not a validator.
52
+ - **Reactivity through `setByPath` requires mutable nested objects.** A
53
+ frozen / readonly model breaks silently. Use a plain ref'd object.
54
+ - **Auto-bind clones the vnode** — keyed lists work, but if a child also
55
+ emits something other than `update:modelValue` via the same `onUpdate`
56
+ pattern, the clone replaces only the listed handlers.
57
+ - **Slot is descended into every render** — performance scales linearly
58
+ with slot depth. For very large forms (hundreds of fields), consider
59
+ splitting into nested `<orio-form>`s by section.
60
+ - **`name` collisions silently overwrite.** Two children with the same
61
+ `name` both bind to the same field; both updates write to the same
62
+ spot. Order is undefined.
63
+
64
+ ## Quick reference
65
+
66
+ ```vue
67
+ <script setup lang="ts">
68
+ interface Profile {
69
+ name: string;
70
+ email: string;
71
+ preferences: { newsletter: boolean };
72
+ }
73
+
74
+ const profile = ref<Profile>({
75
+ name: "",
76
+ email: "",
77
+ preferences: { newsletter: false },
78
+ });
79
+
80
+ function save() {
81
+ // Submit profile.value to API
82
+ }
83
+ </script>
84
+
85
+ <template>
86
+ <orio-form v-model="profile" :loading="saving" @submit="save">
87
+ <orio-input name="name" :label="$t('profile.name')" />
88
+ <orio-input name="email" :label="$t('profile.email')" type="email" />
89
+ <orio-check-box name="preferences.newsletter">
90
+ {{ $t("profile.newsletter") }}
91
+ </orio-check-box>
92
+
93
+ <orio-button type="submit">{{ $t("common.save") }}</orio-button>
94
+ </orio-form>
95
+ </template>
96
+ ```
97
+
98
+ ## Related
99
+
100
+ - `useValidation` — declarative validation rules for form fields.
101
+ - `<orio-control-element>` — wraps every form input with label/error.
102
+ - Public API reference: `docs/components/form.md`.
@@ -0,0 +1,61 @@
1
+ ---
2
+ kind: component
3
+ category: Buttons & indicators
4
+ purpose: icon, SVG renderer, glyph, symbol
5
+ short: SVG icon renderer that pulls from `utils/icon-registry` and renders via v-html
6
+ invariants: true
7
+ ---
8
+
9
+ # Icon — agent-only invariants
10
+
11
+ `<orio-icon>` renders an SVG icon from `utils/icon-registry` into a
12
+ `<span>` via `v-html`. Unknown icon names render empty.
13
+
14
+ ## Invariants
15
+
16
+ - **`name` is required.** Either a registered `IconName` (typed) or any
17
+ string. Unregistered names render as empty string (no visible
18
+ output, no warning).
19
+ - **`size`**: `string | number`. A number is treated as pixels (`"24px"`).
20
+ A string is used verbatim (`"1.5em"`, `"2rem"`, `"100%"`). When
21
+ `undefined`, falls back to the CSS var `--control-icon-size` (default
22
+ `1.5em`).
23
+ - **`color` defaults to `"currentColor"`** — the icon inherits parent
24
+ text color. Pass a CSS color string to override.
25
+ - **`v-html` is used to inject the raw SVG markup.** The icon registry
26
+ is the trust boundary — never render dynamic / user-controlled SVG
27
+ strings through it.
28
+ - **SVGs inside use `fill: currentColor`** so the `color` prop / parent
29
+ color flows through.
30
+ - **`flex-shrink: 0`** is set on the span. The icon never shrinks inside
31
+ a flex layout — important for inline labels with overflow.
32
+ - **The wrapper is `display: inline-flex; align-items: center; justify-content: center`**
33
+ so the SVG is centered when the size exceeds the SVG viewBox.
34
+
35
+ ## Gotchas
36
+
37
+ - **Bypass-XSS surface is the registry.** Anything in `utils/icon-registry`
38
+ is rendered as raw HTML. Do not extend the registry with strings
39
+ derived from user input.
40
+ - **Sizing a string `100%` requires a sized parent.** The span goes to
41
+ the size you pass; the inner SVG fills 100% of that.
42
+ - **`color` is applied via inline style**, so it beats class-based
43
+ styling. To recolor via CSS class, omit the `color` prop.
44
+ - **Spelling errors silently render nothing.** If an icon is missing
45
+ during dev, check the registry — there's no console warning.
46
+
47
+ ## Quick reference
48
+
49
+ ```vue
50
+ <template>
51
+ <orio-icon name="check" :size="24" />
52
+ <orio-icon name="warning" size="1.5rem" color="var(--color-alert)" />
53
+ <orio-icon name="search" />
54
+ </template>
55
+ ```
56
+
57
+ ## Related
58
+
59
+ - `<orio-loading-spinner>` — thin wrapper for the `loading-loop` icon.
60
+ - `utils/icon-registry` — the source of all available icon names.
61
+ - Public API reference: `docs/components/icon.md`.
@@ -1,3 +1,11 @@
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
+
1
9
  # Input — agent-only invariants
2
10
 
3
11
  `<orio-input>` is the text input wrapping `ControlElement`. Read
@@ -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).
@@ -1,3 +1,11 @@
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
+
1
9
  # Modal — agent-only invariants
2
10
 
3
11
  `<orio-modal>` is the teleported overlay dialog.
@@ -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`.
@@ -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.
@@ -31,6 +31,7 @@ const { pressAndHold, stop } = usePressAndHold();
31
31
  appearance="minimal"
32
32
  icon="minus"
33
33
  variant="subdued"
34
+ size="sm"
34
35
  :disabled="isAtMin || disabled"
35
36
  @mousedown="pressAndHold(decrease)"
36
37
  @mouseup="stop"
@@ -40,6 +41,7 @@ const { pressAndHold, stop } = usePressAndHold();
40
41
  appearance="minimal"
41
42
  icon="plus"
42
43
  variant="subdued"
44
+ size="sm"
43
45
  :disabled="isAtMax || disabled"
44
46
  @mousedown="pressAndHold(increase)"
45
47
  @mouseup="stop"
@@ -65,4 +67,7 @@ const { pressAndHold, stop } = usePressAndHold();
65
67
  right: 0;
66
68
  text-align: center;
67
69
  }
70
+ .horizontal :deep(.control-group) {
71
+ z-index: 2;
72
+ }
68
73
  </style>