orio-ui 1.27.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 (64) 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 +78 -0
  12. package/dist/runtime/components/Calendar.USAGE.md +8 -0
  13. package/dist/runtime/components/Canvas/USAGE.md +8 -0
  14. package/dist/runtime/components/CheckBox.USAGE.md +63 -0
  15. package/dist/runtime/components/CheckboxGroup.USAGE.md +95 -0
  16. package/dist/runtime/components/ControlElement.USAGE.md +8 -0
  17. package/dist/runtime/components/DashedContainer.USAGE.md +65 -0
  18. package/dist/runtime/components/EmptyState.USAGE.md +65 -0
  19. package/dist/runtime/components/Form.USAGE.md +102 -0
  20. package/dist/runtime/components/Icon.USAGE.md +61 -0
  21. package/dist/runtime/components/Input.USAGE.md +8 -0
  22. package/dist/runtime/components/ListItem.USAGE.md +84 -0
  23. package/dist/runtime/components/LoadingSpinner.USAGE.md +50 -0
  24. package/dist/runtime/components/LocaleSwitcher.USAGE.md +73 -0
  25. package/dist/runtime/components/Modal.USAGE.md +8 -0
  26. package/dist/runtime/components/NavButton.USAGE.md +80 -0
  27. package/dist/runtime/components/NumberInput/Horizontal.USAGE.md +61 -0
  28. package/dist/runtime/components/NumberInput/USAGE.md +74 -0
  29. package/dist/runtime/components/NumberInput/Vertical.USAGE.md +55 -0
  30. package/dist/runtime/components/Popover.USAGE.md +103 -0
  31. package/dist/runtime/components/RadioButton.USAGE.md +72 -0
  32. package/dist/runtime/components/Selector.USAGE.md +131 -0
  33. package/dist/runtime/components/SwitchButton.USAGE.md +62 -0
  34. package/dist/runtime/components/Tag.USAGE.md +51 -0
  35. package/dist/runtime/components/TaggableSelector.USAGE.md +73 -0
  36. package/dist/runtime/components/Textarea.USAGE.md +72 -0
  37. package/dist/runtime/components/Tooltip.USAGE.md +84 -0
  38. package/dist/runtime/components/ZoomableContainer.USAGE.md +108 -0
  39. package/dist/runtime/components/date/Picker.USAGE.md +8 -0
  40. package/dist/runtime/components/date/PickerTrigger.USAGE.md +65 -0
  41. package/dist/runtime/components/date/RangePicker.USAGE.md +97 -0
  42. package/dist/runtime/components/gallery/Carousel.USAGE.md +98 -0
  43. package/dist/runtime/components/gallery/CarouselPreview.USAGE.md +51 -0
  44. package/dist/runtime/components/upload/USAGE.md +91 -0
  45. package/dist/runtime/components/view/Dates.USAGE.md +67 -0
  46. package/dist/runtime/components/view/KeyBinds.USAGE.md +58 -0
  47. package/dist/runtime/components/view/Separator.USAGE.md +57 -0
  48. package/dist/runtime/components/view/Text.USAGE.md +68 -0
  49. package/dist/runtime/composables/useApi.USAGE.md +64 -0
  50. package/dist/runtime/composables/useControlSize.USAGE.md +73 -0
  51. package/dist/runtime/composables/useDecimalFormatter.USAGE.md +72 -0
  52. package/dist/runtime/composables/useFilter.USAGE.md +120 -0
  53. package/dist/runtime/composables/useFuzzySearch.USAGE.md +68 -0
  54. package/dist/runtime/composables/useInertia.USAGE.md +80 -0
  55. package/dist/runtime/composables/useListKeyboard.USAGE.md +97 -0
  56. package/dist/runtime/composables/useModal.USAGE.md +82 -0
  57. package/dist/runtime/composables/usePinchZoom.USAGE.md +95 -0
  58. package/dist/runtime/composables/usePressAndHold.USAGE.md +70 -0
  59. package/dist/runtime/composables/useRovingGrid.USAGE.md +106 -0
  60. package/dist/runtime/composables/useSound.USAGE.md +74 -0
  61. package/dist/runtime/composables/useTheme.USAGE.md +76 -0
  62. package/dist/runtime/composables/useUrlSync.USAGE.md +91 -0
  63. package/dist/runtime/composables/useValidation.USAGE.md +100 -0
  64. package/package.json +12 -2
