orio-ui 1.23.3 → 1.27.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 +5 -5
- package/dist/module.json +1 -1
- package/dist/runtime/components/Button.d.vue.ts +3 -2
- package/dist/runtime/components/Button.vue +19 -11
- package/dist/runtime/components/Button.vue.d.ts +3 -2
- package/dist/runtime/components/Calendar.USAGE.md +51 -0
- package/dist/runtime/components/Calendar.d.vue.ts +33 -0
- package/dist/runtime/components/Calendar.vue +418 -0
- package/dist/runtime/components/Calendar.vue.d.ts +33 -0
- package/dist/runtime/components/Canvas/USAGE.md +65 -0
- package/dist/runtime/components/CheckBox.vue +9 -3
- package/dist/runtime/components/CheckboxGroup.vue +7 -1
- package/dist/runtime/components/ControlElement.USAGE.md +69 -0
- package/dist/runtime/components/ControlElement.d.vue.ts +42 -27
- package/dist/runtime/components/ControlElement.vue +28 -9
- package/dist/runtime/components/ControlElement.vue.d.ts +42 -27
- package/dist/runtime/components/Form.d.vue.ts +1 -1
- package/dist/runtime/components/Form.vue.d.ts +1 -1
- package/dist/runtime/components/Input.USAGE.md +49 -0
- package/dist/runtime/components/Input.vue +13 -3
- package/dist/runtime/components/Modal.USAGE.md +64 -0
- package/dist/runtime/components/NavButton.d.vue.ts +0 -1
- package/dist/runtime/components/NavButton.vue +9 -5
- package/dist/runtime/components/NavButton.vue.d.ts +0 -1
- package/dist/runtime/components/NumberInput/Horizontal.vue +7 -2
- package/dist/runtime/components/NumberInput/Vertical.vue +7 -2
- package/dist/runtime/components/NumberInput/index.d.vue.ts +0 -2
- package/dist/runtime/components/NumberInput/index.vue +9 -7
- package/dist/runtime/components/NumberInput/index.vue.d.ts +0 -2
- package/dist/runtime/components/RadioButton.d.vue.ts +0 -2
- package/dist/runtime/components/RadioButton.vue +9 -4
- package/dist/runtime/components/RadioButton.vue.d.ts +0 -2
- package/dist/runtime/components/Selector.d.vue.ts +1 -0
- package/dist/runtime/components/Selector.vue +10 -4
- package/dist/runtime/components/Selector.vue.d.ts +1 -0
- package/dist/runtime/components/SwitchButton.d.vue.ts +1 -4
- package/dist/runtime/components/SwitchButton.vue +10 -7
- package/dist/runtime/components/SwitchButton.vue.d.ts +1 -4
- package/dist/runtime/components/TaggableSelector.vue +7 -1
- package/dist/runtime/components/Textarea.vue +13 -3
- package/dist/runtime/components/date/Picker.USAGE.md +44 -0
- package/dist/runtime/components/date/Picker.d.vue.ts +26 -0
- package/dist/runtime/components/date/Picker.vue +60 -0
- package/dist/runtime/components/date/Picker.vue.d.ts +26 -0
- package/dist/runtime/components/date/PickerTrigger.d.vue.ts +23 -0
- package/dist/runtime/components/date/PickerTrigger.vue +86 -0
- package/dist/runtime/components/date/PickerTrigger.vue.d.ts +23 -0
- package/dist/runtime/components/date/RangePicker.d.vue.ts +28 -0
- package/dist/runtime/components/date/RangePicker.vue +154 -0
- package/dist/runtime/components/date/RangePicker.vue.d.ts +28 -0
- package/dist/runtime/components/view/Dates.d.vue.ts +2 -5
- package/dist/runtime/components/view/Dates.vue +17 -23
- package/dist/runtime/components/view/Dates.vue.d.ts +2 -5
- package/dist/runtime/composables/useFilter.d.ts +91 -0
- package/dist/runtime/composables/useFilter.js +111 -0
- package/dist/runtime/composables/useRovingGrid.d.ts +35 -0
- package/dist/runtime/composables/useRovingGrid.js +115 -0
- package/dist/runtime/i18n/en.json +11 -5
- package/dist/runtime/i18n/uk.json +11 -5
- package/dist/runtime/index.d.ts +4 -2
- package/dist/runtime/index.js +6 -2
- package/dist/runtime/utils/date.d.ts +10 -0
- package/dist/runtime/utils/date.js +38 -0
- package/package.json +1 -1
- package/dist/runtime/components/DatePicker.d.vue.ts +0 -15
- package/dist/runtime/components/DatePicker.vue +0 -24
- package/dist/runtime/components/DatePicker.vue.d.ts +0 -15
- package/dist/runtime/components/DateRangePicker.d.vue.ts +0 -18
- package/dist/runtime/components/DateRangePicker.vue +0 -67
- package/dist/runtime/components/DateRangePicker.vue.d.ts +0 -18
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ A delightful, lightweight component library for Nuxt 3+ applications. Built with
|
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
|
-
✨ **
|
|
11
|
+
✨ **58 Components** - Beautiful, accessible components ready to use
|
|
12
12
|
🎨 **Themeable** - 5 built-in accent themes with light/dark mode support
|
|
13
13
|
🚀 **Auto-imported** - Works seamlessly with Nuxt's auto-import system
|
|
14
14
|
📦 **Tree-shakeable** - Only bundle what you use
|
|
@@ -67,7 +67,7 @@ function handleClick() {
|
|
|
67
67
|
|
|
68
68
|
## What's Included
|
|
69
69
|
|
|
70
|
-
### Components (
|
|
70
|
+
### Components (58)
|
|
71
71
|
|
|
72
72
|
#### Form Controls
|
|
73
73
|
|
|
@@ -119,7 +119,7 @@ function handleClick() {
|
|
|
119
119
|
|
|
120
120
|
- **Upload** - File upload component
|
|
121
121
|
|
|
122
|
-
### Composables (
|
|
122
|
+
### Composables (15)
|
|
123
123
|
|
|
124
124
|
- **useTheme** - Theme and color mode management
|
|
125
125
|
- **useModal** - Modal state with animation origin tracking
|
|
@@ -194,8 +194,8 @@ npm run docs:dev
|
|
|
194
194
|
orio-ui/
|
|
195
195
|
├── src/
|
|
196
196
|
│ ├── runtime/
|
|
197
|
-
│ │ ├── components/ #
|
|
198
|
-
│ │ ├── composables/ #
|
|
197
|
+
│ │ ├── components/ # 58 Vue components
|
|
198
|
+
│ │ ├── composables/ # 15 composables
|
|
199
199
|
│ │ ├── assets/css/ # Theme CSS files
|
|
200
200
|
│ │ └── utils/ # Icon registry
|
|
201
201
|
│ └── module.ts # Nuxt Module definition
|
package/dist/module.json
CHANGED
|
@@ -3,13 +3,14 @@ interface Props extends ControlProps {
|
|
|
3
3
|
variant?: "primary" | "secondary" | "subdued";
|
|
4
4
|
icon?: string;
|
|
5
5
|
loading?: boolean;
|
|
6
|
-
disabled?: boolean;
|
|
7
6
|
}
|
|
8
|
-
declare var __VLS_13: {}, __VLS_20: {};
|
|
7
|
+
declare var __VLS_13: {}, __VLS_20: {}, __VLS_22: {};
|
|
9
8
|
type __VLS_Slots = {} & {
|
|
10
9
|
icon?: (props: typeof __VLS_13) => any;
|
|
11
10
|
} & {
|
|
12
11
|
default?: (props: typeof __VLS_20) => any;
|
|
12
|
+
} & {
|
|
13
|
+
'icon-right'?: (props: typeof __VLS_22) => any;
|
|
13
14
|
};
|
|
14
15
|
declare const __VLS_base: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
15
16
|
click: (event: PointerEvent) => any;
|
|
@@ -4,7 +4,6 @@ const props = defineProps({
|
|
|
4
4
|
variant: { type: String, required: false, default: "primary" },
|
|
5
5
|
icon: { type: String, required: false },
|
|
6
6
|
loading: { type: Boolean, required: false },
|
|
7
|
-
disabled: { type: Boolean, required: false },
|
|
8
7
|
appearance: { type: String, required: false },
|
|
9
8
|
error: { type: [String, null], required: false },
|
|
10
9
|
group: { type: Boolean, required: false },
|
|
@@ -12,7 +11,13 @@ const props = defineProps({
|
|
|
12
11
|
label: { type: String, required: false },
|
|
13
12
|
layout: { type: String, required: false },
|
|
14
13
|
size: { type: String, required: false },
|
|
15
|
-
fill: { type: Boolean, required: false }
|
|
14
|
+
fill: { type: Boolean, required: false },
|
|
15
|
+
tabindex: { type: [Number, String], required: false },
|
|
16
|
+
focusKey: { type: String, required: false },
|
|
17
|
+
disabled: { type: Boolean, required: false },
|
|
18
|
+
required: { type: Boolean, required: false },
|
|
19
|
+
name: { type: String, required: false },
|
|
20
|
+
ariaLabel: { type: String, required: false }
|
|
16
21
|
});
|
|
17
22
|
const { loading, disabled } = toRefs(props);
|
|
18
23
|
const slots = useSlots();
|
|
@@ -39,21 +44,25 @@ function onMouseleave(event) {
|
|
|
39
44
|
</script>
|
|
40
45
|
|
|
41
46
|
<template>
|
|
42
|
-
<orio-control-element v-bind="props">
|
|
47
|
+
<orio-control-element v-slot="{ control }" v-bind="props">
|
|
43
48
|
<button
|
|
44
|
-
v-bind="
|
|
49
|
+
v-bind="{ ...$attrs, ...control }"
|
|
45
50
|
:class="[variant, 'gradient-hover', { 'icon-only': isIconOnly }]"
|
|
46
|
-
:disabled
|
|
47
51
|
@click="click"
|
|
48
52
|
@mousedown="onMousedown"
|
|
49
53
|
@mouseup="onMouseup"
|
|
50
54
|
@mouseleave="onMouseleave"
|
|
51
55
|
>
|
|
52
56
|
<orio-loading-spinner v-if="loading" />
|
|
53
|
-
<
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
<template v-else>
|
|
58
|
+
<slot name="icon">
|
|
59
|
+
<orio-icon v-if="icon" :name="icon" />
|
|
60
|
+
</slot>
|
|
61
|
+
|
|
62
|
+
<slot />
|
|
63
|
+
|
|
64
|
+
<slot name="icon-right" />
|
|
65
|
+
</template>
|
|
57
66
|
</button>
|
|
58
67
|
</orio-control-element>
|
|
59
68
|
</template>
|
|
@@ -73,9 +82,8 @@ button {
|
|
|
73
82
|
user-select: none;
|
|
74
83
|
}
|
|
75
84
|
button.icon-only {
|
|
76
|
-
padding: 0;
|
|
77
|
-
border-radius: 50%;
|
|
78
85
|
line-height: 0;
|
|
86
|
+
aspect-ratio: 1;
|
|
79
87
|
}
|
|
80
88
|
button:disabled, button:disabled:hover {
|
|
81
89
|
background-color: var(--color-accent-soft-base);
|
|
@@ -3,13 +3,14 @@ interface Props extends ControlProps {
|
|
|
3
3
|
variant?: "primary" | "secondary" | "subdued";
|
|
4
4
|
icon?: string;
|
|
5
5
|
loading?: boolean;
|
|
6
|
-
disabled?: boolean;
|
|
7
6
|
}
|
|
8
|
-
declare var __VLS_13: {}, __VLS_20: {};
|
|
7
|
+
declare var __VLS_13: {}, __VLS_20: {}, __VLS_22: {};
|
|
9
8
|
type __VLS_Slots = {} & {
|
|
10
9
|
icon?: (props: typeof __VLS_13) => any;
|
|
11
10
|
} & {
|
|
12
11
|
default?: (props: typeof __VLS_20) => any;
|
|
12
|
+
} & {
|
|
13
|
+
'icon-right'?: (props: typeof __VLS_22) => any;
|
|
13
14
|
};
|
|
14
15
|
declare const __VLS_base: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
15
16
|
click: (event: PointerEvent) => any;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Calendar — agent-only invariants
|
|
2
|
+
|
|
3
|
+
`<orio-calendar>` is the month-grid primitive. `date/Picker.vue` and
|
|
4
|
+
`date/RangePicker.vue` compose it inside a popover.
|
|
5
|
+
|
|
6
|
+
## Invariants
|
|
7
|
+
|
|
8
|
+
- **All dates are ISO `YYYY-MM-DD` strings** at the API boundary
|
|
9
|
+
(`selected`, `markers.start`/`end`, `getMarker`, `isDisabled`, `@select`,
|
|
10
|
+
`@dayEnter`). Never pass `Date` objects.
|
|
11
|
+
- **Two reactive states**:
|
|
12
|
+
- `selected` (prop) — the picked day. Calendar does not own it; emit
|
|
13
|
+
`@select` and let the parent update its v-model.
|
|
14
|
+
- `anchor` (v-model:anchor) — the visible month. Calendar owns this when
|
|
15
|
+
uncontrolled, derived from `selected` if not provided. Pass it as
|
|
16
|
+
`v-model:anchor` if you need cross-component sync (RangePicker uses
|
|
17
|
+
this to keep two months in lockstep).
|
|
18
|
+
- **Keyboard a11y via `useRovingGrid`.** Arrow keys, Home/End, PageUp/Down
|
|
19
|
+
navigate. Only one day cell is in the tab order at a time. Do not add
|
|
20
|
+
manual `tabindex` to day cells from outside.
|
|
21
|
+
- **42-cell grid (6 rows × 7 cols).** Leading/trailing days from neighbour
|
|
22
|
+
months are rendered with `inMonth: false`. Selection and disabled checks
|
|
23
|
+
apply to them too — `isDisabled` is called for every cell.
|
|
24
|
+
- **Markers are matched in reverse order** — the last marker in the array
|
|
25
|
+
wins on overlap. `getMarker` takes precedence over `markers[]` when both
|
|
26
|
+
are provided.
|
|
27
|
+
- **`weekStartsOn`** is `0` (Sunday) or `1` (Monday, default). Weekday
|
|
28
|
+
header labels are derived via `Intl.DateTimeFormat` in the active i18n
|
|
29
|
+
locale.
|
|
30
|
+
|
|
31
|
+
## Gotchas
|
|
32
|
+
|
|
33
|
+
- Disabled-day logic should be reactive — `isDisabled` is called inside a
|
|
34
|
+
computed. Wrap any external state it reads in `computed`/`ref` or it will
|
|
35
|
+
not re-render on change.
|
|
36
|
+
- The Calendar emits `@dayEnter` on hover/keyboard-focus, **not** on
|
|
37
|
+
selection. Use for range-picking previews; selection is only `@select`.
|
|
38
|
+
- i18n keys used: `calendar.previousYear`, `calendar.previousMonth`,
|
|
39
|
+
`calendar.nextMonth`, `calendar.nextYear`. Override these if you ship a
|
|
40
|
+
locale not bundled in `runtime/i18n/`.
|
|
41
|
+
|
|
42
|
+
## Quick reference
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<orio-calendar
|
|
46
|
+
:selected="iso"
|
|
47
|
+
:markers="[{ variant: 'accent', start: '2026-06-01', end: '2026-06-07' }]"
|
|
48
|
+
:is-disabled="(iso) => iso < todayIso"
|
|
49
|
+
@select="iso = $event"
|
|
50
|
+
/>
|
|
51
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type MarkerVariant = "accent" | "success" | "alert" | "danger" | "muted";
|
|
2
|
+
export interface CalendarMarker {
|
|
3
|
+
variant: MarkerVariant;
|
|
4
|
+
start: string;
|
|
5
|
+
end: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CalendarProps {
|
|
8
|
+
selected?: string | null;
|
|
9
|
+
markers?: CalendarMarker[];
|
|
10
|
+
getMarker?: (iso: string) => CalendarMarker | null;
|
|
11
|
+
isDisabled?: (iso: string) => boolean;
|
|
12
|
+
weekStartsOn?: 0 | 1;
|
|
13
|
+
}
|
|
14
|
+
type __VLS_Props = CalendarProps;
|
|
15
|
+
type __VLS_ModelProps = {
|
|
16
|
+
"anchor"?: string | null;
|
|
17
|
+
};
|
|
18
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
19
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
20
|
+
select: (iso: string) => any;
|
|
21
|
+
dayEnter: (iso: string) => any;
|
|
22
|
+
"update:anchor": (value: string | null) => any;
|
|
23
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
24
|
+
onSelect?: ((iso: string) => any) | undefined;
|
|
25
|
+
onDayEnter?: ((iso: string) => any) | undefined;
|
|
26
|
+
"onUpdate:anchor"?: ((value: string | null) => any) | undefined;
|
|
27
|
+
}>, {
|
|
28
|
+
selected: string | null;
|
|
29
|
+
markers: CalendarMarker[];
|
|
30
|
+
weekStartsOn: 0 | 1;
|
|
31
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, useId, watch } from "vue";
|
|
3
|
+
import { useI18n } from "vue-i18n";
|
|
4
|
+
import { useRovingGrid } from "../composables/useRovingGrid";
|
|
5
|
+
import {
|
|
6
|
+
addMonths,
|
|
7
|
+
formatISO,
|
|
8
|
+
isSameDay,
|
|
9
|
+
parseISO,
|
|
10
|
+
startOfMonth
|
|
11
|
+
} from "../utils/date";
|
|
12
|
+
const props = defineProps({
|
|
13
|
+
selected: { type: [String, null], required: false, default: null },
|
|
14
|
+
markers: { type: Array, required: false, default: () => [] },
|
|
15
|
+
getMarker: { type: Function, required: false },
|
|
16
|
+
isDisabled: { type: Function, required: false },
|
|
17
|
+
weekStartsOn: { type: Number, required: false, default: 1 }
|
|
18
|
+
});
|
|
19
|
+
const anchor = defineModel("anchor", { type: [String, null], ...{ default: null } });
|
|
20
|
+
const emit = defineEmits(["select", "dayEnter"]);
|
|
21
|
+
const { locale, t } = useI18n();
|
|
22
|
+
const today = /* @__PURE__ */ new Date();
|
|
23
|
+
const visibleMonth = computed(
|
|
24
|
+
() => startOfMonth(
|
|
25
|
+
parseISO(anchor.value) ?? parseISO(props.selected) ?? /* @__PURE__ */ new Date()
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
function shiftMonth(delta) {
|
|
29
|
+
anchor.value = formatISO(addMonths(visibleMonth.value, delta));
|
|
30
|
+
}
|
|
31
|
+
function shiftYear(delta) {
|
|
32
|
+
const target = new Date(visibleMonth.value);
|
|
33
|
+
target.setFullYear(target.getFullYear() + delta);
|
|
34
|
+
anchor.value = formatISO(startOfMonth(target));
|
|
35
|
+
}
|
|
36
|
+
const monthLabel = computed(
|
|
37
|
+
() => new Intl.DateTimeFormat(locale.value, {
|
|
38
|
+
month: "long",
|
|
39
|
+
year: "numeric"
|
|
40
|
+
}).format(visibleMonth.value)
|
|
41
|
+
);
|
|
42
|
+
const titleId = useId();
|
|
43
|
+
const dayLabelFormat = computed(
|
|
44
|
+
() => new Intl.DateTimeFormat(locale.value, {
|
|
45
|
+
weekday: "long",
|
|
46
|
+
day: "numeric",
|
|
47
|
+
month: "long",
|
|
48
|
+
year: "numeric"
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
function fullDateLabel(iso) {
|
|
52
|
+
const date = parseISO(iso);
|
|
53
|
+
return date ? dayLabelFormat.value.format(date) : iso;
|
|
54
|
+
}
|
|
55
|
+
const weekdayLabels = computed(() => {
|
|
56
|
+
const tuesday = new Date(1998, 6, 14);
|
|
57
|
+
return Array.from({ length: 7 }, (_, position) => {
|
|
58
|
+
const date = new Date(tuesday);
|
|
59
|
+
date.setDate(
|
|
60
|
+
tuesday.getDate() + (position + props.weekStartsOn - 2 + 7) % 7
|
|
61
|
+
);
|
|
62
|
+
return new Intl.DateTimeFormat(locale.value, { weekday: "short" }).format(
|
|
63
|
+
date
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
function resolveMarker(iso, reversedMarkers) {
|
|
68
|
+
const matched = props.getMarker?.(iso) ?? reversedMarkers.find(
|
|
69
|
+
(marker) => iso >= marker.start && iso <= marker.end
|
|
70
|
+
) ?? null;
|
|
71
|
+
if (!matched) return null;
|
|
72
|
+
return {
|
|
73
|
+
variant: matched.variant,
|
|
74
|
+
isStart: iso === matched.start,
|
|
75
|
+
isEnd: iso === matched.end
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const days = computed(() => {
|
|
79
|
+
const firstOfMonth = visibleMonth.value;
|
|
80
|
+
const leadingOffset = (firstOfMonth.getDay() - props.weekStartsOn + 7) % 7;
|
|
81
|
+
const gridStart = new Date(firstOfMonth);
|
|
82
|
+
gridStart.setDate(gridStart.getDate() - leadingOffset);
|
|
83
|
+
const selectedDate = parseISO(props.selected);
|
|
84
|
+
const reversedMarkers = [...props.markers].reverse();
|
|
85
|
+
return Array.from({ length: 42 }, (_, dayOffset) => {
|
|
86
|
+
const date = new Date(gridStart);
|
|
87
|
+
date.setDate(gridStart.getDate() + dayOffset);
|
|
88
|
+
const iso = formatISO(date);
|
|
89
|
+
return {
|
|
90
|
+
iso,
|
|
91
|
+
label: date.getDate(),
|
|
92
|
+
inMonth: date.getMonth() === firstOfMonth.getMonth(),
|
|
93
|
+
isToday: isSameDay(date, today),
|
|
94
|
+
isSelected: !!selectedDate && isSameDay(date, selectedDate),
|
|
95
|
+
isDisabled: props.isDisabled?.(iso) ?? false,
|
|
96
|
+
marker: resolveMarker(iso, reversedMarkers)
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
const weeks = computed(
|
|
101
|
+
() => Array.from(
|
|
102
|
+
{ length: 6 },
|
|
103
|
+
(_, weekIndex) => days.value.slice(weekIndex * 7, weekIndex * 7 + 7)
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
function isInVisibleMonth(date) {
|
|
107
|
+
return date.getMonth() === visibleMonth.value.getMonth() && date.getFullYear() === visibleMonth.value.getFullYear();
|
|
108
|
+
}
|
|
109
|
+
function initialActiveISO() {
|
|
110
|
+
const selectedDate = parseISO(props.selected);
|
|
111
|
+
if (selectedDate && isInVisibleMonth(selectedDate)) {
|
|
112
|
+
return formatISO(selectedDate);
|
|
113
|
+
}
|
|
114
|
+
if (isInVisibleMonth(today)) return formatISO(today);
|
|
115
|
+
return formatISO(visibleMonth.value);
|
|
116
|
+
}
|
|
117
|
+
const navButtons = computed(() => [
|
|
118
|
+
{
|
|
119
|
+
key: "year-prev",
|
|
120
|
+
ariaLabel: t("calendar.previousYear"),
|
|
121
|
+
icon: "chevron-left",
|
|
122
|
+
size: "md",
|
|
123
|
+
action: () => shiftYear(-1)
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
key: "month-prev",
|
|
127
|
+
ariaLabel: t("calendar.previousMonth"),
|
|
128
|
+
icon: "chevron-left",
|
|
129
|
+
size: "sm",
|
|
130
|
+
action: () => shiftMonth(-1)
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "month-next",
|
|
134
|
+
ariaLabel: t("calendar.nextMonth"),
|
|
135
|
+
icon: "chevron-right",
|
|
136
|
+
size: "sm",
|
|
137
|
+
action: () => shiftMonth(1)
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
key: "year-next",
|
|
141
|
+
ariaLabel: t("calendar.nextYear"),
|
|
142
|
+
icon: "chevron-right",
|
|
143
|
+
size: "md",
|
|
144
|
+
action: () => shiftYear(1)
|
|
145
|
+
}
|
|
146
|
+
]);
|
|
147
|
+
const navRows = computed(() => [navButtons.value]);
|
|
148
|
+
const navRef = ref(null);
|
|
149
|
+
const {
|
|
150
|
+
tabindexFor: tabindexForNav,
|
|
151
|
+
setActive: setActiveNav,
|
|
152
|
+
onKeydown: onNavKeydown
|
|
153
|
+
} = useRovingGrid({
|
|
154
|
+
rows: navRows,
|
|
155
|
+
gridRef: navRef,
|
|
156
|
+
getKey: (button) => button.key,
|
|
157
|
+
initial: () => "month-prev",
|
|
158
|
+
onActivate: (button) => button.action()
|
|
159
|
+
});
|
|
160
|
+
function onNavButtonClick(button) {
|
|
161
|
+
setActiveNav(button.key, false);
|
|
162
|
+
button.action();
|
|
163
|
+
}
|
|
164
|
+
const ARROW_DAY_DELTA = {
|
|
165
|
+
up: -7,
|
|
166
|
+
down: 7,
|
|
167
|
+
left: -1,
|
|
168
|
+
right: 1
|
|
169
|
+
};
|
|
170
|
+
const gridRef = ref(null);
|
|
171
|
+
const { activeKey, setActive, tabindexFor, onKeydown } = useRovingGrid({
|
|
172
|
+
rows: weeks,
|
|
173
|
+
gridRef,
|
|
174
|
+
getKey: (day) => day.iso,
|
|
175
|
+
initial: initialActiveISO,
|
|
176
|
+
isNavigable: (day) => !day.isDisabled,
|
|
177
|
+
onActivate(day) {
|
|
178
|
+
if (day.isDisabled) return;
|
|
179
|
+
emit("select", day.iso);
|
|
180
|
+
},
|
|
181
|
+
onArrowOverflow(direction, currentKey) {
|
|
182
|
+
const date = parseISO(currentKey);
|
|
183
|
+
if (!date) return null;
|
|
184
|
+
const stepDays = ARROW_DAY_DELTA[direction];
|
|
185
|
+
for (let attempt = 0; attempt < 750; attempt++) {
|
|
186
|
+
date.setDate(date.getDate() + stepDays);
|
|
187
|
+
const iso = formatISO(date);
|
|
188
|
+
if (!props.isDisabled?.(iso)) return iso;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
},
|
|
192
|
+
onPage(direction, bigJump, currentKey) {
|
|
193
|
+
const date = parseISO(currentKey);
|
|
194
|
+
if (!date) return null;
|
|
195
|
+
const monthDelta = (direction === "down" ? 1 : -1) * (bigJump ? 12 : 1);
|
|
196
|
+
const target = new Date(
|
|
197
|
+
date.getFullYear(),
|
|
198
|
+
date.getMonth() + monthDelta,
|
|
199
|
+
1
|
|
200
|
+
);
|
|
201
|
+
const lastDayOfTargetMonth = new Date(
|
|
202
|
+
target.getFullYear(),
|
|
203
|
+
target.getMonth() + 1,
|
|
204
|
+
0
|
|
205
|
+
).getDate();
|
|
206
|
+
target.setDate(Math.min(date.getDate(), lastDayOfTargetMonth));
|
|
207
|
+
return formatISO(target);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
watch(activeKey, (iso) => {
|
|
211
|
+
const date = parseISO(iso);
|
|
212
|
+
if (date && !isInVisibleMonth(date)) {
|
|
213
|
+
anchor.value = formatISO(startOfMonth(date));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
function onDayClick(day) {
|
|
217
|
+
if (day.isDisabled) return;
|
|
218
|
+
setActive(day.iso, false);
|
|
219
|
+
emit("select", day.iso);
|
|
220
|
+
}
|
|
221
|
+
function onDayMouseenter(day) {
|
|
222
|
+
emit("dayEnter", day.iso);
|
|
223
|
+
}
|
|
224
|
+
</script>
|
|
225
|
+
|
|
226
|
+
<template>
|
|
227
|
+
<div class="calendar">
|
|
228
|
+
<div
|
|
229
|
+
ref="navRef"
|
|
230
|
+
class="calendar-nav"
|
|
231
|
+
role="toolbar"
|
|
232
|
+
aria-orientation="horizontal"
|
|
233
|
+
:aria-label="t('calendar.navigation')"
|
|
234
|
+
@keydown="onNavKeydown"
|
|
235
|
+
>
|
|
236
|
+
<template v-for="(button, position) in navButtons" :key="button.key">
|
|
237
|
+
<orio-button
|
|
238
|
+
variant="subdued"
|
|
239
|
+
:icon="button.icon"
|
|
240
|
+
:size="button.size"
|
|
241
|
+
:focus-key="button.key"
|
|
242
|
+
:tabindex="tabindexForNav(button.key)"
|
|
243
|
+
:aria-label="button.ariaLabel"
|
|
244
|
+
@click="onNavButtonClick(button)"
|
|
245
|
+
/>
|
|
246
|
+
<span v-if="position === 1" :id="titleId" class="calendar-month-title">
|
|
247
|
+
{{ monthLabel }}
|
|
248
|
+
</span>
|
|
249
|
+
</template>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="calendar-weekdays">
|
|
252
|
+
<span
|
|
253
|
+
v-for="weekday in weekdayLabels"
|
|
254
|
+
:key="weekday"
|
|
255
|
+
class="calendar-weekday"
|
|
256
|
+
>
|
|
257
|
+
{{ weekday }}
|
|
258
|
+
</span>
|
|
259
|
+
</div>
|
|
260
|
+
<div
|
|
261
|
+
ref="gridRef"
|
|
262
|
+
class="calendar-grid"
|
|
263
|
+
role="grid"
|
|
264
|
+
:aria-labelledby="titleId"
|
|
265
|
+
@keydown="onKeydown"
|
|
266
|
+
>
|
|
267
|
+
<div
|
|
268
|
+
v-for="(week, weekIndex) in weeks"
|
|
269
|
+
:key="weekIndex"
|
|
270
|
+
role="row"
|
|
271
|
+
class="calendar-week"
|
|
272
|
+
>
|
|
273
|
+
<button
|
|
274
|
+
v-for="day in week"
|
|
275
|
+
:key="day.iso"
|
|
276
|
+
type="button"
|
|
277
|
+
role="gridcell"
|
|
278
|
+
class="calendar-day"
|
|
279
|
+
:class="{
|
|
280
|
+
'out-of-month': !day.inMonth,
|
|
281
|
+
today: day.isToday,
|
|
282
|
+
selected: day.isSelected,
|
|
283
|
+
'has-marker': !!day.marker,
|
|
284
|
+
[`marker-${day.marker?.variant}`]: !!day.marker,
|
|
285
|
+
'marker-start': day.marker?.isStart,
|
|
286
|
+
'marker-end': day.marker?.isEnd,
|
|
287
|
+
active: day.iso === activeKey
|
|
288
|
+
}"
|
|
289
|
+
:focus-key="day.iso"
|
|
290
|
+
:tabindex="tabindexFor(day.iso)"
|
|
291
|
+
:aria-selected="day.isSelected"
|
|
292
|
+
:aria-current="day.isToday ? 'date' : void 0"
|
|
293
|
+
:aria-label="fullDateLabel(day.iso)"
|
|
294
|
+
:disabled="day.isDisabled"
|
|
295
|
+
@click="onDayClick(day)"
|
|
296
|
+
@mouseenter="onDayMouseenter(day)"
|
|
297
|
+
>
|
|
298
|
+
<orio-badge v-if="day.isSelected" pill variant="primary">
|
|
299
|
+
{{ day.label }}
|
|
300
|
+
</orio-badge>
|
|
301
|
+
<template v-else>{{ day.label }}</template>
|
|
302
|
+
</button>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
</template>
|
|
307
|
+
|
|
308
|
+
<style scoped>
|
|
309
|
+
.calendar {
|
|
310
|
+
display: inline-block;
|
|
311
|
+
background: var(--color-bg);
|
|
312
|
+
border: 1px solid var(--color-border);
|
|
313
|
+
border-radius: var(--border-radius-md);
|
|
314
|
+
padding: 0.75rem;
|
|
315
|
+
user-select: none;
|
|
316
|
+
font-size: var(--control-font-size, var(--font-md));
|
|
317
|
+
color: var(--color-text);
|
|
318
|
+
width: 22rem;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.calendar-nav {
|
|
322
|
+
display: flex;
|
|
323
|
+
align-items: center;
|
|
324
|
+
gap: 0.25rem;
|
|
325
|
+
margin-bottom: 0.5rem;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.calendar-month-title {
|
|
329
|
+
flex: 1;
|
|
330
|
+
text-align: center;
|
|
331
|
+
font-weight: 600;
|
|
332
|
+
text-transform: capitalize;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.calendar-weekdays,
|
|
336
|
+
.calendar-grid {
|
|
337
|
+
display: grid;
|
|
338
|
+
grid-template-columns: repeat(7, 1fr);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.calendar-week {
|
|
342
|
+
display: contents;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.calendar-weekday {
|
|
346
|
+
text-align: center;
|
|
347
|
+
font-size: var(--font-xs);
|
|
348
|
+
color: var(--color-muted);
|
|
349
|
+
padding: 0.25rem 0;
|
|
350
|
+
text-transform: uppercase;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.calendar-day {
|
|
354
|
+
aspect-ratio: 1;
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
justify-content: center;
|
|
358
|
+
background: transparent;
|
|
359
|
+
border: 0;
|
|
360
|
+
color: inherit;
|
|
361
|
+
cursor: pointer;
|
|
362
|
+
border-radius: var(--border-radius-sm);
|
|
363
|
+
font-size: inherit;
|
|
364
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
365
|
+
}
|
|
366
|
+
.calendar-day:hover:not(:disabled):not(.has-marker):not(.selected) {
|
|
367
|
+
background-color: var(--color-surface);
|
|
368
|
+
}
|
|
369
|
+
.calendar-day:focus-visible {
|
|
370
|
+
outline: 2px solid var(--color-accent);
|
|
371
|
+
outline-offset: -2px;
|
|
372
|
+
}
|
|
373
|
+
.calendar-day.out-of-month {
|
|
374
|
+
color: var(--color-muted);
|
|
375
|
+
opacity: 0.45;
|
|
376
|
+
}
|
|
377
|
+
.calendar-day.today {
|
|
378
|
+
box-shadow: inset 0 0 0 1px var(--color-accent);
|
|
379
|
+
}
|
|
380
|
+
.calendar-day.has-marker {
|
|
381
|
+
background-color: var(--marker-bg);
|
|
382
|
+
color: var(--marker-color);
|
|
383
|
+
border-radius: 0;
|
|
384
|
+
}
|
|
385
|
+
.calendar-day.has-marker.marker-start {
|
|
386
|
+
border-top-left-radius: var(--border-radius-sm);
|
|
387
|
+
border-bottom-left-radius: var(--border-radius-sm);
|
|
388
|
+
}
|
|
389
|
+
.calendar-day.has-marker.marker-end {
|
|
390
|
+
border-top-right-radius: var(--border-radius-sm);
|
|
391
|
+
border-bottom-right-radius: var(--border-radius-sm);
|
|
392
|
+
}
|
|
393
|
+
.calendar-day.marker-accent {
|
|
394
|
+
--marker-bg: var(--color-accent-soft);
|
|
395
|
+
--marker-color: var(--color-accent);
|
|
396
|
+
}
|
|
397
|
+
.calendar-day.marker-success {
|
|
398
|
+
--marker-bg: var(--color-success-soft);
|
|
399
|
+
--marker-color: var(--color-success);
|
|
400
|
+
}
|
|
401
|
+
.calendar-day.marker-alert {
|
|
402
|
+
--marker-bg: var(--color-alert-soft);
|
|
403
|
+
--marker-color: var(--color-alert);
|
|
404
|
+
}
|
|
405
|
+
.calendar-day.marker-danger {
|
|
406
|
+
--marker-bg: var(--color-danger-soft);
|
|
407
|
+
--marker-color: var(--color-danger);
|
|
408
|
+
}
|
|
409
|
+
.calendar-day.marker-muted {
|
|
410
|
+
--marker-bg: var(--color-surface);
|
|
411
|
+
--marker-color: var(--color-muted);
|
|
412
|
+
}
|
|
413
|
+
.calendar-day:disabled {
|
|
414
|
+
color: var(--color-muted);
|
|
415
|
+
cursor: not-allowed;
|
|
416
|
+
opacity: 0.4;
|
|
417
|
+
}
|
|
418
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type MarkerVariant = "accent" | "success" | "alert" | "danger" | "muted";
|
|
2
|
+
export interface CalendarMarker {
|
|
3
|
+
variant: MarkerVariant;
|
|
4
|
+
start: string;
|
|
5
|
+
end: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CalendarProps {
|
|
8
|
+
selected?: string | null;
|
|
9
|
+
markers?: CalendarMarker[];
|
|
10
|
+
getMarker?: (iso: string) => CalendarMarker | null;
|
|
11
|
+
isDisabled?: (iso: string) => boolean;
|
|
12
|
+
weekStartsOn?: 0 | 1;
|
|
13
|
+
}
|
|
14
|
+
type __VLS_Props = CalendarProps;
|
|
15
|
+
type __VLS_ModelProps = {
|
|
16
|
+
"anchor"?: string | null;
|
|
17
|
+
};
|
|
18
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
19
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
20
|
+
select: (iso: string) => any;
|
|
21
|
+
dayEnter: (iso: string) => any;
|
|
22
|
+
"update:anchor": (value: string | null) => any;
|
|
23
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
24
|
+
onSelect?: ((iso: string) => any) | undefined;
|
|
25
|
+
onDayEnter?: ((iso: string) => any) | undefined;
|
|
26
|
+
"onUpdate:anchor"?: ((value: string | null) => any) | undefined;
|
|
27
|
+
}>, {
|
|
28
|
+
selected: string | null;
|
|
29
|
+
markers: CalendarMarker[];
|
|
30
|
+
weekStartsOn: 0 | 1;
|
|
31
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
32
|
+
declare const _default: typeof __VLS_export;
|
|
33
|
+
export default _default;
|