orio-ui 1.24.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 +78 -3
- 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/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 +59 -0
- package/dist/runtime/components/Calendar.vue +254 -87
- package/dist/runtime/components/Canvas/USAGE.md +73 -0
- package/dist/runtime/components/CheckBox.USAGE.md +63 -0
- package/dist/runtime/components/CheckBox.vue +9 -3
- package/dist/runtime/components/CheckboxGroup.USAGE.md +95 -0
- package/dist/runtime/components/CheckboxGroup.vue +7 -1
- package/dist/runtime/components/ControlElement.USAGE.md +77 -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/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 +57 -0
- package/dist/runtime/components/Input.vue +13 -3
- 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 +72 -0
- package/dist/runtime/components/NavButton.USAGE.md +80 -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.USAGE.md +61 -0
- package/dist/runtime/components/NumberInput/Horizontal.vue +7 -2
- 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 +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/Popover.USAGE.md +103 -0
- package/dist/runtime/components/RadioButton.USAGE.md +72 -0
- 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.USAGE.md +131 -0
- 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.USAGE.md +62 -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/Tag.USAGE.md +51 -0
- package/dist/runtime/components/TaggableSelector.USAGE.md +73 -0
- package/dist/runtime/components/TaggableSelector.vue +7 -1
- package/dist/runtime/components/Textarea.USAGE.md +72 -0
- package/dist/runtime/components/Textarea.vue +13 -3
- 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 +52 -0
- package/dist/runtime/components/date/Picker.vue +7 -1
- package/dist/runtime/components/date/PickerTrigger.USAGE.md +65 -0
- package/dist/runtime/components/date/PickerTrigger.vue +9 -3
- package/dist/runtime/components/date/RangePicker.USAGE.md +97 -0
- package/dist/runtime/components/date/RangePicker.vue +7 -1
- 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/useFilter.d.ts +91 -0
- package/dist/runtime/composables/useFilter.js +111 -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/useRovingGrid.d.ts +35 -0
- package/dist/runtime/composables/useRovingGrid.js +115 -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/dist/runtime/i18n/en.json +4 -1
- package/dist/runtime/i18n/uk.json +4 -1
- package/package.json +12 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed } from "vue";
|
|
2
|
+
import { computed, ref, useId, watch } from "vue";
|
|
3
3
|
import { useI18n } from "vue-i18n";
|
|
4
|
+
import { useRovingGrid } from "../composables/useRovingGrid";
|
|
4
5
|
import {
|
|
5
6
|
addMonths,
|
|
6
7
|
formatISO,
|
|
@@ -20,129 +21,286 @@ const emit = defineEmits(["select", "dayEnter"]);
|
|
|
20
21
|
const { locale, t } = useI18n();
|
|
21
22
|
const today = /* @__PURE__ */ new Date();
|
|
22
23
|
const visibleMonth = computed(
|
|
23
|
-
() => startOfMonth(
|
|
24
|
+
() => startOfMonth(
|
|
25
|
+
parseISO(anchor.value) ?? parseISO(props.selected) ?? /* @__PURE__ */ new Date()
|
|
26
|
+
)
|
|
24
27
|
);
|
|
25
|
-
function
|
|
28
|
+
function shiftMonth(delta) {
|
|
26
29
|
anchor.value = formatISO(addMonths(visibleMonth.value, delta));
|
|
27
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
|
+
}
|
|
28
36
|
const monthLabel = computed(
|
|
29
37
|
() => new Intl.DateTimeFormat(locale.value, {
|
|
30
38
|
month: "long",
|
|
31
39
|
year: "numeric"
|
|
32
40
|
}).format(visibleMonth.value)
|
|
33
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
|
+
}
|
|
34
55
|
const weekdayLabels = computed(() => {
|
|
35
|
-
const
|
|
36
|
-
return Array.from({ length: 7 }, (_,
|
|
37
|
-
const
|
|
38
|
-
|
|
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
|
+
);
|
|
39
62
|
return new Intl.DateTimeFormat(locale.value, { weekday: "short" }).format(
|
|
40
|
-
|
|
63
|
+
date
|
|
41
64
|
);
|
|
42
65
|
});
|
|
43
66
|
});
|
|
44
|
-
function resolveMarker(iso) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
for (let i = props.markers.length - 1; i >= 0; i--) {
|
|
56
|
-
const m = props.markers[i];
|
|
57
|
-
if (iso >= m.start && iso <= m.end) {
|
|
58
|
-
return {
|
|
59
|
-
variant: m.variant,
|
|
60
|
-
isStart: iso === m.start,
|
|
61
|
-
isEnd: iso === m.end
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
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
|
+
};
|
|
66
77
|
}
|
|
67
78
|
const days = computed(() => {
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
result.push({
|
|
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 {
|
|
80
90
|
iso,
|
|
81
|
-
label:
|
|
82
|
-
inMonth:
|
|
83
|
-
isToday: isSameDay(
|
|
84
|
-
isSelected: !!
|
|
91
|
+
label: date.getDate(),
|
|
92
|
+
inMonth: date.getMonth() === firstOfMonth.getMonth(),
|
|
93
|
+
isToday: isSameDay(date, today),
|
|
94
|
+
isSelected: !!selectedDate && isSameDay(date, selectedDate),
|
|
85
95
|
isDisabled: props.isDisabled?.(iso) ?? false,
|
|
86
|
-
marker: resolveMarker(iso)
|
|
87
|
-
}
|
|
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));
|
|
88
214
|
}
|
|
89
|
-
return result;
|
|
90
215
|
});
|
|
91
|
-
function
|
|
216
|
+
function onDayClick(day) {
|
|
92
217
|
if (day.isDisabled) return;
|
|
218
|
+
setActive(day.iso, false);
|
|
93
219
|
emit("select", day.iso);
|
|
94
220
|
}
|
|
95
|
-
function
|
|
221
|
+
function onDayMouseenter(day) {
|
|
96
222
|
emit("dayEnter", day.iso);
|
|
97
223
|
}
|
|
98
224
|
</script>
|
|
99
225
|
|
|
100
226
|
<template>
|
|
101
227
|
<div class="calendar">
|
|
102
|
-
<div
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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>
|
|
116
250
|
</div>
|
|
117
251
|
<div class="calendar-weekdays">
|
|
118
|
-
<span
|
|
119
|
-
|
|
252
|
+
<span
|
|
253
|
+
v-for="weekday in weekdayLabels"
|
|
254
|
+
:key="weekday"
|
|
255
|
+
class="calendar-weekday"
|
|
256
|
+
>
|
|
257
|
+
{{ weekday }}
|
|
120
258
|
</span>
|
|
121
259
|
</div>
|
|
122
|
-
<div
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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="{
|
|
129
280
|
'out-of-month': !day.inMonth,
|
|
130
281
|
today: day.isToday,
|
|
131
282
|
selected: day.isSelected,
|
|
132
283
|
'has-marker': !!day.marker,
|
|
133
284
|
[`marker-${day.marker?.variant}`]: !!day.marker,
|
|
134
285
|
'marker-start': day.marker?.isStart,
|
|
135
|
-
'marker-end': day.marker?.isEnd
|
|
286
|
+
'marker-end': day.marker?.isEnd,
|
|
287
|
+
active: day.iso === activeKey
|
|
136
288
|
}"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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>
|
|
146
304
|
</div>
|
|
147
305
|
</div>
|
|
148
306
|
</template>
|
|
@@ -157,18 +315,19 @@ function onEnter(day) {
|
|
|
157
315
|
user-select: none;
|
|
158
316
|
font-size: var(--control-font-size, var(--font-md));
|
|
159
317
|
color: var(--color-text);
|
|
160
|
-
|
|
318
|
+
width: 22rem;
|
|
161
319
|
}
|
|
162
320
|
|
|
163
321
|
.calendar-nav {
|
|
164
322
|
display: flex;
|
|
165
323
|
align-items: center;
|
|
166
|
-
|
|
167
|
-
gap: 0.5rem;
|
|
324
|
+
gap: 0.25rem;
|
|
168
325
|
margin-bottom: 0.5rem;
|
|
169
326
|
}
|
|
170
327
|
|
|
171
328
|
.calendar-month-title {
|
|
329
|
+
flex: 1;
|
|
330
|
+
text-align: center;
|
|
172
331
|
font-weight: 600;
|
|
173
332
|
text-transform: capitalize;
|
|
174
333
|
}
|
|
@@ -179,6 +338,10 @@ function onEnter(day) {
|
|
|
179
338
|
grid-template-columns: repeat(7, 1fr);
|
|
180
339
|
}
|
|
181
340
|
|
|
341
|
+
.calendar-week {
|
|
342
|
+
display: contents;
|
|
343
|
+
}
|
|
344
|
+
|
|
182
345
|
.calendar-weekday {
|
|
183
346
|
text-align: center;
|
|
184
347
|
font-size: var(--font-xs);
|
|
@@ -203,6 +366,10 @@ function onEnter(day) {
|
|
|
203
366
|
.calendar-day:hover:not(:disabled):not(.has-marker):not(.selected) {
|
|
204
367
|
background-color: var(--color-surface);
|
|
205
368
|
}
|
|
369
|
+
.calendar-day:focus-visible {
|
|
370
|
+
outline: 2px solid var(--color-accent);
|
|
371
|
+
outline-offset: -2px;
|
|
372
|
+
}
|
|
206
373
|
.calendar-day.out-of-month {
|
|
207
374
|
color: var(--color-muted);
|
|
208
375
|
opacity: 0.45;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: component
|
|
3
|
+
category: Layout & containers
|
|
4
|
+
purpose: canvas, drawing board, whiteboard, sketch, freeform editor, pluggable tools
|
|
5
|
+
short: pannable workspace with pluggable tools and detached toolbar registry
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Canvas — agent-only invariants
|
|
10
|
+
|
|
11
|
+
Read this before integrating `<orio-canvas>` into a consumer app. Public API
|
|
12
|
+
lives in `docs/components/canvas/`.
|
|
13
|
+
|
|
14
|
+
## Invariants
|
|
15
|
+
|
|
16
|
+
- **`name` prop is required.** It registers this canvas instance in the
|
|
17
|
+
module-level `canvasRegistry` (`./registry.ts`), which is how detached
|
|
18
|
+
toolbars find it. Without a `name`, a toolbar rendered outside the canvas
|
|
19
|
+
subtree cannot bind to it.
|
|
20
|
+
- **Detached toolbar uses `canvas="<name>"`, not `provide/inject`.** Render
|
|
21
|
+
`<orio-canvas-toolbar canvas="editor" />` anywhere in the app — even in a
|
|
22
|
+
sibling subtree — and it resolves the context via the registry. Toolbars
|
|
23
|
+
nested as children of `<orio-canvas>` resolve via `useCanvasContext()`
|
|
24
|
+
inject and do not need the prop.
|
|
25
|
+
- **Tools are pluggable, nothing ships by default.** Pass `:tools="[drawTool(),
|
|
26
|
+
textTool(), ...]"`. There are no implicit defaults.
|
|
27
|
+
- **Tool factories are functions, not singletons.** Call `drawTool()` per
|
|
28
|
+
canvas instance — closure state (e.g. the in-progress stroke id) must not be
|
|
29
|
+
shared across canvases on the same page.
|
|
30
|
+
- **`v-model:nodes` is the persistence boundary.** Serialize the array to JSON
|
|
31
|
+
for save/load. Unknown node types (tools not yet registered on hydrate) are
|
|
32
|
+
silently skipped on render — register the tool, then re-render.
|
|
33
|
+
- **`frozen: true` nodes** are protected from `eraseTool`, `moveTool`,
|
|
34
|
+
`highlightTool` and future selection tools. Drawing/text passes ignore the
|
|
35
|
+
flag.
|
|
36
|
+
- **`setup(api)` runs once on mount** with the full tool API. Use it to seed
|
|
37
|
+
initial nodes (frozen background images, watermark, etc.). It can add node
|
|
38
|
+
types whose tools are not in the user-facing `tools` prop — useful for
|
|
39
|
+
display-only nodes.
|
|
40
|
+
|
|
41
|
+
## Gotchas
|
|
42
|
+
|
|
43
|
+
- Tool `id` doubles as the node `type`. Two tools with the same id collide.
|
|
44
|
+
- `setup` may be async; nodes added inside resolve before the first render.
|
|
45
|
+
- Undo snapshot is finalized on `pointerUp` for `drawTool` — mid-stroke is not
|
|
46
|
+
a history step. If you script node creation outside a tool, call
|
|
47
|
+
`requestRender()` and rely on the next user action to checkpoint.
|
|
48
|
+
- Custom fonts: no bundled loader. Caller is responsible for `document.fonts`
|
|
49
|
+
/ `FontFace` loading before `textTool` renders that font.
|
|
50
|
+
|
|
51
|
+
## Quick reference
|
|
52
|
+
|
|
53
|
+
```vue
|
|
54
|
+
<orio-canvas
|
|
55
|
+
name="editor"
|
|
56
|
+
v-model:nodes="nodes"
|
|
57
|
+
:tools="[drawTool(), textTool(), eraseTool(), undoTool()]"
|
|
58
|
+
:width="800"
|
|
59
|
+
:height="500"
|
|
60
|
+
:setup="(api) => api.addNode({ type: 'bg', frozen: true, ... })"
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<!-- elsewhere in the app -->
|
|
64
|
+
<orio-canvas-toolbar canvas="editor" />
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Where things live
|
|
68
|
+
|
|
69
|
+
- Types & `defineCanvasTool` → `./types.ts`
|
|
70
|
+
- Context (`provide`/`inject`, `useCanvasContext`) → `./context.ts`
|
|
71
|
+
- Registry (module-level Map for detached toolbars) → `./registry.ts`
|
|
72
|
+
- Built-in tools → `./tools/*.ts`
|
|
73
|
+
- Full spec → `./REQUIREMENTS.md`
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: component
|
|
3
|
+
category: Form inputs
|
|
4
|
+
purpose: single checkbox, boolean toggle, opt-in
|
|
5
|
+
short: single boolean checkbox wrapping ControlElement; custom check icon via prop or slot
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# CheckBox — agent-only invariants
|
|
10
|
+
|
|
11
|
+
`<orio-check-box>` is a single boolean checkbox. The native `<input
|
|
12
|
+
type="checkbox">` is visually hidden; the rendered tick lives in a sibling
|
|
13
|
+
`<span class="checkbox-box">`. Read `ControlElement.USAGE.md` first.
|
|
14
|
+
|
|
15
|
+
## Invariants
|
|
16
|
+
|
|
17
|
+
- **v-model is `boolean`**, not required (renders as unchecked when
|
|
18
|
+
unbound).
|
|
19
|
+
- **Default slot is the label text** rendered to the right of the box.
|
|
20
|
+
- **Default tick is a CSS rotate-45-border checkmark** drawn in
|
|
21
|
+
`::after`, applied when no `checkedIcon` is passed.
|
|
22
|
+
- **`checkedIcon` / `uncheckedIcon` props** swap in `<orio-icon>` glyphs
|
|
23
|
+
for either state. Pass icon names registered in `utils/iconRegistry`.
|
|
24
|
+
- **`#icon` slot** lets you render arbitrary indicator content; receives
|
|
25
|
+
`{ checked }`. Overrides both the default tick and any icon props.
|
|
26
|
+
- **ControlElement is passed `fill`** so the checkbox row fills the wrapper
|
|
27
|
+
width. Label slot of ControlElement is bypassed — the visible label is
|
|
28
|
+
the CheckBox's own default slot, not ControlElement's `label` prop.
|
|
29
|
+
- **`--box-size` defaults to `var(--control-icon-size, 1rem)`**. Override
|
|
30
|
+
the CSS var on the host to resize the box.
|
|
31
|
+
|
|
32
|
+
## Gotchas
|
|
33
|
+
|
|
34
|
+
- **The native input has `tabindex="-1"`** in the template, but the
|
|
35
|
+
styles target `:focus-visible` on it. Keyboard focus for the checkbox
|
|
36
|
+
may not behave the way an a11y audit expects — confirm tab order
|
|
37
|
+
before shipping critical forms. If you need keyboard-focusable
|
|
38
|
+
checkboxes, override `tabindex` via `$attrs`.
|
|
39
|
+
- **`required` from `ControlProps` flows through**, but native checkbox
|
|
40
|
+
required validation only fires inside a `<form>` that calls
|
|
41
|
+
`reportValidity`.
|
|
42
|
+
- **No indeterminate state.** v-model is strictly boolean; the
|
|
43
|
+
underlying input never gets `indeterminate = true`.
|
|
44
|
+
- **Passing both `checkedIcon` and `#icon` slot** — the slot wins; the
|
|
45
|
+
prop becomes dead code.
|
|
46
|
+
|
|
47
|
+
## Quick reference
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<template>
|
|
51
|
+
<orio-check-box v-model="agreed" :error="errors.terms">
|
|
52
|
+
{{ $t("signup.acceptTerms") }}
|
|
53
|
+
</orio-check-box>
|
|
54
|
+
|
|
55
|
+
<orio-check-box v-model="bookmarked" checked-icon="bookmark-filled" unchecked-icon="bookmark" />
|
|
56
|
+
</template>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Related
|
|
60
|
+
|
|
61
|
+
- `<orio-checkbox-group>` — multi-value group of checkboxes.
|
|
62
|
+
- `<orio-control-element>` — wrapper; owns label/error/a11y.
|
|
63
|
+
- Public API reference: `docs/components/checkbox.md`.
|
|
@@ -10,19 +10,25 @@ const props = defineProps({
|
|
|
10
10
|
label: { type: String, required: false },
|
|
11
11
|
layout: { type: String, required: false },
|
|
12
12
|
size: { type: String, required: false },
|
|
13
|
-
fill: { type: Boolean, required: false }
|
|
13
|
+
fill: { type: Boolean, required: false },
|
|
14
|
+
tabindex: { type: [Number, String], required: false },
|
|
15
|
+
focusKey: { type: String, required: false },
|
|
16
|
+
disabled: { type: Boolean, required: false },
|
|
17
|
+
required: { type: Boolean, required: false },
|
|
18
|
+
name: { type: String, required: false },
|
|
19
|
+
ariaLabel: { type: String, required: false }
|
|
14
20
|
});
|
|
15
21
|
</script>
|
|
16
22
|
|
|
17
23
|
<template>
|
|
18
|
-
<orio-control-element v-bind="props" class="checkbox" fill>
|
|
24
|
+
<orio-control-element v-slot="{ control }" v-bind="props" class="checkbox" fill>
|
|
19
25
|
<label class="checkbox-label">
|
|
20
26
|
<input
|
|
21
27
|
v-model="modelValue"
|
|
28
|
+
v-bind="{ ...$attrs, ...control }"
|
|
22
29
|
type="checkbox"
|
|
23
30
|
class="checkbox-input"
|
|
24
31
|
tabindex="-1"
|
|
25
|
-
v-bind="$attrs"
|
|
26
32
|
/>
|
|
27
33
|
<span
|
|
28
34
|
class="checkbox-box"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: component
|
|
3
|
+
category: Form inputs
|
|
4
|
+
purpose: group of checkboxes, multi-value boolean group, multi-select boolean
|
|
5
|
+
short: group of CheckBox children bound to an `unknown[]` v-model; supports `options` prop or default slot
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# CheckboxGroup — agent-only invariants
|
|
10
|
+
|
|
11
|
+
`<orio-checkbox-group>` wires a list of checkboxes to a single array
|
|
12
|
+
v-model. It uses `<orio-control-element group>` so the wrapper renders as
|
|
13
|
+
a `role="group"` with an `aria-labelledby` label — semantically a fieldset
|
|
14
|
+
group, not a `<fieldset>` element.
|
|
15
|
+
|
|
16
|
+
## Invariants
|
|
17
|
+
|
|
18
|
+
- **v-model is `unknown[]`** (default `[]`). Each entry is one option's
|
|
19
|
+
`value`. Comparison is strict `===` — primitives or shared references,
|
|
20
|
+
not deep equality.
|
|
21
|
+
- **Two modes**: `options` prop (array of `{ label, value }`) **or** the
|
|
22
|
+
default slot (you render `<orio-check-box>` children yourself). The slot
|
|
23
|
+
wins when present.
|
|
24
|
+
- **In `options` mode**, each rendered checkbox is `appearance="minimal"`
|
|
25
|
+
(hardcoded). To change appearance, switch to the slot.
|
|
26
|
+
- **Toggle replaces the array**: `modelValue.value = modelValue.value.filter(...)`
|
|
27
|
+
/ `[...modelValue.value, value]`. Reactive arrays survive; readonly arrays
|
|
28
|
+
break silently.
|
|
29
|
+
- **Wrapper omits `appearance`, `group`, `id` from `ControlProps`.**
|
|
30
|
+
`group` is forced true; `id` is internal.
|
|
31
|
+
- **Defaults**: `layout: "vertical"`, `size: "md"`, `error: null`.
|
|
32
|
+
- **Horizontal layout aligns label top** (`align-items: flex-start`) so the
|
|
33
|
+
label sits next to the first checkbox, not centered against the full
|
|
34
|
+
column.
|
|
35
|
+
|
|
36
|
+
## Gotchas
|
|
37
|
+
|
|
38
|
+
- **`isChecked` uses `Array.includes` with `===`.** Object option values
|
|
39
|
+
must be the **same reference** as in the model, not a structural match.
|
|
40
|
+
For object values, store ids and resolve to objects in the parent.
|
|
41
|
+
- **No "select all" / "indeterminate parent" affordance.** Build it in the
|
|
42
|
+
consumer if needed.
|
|
43
|
+
- **Label rendering depends on `group` mode in ControlElement** — the
|
|
44
|
+
label becomes a `<span>` with `aria-labelledby` wiring, not a `<legend>`.
|
|
45
|
+
Screen readers announce it as a group label.
|
|
46
|
+
- **`error` is forwarded** to the group wrapper; per-checkbox errors are
|
|
47
|
+
not supported. Surface validation at the group level.
|
|
48
|
+
|
|
49
|
+
## Quick reference — options prop
|
|
50
|
+
|
|
51
|
+
```vue
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
const interests = defineModel<string[]>({ default: () => [] });
|
|
54
|
+
const options = [
|
|
55
|
+
{ label: "Books", value: "books" },
|
|
56
|
+
{ label: "Films", value: "films" },
|
|
57
|
+
{ label: "Music", value: "music" },
|
|
58
|
+
];
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<orio-checkbox-group
|
|
63
|
+
v-model="interests"
|
|
64
|
+
:options="options"
|
|
65
|
+
:label="$t('profile.interests')"
|
|
66
|
+
/>
|
|
67
|
+
</template>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick reference — slot mode (custom rendering)
|
|
71
|
+
|
|
72
|
+
```vue
|
|
73
|
+
<template>
|
|
74
|
+
<orio-checkbox-group v-model="features" :label="$t('settings.features')">
|
|
75
|
+
<orio-check-box
|
|
76
|
+
v-for="feature in availableFeatures"
|
|
77
|
+
:key="feature.id"
|
|
78
|
+
:model-value="features.includes(feature.id)"
|
|
79
|
+
checked-icon="star-filled"
|
|
80
|
+
unchecked-icon="star"
|
|
81
|
+
@update:model-value="toggle(feature.id)"
|
|
82
|
+
>
|
|
83
|
+
{{ feature.name }}
|
|
84
|
+
</orio-check-box>
|
|
85
|
+
</orio-checkbox-group>
|
|
86
|
+
</template>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Related
|
|
90
|
+
|
|
91
|
+
- `<orio-check-box>` — single checkbox; rendered children inside this
|
|
92
|
+
group.
|
|
93
|
+
- `<orio-control-element>` (`group` mode) — wrapper; provides the group
|
|
94
|
+
label semantics.
|
|
95
|
+
- Public API reference: `docs/components/checkbox-group.md`.
|