@abraca/nuxt 2.13.0 → 2.15.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 (45) hide show
  1. package/dist/module.d.mts +15 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +2 -0
  4. package/dist/runtime/assets/editor.css +3 -1
  5. package/dist/runtime/components/ACodeEditor.vue +138 -23
  6. package/dist/runtime/components/ADocViewToggle.d.vue.ts +40 -0
  7. package/dist/runtime/components/ADocViewToggle.vue +234 -0
  8. package/dist/runtime/components/ADocViewToggle.vue.d.ts +40 -0
  9. package/dist/runtime/components/ADocumentTree.vue +1 -1
  10. package/dist/runtime/components/AEditor.vue +183 -15
  11. package/dist/runtime/components/ANodePanel.vue +91 -91
  12. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  13. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  14. package/dist/runtime/components/aware/ASlider.d.vue.ts +1 -1
  15. package/dist/runtime/components/aware/ASlider.vue.d.ts +1 -1
  16. package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
  17. package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
  18. package/dist/runtime/components/editor/AColorPalettePopover.vue +97 -5
  19. package/dist/runtime/components/editor/ADocSuggestMenu.d.vue.ts +7 -0
  20. package/dist/runtime/components/editor/ADocSuggestMenu.vue +68 -0
  21. package/dist/runtime/components/editor/ADocSuggestMenu.vue.d.ts +7 -0
  22. package/dist/runtime/components/editor/AIconPickerPopover.vue +81 -3
  23. package/dist/runtime/components/editor/AMetaNumberStepper.d.vue.ts +40 -0
  24. package/dist/runtime/components/editor/AMetaNumberStepper.vue +214 -0
  25. package/dist/runtime/components/editor/AMetaNumberStepper.vue.d.ts +40 -0
  26. package/dist/runtime/components/renderers/ACalendarRenderer.vue +7 -1
  27. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  28. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  29. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  30. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  31. package/dist/runtime/composables/useDocLinkPick.d.ts +9 -8
  32. package/dist/runtime/composables/useDocLinkPick.js +7 -18
  33. package/dist/runtime/composables/useDocSuggest.d.ts +34 -0
  34. package/dist/runtime/composables/useDocSuggest.js +56 -0
  35. package/dist/runtime/composables/useTouchDrag.d.ts +21 -4
  36. package/dist/runtime/composables/useTouchDrag.js +30 -0
  37. package/dist/runtime/extensions/doc-link-drop.js +2 -2
  38. package/dist/runtime/extensions/doc-suggest.d.ts +28 -0
  39. package/dist/runtime/extensions/doc-suggest.js +85 -0
  40. package/dist/runtime/extensions/views/MetaFieldView.vue +17 -28
  41. package/dist/runtime/utils/codeHighlightStyle.d.ts +15 -0
  42. package/dist/runtime/utils/codeHighlightStyle.js +34 -0
  43. package/dist/runtime/utils/loadCodeMirror.d.ts +1 -0
  44. package/dist/runtime/utils/loadCodeMirror.js +6 -3
  45. package/package.json +2 -1
@@ -1,24 +1,106 @@
1
1
  <script setup>
2
2
  import { ref, computed, watch, nextTick } from "vue";
3
3
  import { COLOR_PALETTES } from "../../utils/colorPalettes";
4
+ const COLS = 11;
4
5
  const props = defineProps({
5
6
  modelValue: { type: String, required: true },
6
7
  open: { type: Boolean, required: true }
7
8
  });
8
9
  const emit = defineEmits(["update:modelValue", "update:open"]);
9
10
  const search = ref("");
11
+ const highlighted = ref(-1);
12
+ const scrollContainer = ref(null);
10
13
  const filteredPalettes = computed(() => {
11
14
  if (!search.value) return COLOR_PALETTES;
12
15
  const q = search.value.toLowerCase();
13
16
  return COLOR_PALETTES.filter((p) => p.name.toLowerCase().includes(q));
14
17
  });