@@ -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).
@@ -0,0 +1,67 @@
1
+ ---
2
+ kind: component
3
+ category: Media & misc
4
+ purpose: read-only date display, formatted date range, date range view
5
+ short: locale-aware read-only date or date range display; inline `<orio-view-text>` for start/end
6
+ invariants: true
7
+ ---
8
+
9
+ # view/Dates — agent-only invariants
10
+
11
+ `<orio-view-dates>` renders a `DateRange` (`{ start, end }`) as inline
12
+ formatted text using the active vue-i18n locale.
13
+
14
+ ## Invariants
15
+
16
+ - **`dates` is required** and typed `DateRange` (ISO strings).
17
+ - **Format options**:
18
+ - Default: `{ day: "numeric", month: "short", year: "numeric" }` →
19
+ `"10 Jun 2026"`.
20
+ - `month: true` → omits the day → `"Jun 2026"`. Use for
21
+ month-resolution ranges (subscription periods, etc.).
22
+ - **Uses `formatDate(iso, locale, options)` from `utils/date`**. Output
23
+ follows the locale — `en` vs `uk` will render different month
24
+ abbreviations.
25
+ - **Separator**: literal `" - "` rendered between start and end when
26
+ both are present. No en-dash, no localization.
27
+ - **Renders two `<orio-view-text>`** with `type` and `size` forwarded
28
+ (defaults `type: "italics"`, `size: "small"`).
29
+ - **`* { display: inline }`** on the wrapper forces both view-text
30
+ blocks inline so they read as one sentence.
31
+
32
+ ## Gotchas
33
+
34
+ - **Only start, only end, or both**: rendering gracefully handles a
35
+ missing `end` (no separator, no second block). A missing `start` with
36
+ an `end` renders the separator alone — degraded UX.
37
+ - **No relative formatting** (e.g. "yesterday", "3 days ago"). For
38
+ relative output, format in the consumer and pass via
39
+ `<orio-view-text>` instead.
40
+ - **`size` is forwarded to view-text** but the wrapper itself has no
41
+ size. Custom CSS that targets the wrapper won't see a size class.
42
+
43
+ ## Quick reference
44
+
45
+ ```vue
46
+ <script setup lang="ts">
47
+ const period = { start: "2026-06-01", end: "2026-06-30" };
48
+ </script>
49
+
50
+ <template>
51
+ <orio-view-dates :dates="period" />
52
+
53
+ <orio-view-dates
54
+ :dates="{ start: subscriptionStart, end: subscriptionEnd }"
55
+ month
56
+ type="title"
57
+ size="medium"
58
+ />
59
+ </template>
60
+ ```
61
+
62
+ ## Related
63
+
64
+ - `<orio-view-text>` — used internally for each end of the range.
65
+ - `<orio-date-range-picker>` — picker that produces `DateRange` values.
66
+ - `utils/date` — `formatDate`, `DateRange` type.
67
+ - Public API reference: `docs/components/view/dates.md` (if present).
@@ -0,0 +1,58 @@
1
+ ---
2
+ kind: component
3
+ category: Media & misc
4
+ purpose: keyboard bindings hint display, shortcut display, kbd renderer
5
+ short: parses a backtick-delimited shortcut string and renders each key as `<kbd>` with separators inline
6
+ invariants: true
7
+ ---
8
+
9
+ # view/KeyBinds — agent-only invariants
10
+
11
+ `<orio-view-key-binds>` parses a backtick-delimited string like
12
+ `` "`Ctrl` + `Z`" `` and renders each backticked token as a `<kbd>`
13
+ element with the surrounding text as separator.
14
+
15
+ ## Invariants
16
+
17
+ - **`bind` is the single string prop.** Tokens between backticks (`` `…` ``)
18
+ become `<kbd>` elements; everything else renders as a `.separator`
19
+ `<span>`.
20
+ - **Regex is `/`([^`]+)`/g`** — non-greedy match inside backticks.
21
+ Empty backticks (`` `` ``) and unmatched openings are passed through
22
+ as plain text.
23
+ - **No tokenization beyond backticks.** `"+"`, `" "`, `","`, `"or"`
24
+ between keys all render as plain separator text. Style them via the
25
+ `.separator` class.
26
+ - **Output structure**: one `<span class="keybinds">` wrapper, with
27
+ `<kbd>` and `<span class="separator">` children inline. Wrapper is
28
+ `inline-flex` with 0.2rem gap.
29
+ - **Kbd styling** is fixed: rgba white background tint, small font,
30
+ border. Designed for dark surfaces — over a light background, the
31
+ contrast may be poor; override `kbd` styles via global CSS.
32
+
33
+ ## Gotchas
34
+
35
+ - **The string is rendered as-is.** No localization, no key-symbol
36
+ substitution (e.g. `Cmd` does not become `⌘`). Build that mapping in
37
+ the consumer if needed.
38
+ - **No `aria-label`.** Screen readers read each `<kbd>` token aloud
39
+ with the separator text — usually fine for `"Ctrl + Z"`, less great
40
+ for `" or "`-separated alternates.
41
+ - **Mismatched backticks render as text.** `` "`Ctrl + Z" `` (missing
42
+ closing tick) becomes plain text starting from the unmatched
43
+ backtick.
44
+
45
+ ## Quick reference
46
+
47
+ ```vue
48
+ <template>
49
+ <orio-view-key-binds bind="`Ctrl` + `Z`" />
50
+ <orio-view-key-binds bind="press `Esc` to close" />
51
+ <orio-view-key-binds bind="`Cmd` + `Shift` + `P` or `F1`" />
52
+ </template>
53
+ ```
54
+
55
+ ## Related
56
+
57
+ - Public API reference: `docs/components/view/key-binds.md` (if
58
+ present).