orio-ui 1.27.0 → 1.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +76 -1
  2. package/bin/orio-ui.mjs +72 -0
  3. package/dist/agents/ROUTING.md +140 -0
  4. package/dist/agents/component-finder.md +142 -0
  5. package/dist/agents/component-worker.md +152 -0
  6. package/dist/agents/snippet.md +6 -0
  7. package/dist/module.json +1 -1
  8. package/dist/runtime/components/AnimatedContainer.USAGE.md +79 -0
  9. package/dist/runtime/components/Badge.USAGE.md +75 -0
  10. package/dist/runtime/components/Banner.USAGE.md +52 -0
  11. package/dist/runtime/components/Button.USAGE.md +83 -0
  12. package/dist/runtime/components/Button.d.vue.ts +1 -0
  13. package/dist/runtime/components/Button.vue +5 -1
  14. package/dist/runtime/components/Button.vue.d.ts +1 -0
  15. package/dist/runtime/components/Calendar.USAGE.md +8 -0
  16. package/dist/runtime/components/Canvas/USAGE.md +8 -0
  17. package/dist/runtime/components/CheckBox.USAGE.md +63 -0
  18. package/dist/runtime/components/CheckboxGroup.USAGE.md +95 -0
  19. package/dist/runtime/components/ControlElement.USAGE.md +8 -0
  20. package/dist/runtime/components/ControlElement.d.vue.ts +1 -1
  21. package/dist/runtime/components/ControlElement.vue.d.ts +1 -1
  22. package/dist/runtime/components/DashedContainer.USAGE.md +65 -0
  23. package/dist/runtime/components/EmptyState.USAGE.md +65 -0
  24. package/dist/runtime/components/Form.USAGE.md +102 -0
  25. package/dist/runtime/components/Icon.USAGE.md +61 -0
  26. package/dist/runtime/components/Input.USAGE.md +8 -0
  27. package/dist/runtime/components/ListItem.USAGE.md +84 -0
  28. package/dist/runtime/components/LoadingSpinner.USAGE.md +50 -0
  29. package/dist/runtime/components/LocaleSwitcher.USAGE.md +73 -0
  30. package/dist/runtime/components/Modal.USAGE.md +8 -0
  31. package/dist/runtime/components/NavButton.USAGE.md +80 -0
  32. package/dist/runtime/components/NumberInput/Horizontal.USAGE.md +61 -0
  33. package/dist/runtime/components/NumberInput/Horizontal.vue +5 -0
  34. package/dist/runtime/components/NumberInput/USAGE.md +74 -0
  35. package/dist/runtime/components/NumberInput/Vertical.USAGE.md +55 -0
  36. package/dist/runtime/components/NumberInput/Vertical.vue +9 -1
  37. package/dist/runtime/components/Popover.USAGE.md +103 -0
  38. package/dist/runtime/components/RadioButton.USAGE.md +72 -0
  39. package/dist/runtime/components/Selector.USAGE.md +131 -0
  40. package/dist/runtime/components/SwitchButton.USAGE.md +62 -0
  41. package/dist/runtime/components/Tag.USAGE.md +51 -0
  42. package/dist/runtime/components/TaggableSelector.USAGE.md +73 -0
  43. package/dist/runtime/components/Textarea.USAGE.md +72 -0
  44. package/dist/runtime/components/Tooltip.USAGE.md +84 -0
  45. package/dist/runtime/components/ZoomableContainer.USAGE.md +108 -0
  46. package/dist/runtime/components/date/Picker.USAGE.md +8 -0
  47. package/dist/runtime/components/date/PickerTrigger.USAGE.md +65 -0
  48. package/dist/runtime/components/date/RangePicker.USAGE.md +97 -0
  49. package/dist/runtime/components/gallery/Carousel.USAGE.md +98 -0
  50. package/dist/runtime/components/gallery/CarouselPreview.USAGE.md +51 -0
  51. package/dist/runtime/components/upload/USAGE.md +91 -0
  52. package/dist/runtime/components/view/Dates.USAGE.md +67 -0
  53. package/dist/runtime/components/view/KeyBinds.USAGE.md +58 -0
  54. package/dist/runtime/components/view/Separator.USAGE.md +57 -0
  55. package/dist/runtime/components/view/Text.USAGE.md +68 -0
  56. package/dist/runtime/composables/useApi.USAGE.md +64 -0
  57. package/dist/runtime/composables/useControlSize.USAGE.md +73 -0
  58. package/dist/runtime/composables/useControlSize.js +12 -0
  59. package/dist/runtime/composables/useDecimalFormatter.USAGE.md +72 -0
  60. package/dist/runtime/composables/useFilter.USAGE.md +120 -0
  61. package/dist/runtime/composables/useFuzzySearch.USAGE.md +68 -0
  62. package/dist/runtime/composables/useInertia.USAGE.md +80 -0
  63. package/dist/runtime/composables/useListKeyboard.USAGE.md +97 -0
  64. package/dist/runtime/composables/useModal.USAGE.md +82 -0
  65. package/dist/runtime/composables/usePinchZoom.USAGE.md +95 -0
  66. package/dist/runtime/composables/usePressAndHold.USAGE.md +70 -0
  67. package/dist/runtime/composables/useRovingGrid.USAGE.md +106 -0
  68. package/dist/runtime/composables/useSound.USAGE.md +74 -0
  69. package/dist/runtime/composables/useTheme.USAGE.md +76 -0
  70. package/dist/runtime/composables/useUrlSync.USAGE.md +91 -0
  71. package/dist/runtime/composables/useValidation.USAGE.md +100 -0
  72. package/package.json +12 -2