18
+ const paletteGroups = computed(() => {
19
+ let start = 0;
20
+ return filteredPalettes.value.map((p) => {
21
+ const group = { name: p.name, shades: p.shades, start };
22
+ start += p.shades.length;
23
+ return group;
24
+ });
25
+ });
26
+ const flatShades = computed(
27
+ () => filteredPalettes.value.flatMap(
28
+ (p) => p.shades.map((s) => ({ hex: s.hex, key: `${p.name}-${s.shade}` }))
29
+ )
30
+ );
31
+ watch(search, () => {
32
+ highlighted.value = -1;
33
+ });
15
34
  function selectHex(hex) {
16
35
  emit("update:modelValue", hex);
17
36
  emit("update:open", false);
18
37
  }
38
+ function scrollHighlightedIntoView() {
39
+ nextTick(() => {
40
+ scrollContainer.value?.querySelector(`[data-idx="${highlighted.value}"]`)?.scrollIntoView({ block: "nearest" });
41
+ });
42
+ }
43
+ function setHighlight(i) {
44
+ const max = flatShades.value.length - 1;
45
+ if (max < 0) return;
46
+ highlighted.value = Math.max(0, Math.min(max, i));
47
+ scrollHighlightedIntoView();
48
+ }
49
+ function onKeydown(e) {
50
+ switch (e.key) {
51
+ case "ArrowDown":
52
+ e.preventDefault();
53
+ setHighlight(highlighted.value < 0 ? 0 : highlighted.value + COLS);
54
+ break;
55
+ case "ArrowUp":
56
+ if (highlighted.value < 0) return;
57
+ e.preventDefault();
58
+ if (highlighted.value < COLS) {
59
+ highlighted.value = -1;
60
+ searchInput.value?.$el?.querySelector("input")?.focus();
61
+ } else {
62
+ setHighlight(highlighted.value - COLS);
63
+ }
64
+ break;
65
+ case "ArrowRight":
66
+ if (highlighted.value < 0) return;
67
+ e.preventDefault();
68
+ setHighlight(highlighted.value + 1);
69
+ break;
70
+ case "ArrowLeft":
71
+ if (highlighted.value < 0) return;
72
+ e.preventDefault();
73
+ setHighlight(highlighted.value - 1);
74
+ break;
75
+ case "Home":
76
+ if (highlighted.value < 0) return;
77
+ e.preventDefault();
78
+ setHighlight(0);
79
+ break;
80
+ case "End":
81
+ if (highlighted.value < 0) return;
82
+ e.preventDefault();
83
+ setHighlight(flatShades.value.length - 1);
84
+ break;
85
+ case "Enter": {
86
+ const idx = highlighted.value >= 0 ? highlighted.value : 0;
87
+ const shade = flatShades.value[idx];
88
+ if (shade) {
89
+ e.preventDefault();
90
+ selectHex(shade.hex);
91
+ }
92
+ break;
93
+ }
94
+ case "Escape":
95
+ e.preventDefault();
96
+ emit("update:open", false);
97
+ break;
98
+ }
99
+ }
19
100
  const searchInput = ref(null);
