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,55 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: number input vertical, chevron stepper, stacked-arrow numeric input
5
+ short: number input variant with chevron up/down stacked on the right and press-and-hold repeat
6
+ invariants: true
7
+ ---
8
+
9
+ # NumberInput/Vertical — agent-only invariants
10
+
11
+ `<orio-number-input-vertical>` is a pre-styled wrapper around
12
+ `<orio-number-input>` that renders stacked chevron up/down buttons on the
13
+ right edge. Read `NumberInput/USAGE.md` first — this variant inherits all
14
+ of its contract.
15
+
16
+ ## Invariants
17
+
18
+ - **Accepts the full `NumberInputProps` interface** plus a `disabled` prop
19
+ for the buttons. Forwarded via `v-bind="$props"`.
20
+ - **Buttons use `usePressAndHold`** — `@mousedown` starts auto-repeat,
21
+ `@mouseup`/`@mouseleave` stops.
22
+ - **Chevron up = `increase`, chevron down = `decrease`.** Standard
23
+ direction; do not swap them in a consumer.
24
+ - **Buttons are stacked vertically** in a column flex (`flex-direction:
25
+ column; justify-content: space-around`) anchored to `right: 3px`.
26
+ - **Input remains left-aligned** — text stays at its natural alignment;
27
+ only the controls move.
28
+
29
+ ## Gotchas
30
+
31
+ - **Padding-right may need a bump on long values.** The chevron stack
32
+ overlaps the input's right edge. For decimal values with many digits,
33
+ numbers may render under the buttons. Pad the input or switch to
34
+ `<orio-number-input-horizontal>`.
35
+ - **No keyboard auto-repeat.** Same limitation as the horizontal variant.
36
+
37
+ ## Quick reference
38
+
39
+ ```vue
40
+ <template>
41
+ <orio-number-input-vertical
42
+ v-model="zoomPercent"
43
+ :min="10"
44
+ :max="400"
45
+ :step="5"
46
+ :label="$t('editor.zoom')"
47
+ />
48
+ </template>
49
+ ```
50
+
51
+ ## Related
52
+
53
+ - `<orio-number-input>` — base; use for custom controls.
54
+ - `<orio-number-input-horizontal>` — minus/plus variant.
55
+ - `usePressAndHold` — composable behind 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();
@@ -6,7 +6,6 @@ export interface NumberInputProps extends Omit<ControlProps, "layout"> {
6
6
  max?: number;
7
7
  step?: number;
8
8
  decimalPlaces?: number;
9
- disabled?: boolean;
10
9
  }
11
10
  type __VLS_Props = NumberInputProps;
12
11
  declare function increase(): void;
@@ -30,7 +29,6 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
30
29
  "onUpdate:modelValue"?: ((value: number) => any) | undefined;
31
30
  }>, {
32
31
  layout: InputLayout;
33
- disabled: boolean;
34
32
  step: number;
35
33
  min: number;
36
34
  max: number;
@@ -6,14 +6,19 @@ const props = 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 },
19
+ required: { type: Boolean, required: false },
20
+ name: { type: String, required: false },
21
+ ariaLabel: { type: String, required: false }
17
22
  });
18
23
  const { min, max, step, decimalPlaces } = toRefs(props);
19
24
  const modelValue = defineModel({ type: Number, ...{ default: 0 } });
