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.
Files changed (70) hide show
  1. package/README.md +5 -5
  2. package/dist/module.json +1 -1
  3. package/dist/runtime/components/Button.d.vue.ts +3 -2
  4. package/dist/runtime/components/Button.vue +19 -11
  5. package/dist/runtime/components/Button.vue.d.ts +3 -2
  6. package/dist/runtime/components/Calendar.USAGE.md +51 -0
  7. package/dist/runtime/components/Calendar.d.vue.ts +33 -0
  8. package/dist/runtime/components/Calendar.vue +418 -0
  9. package/dist/runtime/components/Calendar.vue.d.ts +33 -0
  10. package/dist/runtime/components/Canvas/USAGE.md +65 -0
  11. package/dist/runtime/components/CheckBox.vue +9 -3
  12. package/dist/runtime/components/CheckboxGroup.vue +7 -1
  13. package/dist/runtime/components/ControlElement.USAGE.md +69 -0
  14. package/dist/runtime/components/ControlElement.d.vue.ts +42 -27
  15. package/dist/runtime/components/ControlElement.vue +28 -9
  16. package/dist/runtime/components/ControlElement.vue.d.ts +42 -27
  17. package/dist/runtime/components/Form.d.vue.ts +1 -1
  18. package/dist/runtime/components/Form.vue.d.ts +1 -1
  19. package/dist/runtime/components/Input.USAGE.md +49 -0
  20. package/dist/runtime/components/Input.vue +13 -3
  21. package/dist/runtime/components/Modal.USAGE.md +64 -0
  22. package/dist/runtime/components/NavButton.d.vue.ts +0 -1
  23. package/dist/runtime/components/NavButton.vue +9 -5
  24. package/dist/runtime/components/NavButton.vue.d.ts +0 -1
  25. package/dist/runtime/components/NumberInput/Horizontal.vue +7 -2
  26. package/dist/runtime/components/NumberInput/Vertical.vue +7 -2
  27. package/dist/runtime/components/NumberInput/index.d.vue.ts +0 -2
  28. package/dist/runtime/components/NumberInput/index.vue +9 -7
  29. package/dist/runtime/components/NumberInput/index.vue.d.ts +0 -2
  30. package/dist/runtime/components/RadioButton.d.vue.ts +0 -2
  31. package/dist/runtime/components/RadioButton.vue +9 -4
  32. package/dist/runtime/components/RadioButton.vue.d.ts +0 -2
  33. package/dist/runtime/components/Selector.d.vue.ts +1 -0
  34. package/dist/runtime/components/Selector.vue +10 -4
  35. package/dist/runtime/components/Selector.vue.d.ts +1 -0
  36. package/dist/runtime/components/SwitchButton.d.vue.ts +1 -4
  37. package/dist/runtime/components/SwitchButton.vue +10 -7
  38. package/dist/runtime/components/SwitchButton.vue.d.ts +1 -4
  39. package/dist/runtime/components/TaggableSelector.vue +7 -1
  40. package/dist/runtime/components/Textarea.vue +13 -3
  41. package/dist/runtime/components/date/Picker.USAGE.md +44 -0
  42. package/dist/runtime/components/date/Picker.d.vue.ts +26 -0
  43. package/dist/runtime/components/date/Picker.vue +60 -0
  44. package/dist/runtime/components/date/Picker.vue.d.ts +26 -0
  45. package/dist/runtime/components/date/PickerTrigger.d.vue.ts +23 -0
  46. package/dist/runtime/components/date/PickerTrigger.vue +86 -0
  47. package/dist/runtime/components/date/PickerTrigger.vue.d.ts +23 -0
  48. package/dist/runtime/components/date/RangePicker.d.vue.ts +28 -0
  49. package/dist/runtime/components/date/RangePicker.vue +154 -0
  50. package/dist/runtime/components/date/RangePicker.vue.d.ts +28 -0
  51. package/dist/runtime/components/view/Dates.d.vue.ts +2 -5
  52. package/dist/runtime/components/view/Dates.vue +17 -23
  53. package/dist/runtime/components/view/Dates.vue.d.ts +2 -5
  54. package/dist/runtime/composables/useFilter.d.ts +91 -0
  55. package/dist/runtime/composables/useFilter.js +111 -0
  56. package/dist/runtime/composables/useRovingGrid.d.ts +35 -0
  57. package/dist/runtime/composables/useRovingGrid.js +115 -0
  58. package/dist/runtime/i18n/en.json +11 -5
  59. package/dist/runtime/i18n/uk.json +11 -5
  60. package/dist/runtime/index.d.ts +4 -2
  61. package/dist/runtime/index.js +6 -2
  62. package/dist/runtime/utils/date.d.ts +10 -0
  63. package/dist/runtime/utils/date.js +38 -0
  64. package/package.json +1 -1
  65. package/dist/runtime/components/DatePicker.d.vue.ts +0 -15
  66. package/dist/runtime/components/DatePicker.vue +0 -24
  67. package/dist/runtime/components/DatePicker.vue.d.ts +0 -15
  68. package/dist/runtime/components/DateRangePicker.d.vue.ts +0 -18
  69. package/dist/runtime/components/DateRangePicker.vue +0 -67
  70. 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
- ✨ **56 Components** - Beautiful, accessible components ready to use
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 (56)
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 (13)
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/ # 56 Vue components
198
- │ │ ├── composables/ # 13 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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.0.0 || ^4.0.0"
6
6
  },
7
- "version": "1.23.3",
7
+ "version": "1.27.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -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="$attrs"
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
- <slot v-else name="icon">
54
- <orio-icon v-if="icon" :name="icon" />
55
- </slot>
56
- <slot />
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;