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.
- package/README.md +76 -1
- package/bin/orio-ui.mjs +72 -0
- package/dist/agents/ROUTING.md +140 -0
- package/dist/agents/component-finder.md +142 -0
- package/dist/agents/component-worker.md +152 -0
- package/dist/agents/snippet.md +6 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/AnimatedContainer.USAGE.md +79 -0
- package/dist/runtime/components/Badge.USAGE.md +75 -0
- package/dist/runtime/components/Banner.USAGE.md +52 -0
- package/dist/runtime/components/Button.USAGE.md +83 -0
- package/dist/runtime/components/Button.d.vue.ts +1 -0
- package/dist/runtime/components/Button.vue +5 -1
- package/dist/runtime/components/Button.vue.d.ts +1 -0
- package/dist/runtime/components/Calendar.USAGE.md +8 -0
- package/dist/runtime/components/Canvas/USAGE.md +8 -0
- package/dist/runtime/components/CheckBox.USAGE.md +63 -0
- package/dist/runtime/components/CheckboxGroup.USAGE.md +95 -0
- package/dist/runtime/components/ControlElement.USAGE.md +8 -0
- package/dist/runtime/components/ControlElement.d.vue.ts +1 -1
- package/dist/runtime/components/ControlElement.vue.d.ts +1 -1
- package/dist/runtime/components/DashedContainer.USAGE.md +65 -0
- package/dist/runtime/components/EmptyState.USAGE.md +65 -0
- package/dist/runtime/components/Form.USAGE.md +102 -0
- package/dist/runtime/components/Icon.USAGE.md +61 -0
- package/dist/runtime/components/Input.USAGE.md +8 -0
- package/dist/runtime/components/ListItem.USAGE.md +84 -0
- package/dist/runtime/components/LoadingSpinner.USAGE.md +50 -0
- package/dist/runtime/components/LocaleSwitcher.USAGE.md +73 -0
- package/dist/runtime/components/Modal.USAGE.md +8 -0
- package/dist/runtime/components/NavButton.USAGE.md +80 -0
- package/dist/runtime/components/NumberInput/Horizontal.USAGE.md +61 -0
- package/dist/runtime/components/NumberInput/Horizontal.vue +5 -0
- package/dist/runtime/components/NumberInput/USAGE.md +74 -0
- package/dist/runtime/components/NumberInput/Vertical.USAGE.md +55 -0
- package/dist/runtime/components/NumberInput/Vertical.vue +9 -1
- package/dist/runtime/components/Popover.USAGE.md +103 -0
- package/dist/runtime/components/RadioButton.USAGE.md +72 -0
- package/dist/runtime/components/Selector.USAGE.md +131 -0
- package/dist/runtime/components/SwitchButton.USAGE.md +62 -0
- package/dist/runtime/components/Tag.USAGE.md +51 -0
- package/dist/runtime/components/TaggableSelector.USAGE.md +73 -0
- package/dist/runtime/components/Textarea.USAGE.md +72 -0
- package/dist/runtime/components/Tooltip.USAGE.md +84 -0
- package/dist/runtime/components/ZoomableContainer.USAGE.md +108 -0
- package/dist/runtime/components/date/Picker.USAGE.md +8 -0
- package/dist/runtime/components/date/PickerTrigger.USAGE.md +65 -0
- package/dist/runtime/components/date/RangePicker.USAGE.md +97 -0
- package/dist/runtime/components/gallery/Carousel.USAGE.md +98 -0
- package/dist/runtime/components/gallery/CarouselPreview.USAGE.md +51 -0
- package/dist/runtime/components/upload/USAGE.md +91 -0
- package/dist/runtime/components/view/Dates.USAGE.md +67 -0
- package/dist/runtime/components/view/KeyBinds.USAGE.md +58 -0
- package/dist/runtime/components/view/Separator.USAGE.md +57 -0
- package/dist/runtime/components/view/Text.USAGE.md +68 -0
- package/dist/runtime/composables/useApi.USAGE.md +64 -0
- package/dist/runtime/composables/useControlSize.USAGE.md +73 -0
- package/dist/runtime/composables/useControlSize.js +12 -0
- package/dist/runtime/composables/useDecimalFormatter.USAGE.md +72 -0
- package/dist/runtime/composables/useFilter.USAGE.md +120 -0
- package/dist/runtime/composables/useFuzzySearch.USAGE.md +68 -0
- package/dist/runtime/composables/useInertia.USAGE.md +80 -0
- package/dist/runtime/composables/useListKeyboard.USAGE.md +97 -0
- package/dist/runtime/composables/useModal.USAGE.md +82 -0
- package/dist/runtime/composables/usePinchZoom.USAGE.md +95 -0
- package/dist/runtime/composables/usePressAndHold.USAGE.md +70 -0
- package/dist/runtime/composables/useRovingGrid.USAGE.md +106 -0
- package/dist/runtime/composables/useSound.USAGE.md +74 -0
- package/dist/runtime/composables/useTheme.USAGE.md +76 -0
- package/dist/runtime/composables/useUrlSync.USAGE.md +91 -0
- package/dist/runtime/composables/useValidation.USAGE.md +100 -0
- 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).
|