@@ -0,0 +1,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`.
@@ -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`.
@@ -1,3 +1,11 @@
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
+
1
9
  # date/Picker — agent-only invariants
2
10
 
3
11
  `<orio-date-picker>` is the single-date picker: a `<orio-date-picker-trigger>`
@@ -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/`.
@@ -0,0 +1,97 @@
1
+ ---
2
+ kind: component
3
+ category: Date
4
+ purpose: date range, from-to picker, date range input, calendar range
5
+ short: two-month range picker with hover-preview, min/max bounds, and ISO `{ start, end }` model
6
+ invariants: true
7
+ ---
8
+
9
+ # date/RangePicker — agent-only invariants
10
+
11
+ `<orio-date-range-picker>` is the date range picker: a
12
+ `<orio-date-picker-trigger>` opens a popover containing **two side-by-side
13
+ calendars** (left = start month, right = start month + 1). Read
14
+ `Calendar.USAGE.md` and `date/Picker.USAGE.md` first.
15
+
16
+ ## Invariants
17
+
18
+ - **v-model is `DateRange = { start: string | null; end: string | null }`**
19
+ (ISO date strings). `DateRange` is re-exported from this file. Both
20
+ fields can be null (no selection) or just `start` (mid-pick).
21
+ - **Click sequence is "set start → set end".** First click clears `end`,
22
+ writes `start`. Second click writes `end`. If the second pick is
23
+ earlier than the existing start, start and end **swap automatically**.
24
+ - **The popover closes on range completion** (second pick), via
25
+ `toggle(false)`. A single click leaves it open.
26
+ - **Two-month anchored display** — `leftAnchor` = first of start's month,
27
+ `rightAnchor` = `leftAnchor + 1 month`. They re-sync when the model's
28
+ start changes to a month not currently visible.
29
+ - **Hover preview**: hovering a day in the popover renders an "accent"
30
+ range marker from `start` to the hovered day (or hovered day to start
31
+ if the hover is earlier). Requires `start` to be already picked.
32
+ - **`min` / `max` are ISO strings** that gate selection via the calendar's
33
+ `isDisabled`. Consumer's own `isDisabled(iso)` predicate ORs in.
34
+ - **`getMarker(iso)` is the consumer's marker provider.** The preview
35
+ marker **wins** over consumer markers for days inside the previewed
36
+ range.
37
+ - **Built on `<orio-calendar>` × 2**, side-by-side in a flex row with
38
+ 0.75rem gap.
39
+ - **i18n key**: `dateRangePicker.placeholder` for the empty-display label.
40
+ - **Display string**: `"start – end"` when both exist (en-dash, spaces).
41
+ Just `start` or just `end` if only one. Empty string if both null.
42
+
43
+ ## Gotchas
44
+
45
+ - **The picked `start` does NOT render a marker by itself** when no `end`
46
+ and no hover — `previewMarker` requires both `previewStart` and
47
+ `previewEnd`. There is no visible feedback that a start was picked
48
+ until the user hovers or clicks an end. If you need a "just-start"
49
+ indicator, pass it via `markers` from the consumer.
50
+ - **Both calendars share the same `markers`, `get-marker`, and
51
+ `is-disabled` props.** Consumer markers spanning across months draw
52
+ correctly because they're date-based, not anchor-based.
53
+ - **Hover state clears on `mouseleave` of the popover content**, not on
54
+ picking. Quick double-click sequences clear hover only after the
55
+ second click.
56
+ - **No keyboard support for range selection** — arrow-key roving inside
57
+ Calendar still works, but Enter on the keyboard-focused day picks one
58
+ end at a time, mirroring mouse behavior.
59
+ - **Min/max bounds are ISO string comparisons**: `iso < min` works
60
+ because ISO dates sort lexicographically. Pass YYYY-MM-DD strings, not
61
+ arbitrary Date objects.
62
+
63
+ ## Quick reference
64
+
65
+ ```vue
66
+ <script setup lang="ts">
67
+ import type { DateRange } from "../components/date/RangePicker.vue";
68
+
69
+ const range = defineModel<DateRange>({
70
+ default: () => ({ start: null, end: null }),
71
+ });
72
+
73
+ const bookedDays = ["2026-06-15", "2026-06-16"];
74
+ function isDisabled(iso: string) {
75
+ return bookedDays.includes(iso);
76
+ }
77
+ </script>
78
+
79
+ <template>
80
+ <orio-date-range-picker
81
+ v-model="range"
82
+ :label="$t('booking.dates')"
83
+ min="2026-06-10"
84
+ max="2026-12-31"
85
+ :is-disabled="isDisabled"
86
+ />
87
+ </template>
88
+ ```
89
+
90
+ ## Related
91
+
92
+ - `<orio-calendar>` — the underlying grid; rendered twice.
93
+ - `<orio-date-picker>` — single-date variant.
94
+ - `<orio-date-picker-trigger>` — the shared trigger button.
95
+ - `utils/date` — `DateRange`, `parseISO`, `formatISO`, `addMonths`,
96
+ `startOfMonth`, `formatDate`.
97
+ - Public API reference: `docs/components/date/`.
@@ -0,0 +1,98 @@
1
+ ---
2
+ kind: component
3
+ category: Media & misc
4
+ purpose: carousel, image slider, gallery, lightbox slider, image viewer
5
+ short: image carousel with swipe gestures, prev/next buttons, dynamic sizing, and per-image slot
6
+ invariants: true
7
+ ---
8
+
9
+ # gallery/Carousel — agent-only invariants
10
+
11
+ `<orio-gallery-carousel>` cycles through a list of images with swipe and
12
+ prev/next buttons. Pair it with `<orio-gallery-carousel-preview>` for a
13
+ thumbnail strip that binds to the same `activeImage` model.
14
+
15
+ ## Invariants
16
+
17
+ - **v-model name is `activeImage`** and the type is `string` (the image
18
+ URL or id), **not** an index. Bind via `v-model:active-image="..."`.
19
+ - **`size` prop is a `"W:H"` string** parsed as `width:height` in
20
+ pixels:
21
+ - `"400:550"` → fixed 400×550.
22
+ - `"400:"` (empty after colon) → fixed width, **dynamic height** —
23
+ measured from the slot content via a hidden `.carousel-measure`
24
+ container.
25
+ - `":550"` → fixed height, dynamic width.
26
+ - **`fit`**: `"contain"` (default), `"fill"`, `"cover"`, `"scale-down"`.
27
+ Applied as `object-fit` to the inner image via `v-bind(fit)` CSS
28
+ binding.
29
+ - **`appearance`**: `"default"` (border + background) or `"minimal"` (no
30
+ border, no background; prev/next buttons appear only on hover).
31
+ - **Swipe threshold is 10px** of horizontal pointer movement. Drag-right
32
+ → `previousImage`, drag-left → `nextImage`. Below threshold = no
33
+ change.
34
+ - **Looping is implicit.** `nextImage` past the last → first, `previousImage`
35
+ before the first → last. No flag to disable.
36
+ - **Only 3 items are visible at once**: previous (translated −100%),
37
+ active (0), next (translated +100%). All others have `opacity: 0;
38
+ pointer-events: none`.
39
+ - **`#image` slot** overrides the default `<img>` render. Receives `{ image }`.
40
+ Use for videos, captions, complex viewer markup. Slotted content is
41
+ also rendered into the hidden measure container when `size` has a
42
+ dynamic dimension.
43
+ - **Auto-init on mount**: if `activeImage` is unbound or empty, it is
44
+ set to `images[0]`.
45
+ - **Switch buttons only render when `images.length > 1`.**
46
+ - **Transitions**: opacity + transform, 0.5s ease-in-out.
47
+ - **`max-height` clamp**: when both dimensions are fixed, the carousel
48
+ scales down to `carouselWidth / aspectRatio` to respect the parent
49
+ width while preserving the aspect.
50
+
51
+ ## Gotchas
52
+
53
+ - **Image URLs must be unique** — they are used as v-for keys and the
54
+ active-image model. Duplicate URLs collapse to one logical slide.
55
+ - **No keyboard arrow nav.** Swipe + click only. Add `@keydown` on a
56
+ parent if needed.
57
+ - **Switch buttons use `mix-blend-mode: difference`** on supporting
58
+ browsers (not Safari) to remain visible over any image. Custom themes
59
+ may need to override the `.switch-button :deep(.icon)` styles.
60
+ - **Dynamic sizing causes a one-frame measurement flicker** while the
61
+ hidden measure container resolves. For non-changing content, prefer a
62
+ fixed `size` like `"400:550"`.
63
+ - **The carousel `<img>` has `alt="image-url"`** by default — visually
64
+ fine but bad for accessibility. Override via `#image` slot to render
65
+ proper alt text.
66
+
67
+ ## Quick reference
68
+
69
+ ```vue
70
+ <script setup lang="ts">
71
+ const images = [
72
+ "/photos/1.jpg",
73
+ "/photos/2.jpg",
74
+ "/photos/3.jpg",
75
+ ];
76
+ const active = ref(images[0]);
77
+ </script>
78
+
79
+ <template>
80
+ <orio-gallery-carousel
81
+ v-model:active-image="active"
82
+ :images="images"
83
+ size="600:"
84
+ fit="contain"
85
+ />
86
+ <orio-gallery-carousel-preview
87
+ v-model:active-image="active"
88
+ :images="images"
89
+ />
90
+ </template>
91
+ ```
92
+
93
+ ## Related
94
+
95
+ - `<orio-gallery-carousel-preview>` — thumbnail strip bound to the same
96
+ active-image model.
97
+ - `<orio-modal>` — wrap a carousel for lightbox viewing.
98
+ - Public API reference: `docs/components/gallery/`.
@@ -0,0 +1,51 @@
1
+ ---
2
+ kind: component
3
+ category: Media & misc
4
+ purpose: carousel preview, thumbnails strip, image picker strip, gallery thumbnails
5
+ short: horizontal thumbnail strip for the Carousel; clicking a thumb updates the shared `activeImage` model
6
+ invariants: true
7
+ ---
8
+
9
+ # gallery/CarouselPreview — agent-only invariants
10
+
11
+ `<orio-gallery-carousel-preview>` renders a horizontal scrollable strip of
12
+ thumbnails for `<orio-gallery-carousel>`. Bind both components to the same
13
+ `v-model:active-image` and they stay in sync.
14
+
15
+ ## Invariants
16
+
17
+ - **v-model name is `activeImage`** (same as the Carousel) and the type
18
+ is `string`. Share the same ref between them.
19
+ - **Hidden when `images.length ≤ 1`.** Single-image galleries don't
20
+ render a strip.
21
+ - **Each thumbnail is a `<button>`** with `aria-pressed` (true when
22
+ active) and `aria-label` `"Show image N of M"` for screen readers.
23
+ - **Thumbnails are 3.5rem × 3.5rem** with `object-fit` driven by `fit`
24
+ (default `"cover"`, unlike Carousel's `"contain"` default).
25
+ - **`#image` slot** overrides the default `<img>` render. Receives
26
+ `{ image }`. Same signature as the Carousel slot.
27
+ - **Strip scrolls horizontally** with `overflow-x: auto`. No
28
+ auto-scroll-to-active — clicking a thumb that's offscreen won't
29
+ scroll it into view.
30
+ - **Active thumb gets**: opacity 1, accent border. Inactive: opacity
31
+ 0.6 with a hover bump to 0.85.
32
+
33
+ ## Gotchas
34
+
35
+ - **No keyboard arrow nav between thumbs.** Tab moves between buttons;
36
+ Enter / Space activates. Add roving-focus if needed.
37
+ - **No auto-scroll on active change.** If the consumer changes
38
+ `activeImage` from elsewhere, the strip doesn't follow — scroll it
39
+ into view yourself via `element.scrollIntoView()`.
40
+ - **Alt is `""`** on thumbnails by default — they're treated as
41
+ decorative because the `<button>` carries the accessible name.
42
+
43
+ ## Quick reference
44
+
45
+ See `<orio-gallery-carousel>` USAGE.md.
46
+
47
+ ## Related
48
+
49
+ - `<orio-gallery-carousel>` — the main viewer; share the
50
+ `activeImage` model.
51
+ - Public API reference: `docs/components/gallery/`.
@@ -0,0 +1,91 @@
1
+ ---
2
+ kind: component
3
+ category: Media & misc
4
+ purpose: upload, file picker, drop-to-upload, file input, headless file upload
5
+ short: headless file upload — provides drop-zone state and file-dialog opener via slot props; consumer renders the UI
6
+ invariants: true
7
+ ---
8
+
9
+ # upload — agent-only invariants
10
+
11
+ `<orio-upload>` is a **headless** file-upload component. It owns drag-drop
12
+ detection and file-dialog opening; the consumer renders all UI through the
13
+ default slot. There is no built-in look.
14
+
15
+ ## Invariants
16
+
17
+ - **Template is essentially `<div><slot :is-over-drop-zone :open-dialog /></div>`.**
18
+ No styling, no built-in drop hint, no preview list.
19
+ - **Slot props**:
20
+ - `isOverDropZone: boolean` — true while a dragged file is over the
21
+ zone (and the component is not disabled).
22
+ - `openDialog: () => void` — opens the native file picker.
23
+ - **v-model is `File[]`** (default `[]`). Drops and dialog selections
24
+ **append** to it; the array is then sliced to `maxFiles` if set.
25
+ - **`maxFiles`**:
26
+ - `undefined` (default) → unlimited.
27
+ - `> 1` → multi-select mode (drop & dialog).
28
+ - `1` → single-file mode; new selections replace the array (capped to
29
+ length 1 by the slice).
30
+ - **`allowedTypes`** is forwarded as `dataTypes` to `useDropZone`
31
+ (drop filter) and as `accept` (comma-joined) to the native dialog.
32
+ Be explicit — passing MIME-type strings (`"image/png"`) vs.
33
+ extensions (`".png"`) is the consumer's choice.
34
+ - **`disabled`** blocks both drop and `openDialog` calls. `isOverDropZone`
35
+ is also forced `false` while disabled so the slot UI doesn't flash an
36
+ "active" state during a no-op.
37
+ - **The whole template div is the drop zone.** The slot content sits
38
+ inside it; the consumer's hit area equals whatever they render.
39
+
40
+ ## Gotchas
41
+
42
+ - **No UI at all by default.** A bare `<orio-upload v-model="files" />`
43
+ renders an empty `<div>` — clicking does nothing. You must provide a
44
+ default slot that calls `openDialog`.
45
+ - **Drops append**, including duplicates. Same-named files are added
46
+ again; dedupe in the consumer if needed.
47
+ - **`maxFiles` only enforces on append**. If the model is pre-populated
48
+ with more files than `maxFiles`, they stick around until the next
49
+ drop / dialog truncates them.
50
+ - **`useFileDialog` uses native input.** It's not styleable. The "dialog"
51
+ is the OS chooser; styling lives on the trigger element you render in
52
+ the slot.
53
+ - **No progress / upload semantics.** This component only collects File
54
+ objects. Uploading them to a server is the consumer's job.
55
+ - **`accept` attribute on the dialog vs. drop filter divergence**: the
56
+ drop filter is enforced by browser drag-drop semantics; the dialog's
57
+ `accept` is a hint, not a hard filter — users can choose any file via
58
+ the chooser depending on OS.
59
+
60
+ ## Quick reference
61
+
62
+ ```vue
63
+ <script setup lang="ts">
64
+ const files = ref<File[]>([]);
65
+ </script>
66
+
67
+ <template>
68
+ <orio-upload
69
+ v-model="files"
70
+ :max-files="5"
71
+ :allowed-types="['image/png', 'image/jpeg']"
72
+ >
73
+ <template #default="{ isOverDropZone, openDialog }">
74
+ <orio-dashed-container
75
+ :icon="isOverDropZone ? 'drop' : 'upload'"
76
+ :text="$t(isOverDropZone ? 'upload.drop' : 'upload.choose')"
77
+ @click="openDialog"
78
+ />
79
+ </template>
80
+ </orio-upload>
81
+
82
+ <ul>
83
+ <li v-for="(file, index) in files" :key="index">{{ file.name }}</li>
84
+ </ul>
85
+ </template>
86
+ ```
87
+
88
+ ## Related
89
+
90
+ - `<orio-dashed-container>` — common UI shell for upload tiles.
91
+ - Public API reference: `docs/components/upload.md` (if present).