@@ -49,7 +54,6 @@ const controlProps = computed(() => {
49
54
  max: _max,
50
55
  step: _step,
51
56
  decimalPlaces: _decimalPlaces,
52
- disabled: _disabled,
53
57
  ...rest
54
58
  } = props;
55
59
  return rest;
@@ -64,19 +68,17 @@ const slotExpose = computed(() => ({
64
68
 
65
69
  <template>
66
70
  <orio-control-element
67
- v-slot="{ id }"
71
+ v-slot="{ control }"
68
72
  v-bind="controlProps"
69
73
  :layout="layout === 'inner' ? 'vertical' : layout"
70
74
  :class="{ inner: layout === 'inner' }"
71
75
  >
72
76
  <div class="wrapper">
73
77
  <input
74
- v-bind="$attrs"
75
- :id
78
+ v-bind="{ ...$attrs, ...control }"
76
79
  v-model="modelValue"
77
80
  type="number"
78
81
  class="number-input"
79
- :disabled
80
82
  :min
81
83
  :max
82
84
  :step
@@ -6,7 +6,6 @@ export interface NumberInputProps extends Omit<ControlProps, "layout"> {
6
6
  max?: number;
7
7
  step?: number;
8
8
  decimalPlaces?: number;
9
- disabled?: boolean;
10
9
  }
11
10
  type __VLS_Props = NumberInputProps;
12
11
  declare function increase(): void;
@@ -30,7 +29,6 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {}, {
30
29
  "onUpdate:modelValue"?: ((value: number) => any) | undefined;
31
30
  }>, {
32
31
  layout: InputLayout;
33
- disabled: boolean;
34
32
  step: number;
35
33
  min: number;
36
34
  max: number;
@@ -0,0 +1,103 @@
1
+ ---
2
+ kind: component
3
+ category: Layout & containers
4
+ purpose: popover, anchored floating panel, dropdown menu base, contextual menu
5
+ short: anchored floating panel teleported to body with auto-flip placement and click-outside dismissal
6
+ invariants: true
7
+ ---
8
+
9
+ # Popover — agent-only invariants
10
+
11
+ `<orio-popover>` anchors a floating panel to a trigger element. The trigger
12
+ is the default slot; the panel content is the `#content` slot.
13
+
14
+ ## Invariants
15
+
16
+ - **Two slots, both get `{ toggle, isOpen }`.** Default slot wraps the
17
+ trigger; `#content` is the floating panel. You **must** call `toggle`
18
+ yourself — the component does not auto-attach a click handler to the
19
+ trigger.
20
+ - **Panel is teleported to `<body>`**. Parent CSS scoping does not reach
21
+ it. Use `:deep()` from a parent, global styles, or scope the panel via
22
+ classes you control.
23
+ - **`toggle(force?)`** — `toggle(true)` forces open, `toggle(false)` forces
24
+ close, `toggle()` flips. While `disabled` is true, all three are no-ops.
25
+ - **`position` syntax is `main-sub`, not "diagonal".** `top-left` means
26
+ *above the trigger* with the panel's **right edge** aligned to the
27
+ trigger's right edge — NOT "above and to the left of the trigger".
28
+ `left-top` means *left of the trigger* with **top** edges aligned.
29
+ Single-word values (`"top"`, `"left"`, …) center on the cross axis.
30
+ - **Placement auto-flips.** If the requested position doesn't fit in the
31
+ viewport, the component tries the opposite-main, then center variants,
32
+ then perpendicular axes — first fit wins. `currentPosition` is the
33
+ resolved value. No way to disable the fallback.
34
+ - **`offset` is gap in px between trigger and panel.** Default `10`.
35
+ - **Click outside closes**, with `triggerRef` in the ignore list.
36
+ - **Reposition on scroll/resize** uses capture-phase listeners on `window`,
37
+ so it catches nested scrolling ancestors. Panel resizes via
38
+ `useElementBounding` also trigger reposition.
39
+ - **No keyboard support.** No Esc-to-close, no focus trap, no return-focus.
40
+ If you need those, wire them at the consumer level on the `#content`
41
+ slot.
42
+ - **No visual chrome.** The wrapper is `background: transparent; border: 0;
43
+ position: fixed; z-index: 999999`. The `#content` slot must render its
44
+ own surface (card, panel, menu).
45
+
46
+ ## Gotchas
47
+
48
+ - **Trigger must accept the slot props.** Use `v-slot="{ toggle, isOpen }"`
49
+ on the default slot — without it, the trigger has no way to open the
50
+ popover.
51
+ - **No arrow / caret.** Add your own pseudo-element on the content slot if
52
+ needed; the wrapper offers no anchor point.
53
+ - **First-paint flicker is avoided via `visibility: hidden`** during
54
+ measurement, but only briefly. If your trigger animates while opening,
55
+ the trigger rect read on `nextTick` may be mid-animation — measure after
56
+ the animation, or open without animating the trigger.
57
+ - **`appear` transition fires only on first paint.** Reopening the same
58
+ popover does not re-trigger `appear` — only the standard enter
59
+ transition. Make sure your `animate-fade-slide` styles cover both.
60
+ - **z-index is hard-coded to `999999`.** Stacking with other body-teleports
61
+ (Modal at the same z-index) is order-dependent — the later-mounted
62
+ element wins. Mount-order is not stable across HMR.
63
+ - **No `v-model:show`.** State is internal. Drive it through `toggle` (via
64
+ the slot prop) or by mounting/unmounting the component with `v-if`.
65
+
66
+ ## Quick reference
67
+
68
+ ```vue
69
+ <template>
70
+ <orio-popover position="bottom-left" :offset="8">
71
+ <template #default="{ toggle, isOpen }">
72
+ <orio-button @click="toggle()">
73
+ Menu {{ isOpen ? "▴" : "▾" }}
74
+ </orio-button>
75
+ </template>
76
+
77
+ <template #content="{ toggle }">
78
+ <div class="menu-panel">
79
+ <button @click="onEdit(); toggle(false)">Edit</button>
80
+ <button @click="onDelete(); toggle(false)">Delete</button>
81
+ </div>
82
+ </template>
83
+ </orio-popover>
84
+ </template>
85
+
86
+ <style scoped>
87
+ .menu-panel {
88
+ background: var(--color-surface);
89
+ border: 1px solid var(--color-border);
90
+ border-radius: var(--border-radius-md);
91
+ padding: 0.5rem;
92
+ display: flex;
93
+ flex-direction: column;
94
+ }
95
+ </style>
96
+ ```
97
+
98
+ ## Related
99
+
100
+ - `<orio-tooltip>` — for hover/focus hints; do not use Popover for tooltips.
101
+ - `<orio-modal>` — when you need a backdrop, focus trap, and centered
102
+ dialog.
103
+ - Public API reference: `docs/components/popover.md`.
@@ -0,0 +1,72 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: radio, radio button, single-choice from group
5
+ short: single radio option wrapping ControlElement; group by sharing the same v-model
6
+ invariants: true
7
+ ---
8
+
9
+ # RadioButton — agent-only invariants
10
+
11
+ `<orio-radio-button>` is one radio option. There is **no `RadioGroup`** —
12
+ grouping is done by sharing the same v-model across multiple instances.
13
+
14
+ ## Invariants
15
+
16
+ - **`value` is the option's payload.** It is what gets written to v-model
17
+ when this radio is selected. Comparison is strict (`===`).
18
+ - **v-model is `unknown`** (typically `string` / `number` / object id) —
19
+ the selected value. Two `<orio-radio-button>`s with the same v-model
20
+ form a group; whichever value matches the model is checked.
21
+ - **`text` prop _or_ default slot** for the inline label. Slot wins.
22
+ Render nothing if both are absent.
23
+ - **`hideLabel` prop** applies `.sr-only` to the text — visually hidden,
24
+ still announced by screen readers. Use for icon-only radios that still
25
+ need an accessible name.
26
+ - **Visual is a CSS-only rounded box** with `::after` for the inner dot
27
+ when checked.
28
+ - **Native `<input type="radio">` has `tabindex="-1"`** in the template.
29
+ Default tab order skips it; clicking the label still toggles. If you
30
+ need keyboard arrow-key roving across the group, layer that on top —
31
+ it is not provided.
32
+ - **No `name` HTML attribute by default.** Two browser radios with the
33
+ same DOM `name` form a native group; this component groups via Vue
34
+ v-model only. Set `name` via `$attrs` if a `<form>` submission needs
35
+ the radio group serialized natively.
36
+
37
+ ## Gotchas
38
+
39
+ - **Multiple instances bound to the same v-model are not visually grouped.**
40
+ Layout (column / row) is up to the consumer — wrap them in a div or
41
+ `<orio-control-element group>` with a shared label.
42
+ - **Object values must be the same reference** as the one stored in the
43
+ model. Comparing `{ id: 1 }` to a new `{ id: 1 }` is false. Bind to
44
+ primitive ids when possible.
45
+ - **No "deselect" behavior.** Once a radio is selected, clicking it again
46
+ does not clear. To allow clearing, expose a separate "None" radio with
47
+ `value: null`.
48
+ - **`required` from `ControlProps`** flows through but only takes effect
49
+ inside a `<form>` that calls `reportValidity`.
50
+
51
+ ## Quick reference
52
+
53
+ ```vue
54
+ <script setup lang="ts">
55
+ const plan = defineModel<"basic" | "pro" | "team">();
56
+ </script>
57
+
58
+ <template>
59
+ <orio-control-element group :label="$t('billing.plan')">
60
+ <orio-radio-button v-model="plan" value="basic" :text="$t('billing.basic')" />
61
+ <orio-radio-button v-model="plan" value="pro" :text="$t('billing.pro')" />
62
+ <orio-radio-button v-model="plan" value="team" :text="$t('billing.team')" />
63
+ </orio-control-element>
64
+ </template>
65
+ ```
66
+
67
+ ## Related
68
+
69
+ - `<orio-control-element>` (group mode) — wrap multiple radios for an
70
+ accessible group label.
71
+ - `<orio-switch-button>` — boolean on/off pill button.
72
+ - Public API reference: `docs/components/radio-button.md`.
@@ -2,8 +2,6 @@ import type { ControlProps } from "./ControlElement.vue.js";
2
2
  export interface RadioButtonProps extends ControlProps {
3
3
  /** The value this radio represents; compared to v-model to determine checked state */
4
4
  value?: unknown;
5
- /** HTML name attribute — groups radios together so only one is selected at a time */
6
- name?: string;
7
5
  /** Inline label text (alternative to default slot) */
8
6
  text?: string;
9
7
  /** Visually hides the label while keeping it accessible to SR (screen readers) */
@@ -2,7 +2,6 @@
2
2
  const modelValue = defineModel({ type: null });
3
3
  const props = defineProps({
4
4
  value: { type: null, required: false },
5
- name: { type: String, required: false },
6
5
  text: { type: String, required: false },
7
6
  hideLabel: { type: Boolean, required: false },
8
7
  appearance: { type: String, required: false },
@@ -12,17 +11,23 @@ const props = defineProps({
12
11
  label: { type: String, required: false },
13
12
  layout: { type: String, required: false },
14
13
  size: { type: String, required: false },
15
- fill: { type: Boolean, required: false }
14
+ fill: { type: Boolean, required: false },
15
+ tabindex: { type: [Number, String], required: false },
16
+ focusKey: { type: String, required: false },
17
+ disabled: { type: Boolean, required: false },
18
+ required: { type: Boolean, required: false },
19
+ name: { type: String, required: false },
20
+ ariaLabel: { type: String, required: false }
16
21
  });
17
22
  </script>
18
23
 
19
24
  <template>
20
- <orio-control-element v-bind="props" class="radio">
25
+ <orio-control-element v-slot="{ control }" v-bind="props" class="radio">
21
26
  <label class="radio-label">
22
27
  <input
23
28
  v-model="modelValue"
29
+ v-bind="control"
24
30
  type="radio"
25
- :name="name"
26
31
  :value="value"
27
32
  class="radio-input"
28
33
  tabindex="-1"
@@ -2,8 +2,6 @@ import type { ControlProps } from "./ControlElement.vue.js";
2
2
  export interface RadioButtonProps extends ControlProps {
3
3
  /** The value this radio represents; compared to v-model to determine checked state */
4
4
  value?: unknown;
5
- /** HTML name attribute — groups radios together so only one is selected at a time */
6
- name?: string;
7
5
  /** Inline label text (alternative to default slot) */
8
6
  text?: string;
9
7
  /** Visually hides the label while keeping it accessible to SR (screen readers) */
@@ -0,0 +1,131 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: select, dropdown, combobox, listbox picker, single or multi-select
5
+ short: button-triggered listbox in a popover; supports single and multi-select with string or object options
6
+ invariants: true
7
+ ---
8
+
9
+ # Selector — agent-only invariants
10
+
11
+ `<orio-selector>` is a button-triggered listbox rendered inside an
12
+ `<orio-popover>`. It is **not** a `<select>` element and **not** a
13
+ combobox — there is no text input or filter. For free-text + filter,
14
+ combine with `useFuzzySearch` and slot `#options-addon`. Generic over
15
+ `T extends object`.
16
+
17
+ ## Invariants
18
+
19
+ - **`options` accepts `string` or object items** (typed
20
+ `SelectableOption<T> = string | T`). Mixing is allowed but uncommon —
21
+ treat the array as homogenous.
22
+ - **For object options, set `field`** (defaults to `"id"`) to the
23
+ uniqueness key, and **`optionName`** to the display label key. Without
24
+ `optionName`, objects render as `JSON.stringify(option)` — visible bug.
25
+ - **v-model is required.** Type: `SelectableOption | SelectableOption[] |
26
+ null | undefined`. Single-select binds the option (or its primitive);
27
+ multi-select binds an array.
28
+ - **`multiple: true` mutates the bound array in place** via
29
+ `modelValue.value.splice(...)` / `modelValue.value.push(...)`. The bound
30
+ ref must be a writable array — a deep `readonly` v-model blocks those
31
+ mutations: Vue warns in dev (`Set operation on key … failed: target is
32
+ readonly`) and fails silently in prod, so the selection won't update.
33
+ - **Single-select closes the popover on choice**; multi-select keeps it
34
+ open. Wire your own close on multi-select via the `option` slot
35
+ `toggle` prop if needed.
36
+ - **Built on `<orio-popover>` with `position="bottom-right"` offset 5**.
37
+ Same teleport/scroll/auto-flip caveats apply.
38
+ - **Built on `<orio-list-item>` rows** with `role="option"` and
39
+ `aria-selected`. Listbox itself has `role="listbox"` and
40
+ `aria-multiselectable` when `multiple` is set.
41
+ - **Keyboard via `useListKeyboard`**: arrow-down opens (or moves
42
+ highlight), Enter selects, Esc closes. `popoverToggleRef` is captured
43
+ lazily from the trigger slot — the first keydown after mount may
44
+ no-op if the popover hasn't initialized; subsequent keys work.
45
+ - **i18n keys are required.** The component calls `useI18n()` and
46
+ references `selector.placeholder`, `selector.selected` ({count}), and
47
+ `selector.noOptions`. Consumers must have these keys in their locale
48
+ files.
49
+ - **`useControlTokens(size)`** injects CSS vars (`--control-py`,
50
+ `--control-px`, etc.) onto the popover content — size prop on the
51
+ Selector flows through to dropdown padding.
52
+
53
+ ## Slots
54
+
55
+ - `#trigger` — replaces the entire button. Receives `{ toggle, control }`.
56
+ - `#trigger-content` — replaces the *inside* of the default button.
57
+ Receives `{ toggle, getOptionKey, getOptionLabel, attrs }`. Use to
58
+ customize the label/chevron without rebuilding the button shell.
59
+ - `#trigger-label` — replaces only the label text. Receives same props.
60
+ Use to render tags for multi-select instead of "N selected".
61
+ - `#option` — replaces each row's content. Receives `{ option, toggle,
62
+ selected, getOptionKey, getOptionLabel }`.
63
+ - `#no-options` — replaces the default `<orio-empty-state>` when
64
+ `options.length === 0`.
65
+ - `#options-addon` — extra content rendered **after** the list (e.g. a
66
+ "create new" button, a search input).
67
+
68
+ ## Gotchas
69
+
70
+ - **No built-in search.** For filterable selectors, render an
71
+ `<orio-input>` in `#options-addon` and pass a filtered array to
72
+ `:options`.
73
+ - **Multi-select trigger shows "N selected" by default.** Override via
74
+ `#trigger-label` to render tags — consider `<orio-tag>` chips. Removal
75
+ must call `toggleOption` (exposed via `#option` slot, not the trigger).
76
+ - **String options bypass `field`/`optionName`.** Selection equality is
77
+ `===`. Object options compare via `field`.
78
+ - **`placeholder` falls back to `t("selector.placeholder")`**. Pass an
79
+ explicit `placeholder` to override per-instance; do not assume English.
80
+ - **`controlProps` strip is exhaustive** — `options`, `multiple`,
81
+ `field`, `optionName`, `placeholder` never reach the ControlElement
82
+ wrapper. Adding new selector-specific props requires adding them to the
83
+ strip list.
84
+ - **Trigger is a `<button>`**, not a real `<select>`. Form serialization
85
+ and native submission do not include the value — handle submit
86
+ manually.
87
+
88
+ ## Quick reference — single-select with object options
89
+
90
+ ```vue
91
+ <script setup lang="ts">
92
+ interface Country { id: string; name: string }
93
+ const countries: Country[] = [
94
+ { id: "uk", name: "United Kingdom" },
95
+ { id: "fi", name: "Finland" },
96
+ ];
97
+ const country = defineModel<Country | null>({ default: null });
98
+ </script>
99
+
100
+ <template>
101
+ <orio-selector
102
+ v-model="country"
103
+ :options="countries"
104
+ field="id"
105
+ option-name="name"
106
+ :label="$t('settings.country')"
107
+ />
108
+ </template>
109
+ ```
110
+
111
+ ## Quick reference — multi-select with tag chips
112
+
113
+ ```vue
114
+ <template>
115
+ <orio-selector v-model="tags" :options="allTags" multiple field="id" option-name="label">
116
+ <template #trigger-label>
117
+ <orio-tag v-for="tag in tags" :key="tag.id" removable @remove="removeTag(tag)">
118
+ {{ tag.label }}
119
+ </orio-tag>
120
+ </template>
121
+ </orio-selector>
122
+ </template>
123
+ ```
124
+
125
+ ## Related
126
+
127
+ - `<orio-taggable-selector>` — pre-built multi-select with tag chips.
128
+ - `<orio-list-item>` — the row primitive used inside the listbox.
129
+ - `useListKeyboard` — keyboard nav composable.
130
+ - `useFuzzySearch` — pair with `#options-addon` for a search filter.
131
+ - Public API reference: `docs/components/selector.md`.
@@ -20,6 +20,7 @@ declare const __VLS_export: <T extends object>(__VLS_props: NonNullable<Awaited<
20
20
  slots: {
21
21
  trigger?: (props: {
22
22
  toggle: any;
23
+ control: any;
23
24
  }) => any;
24
25
  } & {
25
26
  'trigger-content'?: (props: {
@@ -17,7 +17,13 @@ const props = defineProps({
17
17
  label: { type: String, required: false },
18
18
  layout: { type: String, required: false },
19
19
  size: { type: String, required: false },
20
- fill: { type: Boolean, required: false }
20
+ fill: { type: Boolean, required: false },
21
+ tabindex: { type: [Number, String], required: false },
22
+ focusKey: { type: String, required: false },
23
+ disabled: { type: Boolean, required: false },
24
+ required: { type: Boolean, required: false },
25
+ name: { type: String, required: false },
26
+ ariaLabel: { type: String, required: false }
21
27
  });
22
28
  const { field, optionName } = toRefs(props);
23
29
  const resolvedPlaceholder = computed(
@@ -107,12 +113,12 @@ const {
107
113
  </script>
108
114
 
109
115
  <template>
110
- <orio-control-element v-bind="controlProps">
116
+ <orio-control-element v-slot="{ control }" v-bind="controlProps">
111
117
  <orio-popover position="bottom-right" :offset="5">
112
118
  <template #default="{ toggle, isOpen }">
113
- <slot name="trigger" :toggle>
119
+ <slot name="trigger" :toggle :control>
114
120
  <button
115
- :id="props.id"
121
+ v-bind="control"
116
122
  type="button"
117
123
  class="selector-trigger"
118
124
  aria-haspopup="listbox"
@@ -20,6 +20,7 @@ declare const __VLS_export: <T extends object>(__VLS_props: NonNullable<Awaited<
20
20
  slots: {
21
21
  trigger?: (props: {
22
22
  toggle: any;
23
+ control: any;
23
24
  }) => any;
24
25
  } & {
25
26
  'trigger-content'?: (props: {
@@ -0,0 +1,62 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: toggle, on/off switch, pill toggle, boolean button
5
+ short: boolean on/off pill button (not a sliding switch) wrapping ControlElement; click/Enter/Space toggle
6
+ invariants: true
7
+ ---
8
+
9
+ # SwitchButton — agent-only invariants
10
+
11
+ `<orio-switch-button>` is a **pill-shaped button** that toggles a boolean.
12
+ Despite the name it is **not** a sliding toggle switch — it is a chip-style
13
+ button that flips between active and inactive states.
14
+
15
+ ## Invariants
16
+
17
+ - **Renders a `<button type=...>` element**, not a checkbox or sliding
18
+ knob. The "switch" is purely visual state via the `.active` class.
19
+ - **v-model is `boolean`** (not required — renders as off when unbound).
20
+ - **Click, Enter, and Space all toggle.** `Enter` and `Space` use
21
+ `.prevent` to avoid form submit / page scroll.
22
+ - **`disabled` blocks toggling** and applies `.disabled` styles (0.5
23
+ opacity, `cursor: not-allowed`). Keyboard activation is also gated.
24
+ - **Default slot is the button content** (label, icon, or both). Visual
25
+ active state changes background/border/color; the slot does not
26
+ receive any reactive prop.
27
+ - **Wraps `<orio-control-element>`** — supports the standard `label`,
28
+ `error`, `layout`, `size`, etc. The control bag is spread on the
29
+ inner `<button>` along with `$attrs`.
30
+
31
+ ## Gotchas
32
+
33
+ - **The component name is misleading.** If you want a sliding toggle
34
+ switch (knob that animates), this is not it. Build that yourself or
35
+ pick another primitive.
36
+ - **No `aria-pressed`.** The active state is visual only. For correct
37
+ screen reader semantics, pass `:aria-pressed="modelValue"` via
38
+ `$attrs`.
39
+ - **`@keydown.enter.prevent`** swallows form-submit Enter inside a
40
+ `<form>`. If the SwitchButton is inside a form, Enter on it will
41
+ toggle but not submit.
42
+
43
+ ## Quick reference
44
+
45
+ ```vue
46
+ <template>
47
+ <orio-switch-button
48
+ v-model="notifications"
49
+ :label="$t('settings.notifications')"
50
+ :aria-pressed="notifications"
51
+ >
52
+ <orio-icon :name="notifications ? 'bell' : 'bell-off'" />
53
+ {{ notifications ? $t("common.on") : $t("common.off") }}
54
+ </orio-switch-button>
55
+ </template>
56
+ ```
57
+
58
+ ## Related
59
+
60
+ - `<orio-check-box>` — when you want a real checkbox.
61
+ - `<orio-radio-button>` — single-choice from a group.
62
+ - Public API reference: `docs/components/switch-button.md`.
@@ -1,8 +1,5 @@
1
1
  import type { ControlProps } from "./ControlElement.vue.js";
2
- interface Props extends ControlProps {
3
- disabled?: boolean;
4
- }
5
- type __VLS_Props = Props;
2
+ type __VLS_Props = ControlProps;
6
3
  type __VLS_ModelProps = {
7
4
  modelValue?: boolean;
8
5
  };