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
@@ -1,6 +1,5 @@
1
1
  <script setup>
2
2
  const props = defineProps({
3
- disabled: { type: Boolean, required: false, default: false },
4
3
  appearance: { type: String, required: false },
5
4
  error: { type: [String, null], required: false },
6
5
  group: { type: Boolean, required: false },
@@ -8,7 +7,13 @@ const props = defineProps({
8
7
  label: { type: String, required: false },
9
8
  layout: { type: String, required: false },
10
9
  size: { type: String, required: false },
11
- fill: { type: Boolean, required: false }
10
+ fill: { type: Boolean, required: false },
11
+ tabindex: { type: [Number, String], required: false },
12
+ focusKey: { type: String, required: false },
13
+ disabled: { type: Boolean, required: false, default: false },
14
+ required: { type: Boolean, required: false },
15
+ name: { type: String, required: false },
16
+ ariaLabel: { type: String, required: false }
12
17
  });
13
18
  const modelValue = defineModel({ type: Boolean, ...{ required: false } });
14
19
  function toggle() {
@@ -18,13 +23,11 @@ function toggle() {
18
23
  </script>
19
24
 
20
25
  <template>
21
- <orio-control-element v-slot="{ id }" v-bind="props">
26
+ <orio-control-element v-slot="{ control }" v-bind="props">
22
27
  <button
23
- :id
24
- v-bind="$attrs"
28
+ v-bind="{ ...$attrs, ...control }"
25
29
  class="switch-button"
26
- :class="{ active: modelValue, disabled }"
27
- :disabled="disabled"
30
+ :class="{ active: modelValue, disabled: props.disabled }"
28
31
  @click="toggle"
29
32
  @keydown.enter.prevent="toggle"
30
33
  @keydown.space.prevent="toggle"
@@ -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
  };
@@ -0,0 +1,51 @@
1
+ ---
2
+ kind: component
3
+ category: Buttons & indicators
4
+ purpose: tag, chip, label, removable chip, category pill
5
+ short: small text chip with neutral or accent variant; static display only (no remove behavior)
6
+ invariants: true
7
+ ---
8
+
9
+ # Tag — agent-only invariants
10
+
11
+ `<orio-tag>` is a static text chip. No close button, no click handler, no
12
+ removal behavior — display only.
13
+
14
+ ## Invariants
15
+
16
+ - **`text` is required.** It is the only way to set the chip's visible
17
+ content — there is no default slot.
18
+ - **`variant`**: `"neutral"` (default, gray) or `"accent"` (themed
19
+ accent). No `danger` / `alert` colors here — use `<orio-badge>` for
20
+ that.
21
+ - **`id` is an optional uniqueness key** used by upstream components
22
+ (`<orio-taggable-selector>`) to match selected tags. Pass it whenever
23
+ the tag participates in selection state.
24
+ - **`user-select: none`** — text inside the chip cannot be selected with
25
+ the cursor.
26
+
27
+ ## Gotchas
28
+
29
+ - **No remove / close button.** The `TagProps` shape is intentionally
30
+ passive. For removable chips, wrap a custom button next to it or build
31
+ your own primitive.
32
+ - **No icon support.** No `icon` prop, no slot. For icon+text chips,
33
+ build directly from a `<span>` with `<orio-icon>`.
34
+ - **No size variants.** Padding (`0.25rem 0.6rem`) and font size
35
+ (`--font-sm`) are fixed.
36
+
37
+ ## Quick reference
38
+
39
+ ```vue
40
+ <template>
41
+ <orio-tag :text="$t('category.urgent')" variant="accent" />
42
+ <orio-tag :text="$t('category.draft')" />
43
+ </template>
44
+ ```
45
+
46
+ ## Related
47
+
48
+ - `<orio-taggable-selector>` — uses `TagProps` as its option shape.
49
+ - `<orio-badge>` — when you need a status pill with semantic color
50
+ variants.
51
+ - Public API reference: `docs/components/tag.md`.
@@ -0,0 +1,73 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: multi-select with tag chips, taggable selector, chip picker
5
+ short: multi-select Selector that renders chosen options as tag chips in the trigger
6
+ invariants: true
7
+ ---
8
+
9
+ # TaggableSelector — agent-only invariants
10
+
11
+ `<orio-taggable-selector>` is a thin wrapper over `<orio-selector>` that
12
+ forces `multiple` mode and renders the selection as a row of `<orio-tag>`
13
+ chips inside the trigger. Read `Selector.USAGE.md` for the full contract.
14
+
15
+ ## Invariants
16
+
17
+ - **Always multi-select.** The internal `<orio-selector>` is passed
18
+ `multiple` unconditionally; the prop cannot be turned off.
19
+ - **`optionName` defaults to `"text"`** to align with `TagProps.text`. If
20
+ your option type uses a different label key, pass `option-name`.
21
+ - **v-model type is `TagProps[]`** — `{ id?: string; text: string;
22
+ variant?: "neutral" | "accent" }[]`. Options must be tag-shaped or
23
+ augmented to satisfy this contract.
24
+ - **Tags in the trigger are display-only.** Clicking a chip does **not**
25
+ remove the option. The user must reopen the dropdown and uncheck it.
26
+ This is a real usability tradeoff — for click-to-remove, fall back to
27
+ `<orio-selector>` with a custom `#trigger-label`.
28
+ - **Trigger lays chips out as `flex-wrap`** with `0.5rem` gap, left-aligned.
29
+ Selecting many chips grows the trigger height; constrain it if your
30
+ layout depends on a fixed height.
31
+
32
+ ## Gotchas
33
+
34
+ - **`field` (uniqueness key) still defaults to `"id"`** from the underlying
35
+ Selector. Each tag should have a stable `id` — without one, multi-select
36
+ add/remove will mis-match items with identical `text`.
37
+ - **No empty-state placeholder by default in the trigger.** When the
38
+ selection is empty, the trigger is empty space. Pass a `placeholder` so
39
+ the dropdown shows a hint, but the trigger label itself will not show
40
+ it (the `#trigger-label` slot renders an empty chip list).
41
+ - **Variant per chip is per-option.** Set `variant: "accent"` on individual
42
+ option objects to tint specific chips.
43
+
44
+ ## Quick reference
45
+
46
+ ```vue
47
+ <script setup lang="ts">
48
+ import type { TagProps } from "../components/Tag.vue";
49
+
50
+ const allCategories: TagProps[] = [
51
+ { id: "fiction", text: "Fiction" },
52
+ { id: "non-fiction", text: "Non-fiction", variant: "accent" },
53
+ { id: "poetry", text: "Poetry" },
54
+ ];
55
+
56
+ const selected = defineModel<TagProps[]>({ default: () => [] });
57
+ </script>
58
+
59
+ <template>
60
+ <orio-taggable-selector
61
+ v-model="selected"
62
+ :options="allCategories"
63
+ :label="$t('book.categories')"
64
+ />
65
+ </template>
66
+ ```
67
+
68
+ ## Related
69
+
70
+ - `<orio-selector>` — base; use with custom `#trigger-label` for
71
+ removable chips or different layouts.
72
+ - `<orio-tag>` — the chip primitive rendered in the trigger.
73
+ - Public API reference: `docs/components/taggable-selector.md`.
@@ -12,7 +12,13 @@ const props = defineProps({
12
12
  label: { type: String, required: false },
13
13
  layout: { type: String, required: false },
14
14
  size: { type: String, required: false },
15
- 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 }
16
22
  });
17
23
  const modelValue = defineModel({ type: Array });
18
24
  </script>
@@ -0,0 +1,72 @@
1
+ ---
2
+ kind: component
3
+ category: Form inputs
4
+ purpose: textarea, multi-line text, long text input
5
+ short: multi-line text input wrapping ControlElement; supports inner-floating label and vertical resize
6
+ invariants: true
7
+ ---
8
+
9
+ # Textarea — agent-only invariants
10
+
11
+ `<orio-textarea>` is the multi-line counterpart to `<orio-input>`. Same
12
+ ControlElement wrapping, same layout modes, same slot-bag flow. Read
13
+ `ControlElement.USAGE.md` and `Input.USAGE.md` first — most of the contract
14
+ lives there.
15
+
16
+ ## Invariants
17
+
18
+ - **Extends `ControlProps`** with one override: `layout?: InputLayout`
19
+ where `InputLayout = ControlLayout | "inner"`. The `"inner"` mode floats
20
+ the label inside the textarea chrome (same trick as Input).
21
+ - **`layout="inner"` translates to `vertical`** on the inner ControlElement
22
+ and adds an `.inner` class. The label reposition is driven by `:deep()`
23
+ styles, not a duplicate label DOM.
24
+ - **v-model is `string`** (default `""`).
25
+ - **`rows="4"` is the hard default** rendered on the `<textarea>`. Override
26
+ via `$attrs` — `<orio-textarea :rows="2">` flows through.
27
+ - **`resize: vertical`** is set in CSS. The user can drag the bottom edge
28
+ but cannot resize horizontally.
29
+ - **Horizontal layout aligns the label to the top.** When `layout="horizontal"`,
30
+ the `.control-label` is padded with `--control-py` and the row uses
31
+ `align-items: flex-start` — so the label sits next to the top of a tall
32
+ textarea, not centered vertically.
33
+ - **`$attrs` is spread before the control bag** on the inner `<textarea>`:
34
+ `v-bind="{ ...$attrs, ...control }"`. Native attrs (`placeholder`,
35
+ `maxlength`, `rows`, `wrap`) work on `<orio-textarea>` and reach the
36
+ underlying element.
37
+
38
+ ## Gotchas
39
+
40
+ - **Resize handle ignores `layout="inner"`.** The drag handle still appears
41
+ bottom-right — the inner label can overlap an aggressively resized
42
+ textarea's content. Cap `max-height` if that matters.
43
+ - **`rows` only sets the initial height** (in line-heights). After the
44
+ user resizes, the manual height wins. To reset, re-mount the component.
45
+ - **The textarea is `width: 100%`** of the slot wrapper, which itself
46
+ follows the wrapper border. Setting `cols` has no effect on rendered
47
+ width.
48
+
49
+ ## Quick reference
50
+
51
+ ```vue
52
+ <script setup lang="ts">
53
+ const note = defineModel<string>({ default: "" });
54
+ </script>
55
+
56
+ <template>
57
+ <orio-textarea
58
+ v-model="note"
59
+ :label="$t('order.notes.label')"
60
+ :placeholder="$t('order.notes.placeholder')"
61
+ layout="inner"
62
+ :rows="6"
63
+ maxlength="500"
64
+ />
65
+ </template>
66
+ ```
67
+
68
+ ## Related
69
+
70
+ - `<orio-input>` — single-line text. Same contract.
71
+ - `<orio-control-element>` — the wrapper; owns label/error/a11y.
72
+ - Public API reference: `docs/components/textarea.md`.
@@ -8,18 +8,28 @@ const props = defineProps({
8
8
  id: { type: String, required: false },
9
9
  label: { type: String, required: false },
10
10
  size: { type: String, required: false },
11
- fill: { type: Boolean, required: false }
11
+ fill: { type: Boolean, required: false },
12
+ tabindex: { type: [Number, String], required: false },
13
+ focusKey: { type: String, required: false },
14
+ disabled: { type: Boolean, required: false },
15
+ required: { type: Boolean, required: false },
16
+ name: { type: String, required: false },
17
+ ariaLabel: { type: String, required: false }
12
18
  });
13
19
  </script>
14
20
 
15
21
  <template>
16
22
  <orio-control-element
17
- v-slot="{ id }"
23
+ v-slot="{ control }"
18
24
  v-bind="props"
19
25
  :layout="layout === 'inner' ? 'vertical' : layout"
20
26
  :class="{ inner: layout === 'inner' }"
21
27
  >
22
- <textarea :id v-model="modelValue" rows="4" v-bind="$attrs" />
28
+ <textarea
29
+ v-model="modelValue"
30
+ rows="4"
31
+ v-bind="{ ...$attrs, ...control }"
32
+ />
23
33
  </orio-control-element>
24
34
  </template>
25
35
 
@@ -0,0 +1,84 @@
1
+ ---
2
+ kind: component
3
+ category: Layout & containers
4
+ purpose: tooltip, hover hint, focus hint, label-on-hover
5
+ short: hover/focus-triggered tooltip teleported to body, with delay, arrow, and four placements
6
+ invariants: true
7
+ ---
8
+
9
+ # Tooltip — agent-only invariants
10
+
11
+ `<orio-tooltip>` wraps a trigger in an `inline-flex` div and shows a small
12
+ floating bubble on hover or focus. It is not for click-driven menus — use
13
+ `<orio-popover>` for that.
14
+
15
+ ## Invariants
16
+
17
+ - **Trigger wrapper is `display: inline-flex`** (centered). The slot lives
18
+ inside it. Block-level children are coerced into the flex layout.
19
+ - **Mouse + keyboard both trigger.** `@mouseenter`/`@focus` show,
20
+ `@mouseleave`/`@blur` hide. Touch is not supported.
21
+ - **`delay` (default 500 ms) gates open only.** Hide is immediate. Setting
22
+ `delay: 0` makes it instant.
23
+ - **`disabled` watcher closes an open tooltip.** Flipping to disabled mid-
24
+ display hides it. Re-enabling does not reopen — the user has to re-hover.
25
+ - **Two content sources, slot wins.** `#content` slot renders if provided,
26
+ otherwise the `text` prop. Both being empty renders an empty bubble.
27
+ - **Teleported to body only while visible.** Unlike Popover, the tooltip is
28
+ mounted/unmounted around the visible window — no leftover DOM at rest.
29
+ - **No placement fallback.** `placement` (`top`/`bottom`/`left`/`right`) is
30
+ honored as-is. If the bubble overflows the viewport, it stays offscreen.
31
+ Compare with Popover, which auto-flips.
32
+ - **Position is recalculated on scroll/resize** while visible (capture-phase
33
+ listeners catch nested scrollers).
34
+ - **`pointer-events: none` on the bubble.** It never intercepts the cursor —
35
+ so leaving the trigger always closes it.
36
+ - **Has a CSS-triangle arrow** (`.orio-tooltip-arrow-{placement}`) anchored
37
+ on the appropriate edge.
38
+ - **Styles are intentionally unscoped** in the second `<style>` block,
39
+ because teleported nodes escape scoped CSS. Class names use the
40
+ `orio-tooltip-` prefix to avoid collisions. Consumers **can** override
41
+ them globally — useful for theming, dangerous if not namespaced.
42
+ - **`white-space: nowrap`** on the bubble. Long `text` does not wrap. For
43
+ multi-line, render `#content` with your own line-breaking CSS.
44
+ - **A11y is partial.** The bubble has `role="tooltip"` and `aria-hidden`,
45
+ but the trigger does **not** receive `aria-describedby`. Screen readers
46
+ may not announce the tooltip. Wire `aria-describedby` on the trigger
47
+ child yourself if it matters.
48
+
49
+ ## Gotchas
50
+
51
+ - **`text` defaults to English.** Project convention: pass an i18n key —
52
+ `:text="$t('action.delete.hint')"`.
53
+ - **`inline-flex` wrapper can change layout.** Wrapping a block-level
54
+ element (a card, a list row) in a Tooltip squashes it. Wrap a smaller
55
+ trigger (button, icon) instead.
56
+ - **`delay` does not debounce reopens.** Rapid hover toggles can still
57
+ flash the tooltip on the second mount if the first delay completed.
58
+ - **Z-index is `9999`** on the bubble — less than Popover (`999999`) and
59
+ Modal. Tooltips above an open popover may render behind it.
60
+ - **No click-to-dismiss.** Clicking the trigger does not close the bubble.
61
+ The user has to move focus or hover away.
62
+
63
+ ## Quick reference
64
+
65
+ ```vue
66
+ <template>
67
+ <orio-tooltip :text="$t('action.delete.hint')" placement="top" :delay="200">
68
+ <orio-button icon-only icon="trash" @click="onDelete" />
69
+ </orio-tooltip>
70
+
71
+ <orio-tooltip placement="right">
72
+ <span>Hover me</span>
73
+ <template #content>
74
+ <strong>Custom</strong> content with <em>markup</em>.
75
+ </template>
76
+ </orio-tooltip>
77
+ </template>
78
+ ```
79
+
80
+ ## Related
81
+
82
+ - `<orio-popover>` — click-driven anchored panel; do not reach for Tooltip
83
+ for menus.
84
+ - Public API reference: `docs/components/tooltip.md`.
@@ -0,0 +1,108 @@
1
+ ---
2
+ kind: component
3
+ category: Layout & containers
4
+ purpose: pinch/scroll zoom viewport, pan-zoom canvas, infinite board, image inspector
5
+ short: pan + pinch/wheel zoom viewport with inertia, momentum, space-to-grab and bounds clamping
6
+ invariants: true
7
+ ---
8
+
9
+ # ZoomableContainer — agent-only invariants
10
+
11
+ `<orio-zoomable-container>` is a pan-zoom viewport that transforms a "world"
12
+ (its slot) inside a fixed-size viewport. Pinch on touch, ctrl/cmd+wheel on
13
+ mouse, drag-to-pan with space-or-middle-button.
14
+
15
+ ## Invariants
16
+
17
+ - **Viewport sizes to its parent.** The viewport is `width: 100%; height:
18
+ 100%`. **A parent with explicit dimensions is required** — without it,
19
+ the viewport collapses to 0 and nothing is visible.
20
+ - **World is the slot.** It is `position: absolute; transform-origin: 0 0`.
21
+ The container measures it via `ResizeObserver`, so the slot can be any
22
+ size — fixed, content-driven, or dynamic.
23
+ - **First-mount auto-center is one-shot.** Once viewport and world both
24
+ have non-zero dimensions, the world centers exactly once. Later size
25
+ changes call `applyBounds` only — they do **not** re-center. Use the
26
+ exposed `centerWorld()` to re-center on demand.
27
+ - **Drag-to-pan needs space, middle button, OR clicking the viewport background.**
28
+ Pointer-down on slot content does **not** pan by default. Hold `Space`
29
+ (cursor becomes `grab`/`grabbing`) or click an area outside the world to
30
+ pan. This is `shouldPan(e)` — confirm interactive children inside the
31
+ slot stop propagation if needed.
32
+ - **Touch gestures use `usePinchZoom`**, mouse/pen uses pointer-capture
33
+ drag. Wheel does pan; **ctrl/cmd + wheel** zooms at cursor; **shift +
34
+ vertical wheel** pans horizontally.
35
+ - **`v-model:scale` works; `v-model:translate` does NOT.** `update:scale`
36
+ emits one value, but `update:translate` emits `(x, y)` as two args — not
37
+ a tuple. Bind a callback to read translate, or read it via `ref` on the
38
+ exposed `tx`/`ty`.
39
+ - **Exposed methods (via `defineExpose`)**: `scale`, `tx`, `ty`,
40
+ `setScaleAt(target, px, py)`, `panBy(dx, dy)`, `resetView()`,
41
+ `centerWorld()`. Get a `ref` on the component to call them.
42
+ - **Pan bounds let the world drift halfway off either edge.** Clamp is
43
+ `tx ∈ [vw/2 − worldW, vw/2]`, so the world can move until its trailing
44
+ edge reaches viewport center. Intentional — keeps something always
45
+ reachable.
46
+ - **`touch-action: none` and `user-select: none`** are set on the viewport.
47
+ Children inside cannot select text or trigger native touch scrolling.
48
+ - **Context menu is suppressed** inside the viewport (`@contextmenu.prevent`).
49
+ - **Global keydown listener for Space.** `useEventListener("keydown", ...)`
50
+ binds to `document`, so holding space in a text input elsewhere on the
51
+ page still flips `spaceHeld` — be aware when composing with form inputs.
52
+
53
+ ## Gotchas
54
+
55
+ - **Slot prop shape**: `<template v-slot="{ scale, tx, ty }">`. Use these
56
+ for overlays that need to track the world transform (rulers, minimaps).
57
+ - **No `v-model:translate` shortcut.** Wire it as
58
+ `@update:translate="(x, y) => { ... }"`.
59
+ - **Pinch zoom resets `dragId` on `onPinchStart`** to cancel any in-flight
60
+ single-pointer drag. If you mix touch + pen on a hybrid device, the drag
61
+ state can drop mid-gesture.
62
+ - **`zoomSpeed` is exponential**, not linear. `factor = exp(-deltaY *
63
+ zoomSpeed)`. Default `0.0015` is tuned for typical mousewheel deltas;
64
+ trackpad wheel events with tiny deltas barely zoom — bump to ~0.005 if
65
+ your audience is trackpad-heavy.
66
+ - **`minScale` / `maxScale` are clamped, not normalized.** Setting
67
+ `initialScale` outside the range still applies the clamp on the first
68
+ zoom interaction.
69
+
70
+ ## Quick reference
71
+
72
+ ```vue
73
+ <script setup lang="ts">
74
+ import { ref, useTemplateRef } from "vue";
75
+
76
+ const board = useTemplateRef<{ resetView: () => void; centerWorld: () => void }>("board");
77
+ const scale = ref(1);
78
+ const translate = ref({ x: 0, y: 0 });
79
+ </script>
80
+
81
+ <template>
82
+ <div style="width: 100%; height: 80vh">
83
+ <orio-zoomable-container
84
+ ref="board"
85
+ v-model:scale="scale"
86
+ :min-scale="0.25"
87
+ :max-scale="4"
88
+ @update:translate="(x, y) => (translate = { x, y })"
89
+ >
90
+ <template #default="{ scale: worldScale }">
91
+ <div class="board-world" :style="{ width: '2000px', height: '1500px' }">
92
+ <p>Zoom: {{ worldScale.toFixed(2) }}×</p>
93
+ </div>
94
+ </template>
95
+ </orio-zoomable-container>
96
+ </div>
97
+
98
+ <orio-button @click="board?.resetView()">Reset</orio-button>
99
+ </template>
100
+ ```
101
+
102
+ ## Related
103
+
104
+ - `usePinchZoom` — pinch gesture composable used internally.
105
+ - `useInertia` — momentum/decay used for release-after-drag.
106
+ - `<orio-canvas>` — when you need a tool-driven editor, not just a pan-zoom
107
+ viewport. Canvas is built on the same gestures with extras.
108
+ - Public API reference: `docs/components/zoomable-container.md`.
@@ -0,0 +1,52 @@
1
+ ---
2
+ kind: component
3
+ category: Date
4
+ purpose: date input, single date picker, "pick a date"
5
+ short: single date picker built from Calendar plus PickerTrigger
6
+ invariants: true
7
+ ---
8
+
9
+ # date/Picker — agent-only invariants
10
+
11
+ `<orio-date-picker>` is the single-date picker: a `<orio-date-picker-trigger>`
12
+ button that opens a popover containing `<orio-calendar>`. For ranges use
13
+ `<orio-date-range-picker>`.
14
+
15
+ ## Invariants
16
+
17
+ - **Extends `ControlProps`.** Pass `label`, `error`, `size`, `disabled`,
18
+ `required` straight through — they reach the trigger via ControlElement.
19
+ See `ControlElement.USAGE.md`.
20
+ - **v-model is `string | null`** in ISO `YYYY-MM-DD` form. `null` means
21
+ unpicked.
22
+ - **`min` / `max`** are ISO strings. They merge with the consumer's
23
+ `isDisabled(iso)` callback — Picker's `calendarIsDisabled` returns true
24
+ when either the min/max bound is violated OR the consumer says so.
25
+ - **Selecting a day closes the popover** automatically (`toggle(false)` is
26
+ called inside the `@select` handler). Do not wire your own close.
27
+ - **Markers + getMarker** are forwarded to the inner Calendar unchanged.
28
+ See `Calendar.USAGE.md` for the matching rules.
29
+ - **Placeholder text** falls back to the i18n key `datePicker.placeholder`
30
+ if no `placeholder` prop is given.
31
+
32
+ ## Gotchas
33
+
34
+ - The trigger displays the date via `formatDate(value, locale)` from
35
+ `../../utils/date`. If you need a custom display format, wrap the
36
+ picker — the prop is not exposed.
37
+ - `<orio-date-picker-trigger>` is the popover host. Wiring up your own
38
+ trigger means re-creating popover focus management — prefer composing
39
+ with the existing trigger via its `#default` scoped slot if you need
40
+ custom calendar content (this is exactly how Picker itself works).
41
+
42
+ ## Quick reference
43
+
44
+ ```vue
45
+ <orio-date-picker
46
+ v-model="checkInDate"
47
+ label="Check-in"
48
+ :min="todayIso"
49
+ :max="maxBookingIso"
50
+ :is-disabled="(iso) => blockedDates.has(iso)"
51
+ />
52
+ ```
@@ -16,7 +16,13 @@ const props = defineProps({
16
16
  label: { type: String, required: false },
17
17
  layout: { type: String, required: false },
18
18
  size: { type: String, required: false },
19
- fill: { type: Boolean, required: false }
19
+ fill: { type: Boolean, required: false },
20
+ tabindex: { type: [Number, String], required: false },
21
+ focusKey: { type: String, required: false },
22
+ disabled: { type: Boolean, required: false },
23
+ required: { type: Boolean, required: false },
24
+ name: { type: String, required: false },
25
+ ariaLabel: { type: String, required: false }
20
26
  });
21
27
  const value = defineModel({ type: [String, null], ...{ default: null } });
22
28
  const { locale, t } = useI18n();
@@ -0,0 +1,65 @@
1
+ ---
2
+ kind: component
3
+ category: Date
4
+ purpose: date picker trigger button, date input button, popover-anchored date trigger
5
+ short: shared button + popover trigger used by date Picker and RangePicker; default slot renders the picker body
6
+ invariants: true
7
+ ---
8
+
9
+ # date/PickerTrigger — agent-only invariants
10
+
11
+ `<orio-date-picker-trigger>` is the trigger button shared by
12
+ `<orio-date-picker>` and `<orio-date-range-picker>`. You usually do not
13
+ use it directly — pick those higher-level pickers unless you are building
14
+ a new date primitive.
15
+
16
+ ## Invariants
17
+
18
+ - **Renders a `<button>` inside `<orio-control-element>` and an
19
+ `<orio-popover>`** with `position="bottom-right"`, `offset: 5`.
20
+ - **`text` prop is the visible display text.** When empty, `placeholder`
21
+ renders muted.
22
+ - **`text` and `placeholder` are stripped from `controlProps`** — they
23
+ don't leak to the ControlElement wrapper.
24
+ - **Default slot is the popover content** and receives `{ toggle }`. Call
25
+ `toggle(false)` to close after a user picks.
26
+ - **Calendar icon (`name="calendar"`) is hardcoded** on the right of the
27
+ button. No prop to swap it.
28
+ - **`aria-expanded` reflects popover state** for screen reader support.
29
+ - **Inherits all ControlElement contract**: `label`, `error`, `size`,
30
+ `layout`, etc. The control bag is bound to the inner `<button>`.
31
+
32
+ ## Gotchas
33
+
34
+ - **Not for general "click-to-open" needs.** For a non-date trigger, use
35
+ `<orio-popover>` directly — this one is calendar-themed (icon, padding,
36
+ i18n placeholder).
37
+ - **No multi-popover stacking story.** Both single and range pickers use
38
+ this same component, with the same `bottom-right` placement. Side-by-
39
+ side pickers may collide.
40
+
41
+ ## Quick reference
42
+
43
+ You normally consume this through `<orio-date-picker>` or
44
+ `<orio-date-range-picker>`. Direct use:
45
+
46
+ ```vue
47
+ <template>
48
+ <orio-date-picker-trigger
49
+ :text="display"
50
+ :placeholder="$t('date.placeholder')"
51
+ :label="$t('date.label')"
52
+ >
53
+ <template #default="{ toggle }">
54
+ <orio-calendar v-model:anchor="anchor" @select="onSelect($event, toggle)" />
55
+ </template>
56
+ </orio-date-picker-trigger>
57
+ </template>
58
+ ```
59
+
60
+ ## Related
61
+
62
+ - `<orio-date-picker>` — single-date picker built on this trigger.
63
+ - `<orio-date-range-picker>` — range picker built on this trigger.
64
+ - `<orio-popover>` — for non-date trigger needs.
65
+ - Public API reference: `docs/components/date/`.
@@ -10,7 +10,13 @@ const props = defineProps({
10
10
  label: { type: String, required: false },
11
11
  layout: { type: String, required: false },
12
12
  size: { type: String, required: false },
13
- fill: { type: Boolean, required: false }
13
+ fill: { type: Boolean, required: false },
14
+ tabindex: { type: [Number, String], required: false },
15
+ focusKey: { type: String, required: false },
16
+ disabled: { type: Boolean, required: false },
17
+ required: { type: Boolean, required: false },
18
+ name: { type: String, required: false },
19
+ ariaLabel: { type: String, required: false }
14
20
  });
15
21
  const controlProps = computed(() => {
16
22
  const { text, placeholder, ...rest } = props;
@@ -19,11 +25,11 @@ const controlProps = computed(() => {
19
25
  </script>
20
26
 
21
27
  <template>
22
- <orio-control-element v-slot="{ id }" v-bind="controlProps">
28
+ <orio-control-element v-slot="{ control }" v-bind="controlProps">
23
29
  <orio-popover position="bottom-right" :offset="5">
24
30
  <template #default="{ toggle, isOpen }">
25
31
  <button
26
- :id
32
+ v-bind="control"
27
33
  type="button"
28
34
  class="date-trigger"
29
35
  :aria-expanded="isOpen"