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.
Files changed (102) hide show
  1. package/README.md +78 -3
  2. package/bin/orio-ui.mjs +72 -0
  3. package/dist/agents/ROUTING.md +140 -0
  4. package/dist/agents/component-finder.md +142 -0
  5. package/dist/agents/component-worker.md +152 -0
  6. package/dist/agents/snippet.md +6 -0
  7. package/dist/module.json +1 -1
  8. package/dist/runtime/components/AnimatedContainer.USAGE.md +79 -0
  9. package/dist/runtime/components/Badge.USAGE.md +75 -0
  10. package/dist/runtime/components/Banner.USAGE.md +52 -0
  11. package/dist/runtime/components/Button.USAGE.md +78 -0
  12. package/dist/runtime/components/Button.d.vue.ts +3 -2
  13. package/dist/runtime/components/Button.vue +19 -11
  14. package/dist/runtime/components/Button.vue.d.ts +3 -2
  15. package/dist/runtime/components/Calendar.USAGE.md +59 -0
  16. package/dist/runtime/components/Calendar.vue +254 -87
  17. package/dist/runtime/components/Canvas/USAGE.md +73 -0
  18. package/dist/runtime/components/CheckBox.USAGE.md +63 -0
  19. package/dist/runtime/components/CheckBox.vue +9 -3
  20. package/dist/runtime/components/CheckboxGroup.USAGE.md +95 -0
  21. package/dist/runtime/components/CheckboxGroup.vue +7 -1
  22. package/dist/runtime/components/ControlElement.USAGE.md +77 -0
  23. package/dist/runtime/components/ControlElement.d.vue.ts +42 -27
  24. package/dist/runtime/components/ControlElement.vue +28 -9
  25. package/dist/runtime/components/ControlElement.vue.d.ts +42 -27
  26. package/dist/runtime/components/DashedContainer.USAGE.md +65 -0
  27. package/dist/runtime/components/EmptyState.USAGE.md +65 -0
  28. package/dist/runtime/components/Form.USAGE.md +102 -0
  29. package/dist/runtime/components/Icon.USAGE.md +61 -0
  30. package/dist/runtime/components/Input.USAGE.md +57 -0
  31. package/dist/runtime/components/Input.vue +13 -3
  32. package/dist/runtime/components/ListItem.USAGE.md +84 -0
  33. package/dist/runtime/components/LoadingSpinner.USAGE.md +50 -0
  34. package/dist/runtime/components/LocaleSwitcher.USAGE.md +73 -0
  35. package/dist/runtime/components/Modal.USAGE.md +72 -0
  36. package/dist/runtime/components/NavButton.USAGE.md +80 -0
  37. package/dist/runtime/components/NavButton.d.vue.ts +0 -1
  38. package/dist/runtime/components/NavButton.vue +9 -5
  39. package/dist/runtime/components/NavButton.vue.d.ts +0 -1
  40. package/dist/runtime/components/NumberInput/Horizontal.USAGE.md +61 -0
  41. package/dist/runtime/components/NumberInput/Horizontal.vue +7 -2
  42. package/dist/runtime/components/NumberInput/USAGE.md +74 -0
  43. package/dist/runtime/components/NumberInput/Vertical.USAGE.md +55 -0
  44. package/dist/runtime/components/NumberInput/Vertical.vue +7 -2
  45. package/dist/runtime/components/NumberInput/index.d.vue.ts +0 -2
  46. package/dist/runtime/components/NumberInput/index.vue +9 -7
  47. package/dist/runtime/components/NumberInput/index.vue.d.ts +0 -2
  48. package/dist/runtime/components/Popover.USAGE.md +103 -0
  49. package/dist/runtime/components/RadioButton.USAGE.md +72 -0
  50. package/dist/runtime/components/RadioButton.d.vue.ts +0 -2
  51. package/dist/runtime/components/RadioButton.vue +9 -4
  52. package/dist/runtime/components/RadioButton.vue.d.ts +0 -2
  53. package/dist/runtime/components/Selector.USAGE.md +131 -0
  54. package/dist/runtime/components/Selector.d.vue.ts +1 -0
  55. package/dist/runtime/components/Selector.vue +10 -4
  56. package/dist/runtime/components/Selector.vue.d.ts +1 -0
  57. package/dist/runtime/components/SwitchButton.USAGE.md +62 -0
  58. package/dist/runtime/components/SwitchButton.d.vue.ts +1 -4
  59. package/dist/runtime/components/SwitchButton.vue +10 -7
  60. package/dist/runtime/components/SwitchButton.vue.d.ts +1 -4
  61. package/dist/runtime/components/Tag.USAGE.md +51 -0
  62. package/dist/runtime/components/TaggableSelector.USAGE.md +73 -0
  63. package/dist/runtime/components/TaggableSelector.vue +7 -1
  64. package/dist/runtime/components/Textarea.USAGE.md +72 -0
  65. package/dist/runtime/components/Textarea.vue +13 -3
  66. package/dist/runtime/components/Tooltip.USAGE.md +84 -0
  67. package/dist/runtime/components/ZoomableContainer.USAGE.md +108 -0
  68. package/dist/runtime/components/date/Picker.USAGE.md +52 -0
  69. package/dist/runtime/components/date/Picker.vue +7 -1
  70. package/dist/runtime/components/date/PickerTrigger.USAGE.md +65 -0
  71. package/dist/runtime/components/date/PickerTrigger.vue +9 -3
  72. package/dist/runtime/components/date/RangePicker.USAGE.md +97 -0
  73. package/dist/runtime/components/date/RangePicker.vue +7 -1
  74. package/dist/runtime/components/gallery/Carousel.USAGE.md +98 -0
  75. package/dist/runtime/components/gallery/CarouselPreview.USAGE.md +51 -0
  76. package/dist/runtime/components/upload/USAGE.md +91 -0
  77. package/dist/runtime/components/view/Dates.USAGE.md +67 -0
  78. package/dist/runtime/components/view/KeyBinds.USAGE.md +58 -0
  79. package/dist/runtime/components/view/Separator.USAGE.md +57 -0
  80. package/dist/runtime/components/view/Text.USAGE.md +68 -0
  81. package/dist/runtime/composables/useApi.USAGE.md +64 -0
  82. package/dist/runtime/composables/useControlSize.USAGE.md +73 -0
  83. package/dist/runtime/composables/useDecimalFormatter.USAGE.md +72 -0
  84. package/dist/runtime/composables/useFilter.USAGE.md +120 -0
  85. package/dist/runtime/composables/useFilter.d.ts +91 -0
  86. package/dist/runtime/composables/useFilter.js +111 -0
  87. package/dist/runtime/composables/useFuzzySearch.USAGE.md +68 -0
  88. package/dist/runtime/composables/useInertia.USAGE.md +80 -0
  89. package/dist/runtime/composables/useListKeyboard.USAGE.md +97 -0
  90. package/dist/runtime/composables/useModal.USAGE.md +82 -0
  91. package/dist/runtime/composables/usePinchZoom.USAGE.md +95 -0
  92. package/dist/runtime/composables/usePressAndHold.USAGE.md +70 -0
  93. package/dist/runtime/composables/useRovingGrid.USAGE.md +106 -0
  94. package/dist/runtime/composables/useRovingGrid.d.ts +35 -0
  95. package/dist/runtime/composables/useRovingGrid.js +115 -0
  96. package/dist/runtime/composables/useSound.USAGE.md +74 -0
  97. package/dist/runtime/composables/useTheme.USAGE.md +76 -0
  98. package/dist/runtime/composables/useUrlSync.USAGE.md +91 -0
  99. package/dist/runtime/composables/useValidation.USAGE.md +100 -0
  100. package/dist/runtime/i18n/en.json +4 -1
  101. package/dist/runtime/i18n/uk.json +4 -1
  102. 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(parseISO(anchor.value) ?? /* @__PURE__ */ new Date())
