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
@@ -6,7 +6,13 @@ const props = defineProps({
6
6
  label: { type: String, required: false },
7
7
  layout: { type: String, required: false, default: "vertical" },
8
8
  size: { type: String, required: false, default: "md" },
9
- fill: { type: Boolean, required: false }
9
+ fill: { type: Boolean, required: false },
10
+ tabindex: { type: [Number, String], required: false },
11
+ focusKey: { type: String, required: false },
12
+ disabled: { type: Boolean, required: false },
13
+ required: { type: Boolean, required: false },
14
+ name: { type: String, required: false },
15
+ ariaLabel: { type: String, required: false }
10
16
  });
11
17
  const modelValue = defineModel({ type: Array, ...{ default: () => [] } });
12
18
  function isChecked(value) {
@@ -0,0 +1,77 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: label + error + a11y wrapper for any form control
5
+ short: label/legend wrapper, owns a11y attrs, exposes the `control` slot prop bag
6
+ invariants: true
7
+ ---
8
+
9
+ # ControlElement — agent-only invariants
10
+
11
+ `ControlElement` is the wrapper every form input uses (Input, Textarea,
12
+ NumberInput, Selector, CheckBox, etc.). When you build a new orio form
13
+ component, wrap it in `<orio-control-element>`. When you consume an existing
14
+ one, pass `ControlProps` straight through — they are usually re-exported.
15
+
16
+ ## Invariants
17
+
18
+ - **`inheritAttrs: false`.** Attrs do **not** auto-flow onto the wrapper or
19
+ the inner element. The component exposes a `control` slot prop containing
20
+ the a11y/form attr bag (`id`, `ariaDescribedby`, `ariaInvalid`,
21
+ `ariaRequired`, plus passthrough `tabindex`, `focusKey`, `disabled`,
22
+ `required`, `name`, `ariaLabel`). The inner element **must** spread it:
23
+ ```vue
24
+ <orio-control-element v-slot="{ control }" v-bind="props">
25
+ <input v-bind="control" />
26
+ </orio-control-element>
27
+ ```
28
+ - **`group` prop changes the semantic root.** When `true`, the wrapper gets
29
+ `role="group"` + `aria-labelledby`, and the label renders as `<span>`
30
+ (still id-linked) instead of `<label>`. Use this for `CheckboxGroup`,
31
+ radio groups, anything where the "control" is multiple inputs.
32
+ - **Error wiring is automatic.** Setting `error` to a non-null string:
33
+ - Renders a `.control-error` span below the slot.
34
+ - Sets `aria-invalid` and `aria-describedby` on the inner element via the
35
+ `control` slot prop.
36
+ - Adds a red border to `.slot-wrapper` (unless the wrapper contains a
37
+ `:deep(.error-fields)` element, in which case the inner component owns
38
+ error styling — see TaggableSelector).
39
+ - **`size` is provided to descendants** via `provideControlSize`. Children
40
+ that use `useControlSize()` (e.g. inner buttons, icons) inherit it
41
+ automatically — do not re-pass `size` down manually.
42
+ - **`appearance="minimal"`** zeros margin and strips border + box-shadow from
43
+ the first slot child. Use for inputs embedded in a row with their own
44
+ surrounding chrome.
45
+
46
+ ## Gotchas
47
+
48
+ - The slot's `id` matches what the `<label>` points to via `for`. The inner
49
+ element receives the same `id` through the `control` bag. Do **not**
50
+ override it — it is `useId()` by default and accessibility breaks if two
51
+ inputs in the same render share an id.
52
+ - `disabled` is forwarded to the inner element AND drives the wrapper's
53
+ `.disabled` class for styling. Pass `disabled` on the wrapper, never on the
54
+ inner element directly.
55
+ - The exported types are the contract:
56
+ - `ControlProps` — what consumers pass to ControlElement.
57
+ - `ControlPassthroughProps` — the subset that travels to the inner element.
58
+ - `ControlSlotAttrs` — the bag exposed via the `control` slot prop.
59
+ - `ControlLayout = "vertical" | "horizontal"`.
60
+ Components that wrap ControlElement extend `ControlProps`, e.g.
61
+ `interface Props extends ControlProps { ... }`.
62
+
63
+ ## Quick reference
64
+
65
+ ```vue
66
+ <script setup lang="ts">
67
+ import type { ControlProps } from "./ControlElement.vue";
68
+ interface Props extends ControlProps { /* component-specific props */ }
69
+ const props = defineProps<Props>();
70
+ </script>
71
+
72
+ <template>
73
+ <orio-control-element v-slot="{ control }" v-bind="props">
74
+ <my-inner-element v-bind="control" />
75
+ </orio-control-element>
76
+ </template>
77
+ ```
@@ -1,43 +1,58 @@
1
1
  export type ControlLayout = "vertical" | "horizontal";
2
2
  export type ControlSize = "sm" | "md" | "lg" | "xl";
3
- export interface ControlProps {
4
- /**
5
- * Minimal will reset margin and remove border and box shadow from every element inside the slot
6
- */
3
+ /**
4
+ * A11y + form attrs that flow from the caller through ControlElement to the
5
+ * actual interactive element via the `control` slot prop. Never rendered on
6
+ * the wrapper itself.
7
+ */
8
+ export interface ControlPassthroughProps {
9
+ /** Native `tabindex` for the inner element. */
10
+ tabindex?: number | string;
11
+ /** Roving-focus identifier — see useRovingGrid. Rendered as `focus-key` DOM attr. */
12
+ focusKey?: string;
13
+ /** Disables the inner element AND drives wrapper disabled styling. */
14
+ disabled?: boolean;
15
+ /** Marks the inner element required and sets `aria-required`. */
16
+ required?: boolean;
17
+ /** Native `name` attribute for the inner form element. */
18
+ name?: string;
19
+ /** Accessible name for the inner element when no visible label is set. */
20
+ ariaLabel?: string;
21
+ }
22
+ export interface ControlProps extends ControlPassthroughProps {
23
+ /** Minimal resets margin and removes border/box-shadow from every element inside the slot. */
7
24
  appearance?: "normal" | "minimal";
8
- /**
9
- * Error message to display below the control
10
- */
25
+ /** Error message to display below the control. Also drives `aria-invalid` and `aria-describedby` on the inner element. */
11
26
  error?: string | null;
12
- /**
13
- * Marks this control as a group (adds role="group" and aria-labelledby).
14
- * The label renders as a <span> instead of <label>.
15
- * Use for groups of related controls (e.g. CheckboxGroup).
16
- */
27
+ /** Marks this control as a group (adds role="group" + aria-labelledby on the wrapper). The label renders as a <span> instead of <label>. Use for groups of related controls (e.g. CheckboxGroup). */
17
28
  group?: boolean;
18
- /**
19
- * ID for the control's form element, auto-generated if not provided
20
- */
29
+ /** ID for the inner form element. Auto-generated if not provided. */
21
30
  id?: string;
22
- /**
23
- * Label text for the control (or legend text when group is true)
24
- */
31
+ /** Label text for the control (or legend text when group is true). */
25
32
  label?: string;
26
- /**
27
- * Label position relative to the control
28
- */
33
+ /** Label position relative to the control. */
29
34
  layout?: ControlLayout;
30
- /**
31
- * Size of the control and its inner elements
32
- */
35
+ /** Size of the control and its inner elements. */
33
36
  size?: ControlSize;
34
- /**
35
- * Whether element should fill the container
36
- */
37
+ /** Whether the element should fill the container. */
37
38
  fill?: boolean;
38
39
  }
40
+ /**
41
+ * Bag the consumer spreads onto the inner interactive element via the `control`
42
+ * slot prop: `<input v-bind="control" />`. Extends the caller-facing
43
+ * passthrough props with attrs derived from ControlElement state
44
+ * (`aria-invalid` from `error`, `aria-describedby` pointing at the error span,
45
+ * etc.) and the required `id`.
46
+ */
47
+ export interface ControlSlotAttrs extends ControlPassthroughProps {
48
+ id: string;
49
+ ariaDescribedby?: string;
50
+ ariaInvalid?: boolean;
51
+ ariaRequired?: boolean;
52
+ }
39
53
  declare var __VLS_7: {
40
54
  id: string;
55
+ control: ControlSlotAttrs;
41
56
  };
42
57
  type __VLS_Slots = {} & {
43
58
  default?: (props: typeof __VLS_7) => any;
@@ -10,10 +10,29 @@ const props = defineProps({
10
10
  label: { type: String, required: false },
11
11
  layout: { type: String, required: false, default: "vertical" },
12
12
  size: { type: String, required: false, default: "md" },
13
- fill: { type: Boolean, required: false, default: false }
13
+ fill: { type: Boolean, required: false, default: 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 }
14
20
  });
15
21
  provideControlSize(toRef(props, "size"));
16
22
  const sizeStyle = computed(() => sizeTokens[props.size]);
23
+ const errorId = computed(() => props.error ? `${props.id}-error` : void 0);
24
+ const control = computed(() => ({
25
+ id: props.id,
26
+ tabindex: props.tabindex,
27
+ focusKey: props.focusKey,
28
+ disabled: props.disabled || void 0,
29
+ required: props.required || void 0,
30
+ name: props.name,
31
+ ariaLabel: props.ariaLabel,
32
+ ariaDescribedby: errorId.value,
33
+ ariaInvalid: props.error ? true : void 0,
34
+ ariaRequired: props.required || void 0
35
+ }));
17
36
  </script>
18
37
 
19
38
  <template>
@@ -23,13 +42,13 @@ const sizeStyle = computed(() => sizeTokens[props.size]);
23
42
  appearance,
24
43
  layout,
25
44
  `size-${size}`,
26
- { 'has-error': error, group, fill }
45
+ { 'has-error': error, group, fill, disabled },
46
+ $attrs.class
27
47
  ]"
28
- :style="sizeStyle"
29
- v-bind="{
30
- ...$attrs,
31
- ...group ? { role: 'group', ...label ? { 'aria-labelledby': id } : {} } : {}
32
- }"
48
+ :style="[sizeStyle, $attrs.style]"
49
+ v-bind="
50
+ group ? { role: 'group', ...label ? { 'aria-labelledby': id } : {} } : {}
51
+ "
33
52
  >
34
53
  <component
35
54
  :is="group ? 'span' : 'label'"
@@ -41,9 +60,9 @@ const sizeStyle = computed(() => sizeTokens[props.size]);
41
60
  </component>
42
61
  <div class="control-group">
43
62
  <div class="slot-wrapper">
44
- <slot :id />
63
+ <slot :id :control />
45
64
  </div>
46
- <span v-if="error" class="control-error">{{ error }}</span>
65
+ <span v-if="error" :id="errorId" class="control-error">{{ error }}</span>
47
66
  </div>
48
67
  </div>
49
68
  </template>
@@ -1,43 +1,58 @@
1
1
  export type ControlLayout = "vertical" | "horizontal";
2
2
  export type ControlSize = "sm" | "md" | "lg" | "xl";
3
- export interface ControlProps {
4
- /**
5
- * Minimal will reset margin and remove border and box shadow from every element inside the slot
6
- */
3
+ /**
4
+ * A11y + form attrs that flow from the caller through ControlElement to the
5
+ * actual interactive element via the `control` slot prop. Never rendered on
6
+ * the wrapper itself.
7
+ */
8
+ export interface ControlPassthroughProps {
9
+ /** Native `tabindex` for the inner element. */
10
+ tabindex?: number | string;
11
+ /** Roving-focus identifier — see useRovingGrid. Rendered as `focus-key` DOM attr. */
12
+ focusKey?: string;
13
+ /** Disables the inner element AND drives wrapper disabled styling. */
14
+ disabled?: boolean;
15
+ /** Marks the inner element required and sets `aria-required`. */
16
+ required?: boolean;
17
+ /** Native `name` attribute for the inner form element. */
18
+ name?: string;
19
+ /** Accessible name for the inner element when no visible label is set. */
20
+ ariaLabel?: string;
21
+ }
22
+ export interface ControlProps extends ControlPassthroughProps {
23
+ /** Minimal resets margin and removes border/box-shadow from every element inside the slot. */
7
24
  appearance?: "normal" | "minimal";
8
- /**
9
- * Error message to display below the control
10
- */
25
+ /** Error message to display below the control. Also drives `aria-invalid` and `aria-describedby` on the inner element. */
11
26
  error?: string | null;
12
- /**
13
- * Marks this control as a group (adds role="group" and aria-labelledby).
14
- * The label renders as a <span> instead of <label>.
15
- * Use for groups of related controls (e.g. CheckboxGroup).
16
- */
27
+ /** Marks this control as a group (adds role="group" + aria-labelledby on the wrapper). The label renders as a <span> instead of <label>. Use for groups of related controls (e.g. CheckboxGroup). */
17
28
  group?: boolean;
18
- /**
19
- * ID for the control's form element, auto-generated if not provided
20
- */
29
+ /** ID for the inner form element. Auto-generated if not provided. */
21
30
  id?: string;
22
- /**
23
- * Label text for the control (or legend text when group is true)
24
- */
31
+ /** Label text for the control (or legend text when group is true). */
25
32
  label?: string;
26
- /**
27
- * Label position relative to the control
28
- */
33
+ /** Label position relative to the control. */
29
34
  layout?: ControlLayout;
30
- /**
31
- * Size of the control and its inner elements
32
- */
35
+ /** Size of the control and its inner elements. */
33
36
  size?: ControlSize;
34
- /**
35
- * Whether element should fill the container
36
- */
37
+ /** Whether the element should fill the container. */
37
38
  fill?: boolean;
38
39
  }
40
+ /**
41
+ * Bag the consumer spreads onto the inner interactive element via the `control`
42
+ * slot prop: `<input v-bind="control" />`. Extends the caller-facing
43
+ * passthrough props with attrs derived from ControlElement state
44
+ * (`aria-invalid` from `error`, `aria-describedby` pointing at the error span,
45
+ * etc.) and the required `id`.
46
+ */
47
+ export interface ControlSlotAttrs extends ControlPassthroughProps {
48
+ id: string;
49
+ ariaDescribedby?: string;
50
+ ariaInvalid?: boolean;
51
+ ariaRequired?: boolean;
52
+ }
39
53
  declare var __VLS_7: {
40
54
  id: string;
55
+ control: ControlSlotAttrs;
41
56
  };
42
57
  type __VLS_Slots = {} & {
43
58
  default?: (props: typeof __VLS_7) => any;
@@ -0,0 +1,65 @@
1
+ ---
2
+ kind: component
3
+ category: Layout & containers
4
+ purpose: dashed empty/drop zone, add-item card, upload tile, empty state with action
5
+ short: clickable dashed-border tile with icon and label, used for add/upload affordances
6
+ invariants: true
7
+ ---
8
+
9
+ # DashedContainer — agent-only invariants
10
+
11
+ `<orio-dashed-container>` is a self-styled clickable tile that renders an
12
+ optional icon and a label inside a dashed border. It is **not** a generic
13
+ slot wrapper.
14
+
15
+ ## Invariants
16
+
17
+ - **No default slot.** The template only renders `icon` and `text` props.
18
+ Children placed between the tags are dropped. To compose richer content,
19
+ use a different primitive.
20
+ - **Always clickable.** The wrapper has `cursor: pointer` and emits
21
+ `click` unconditionally — even with no listener attached, the tile looks
22
+ interactive. Do not use as a passive container.
23
+ - **`size` only scales the icon.** `small` → 2rem, `medium` → 3rem, `large`
24
+ → 5rem. Padding (2rem), gap (0.5rem), text size, and border are fixed
25
+ regardless of `size`.
26
+ - **`icon` is forwarded to `<orio-icon :name>`.** Must be a name registered
27
+ in `utils/iconRegistry`. Missing names render nothing; check the registry
28
+ before passing a string.
29
+ - **Hover effect comes from the global `gradient-hover` class**, not from
30
+ scoped styles. The dashed border, padding, and layout are scoped; the
31
+ hover gradient is project-global.
32
+
33
+ ## Gotchas
34
+
35
+ - **`text` defaults to English in consumer code.** Project convention
36
+ (see CLAUDE.md / translations note) is to pass an i18n key: `:text="$t('addItem')"`,
37
+ not `text="Add Item"`.
38
+ - **Both `icon` and `text` are optional.** With neither, the tile is an
39
+ empty dashed box that still emits `click`. Confirm at least one is set
40
+ unless that empty look is intentional.
41
+ - **The `<span :size>` shorthand** in the template forwards `size` as a
42
+ DOM attribute on the label. It's not styled — harmless, but visible
43
+ in devtools.
44
+ - **No `disabled` state.** If a consumer needs disabled-looking behaviour,
45
+ wrap or override styles externally; do not rely on a prop.
46
+
47
+ ## Quick reference
48
+
49
+ ```vue
50
+ <template>
51
+ <orio-dashed-container
52
+ icon="plus"
53
+ :text="$t('gallery.addImage')"
54
+ size="medium"
55
+ @click="openPicker"
56
+ />
57
+ </template>
58
+ ```
59
+
60
+ ## Related
61
+
62
+ - `orio-icon` — icon renderer driven by the same `name` string.
63
+ - `orio-upload` — full file-picker widget; prefer it over hand-rolling
64
+ click-to-upload on a `DashedContainer`.
65
+ - Public API reference: `docs/components/dashed-container.md`.
@@ -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`.