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.
- 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 +78 -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/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/USAGE.md +74 -0
- package/dist/runtime/components/NumberInput/Vertical.USAGE.md +55 -0
- 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/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,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: composable
|
|
3
|
+
category: Composables
|
|
4
|
+
purpose: arrow-key flat list navigation, listbox keyboard, dropdown keys
|
|
5
|
+
short: arrow / Home / End / Enter / Space / Esc handling for a flat indexable list with auto scroll-into-view
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# useListKeyboard — agent-only invariants
|
|
10
|
+
|
|
11
|
+
`useListKeyboard` wires keyboard navigation for a flat list (listbox,
|
|
12
|
+
menu, dropdown). It owns the highlighted index, scrolls the highlighted
|
|
13
|
+
item into view, and emits open/close/select callbacks. Used internally by
|
|
14
|
+
`<orio-selector>`.
|
|
15
|
+
|
|
16
|
+
## Invariants
|
|
17
|
+
|
|
18
|
+
- **`options.count()` is called on every navigation** — pass a function,
|
|
19
|
+
not a number. Lists that change size during interaction stay correct.
|
|
20
|
+
- **`options.onSelect(index)`** fires on Enter or Space when the list is
|
|
21
|
+
open and a row is highlighted (`highlightedIndex >= 0`).
|
|
22
|
+
- **`options.onOpen()`** fires when the user presses Arrow Down/Up,
|
|
23
|
+
Enter, or Space while the list is closed. Optional — without it,
|
|
24
|
+
closed-state key presses are no-ops.
|
|
25
|
+
- **`options.onClose()`** fires on Escape (plus resets the highlight to
|
|
26
|
+
`-1`). Optional.
|
|
27
|
+
- **`options.initialIndex()`** is called by `reset()` to set the
|
|
28
|
+
highlight on (re)open — typically returns the currently selected row.
|
|
29
|
+
Defaults to `0` when not provided.
|
|
30
|
+
- **Returned `listRef` MUST be attached to the list container** (the
|
|
31
|
+
`<ul>` / `<ol>`) so `scrollIntoView` can find the highlighted child.
|
|
32
|
+
Without it, the highlight still moves but never scrolls.
|
|
33
|
+
- **`onKeydown(e, isOpen)`** is the handler to wire on the trigger or
|
|
34
|
+
list. The caller passes `isOpen` because the composable does not own
|
|
35
|
+
open state.
|
|
36
|
+
- **Every matched key calls `e.preventDefault()`.** Unmatched keys
|
|
37
|
+
bubble. Space is registered as `" "` (single space string).
|
|
38
|
+
- **Highlight clamps to `[0, count - 1]`.** `highlight(-1)` lands on 0,
|
|
39
|
+
`highlight(99)` on `count - 1`.
|
|
40
|
+
|
|
41
|
+
## Gotchas
|
|
42
|
+
|
|
43
|
+
- **Children must be direct descendants of the `listRef` element** for
|
|
44
|
+
scrollIntoView to target the correct row. Nested wrappers (a `<li>`
|
|
45
|
+
with deep children) break the index → `ul.children[index]` lookup.
|
|
46
|
+
- **Space (`" "`) selection collides with form behaviors.** Inside a
|
|
47
|
+
text input or button that already handles space, the preventDefault
|
|
48
|
+
may swallow it. Wire `onKeydown` on the listbox container or wrapping
|
|
49
|
+
focusable element, not on inputs.
|
|
50
|
+
- **`reset()` calls `scrollIntoView` on `nextTick`** — the list element
|
|
51
|
+
must be in the DOM by then. Calling `reset()` before mounting the
|
|
52
|
+
list won't scroll until after the next tick.
|
|
53
|
+
- **No type-ahead** ("press 'a' to jump to next item starting with a").
|
|
54
|
+
Implement upstream if needed.
|
|
55
|
+
|
|
56
|
+
## Quick reference
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { ref } from "vue";
|
|
60
|
+
import { useListKeyboard } from "../composables/useListKeyboard";
|
|
61
|
+
|
|
62
|
+
const isOpen = ref(false);
|
|
63
|
+
const items = ref<string[]>(["Alpha", "Bravo", "Charlie"]);
|
|
64
|
+
const selected = ref<string | null>(null);
|
|
65
|
+
|
|
66
|
+
const { highlightedIndex, listRef, onKeydown, reset } = useListKeyboard({
|
|
67
|
+
count: () => items.value.length,
|
|
68
|
+
onSelect: (index) => {
|
|
69
|
+
selected.value = items.value[index];
|
|
70
|
+
isOpen.value = false;
|
|
71
|
+
},
|
|
72
|
+
onOpen: () => {
|
|
73
|
+
isOpen.value = true;
|
|
74
|
+
reset();
|
|
75
|
+
},
|
|
76
|
+
onClose: () => (isOpen.value = false),
|
|
77
|
+
initialIndex: () => items.value.indexOf(selected.value ?? ""),
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<template>
|
|
83
|
+
<button @keydown="onKeydown($event, isOpen)" @click="onOpen">Open</button>
|
|
84
|
+
<ul v-if="isOpen" ref="listRef">
|
|
85
|
+
<li v-for="(item, index) in items" :key="item"
|
|
86
|
+
:class="{ highlighted: index === highlightedIndex }">
|
|
87
|
+
{{ item }}
|
|
88
|
+
</li>
|
|
89
|
+
</ul>
|
|
90
|
+
</template>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Related
|
|
94
|
+
|
|
95
|
+
- `<orio-selector>` — uses this composable internally.
|
|
96
|
+
- `useRovingGrid` — 2D arrow-key roving focus.
|
|
97
|
+
- Public API reference: `docs/composables/use-list-keyboard.md`.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: composable
|
|
3
|
+
category: Composables
|
|
4
|
+
purpose: programmatic modal control, open modal from code, modal binding bag
|
|
5
|
+
short: returns a `modalProps` bag (show, origin, update handler) plus `openModal(event?)` that derives origin from a click target
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# useModal — agent-only invariants
|
|
10
|
+
|
|
11
|
+
`useModal` returns a binding bag for `<orio-modal>` plus an `openModal`
|
|
12
|
+
helper that captures the click target's rect for the open-from-origin
|
|
13
|
+
animation. Spread `modalProps` on the Modal — no manual `v-model:show`,
|
|
14
|
+
no `:origin` wiring.
|
|
15
|
+
|
|
16
|
+
## Invariants
|
|
17
|
+
|
|
18
|
+
- **`modalProps` ref** has shape:
|
|
19
|
+
```ts
|
|
20
|
+
{
|
|
21
|
+
show: boolean;
|
|
22
|
+
origin: OriginRect | null;
|
|
23
|
+
"onUpdate:show": (state: boolean) => void;
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
Designed to be spread on `<orio-modal v-bind="modalProps">`. The
|
|
27
|
+
`"onUpdate:show"` listener is wired internally so the modal's close
|
|
28
|
+
button / backdrop click syncs back to the bag.
|
|
29
|
+
- **`OriginRect`** (exported): `{ x, y, width, height }` — derived from
|
|
30
|
+
`getBoundingClientRect()` of the event target.
|
|
31
|
+
- **`openModal(event?)`**:
|
|
32
|
+
- With an event: reads `event.target.getBoundingClientRect()` and
|
|
33
|
+
sets `origin` so the modal animates from the target. Sets `show`
|
|
34
|
+
to true.
|
|
35
|
+
- Without an event: clears `origin` to `null` and opens with a plain
|
|
36
|
+
fade.
|
|
37
|
+
- **No close helper** is returned — `modalProps.show = false` works, or
|
|
38
|
+
use the modal's built-in close button.
|
|
39
|
+
|
|
40
|
+
## Gotchas
|
|
41
|
+
|
|
42
|
+
- **`event.target` is the clicked DOM node**, which may be a child of
|
|
43
|
+
the actual button (e.g. an icon inside). The rect then comes from
|
|
44
|
+
the icon, not the button — the animation origin will be tiny. To
|
|
45
|
+
capture the button itself, use `event.currentTarget` semantics by
|
|
46
|
+
reading the rect manually before calling `openModal`:
|
|
47
|
+
```ts
|
|
48
|
+
function onClick(event: MouseEvent) {
|
|
49
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
50
|
+
modalProps.value.origin = rect;
|
|
51
|
+
modalProps.value.show = true;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
- **Only one modal per call site.** For multiple modals, call
|
|
55
|
+
`useModal()` once per modal — they don't share state.
|
|
56
|
+
- **No `v-model:show`** on the returned bag — the bag uses the explicit
|
|
57
|
+
`onUpdate:show` listener form because spreading an object cannot
|
|
58
|
+
declare v-model sugar.
|
|
59
|
+
|
|
60
|
+
## Quick reference
|
|
61
|
+
|
|
62
|
+
```vue
|
|
63
|
+
<script setup lang="ts">
|
|
64
|
+
import { useModal } from "../composables/useModal";
|
|
65
|
+
|
|
66
|
+
const { modalProps, openModal } = useModal();
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<template>
|
|
70
|
+
<orio-button @click="openModal">Open</orio-button>
|
|
71
|
+
|
|
72
|
+
<orio-modal v-bind="modalProps" title="Settings">
|
|
73
|
+
<p>Modal body…</p>
|
|
74
|
+
</orio-modal>
|
|
75
|
+
</template>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Related
|
|
79
|
+
|
|
80
|
+
- `<orio-modal>` — the consumer of this bag; supports `v-model:show`
|
|
81
|
+
and `origin` as plain props if you prefer manual wiring.
|
|
82
|
+
- Public API reference: `docs/composables/use-modal.md`.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: composable
|
|
3
|
+
category: Composables
|
|
4
|
+
purpose: pinch-to-zoom, two-finger touch zoom, pinch gesture
|
|
5
|
+
short: touch-only pinch handler that tracks up to 2 pointers and exposes scale factor, midpoint, and midpoint delta
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# usePinchZoom — agent-only invariants
|
|
10
|
+
|
|
11
|
+
`usePinchZoom(callbacks)` tracks **touch** pointers (up to 2) and emits a
|
|
12
|
+
`scaleFactor` for two-finger gestures plus delegate callbacks for single-
|
|
13
|
+
finger phases. The consumer wires the returned handlers to pointer events.
|
|
14
|
+
|
|
15
|
+
## Invariants
|
|
16
|
+
|
|
17
|
+
- **Touch-only.** `onPointerDown` returns `false` for non-touch (mouse,
|
|
18
|
+
pen) events. Mouse zoom needs separate wheel handling.
|
|
19
|
+
- **Tracks at most 2 pointers.** A third touch is ignored.
|
|
20
|
+
- **Callback shape**:
|
|
21
|
+
- `onPinchStart()` — fires when the second pointer goes down.
|
|
22
|
+
- `onPinchMove(scaleFactor, midX, midY, dx, dy)` — fires on each
|
|
23
|
+
two-finger move:
|
|
24
|
+
- `scaleFactor` = `currentDistance / startDistance` (cumulative
|
|
25
|
+
since pinch start).
|
|
26
|
+
- `midX`, `midY` = current viewport-coord midpoint between fingers.
|
|
27
|
+
- `dx`, `dy` = delta of the midpoint since the last move (for
|
|
28
|
+
pan-during-pinch).
|
|
29
|
+
- `onSingleDown(event)` — fires when the FIRST pointer goes down.
|
|
30
|
+
- `onSingleUp(remainingId, x, y)` — fires when one of two pointers
|
|
31
|
+
lifts, leaving one behind. Pass these to the caller's single-
|
|
32
|
+
pointer drag logic.
|
|
33
|
+
- `onAllUp()` — fires when the last pointer lifts.
|
|
34
|
+
- **Pointer capture is set on `event.currentTarget`** for every down.
|
|
35
|
+
The consumer must wire `onPointerDown` to a real element so capture
|
|
36
|
+
works.
|
|
37
|
+
- **`scaleFactor` is relative to pinch-start distance**, not the
|
|
38
|
+
previous frame. Multiply the consumer's "baseline scale at pinch
|
|
39
|
+
start" by `scaleFactor` to derive the new scale.
|
|
40
|
+
- **`startDist` resets** when pointer count drops from 2 → 1 or 0.
|
|
41
|
+
- **`pointers` is an exposed `Map`** keyed by pointerId — read it for
|
|
42
|
+
debugging.
|
|
43
|
+
|
|
44
|
+
## Gotchas
|
|
45
|
+
|
|
46
|
+
- **Single-pointer drag is the consumer's responsibility.** The
|
|
47
|
+
composable only signals down/up; you wire the actual move handling
|
|
48
|
+
outside.
|
|
49
|
+
- **No pinch-axis lock.** Both x and y midpoint changes flow through;
|
|
50
|
+
diagonal pinch produces both zoom and pan. Filter in
|
|
51
|
+
`onPinchMove` if you want zoom-only.
|
|
52
|
+
- **`scaleFactor` can exceed sensible ranges** on fast pinches. Clamp
|
|
53
|
+
upstream (e.g. against `minScale` / `maxScale`).
|
|
54
|
+
- **PointerType check is exact `"touch"`.** Microsoft Surface pen
|
|
55
|
+
inputs and Apple Pencil have `pointerType: "pen"` — those won't
|
|
56
|
+
pinch via this composable.
|
|
57
|
+
|
|
58
|
+
## Quick reference
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { usePinchZoom } from "../composables/usePinchZoom";
|
|
62
|
+
|
|
63
|
+
let baseScale = 1;
|
|
64
|
+
const scale = ref(1);
|
|
65
|
+
const tx = ref(0);
|
|
66
|
+
const ty = ref(0);
|
|
67
|
+
|
|
68
|
+
const pinch = usePinchZoom({
|
|
69
|
+
onPinchStart() { baseScale = scale.value; },
|
|
70
|
+
onPinchMove(scaleFactor, midX, midY, dx, dy) {
|
|
71
|
+
scale.value = baseScale * scaleFactor;
|
|
72
|
+
tx.value += dx;
|
|
73
|
+
ty.value += dy;
|
|
74
|
+
// (also re-anchor scale around midpoint here)
|
|
75
|
+
},
|
|
76
|
+
onSingleDown(event) { /* hand off to single-pointer drag */ },
|
|
77
|
+
onSingleUp(id, x, y) { /* resume drag tracking on remaining finger */ },
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<template>
|
|
83
|
+
<div
|
|
84
|
+
@pointerdown="pinch.onPointerDown"
|
|
85
|
+
@pointermove="pinch.onPointerMove"
|
|
86
|
+
@pointerup="pinch.onPointerUp"
|
|
87
|
+
/>
|
|
88
|
+
</template>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Related
|
|
92
|
+
|
|
93
|
+
- `<orio-zoomable-container>` — uses this for touch pinch.
|
|
94
|
+
- `<orio-canvas>` — same.
|
|
95
|
+
- Public API reference: `docs/composables/use-pinch-zoom.md`.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: composable
|
|
3
|
+
category: Composables
|
|
4
|
+
purpose: long-press detection, press-and-hold, auto-repeat, mousedown-hold ramp
|
|
5
|
+
short: fires a callback once immediately, then repeats every 50 ms after a 500 ms hold
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# usePressAndHold — agent-only invariants
|
|
10
|
+
|
|
11
|
+
`usePressAndHold()` returns `{ pressAndHold, stop }` for press-and-hold
|
|
12
|
+
auto-repeat. Used by `<orio-number-input-horizontal>` and
|
|
13
|
+
`<orio-number-input-vertical>` to ramp values while a spinner button is
|
|
14
|
+
held.
|
|
15
|
+
|
|
16
|
+
## Invariants
|
|
17
|
+
|
|
18
|
+
- **`pressAndHold(callback)` calls the callback once immediately**, then
|
|
19
|
+
starts a 500 ms timeout. After 500 ms, it begins calling the callback
|
|
20
|
+
every 50 ms via `setInterval`.
|
|
21
|
+
- **`stop()` clears both the pending timeout and the active interval.**
|
|
22
|
+
Always call on `mouseup`, `mouseleave`, and `pointercancel` — leaks
|
|
23
|
+
fire forever otherwise.
|
|
24
|
+
- **No reactivity, no state ref.** Just two functions. The internal
|
|
25
|
+
refs hold `setTimeout` / `setInterval` IDs only.
|
|
26
|
+
- **No press duration / counter.** The composable only knows
|
|
27
|
+
"callback fires." Track elapsed time in the callback if needed.
|
|
28
|
+
|
|
29
|
+
## Gotchas
|
|
30
|
+
|
|
31
|
+
- **Timer leaks if `stop()` isn't called.** Common pitfall: forgetting
|
|
32
|
+
to wire `@mouseleave` means a user dragging off the button leaves
|
|
33
|
+
the interval running.
|
|
34
|
+
- **Touch behavior is not handled.** `@mousedown` does fire on touch
|
|
35
|
+
in most browsers but not all. For reliable mobile press-and-hold,
|
|
36
|
+
consider also wiring `@touchstart` / `@touchend`.
|
|
37
|
+
- **Re-entrancy not guarded.** Calling `pressAndHold(fn)` while a
|
|
38
|
+
previous press is active will create overlapping timers. Always
|
|
39
|
+
`stop()` before starting a new one.
|
|
40
|
+
- **Fixed timing (500 ms delay, 50 ms repeat).** Not configurable. Fork
|
|
41
|
+
if you need different cadence.
|
|
42
|
+
|
|
43
|
+
## Quick reference
|
|
44
|
+
|
|
45
|
+
```vue
|
|
46
|
+
<script setup lang="ts">
|
|
47
|
+
import { usePressAndHold } from "../composables/usePressAndHold";
|
|
48
|
+
|
|
49
|
+
const { pressAndHold, stop } = usePressAndHold();
|
|
50
|
+
|
|
51
|
+
let count = ref(0);
|
|
52
|
+
function increment() { count.value += 1; }
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<button
|
|
57
|
+
@mousedown="pressAndHold(increment)"
|
|
58
|
+
@mouseup="stop"
|
|
59
|
+
@mouseleave="stop"
|
|
60
|
+
>
|
|
61
|
+
+1 ({{ count }})
|
|
62
|
+
</button>
|
|
63
|
+
</template>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Related
|
|
67
|
+
|
|
68
|
+
- `<orio-number-input-horizontal>` — uses this for ± buttons.
|
|
69
|
+
- `<orio-number-input-vertical>` — uses this for chevron buttons.
|
|
70
|
+
- Public API reference: `docs/composables/use-press-and-hold.md`.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: composable
|
|
3
|
+
category: Composables
|
|
4
|
+
purpose: roving-focus tabindex for 2D grids, grid keyboard navigation, calendar keyboard, table arrow nav
|
|
5
|
+
short: 2D arrow-key roving focus for grid-like UIs; handles arrows, Home/End, PageUp/Down, Enter/Space, and edge-overflow callbacks
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# useRovingGrid — agent-only invariants
|
|
10
|
+
|
|
11
|
+
`useRovingGrid` implements the WAI-ARIA roving-tabindex pattern for a 2D
|
|
12
|
+
grid. Generic over `Cell`. Used internally by `<orio-calendar>`.
|
|
13
|
+
|
|
14
|
+
## Invariants
|
|
15
|
+
|
|
16
|
+
- **Only one cell has `tabindex="0"` at a time** — the active one. All
|
|
17
|
+
others get `-1`. The consumer wires this via `tabindexFor(key)`.
|
|
18
|
+
- **Cells must carry a `focus-key="<key>"` attribute** matching
|
|
19
|
+
`options.getKey(cell)` — `focusActive()` queries
|
|
20
|
+
`[focus-key="..."]` with `CSS.escape` for the lookup.
|
|
21
|
+
- **`rows`** is a `Ref` to a 2D array of cells. Rows may have
|
|
22
|
+
different column counts; navigation walks until it finds a valid
|
|
23
|
+
cell or hits an undefined.
|
|
24
|
+
- **`activeKey`** is a `ComputedRef<string>`:
|
|
25
|
+
- If the user has focused a cell, returns that key.
|
|
26
|
+
- Otherwise returns `options.initial()` — the consumer's "default
|
|
27
|
+
focus target" (typically today's date in a calendar).
|
|
28
|
+
- **`isNavigable(cell)`** (optional) — return `false` to skip cells
|
|
29
|
+
during arrow nav. Skipped cells are stepped past in the same
|
|
30
|
+
direction until a navigable cell is found, or the edge is hit.
|
|
31
|
+
- **`onArrowOverflow(direction, activeKey)`** — fires when an arrow
|
|
32
|
+
would move past the rendered grid edge. Return a new `key` to focus
|
|
33
|
+
(e.g. after paging to the next month). Return `null` for no-op.
|
|
34
|
+
- **`onPage(direction, bigJump, activeKey)`** — PageUp / PageDown.
|
|
35
|
+
`bigJump` is `event.shiftKey`. Same return contract as overflow.
|
|
36
|
+
- **`onActivate(cell)`** fires on Enter or Space on the active cell.
|
|
37
|
+
- **`onKeydown` calls `preventDefault` on every handled key**. Arrow,
|
|
38
|
+
Home, End, PageUp, PageDown, Enter, Space. Other keys bubble.
|
|
39
|
+
- **`focusActive()` runs on `nextTick`** — the DOM must reflect the
|
|
40
|
+
active key by then. `scrollIntoView` with `block: nearest, inline:
|
|
41
|
+
nearest, behavior: smooth`. Focus uses `preventScroll: true` (the
|
|
42
|
+
scroll already happened).
|
|
43
|
+
|
|
44
|
+
## Gotchas
|
|
45
|
+
|
|
46
|
+
- **`getKey` must return strings** unique within the grid. Non-string
|
|
47
|
+
keys break the `focus-key` attribute lookup.
|
|
48
|
+
- **No `aria-activedescendant` mode.** This is a roving-tabindex
|
|
49
|
+
implementation only. For activedescendant grids, build separately.
|
|
50
|
+
- **`onArrowOverflow` must update backing state BEFORE returning the
|
|
51
|
+
new key.** The caller relies on the new key being findable in
|
|
52
|
+
`rows` on the next paint — usually the consumer mutates rows in
|
|
53
|
+
the same tick and returns the new key synchronously.
|
|
54
|
+
- **`isNavigable: () => false` everywhere** would freeze arrow nav.
|
|
55
|
+
Always leave at least the active cell navigable.
|
|
56
|
+
|
|
57
|
+
## Quick reference — calendar-style 2D grid
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { useRovingGrid } from "../composables/useRovingGrid";
|
|
61
|
+
|
|
62
|
+
interface Day { iso: string; inMonth: boolean }
|
|
63
|
+
|
|
64
|
+
const rows = ref<Day[][]>([...]); // 6 weeks × 7 days
|
|
65
|
+
const gridRef = ref<HTMLElement | null>(null);
|
|
66
|
+
|
|
67
|
+
const { activeKey, tabindexFor, onKeydown } = useRovingGrid({
|
|
68
|
+
rows,
|
|
69
|
+
gridRef,
|
|
70
|
+
getKey: (day) => day.iso,
|
|
71
|
+
initial: () => today.iso,
|
|
72
|
+
isNavigable: (day) => day.inMonth,
|
|
73
|
+
onActivate: (day) => emit("select", day.iso),
|
|
74
|
+
onArrowOverflow: (direction, activeKey) => {
|
|
75
|
+
pageMonth(direction); // mutates rows
|
|
76
|
+
return findNeighborIsoForOverflow(direction, activeKey);
|
|
77
|
+
},
|
|
78
|
+
onPage: (direction, bigJump) => {
|
|
79
|
+
pageMonth(direction, bigJump ? 12 : 1);
|
|
80
|
+
return today.iso;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```vue
|
|
86
|
+
<template>
|
|
87
|
+
<table ref="gridRef" @keydown="onKeydown">
|
|
88
|
+
<tr v-for="(week, weekIndex) in rows" :key="weekIndex">
|
|
89
|
+
<td
|
|
90
|
+
v-for="day in week"
|
|
91
|
+
:key="day.iso"
|
|
92
|
+
:focus-key="day.iso"
|
|
93
|
+
:tabindex="tabindexFor(day.iso)"
|
|
94
|
+
>
|
|
95
|
+
{{ day.iso }}
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
</table>
|
|
99
|
+
</template>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Related
|
|
103
|
+
|
|
104
|
+
- `<orio-calendar>` — uses this composable internally.
|
|
105
|
+
- `useListKeyboard` — 1D version for flat lists.
|
|
106
|
+
- Public API reference: `docs/composables/use-roving-grid.md`.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: composable
|
|
3
|
+
category: Composables
|
|
4
|
+
purpose: audio cue playback, sound effect, UI click sound, beep
|
|
5
|
+
short: low-latency Web Audio playback with a shared module-level AudioContext and per-URL buffer cache
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# useSound — agent-only invariants
|
|
10
|
+
|
|
11
|
+
`useSound` returns `{ play, prefetch }`. It uses the Web Audio API (not
|
|
12
|
+
`<audio>` elements) for low-latency, gapless playback. State is
|
|
13
|
+
module-level — every consumer shares the same `AudioContext` and buffer
|
|
14
|
+
cache.
|
|
15
|
+
|
|
16
|
+
## Invariants
|
|
17
|
+
|
|
18
|
+
- **Module-level singleton `AudioContext`**, created lazily on first
|
|
19
|
+
call. Same instance is reused across every `useSound()` consumer in
|
|
20
|
+
the page.
|
|
21
|
+
- **Module-level `bufferCache`** keyed by source URL. Multiple
|
|
22
|
+
consumers with the same `src` share decoded buffers.
|
|
23
|
+
- **Default sound is a remote URL** —
|
|
24
|
+
`https://cdn.jsdelivr.net/gh/oriondor/orio-ui@main/docs/public/sounds/mechanical-switch.wav`.
|
|
25
|
+
No offline / bundled fallback. Calling `play()` without a custom
|
|
26
|
+
`src` requires network on first use (cached thereafter by the
|
|
27
|
+
browser).
|
|
28
|
+
- **Default `volume` is `0.3`** (Web Audio Gain node). Range `0..1`;
|
|
29
|
+
values above clip ungracefully.
|
|
30
|
+
- **`prefetch: true`** in options eagerly fetches + decodes the buffer
|
|
31
|
+
at construction. Without it, the first `play()` includes
|
|
32
|
+
fetch + decode latency.
|
|
33
|
+
- **`play()` auto-resumes a suspended context.** Browsers gate audio on
|
|
34
|
+
a user gesture; the first call after a click/key event will resume
|
|
35
|
+
the context.
|
|
36
|
+
- **Returned `prefetch` warms the cache** for the configured `src` —
|
|
37
|
+
same as constructor `prefetch: true`, callable on demand.
|
|
38
|
+
|
|
39
|
+
## Gotchas
|
|
40
|
+
|
|
41
|
+
- **Cross-origin / CDN dependency**: the default sound relies on
|
|
42
|
+
`cdn.jsdelivr.net`. For self-hosted apps, pass your own `src`.
|
|
43
|
+
- **No volume reactivity**: changing `volume` after construction does
|
|
44
|
+
nothing. Recreate `useSound({ volume: 0.5 })` to change the level.
|
|
45
|
+
- **Errors are swallowed** with `console.error`. There is no `loaded`
|
|
46
|
+
ref, no `error` ref, no promise resolution to await. `play()` returns
|
|
47
|
+
a resolved promise even if the buffer never loaded.
|
|
48
|
+
- **No autoplay before user gesture.** Calling `play()` before any user
|
|
49
|
+
interaction will silently fail in most browsers — the resume call
|
|
50
|
+
succeeds but the buffer source won't produce audio. Wire to a click
|
|
51
|
+
or keydown.
|
|
52
|
+
- **`AudioContext` is never closed.** Long-lived apps accumulate one
|
|
53
|
+
context; for very long sessions, this is usually fine because the
|
|
54
|
+
context is shared.
|
|
55
|
+
|
|
56
|
+
## Quick reference
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { useSound } from "../composables/useSound";
|
|
60
|
+
|
|
61
|
+
const { play, prefetch } = useSound({
|
|
62
|
+
src: "/sounds/click.wav",
|
|
63
|
+
volume: 0.4,
|
|
64
|
+
prefetch: true, // load + decode now, before first user interaction
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Later, on click:
|
|
68
|
+
button.addEventListener("click", () => play());
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Related
|
|
72
|
+
|
|
73
|
+
- `<orio-animated-container>` — exposes `play` via the slot prop bag.
|
|
74
|
+
- Public API reference: `docs/composables/use-sound.md`.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: composable
|
|
3
|
+
category: Composables
|
|
4
|
+
purpose: theme tokens, light/dark, theme switcher, color theme
|
|
5
|
+
short: cookie-backed theme and mode (light/dark) accessor that writes `data-theme` and `data-mode` on `<html>`
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# useTheme — agent-only invariants
|
|
10
|
+
|
|
11
|
+
Returns `{ theme, setTheme, mode, setMode }`. State lives in cookies
|
|
12
|
+
(`COOKIE_NAMES.theme`, `COOKIE_NAMES.mode`) and is mirrored to
|
|
13
|
+
`data-theme` / `data-mode` attributes on the document element so CSS
|
|
14
|
+
selectors like `[data-theme="dark"]` work.
|
|
15
|
+
|
|
16
|
+
## Invariants
|
|
17
|
+
|
|
18
|
+
- **Backed by `useCookies` from `@vueuse/integrations`.** Cookies are
|
|
19
|
+
set at path `/`. SSR-friendly because `useCookies` reads request
|
|
20
|
+
cookies during render.
|
|
21
|
+
- **Defaults come from `constants/theme.ts`** — `THEME_DEFAULTS.theme`
|
|
22
|
+
and `THEME_DEFAULTS.mode`. Override the defaults there, not at the
|
|
23
|
+
call site.
|
|
24
|
+
- **`theme` and `mode` are computed refs** with `get` / `set`. Writing
|
|
25
|
+
to them updates the cookie immediately. The `data-*` attributes on
|
|
26
|
+
`<html>` only refresh when you call `setTheme` / `setMode`, not on
|
|
27
|
+
raw assignment.
|
|
28
|
+
- **`onMounted(setHtmlAttrs)`** runs once per call site to apply
|
|
29
|
+
cookies to the document on the client.
|
|
30
|
+
- **SSR-safe**: `setHtmlAttrs` early-returns when `document` is
|
|
31
|
+
undefined.
|
|
32
|
+
|
|
33
|
+
## Gotchas
|
|
34
|
+
|
|
35
|
+
- **Raw `theme.value = "dark"` writes the cookie but doesn't touch
|
|
36
|
+
`<html>` attrs.** Always go through `setTheme` / `setMode` if you
|
|
37
|
+
need the DOM to update in the same call.
|
|
38
|
+
- **Multiple `useTheme()` consumers don't share an in-memory state** —
|
|
39
|
+
the cookie is the source of truth. Reactivity across components
|
|
40
|
+
requires reading the same cookie via `useCookies` again.
|
|
41
|
+
- **Cookies are set at `path: "/"`.** If your app lives under a
|
|
42
|
+
sub-path, this still works but cookies are global to the domain. For
|
|
43
|
+
finer scoping, fork the composable.
|
|
44
|
+
- **No system / OS preference detection.** No `prefers-color-scheme`
|
|
45
|
+
fallback. Wire that in the consumer if needed.
|
|
46
|
+
- **`onMounted` per call** means N components → N attribute writes per
|
|
47
|
+
navigation. Cheap, but not idempotent at a single point.
|
|
48
|
+
|
|
49
|
+
## Quick reference
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { useTheme } from "../composables/useTheme";
|
|
53
|
+
|
|
54
|
+
const { theme, setTheme, mode, setMode } = useTheme();
|
|
55
|
+
|
|
56
|
+
// Switch theme
|
|
57
|
+
setTheme("ocean");
|
|
58
|
+
setMode("dark");
|
|
59
|
+
|
|
60
|
+
// Read current
|
|
61
|
+
console.log(theme.value, mode.value);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```vue
|
|
65
|
+
<template>
|
|
66
|
+
<orio-button @click="setMode(mode === 'dark' ? 'light' : 'dark')">
|
|
67
|
+
{{ mode === "dark" ? "☀" : "🌙" }}
|
|
68
|
+
</orio-button>
|
|
69
|
+
</template>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Related
|
|
73
|
+
|
|
74
|
+
- `<orio-locale-switcher>` — sibling switch for vue-i18n locale.
|
|
75
|
+
- `constants/theme.ts` — defaults and cookie names live here.
|
|
76
|
+
- Public API reference: `docs/composables/use-theme.md`.
|