20
101
  watch(() => props.open, async (open) => {
21
102
  if (open) {
103
+ highlighted.value = -1;
22
104
  await nextTick();
23
105
  searchInput.value?.$el?.querySelector("input")?.focus();
24
106
  }
@@ -33,7 +115,10 @@ watch(() => props.open, async (open) => {
33
115
  >
34
116
  <slot />
35
117
  <template #content>
36
- <div class="w-80 p-2 flex flex-col gap-2">
118
+ <div
119
+ class="w-80 p-2 flex flex-col gap-2"
120
+ @keydown="onKeydown"
121
+ >
37
122
  <UInput
38
123
  ref="searchInput"
39
124
  v-model="search"
@@ -41,9 +126,12 @@ watch(() => props.open, async (open) => {
41
126
  placeholder="Search colors..."
42
127
  icon="i-lucide-search"
43
128
  />
44
- <div class="max-h-64 overflow-y-auto flex flex-col gap-1.5">
129
+ <div
130
+ ref="scrollContainer"
131
+ class="max-h-64 overflow-y-auto flex flex-col gap-1.5"
132
+ >
45
133
  <div
46
- v-for="palette in filteredPalettes"
134
+ v-for="palette in paletteGroups"
47
135
  :key="palette.name"
48
136
  >
49
137
  <p class="text-xs text-(--ui-text-muted) mb-0.5 capitalize">
@@ -51,13 +139,17 @@ watch(() => props.open, async (open) => {
51
139
  </p>
52
140
  <div class="grid grid-cols-11 gap-0.5">
53
141
  <UTooltip
54
- v-for="shade in palette.shades"
142
+ v-for="(shade, shadeIdx) in palette.shades"
55
143
  :key="shade.shade"
56
144
  :text="`${palette.name}-${shade.shade}`"
57
145
  >
58
146
  <button
147
+ :data-idx="palette.start + shadeIdx"
59
148
  class="size-5 rounded-sm cursor-pointer transition-transform hover:scale-110"
60
- :class="modelValue === shade.hex ? 'ring-2 ring-offset-1 ring-(--ui-color-neutral-900) dark:ring-(--ui-color-neutral-100) scale-110' : ''"
149
+ :class="[
150
+ modelValue === shade.hex ? 'ring-2 ring-offset-1 ring-(--ui-color-neutral-900) dark:ring-(--ui-color-neutral-100) scale-110' : '',
151
+ highlighted === palette.start + shadeIdx ? 'ring-2 ring-(--ui-primary) scale-110' : ''
152
+ ]"
61
153
  :style="{ background: shade.hex }"
62
154
  @click="selectHex(shade.hex)"
63
155
  />
@@ -0,0 +1,7 @@
1
+ import type { DocSuggestPopupState } from '../../composables/useDocSuggest.js';
2
+ type __VLS_Props = {
3
+ state: DocSuggestPopupState;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,68 @@
1
+ <script setup>
2
+ import { computed, ref, watch, nextTick } from "vue";
3
+ const props = defineProps({
4
+ state: { type: Object, required: true }
5
+ });
6
+ const listRef = ref(null);
7
+ const style = computed(() => {
8
+ const r = props.state.rect;
9
+ if (!r) return {};
10
+ return {
11
+ left: `${Math.round(r.left)}px`,
12
+ top: `${Math.round(r.bottom + 6)}px`
13
+ };
14
+ });
15
+ watch(() => props.state.index, async () => {
16
+ await nextTick();
17
+ listRef.value?.querySelector('[data-active="true"]')?.scrollIntoView({ block: "nearest" });
18
+ });
19
+ function pick(item) {
20
+ props.state.onSelect?.(item);
21
+ }
22
+ </script>
23
+
24
+ <template>
25
+ <Teleport to="body">
26
+ <div
27
+ v-if="state.active && state.rect && state.items.length"
28
+ ref="listRef"
29
+ class="fixed z-[60] w-72 max-h-72 overflow-y-auto overflow-x-hidden rounded-(--ui-radius) border border-(--ui-border) bg-(--ui-bg) shadow-lg p-1"
30
+ :style="style"
31
+ @mousedown.prevent
32
+ >
33
+ <div class="flex items-center gap-1.5 px-2 py-1 text-[11px] font-medium text-(--ui-text-muted)">
34
+ <UIcon
35
+ :name="state.mode === 'embed' ? 'i-lucide-file-box' : 'i-lucide-file-symlink'"
36
+ class="size-3.5 shrink-0"
37
+ />
38
+ <span>{{ state.mode === "embed" ? "Embed document" : "Link document" }}</span>
39
+ </div>
40
+
41
+ <button
42
+ v-for="(item, i) in state.items"
43
+ :key="item.id + ':' + i"
44
+ type="button"
45
+ :data-active="i === state.index"
46
+ class="w-full min-w-0 flex items-center gap-2 px-2 py-1.5 rounded-(--ui-radius) text-sm text-left text-(--ui-text)"
47
+ :class="i === state.index ? 'bg-(--ui-bg-elevated)' : 'hover:bg-(--ui-bg-elevated)/60'"
48
+ @click="pick(item)"
49
+ >
50
+ <UIcon
51
+ :name="item.icon"
52
+ class="size-4 shrink-0"
53
+ :class="item.isCreate ? 'text-(--ui-primary)' : 'text-(--ui-text-dimmed)'"
54
+ />
55
+ <span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
56
+ <span
57
+ v-if="item.prefix"
58
+ class="text-xs text-(--ui-text-dimmed) truncate shrink-0 max-w-[45%]"
59
+ >{{ item.prefix }} /</span>
60
+ <span
61
+ class="truncate"
62
+ :class="item.isCreate ? 'text-(--ui-primary)' : ''"
63
+ >{{ item.label }}</span>
64
+ </span>
65
+ </button>
66
+ </div>
67
+ </Teleport>
68
+ </template>
@@ -0,0 +1,7 @@
1
+ import type { DocSuggestPopupState } from '../../composables/useDocSuggest.js';
2
+ type __VLS_Props = {
3
+ state: DocSuggestPopupState;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -2,6 +2,7 @@
2
2
  import { ref, computed, watch, nextTick, onBeforeUnmount } from "vue";
3
3
  import { LUCIDE_ICON_NAMES } from "../../utils/lucideIcons";
4
4
  const BATCH_SIZE = 80;
5
+ const COLS = 8;
5
6
  const props = defineProps({
6
7
  modelValue: { type: String, required: true },
7
8
  open: { type: Boolean, required: true },
@@ -12,6 +13,7 @@ const search = ref("");
12
13
  const loadedCount = ref(BATCH_SIZE);
13
14
  const sentinel = ref(null);
14
15
  const scrollContainer = ref(null);
16
+ const highlighted = ref(-1);
15
17
  const sourceIcons = computed(
16
18
  () => props.options?.length ? props.options : LUCIDE_ICON_NAMES
17
19
  );
@@ -26,6 +28,7 @@ const visibleIcons = computed(
26
28
  const hasMore = computed(() => loadedCount.value < filteredIcons.value.length);
27
29
  watch(search, () => {
28
30
  loadedCount.value = BATCH_SIZE;
31
+ highlighted.value = -1;
29
32
  scrollContainer.value?.scrollTo(0, 0);
30
33
  });
31
34
  let observer = null;
@@ -47,6 +50,7 @@ watch(() => props.open, async (open) => {
47
50
  if (open) {
48
51
  loadedCount.value = BATCH_SIZE;
49
52
  search.value = "";
53
+ highlighted.value = -1;
50
54
  await nextTick();
51
55
  searchInput.value?.$el?.querySelector("input")?.focus();
52
56
  await nextTick();
@@ -60,6 +64,73 @@ function selectIcon(name) {
60
64
  emit("update:modelValue", name);
61
65
  emit("update:open", false);
62
66
  }
67
+ function scrollHighlightedIntoView() {
68
+ nextTick(() => {
69
+ scrollContainer.value?.querySelector(`[data-idx="${highlighted.value}"]`)?.scrollIntoView({ block: "nearest" });
70
+ });
71
+ }
72
+ function setHighlight(i) {
73
+ const max = visibleIcons.value.length - 1;
74
+ if (max < 0) return;
75
+ if (i > max && hasMore.value) {
76
+ loadedCount.value += BATCH_SIZE;
77
+ nextTick(() => setHighlight(i));
78
+ return;
79
+ }
80
+ highlighted.value = Math.max(0, Math.min(max, i));
81
+ scrollHighlightedIntoView();
82
+ }
83
+ function onKeydown(e) {
84
+ switch (e.key) {
85
+ case "ArrowDown":
86
+ e.preventDefault();
87
+ setHighlight(highlighted.value < 0 ? 0 : highlighted.value + COLS);
88
+ break;
89
+ case "ArrowUp":
90
+ if (highlighted.value < 0) return;
91
+ e.preventDefault();
92
+ if (highlighted.value < COLS) {
93
+ highlighted.value = -1;
94
+ searchInput.value?.$el?.querySelector("input")?.focus();
95
+ } else {
96
+ setHighlight(highlighted.value - COLS);
97
+ }
98
+ break;
99
+ case "ArrowRight":
100
+ if (highlighted.value < 0) return;
101
+ e.preventDefault();
102
+ setHighlight(highlighted.value + 1);
103
+ break;
104
+ case "ArrowLeft":
105
+ if (highlighted.value < 0) return;
106
+ e.preventDefault();
107
+ setHighlight(highlighted.value - 1);
108
+ break;
109
+ case "Home":
110
+ if (highlighted.value < 0) return;
111
+ e.preventDefault();
112
+ setHighlight(0);
113
+ break;
114
+ case "End":
115
+ if (highlighted.value < 0) return;
116
+ e.preventDefault();
117
+ setHighlight(visibleIcons.value.length - 1);
118
+ break;
119
+ case "Enter": {
120
+ const idx = highlighted.value >= 0 ? highlighted.value : 0;
121
+ const name = visibleIcons.value[idx];
122
+ if (name) {
123
+ e.preventDefault();
124
+ selectIcon(name);
125
+ }
126
+ break;
127
+ }
128
+ case "Escape":
129
+ e.preventDefault();
130
+ emit("update:open", false);
131
+ break;
132
+ }
133
+ }
63
134
  const searchInput = ref(null);
64
135
  </script>
65
136
 
@@ -71,7 +142,10 @@ const searchInput = ref(null);
71
142
  >
72
143
  <slot />
73
144
  <template #content>
74
- <div class="w-72 p-2 flex flex-col gap-2">
145
+ <div
146
+ class="w-72 p-2 flex flex-col gap-2"
147
+ @keydown="onKeydown"
148
+ >
75
149
  <UInput
76
150
  ref="searchInput"
77
151
  v-model="search"
@@ -85,13 +159,17 @@ const searchInput = ref(null);
85
159
  >
86
160
  <div class="grid grid-cols-8 gap-0.5">
87
161
  <UTooltip
88
- v-for="name in visibleIcons"
162
+ v-for="(name, idx) in visibleIcons"
89
163
  :key="name"
90
164
  :text="name"
91
165
  >
92
166
  <button
167
+ :data-idx="idx"
93
168
  class="size-8 flex items-center justify-center rounded cursor-pointer hover:bg-(--ui-bg-elevated)"
94
- :class="modelValue === name ? 'bg-(--ui-bg-accented)' : ''"
169
+ :class="[
170
+ modelValue === name ? 'bg-(--ui-bg-accented)' : '',
171
+ highlighted === idx ? 'ring-2 ring-(--ui-primary)' : ''
172
+ ]"
95
173
  @click="selectIcon(name)"
96
174
  >
97
175
  <UIcon
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Edge-flush number stepper. The whole pill IS the control:
3
+ * [ label ] − ‹value› + [ unit ]
4
+ * • − / + flush ghost buttons (accent glyphs), press-and-hold to repeat
5
+ * • centre value: type to edit, vertical drag (mouse + touch) to scrub
6
+ * • scroll wheel to step (Shift = ×10), arrow keys to step (Shift = ×10)
7
+ * • value flashes on programmatic change (step / drag / wheel) for feedback
8
+ */
9
+ type __VLS_Props = {
10
+ modelValue?: number | null;
11
+ min?: number;
12
+ max?: number;
13
+ step?: number;
14
+ unit?: string;
15
+ disabled?: boolean;
16
+ };
17
+ declare var __VLS_1: {};
18
+ type __VLS_Slots = {} & {
19
+ label?: (props: typeof __VLS_1) => any;
20
+ };
21
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
22
+ "update:modelValue": (value: number | undefined) => any;
23
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
24
+ "onUpdate:modelValue"?: ((value: number | undefined) => any) | undefined;
25
+ }>, {
26
+ unit: string;
27
+ disabled: boolean;
28
+ modelValue: number | null;
29
+ min: number;
30
+ max: number;
31
+ step: number;
32
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
34
+ declare const _default: typeof __VLS_export;
35
+ export default _default;
36
+ type __VLS_WithSlots<T, S> = T & {
37
+ new (): {
38
+ $slots: S;
39
+ };
40
+ };
@@ -0,0 +1,214 @@
1
+ <script setup>
2
+ import { computed, ref, onBeforeUnmount } from "vue";
3
+ const props = defineProps({
4
+ modelValue: { type: [Number, null], required: false, default: void 0 },
5
+ min: { type: Number, required: false, default: Number.NEGATIVE_INFINITY },
6
+ max: { type: Number, required: false, default: Number.POSITIVE_INFINITY },
7
+ step: { type: Number, required: false, default: 1 },
8
+ unit: { type: String, required: false, default: "" },
9
+ disabled: { type: Boolean, required: false, default: false }
10
+ });
11
+ const emit = defineEmits(["update:modelValue"]);
12
+ const PX_PER_STEP = 8;
13
+ const valueEl = ref(null);
14
+ const focused = ref(false);
15
+ const dragging = ref(false);
16
+ const flash = ref(false);
17
+ const draft = ref("");
18
+ const current = computed(() => typeof props.modelValue === "number" && Number.isFinite(props.modelValue) ? props.modelValue : 0);
19
+ const atMin = computed(() => current.value <= props.min);
20
+ const atMax = computed(() => current.value >= props.max);
21
+ const displayValue = computed(
22
+ () => focused.value ? draft.value : typeof props.modelValue === "number" ? String(props.modelValue) : ""
23
+ );
24
+ function clamp(n) {
25
+ return Math.max(props.min, Math.min(props.max, n));
26
+ }
27
+ function pulse() {
28
+ flash.value = false;
29
+ requestAnimationFrame(() => {
30
+ flash.value = true;
31
+ });
32
+ }
33
+ function commit(n, withFlash = true) {
34
+ const next = clamp(n);
35
+ if (next === props.modelValue) return;
36
+ emit("update:modelValue", next);
37
+ if (withFlash) pulse();
38
+ }
39
+ function stepBy(dir, multiplier = 1) {
40
+ if (props.disabled) return;
41
+ commit(current.value + dir * props.step * multiplier);
42
+ }
43
+ let holdTimer = null;
44
+ let repeatTimer = null;
45
+ function clearHold() {
46
+ if (holdTimer) {
47
+ clearTimeout(holdTimer);
48
+ holdTimer = null;
49
+ }
50
+ if (repeatTimer) {
51
+ clearInterval(repeatTimer);
52
+ repeatTimer = null;
53
+ }
54
+ window.removeEventListener("pointerup", clearHold);
55
+ }
56
+ function onStepPress(dir) {
57
+ if (props.disabled) return;
58
+ stepBy(dir);
59
+ holdTimer = setTimeout(() => {
60
+ repeatTimer = setInterval(() => stepBy(dir), 60);
61
+ }, 400);
62
+ window.addEventListener("pointerup", clearHold, { once: true });
63
+ }
64
+ function onWheel(e) {
65
+ if (props.disabled) return;
66
+ const dir = -Math.sign(e.deltaY);
67
+ if (!dir) return;
68
+ stepBy(dir, e.shiftKey ? 10 : 1);
69
+ }
70
+ let dragStartY = 0;
71
+ let dragStartVal = 0;
72
+ let moved = false;
73
+ function onValuePointerDown(e) {
74
+ if (props.disabled) return;
75
+ dragStartY = e.clientY;
76
+ dragStartVal = current.value;
77
+ moved = false;
78
+ window.addEventListener("pointermove", onValuePointerMove);
79
+ window.addEventListener("pointerup", onValuePointerUp, { once: true });
80
+ }
81
+ function onValuePointerMove(e) {
82
+ const dy = dragStartY - e.clientY;
83
+ if (!moved && Math.abs(dy) < 4) return;
84
+ if (!moved) {
85
+ moved = true;
86
+ dragging.value = true;
87
+ valueEl.value?.blur();
88
+ }
89
+ e.preventDefault();
90
+ const steps = Math.round(dy / PX_PER_STEP);
91
+ commit(dragStartVal + steps * props.step);
92
+ }
93
+ function onValuePointerUp() {
94
+ window.removeEventListener("pointermove", onValuePointerMove);
95
+ dragging.value = false;
96
+ if (!moved) {
97
+ valueEl.value?.focus();
98
+ valueEl.value?.select();
99
+ }
100
+ }
101
+ function onFocus() {
102
+ focused.value = true;
103
+ draft.value = typeof props.modelValue === "number" ? String(props.modelValue) : "";
104
+ }
105
+ function onInput(e) {
106
+ draft.value = e.target.value;
107
+ const trimmed = draft.value.trim();
108
+ if (trimmed === "") return;
109
+ const n = Number(trimmed);
110
+ if (Number.isFinite(n)) commit(n, false);
111
+ }
112
+ function onBlur() {
113
+ focused.value = false;
114
+ const trimmed = draft.value.trim();
115
+ if (trimmed === "") {
116
+ if (props.modelValue !== void 0) emit("update:modelValue", void 0);
117
+ return;
118
+ }
119
+ const n = Number(trimmed);
120
+ if (Number.isFinite(n)) commit(n, false);
121
+ }
122
+ function onKeydown(e) {
123
+ if (e.key === "ArrowUp") {
124
+ e.preventDefault();
125
+ stepBy(1, e.shiftKey ? 10 : 1);
126
+ } else if (e.key === "ArrowDown") {
127
+ e.preventDefault();
128
+ stepBy(-1, e.shiftKey ? 10 : 1);
129
+ } else if (e.key === "Enter") {
130
+ e.preventDefault();
131
+ valueEl.value?.blur();
132
+ } else if (e.key === "Escape") {
133
+ e.preventDefault();
134
+ draft.value = typeof props.modelValue === "number" ? String(props.modelValue) : "";
135
+ valueEl.value?.blur();
136
+ }
137
+ }
138
+ onBeforeUnmount(() => {
139
+ clearHold();
140
+ window.removeEventListener("pointermove", onValuePointerMove);
141
+ });
142
+ </script>
143
+
144
+ <template>
145
+ <div
146
+ class="meta-number-stepper flex items-center h-7 rounded-md border border-(--ui-border) text-sm overflow-hidden select-none transition-shadow"
147
+ :class="{
148
+ 'opacity-60 pointer-events-none': disabled,
149
+ 'ring-1 ring-(--ui-primary) ring-inset': dragging || focused
150
+ }"
151
+ @mousedown.stop
152
+ @touchstart.stop
153
+ @wheel.prevent="onWheel"
154
+ >
155
+ <span
156
+ v-if="$slots.label"
157
+ class="pl-2.5 pr-1 shrink-0 flex items-center"
158
+ >
159
+ <slot name="label" />
160
+ </span>
161
+
162
+ <button
163
+ type="button"
164
+ tabindex="-1"
165
+ class="h-full px-2 flex items-center justify-center text-(--ui-primary) hover:bg-(--ui-bg-elevated) active:scale-90 transition-transform disabled:opacity-30 disabled:pointer-events-none cursor-pointer"
166
+ :disabled="disabled || atMin"
167
+ aria-label="Decrease"
168
+ @pointerdown.stop.prevent="onStepPress(-1)"
169
+ >
170
+ <UIcon
171
+ name="i-lucide-minus"
172
+ class="size-3.5"
173
+ />
174
+ </button>
175
+
176
+ <input
177
+ ref="valueEl"
178
+ type="text"
179
+ inputmode="decimal"
180
+ class="meta-number-value flex-1 min-w-0 w-10 bg-transparent text-center outline-none tabular-nums cursor-ns-resize"
181
+ :class="{ 'meta-number-value--flash': flash }"
182
+ :value="displayValue"
183
+ :disabled="disabled"
184
+ @pointerdown="onValuePointerDown"
185
+ @focus="onFocus"
186
+ @blur="onBlur"
187
+ @input="onInput"
188
+ @keydown="onKeydown"
189
+ >
190
+
191
+ <button
192
+ type="button"
193
+ tabindex="-1"
194
+ class="h-full px-2 flex items-center justify-center text-(--ui-primary) hover:bg-(--ui-bg-elevated) active:scale-90 transition-transform disabled:opacity-30 disabled:pointer-events-none cursor-pointer"
195
+ :disabled="disabled || atMax"
196
+ aria-label="Increase"
197
+ @pointerdown.stop.prevent="onStepPress(1)"
198
+ >
199
+ <UIcon
200
+ name="i-lucide-plus"
201
+ class="size-3.5"
202
+ />
203
+ </button>
204
+
205
+ <span
206
+ v-if="unit"
207
+ class="pr-2.5 pl-0.5 shrink-0 text-(--ui-text-muted) text-xs"
208
+ >{{ unit }}</span>
209
+ </div>
210
+ </template>
211
+
212
+ <style scoped>
213
+ .meta-number-value{touch-action:none}.meta-number-value::-webkit-inner-spin-button,.meta-number-value::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.meta-number-value--flash{animation:meta-number-flash .22s ease-out}@keyframes meta-number-flash{0%{color:var(--ui-primary);transform:scale(1.22)}to{color:inherit;transform:scale(1)}}
214
+ </style>
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Edge-flush number stepper. The whole pill IS the control:
3
+ * [ label ] − ‹value› + [ unit ]
4
+ * • − / + flush ghost buttons (accent glyphs), press-and-hold to repeat
5
+ * • centre value: type to edit, vertical drag (mouse + touch) to scrub
6
+ * • scroll wheel to step (Shift = ×10), arrow keys to step (Shift = ×10)
7
+ * • value flashes on programmatic change (step / drag / wheel) for feedback
8
+ */
9
+ type __VLS_Props = {
10
+ modelValue?: number | null;
11
+ min?: number;
12
+ max?: number;
13
+ step?: number;
14
+ unit?: string;
15
+ disabled?: boolean;
16
+ };
17
+ declare var __VLS_1: {};
18
+ type __VLS_Slots = {} & {
19
+ label?: (props: typeof __VLS_1) => any;
20
+ };
21
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
22
+ "update:modelValue": (value: number | undefined) => any;
23
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
24
+ "onUpdate:modelValue"?: ((value: number | undefined) => any) | undefined;
25
+ }>, {
26
+ unit: string;
27
+ disabled: boolean;
28
+ modelValue: number | null;
29
+ min: number;
30
+ max: number;
31
+ step: number;
32
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
34
+ declare const _default: typeof __VLS_export;
35
+ export default _default;
36
+ type __VLS_WithSlots<T, S> = T & {
37
+ new (): {
38
+ $slots: S;
39
+ };
40
+ };
@@ -80,7 +80,13 @@ function onCreateEvent(payload) {
80
80
  }
81
81
  }
82
82
  const dragPreviewTime = ref(null);
83
+ const bodyRef = ref(null);
83
84
  const { dragId, dragOverContainer, handlePointerDown } = useTouchDrag({
85
+ edgeNav: {
86
+ el: bodyRef,
87
+ onPrev: () => cal.prevPeriod(),
88
+ onNext: () => cal.nextPeriod()
89
+ },
84
90
  onMoveToContainer: (eventId, dateStr) => {
85
91
  if (!props.editable) return;
86
92
  const entry = tree.entries.value.find((e) => e.id === eventId);
@@ -204,7 +210,7 @@ defineExpose({ connectedUsers });
204
210
  </script>
205
211
 
206
212
  <template>
207
- <div class="flex-1 min-h-0 flex flex-col relative">
213
+ <div ref="bodyRef" class="flex-1 min-h-0 flex flex-col relative">
208
214
  <!-- Toolbar -->
209
215
  <ACalendarToolbar
210
216
  :view-mode="cal.viewMode.value"