24
+ () => startOfMonth(
25
+ parseISO(anchor.value) ?? parseISO(props.selected) ?? /* @__PURE__ */ new Date()
26
+ )
24
27
  );
25
- function shift(delta) {
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 sunday = new Date(2024, 0, 7);
36
- return Array.from({ length: 7 }, (_, i) => {
37
- const d = new Date(sunday);
38
- d.setDate(sunday.getDate() + (i + props.weekStartsOn) % 7);
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
- d
63
+ date
41
64
  );
42
65
  });
43
66
  });
44
- function resolveMarker(iso) {
45
- if (props.getMarker) {
46
- const m = props.getMarker(iso);
47
- if (m) {
48
- return {
49
- variant: m.variant,
50
- isStart: iso === m.start,
51
- isEnd: iso === m.end
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 first = visibleMonth.value;
69
- const startWeekday = first.getDay();
70
- const offset = (startWeekday - props.weekStartsOn + 7) % 7;
71
- const gridStart = new Date(first);
72
- gridStart.setDate(gridStart.getDate() - offset);
73
- const sel = parseISO(props.selected);
74
- const result = [];
75
- for (let i = 0; i < 42; i++) {
76
- const d = new Date(gridStart);
77
- d.setDate(gridStart.getDate() + i);
78
- const iso = formatISO(d);
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: d.getDate(),
82
- inMonth: d.getMonth() === first.getMonth(),
83
- isToday: isSameDay(d, today),
84
- isSelected: !!sel && isSameDay(d, sel),
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 onSelect(day) {
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 onEnter(day) {
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 class="calendar-nav">
103
- <orio-button
104
- variant="subdued"
105
- icon="chevron-left"
106
- :aria-label="t('calendar.previousMonth')"
107
- @click="shift(-1)"
108
- />
109
- <span class="calendar-month-title">{{ monthLabel }}</span>
110
- <orio-button
111
- variant="subdued"
112
- icon="chevron-right"
113
- :aria-label="t('calendar.nextMonth')"
114
- @click="shift(1)"
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 v-for="w in weekdayLabels" :key="w" class="calendar-weekday">
119
- {{ w }}
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 class="calendar-grid">
123
- <button
124
- v-for="day in days"
125
- :key="day.iso"
126
- type="button"
127
- class="calendar-day"
128
- :class="{
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
- :disabled="day.isDisabled"
138
- @click="onSelect(day)"
139
- @mouseenter="onEnter(day)"
140
- >
141
- <orio-badge v-if="day.isSelected" pill variant="primary">
142
- {{ day.label }}
143
- </orio-badge>
144
- <template v-else>{{ day.label }}</template>
145
- </button>
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
- min-width: 17rem;
318
+ width: 22rem;
161
319
  }
162
320
 
163
321
  .calendar-nav {
164
322
  display: flex;
165
323
  align-items: center;
166
- justify-content: space-between;
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`.