@abraca/nuxt 2.14.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 (24) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/assets/editor.css +3 -1
  3. package/dist/runtime/components/ACodeEditor.vue +16 -2
  4. package/dist/runtime/components/ANodePanel.vue +7 -5
  5. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  6. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  7. package/dist/runtime/components/aware/ASlider.d.vue.ts +1 -1
  8. package/dist/runtime/components/aware/ASlider.vue.d.ts +1 -1
  9. package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
  10. package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
  11. package/dist/runtime/components/editor/AColorPalettePopover.vue +97 -5
  12. package/dist/runtime/components/editor/AIconPickerPopover.vue +81 -3
  13. package/dist/runtime/components/editor/AMetaNumberStepper.d.vue.ts +40 -0
  14. package/dist/runtime/components/editor/AMetaNumberStepper.vue +214 -0
  15. package/dist/runtime/components/editor/AMetaNumberStepper.vue.d.ts +40 -0
  16. package/dist/runtime/components/renderers/ACalendarRenderer.vue +7 -1
  17. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  18. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  19. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  20. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  21. package/dist/runtime/composables/useTouchDrag.d.ts +21 -4
  22. package/dist/runtime/composables/useTouchDrag.js +30 -0
  23. package/dist/runtime/extensions/views/MetaFieldView.vue +17 -28
  24. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "2.14.0",
7
+ "version": "2.15.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -1 +1,3 @@
1
- html.dark .tiptap .shiki,html.dark .tiptap .shiki span{background-color:var(--ui-bg-muted)!important;color:var(--shiki-dark)!important}.collaboration-carets__caret{border-left:1px solid #0d0d0d;border-right:1px solid #0d0d0d;margin-left:-1px;margin-right:-1px;opacity:0;pointer-events:none;position:relative;transition:opacity .3s ease;word-break:normal}.collaboration-carets__caret.is-hidden{display:none}.collaboration-carets__caret.is-active{opacity:1}.collaboration-carets__label{border-radius:3px 3px 3px 0;color:#0d0d0d;font-size:12px;font-style:normal;font-weight:600;left:-1px;line-height:normal;padding:.1rem .3rem;position:absolute;top:-1.4em;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.ProseMirror-yjs-selection,.collaboration-carets__selection{background-color:var(--collaboration-selection-color)!important;pointer-events:none}.search-highlight{background-color:color-mix(in srgb,var(--color-primary-400) 35%,transparent);border-radius:2px;padding:0 1px}.doc-passage-highlight{background-color:color-mix(in srgb,var(--color-success-400) 35%,transparent);border-radius:2px;padding:0 1px}.tiptap{min-height:100%;padding-bottom:8rem}.tiptap.file-drop-active{border-radius:4px;outline:2px dashed var(--ui-primary);outline-offset:4px}.tiptap .document-header{font-size:2.5rem;font-weight:800;letter-spacing:-.025em;line-height:1.2;margin-bottom:1rem}.tiptap [data-type=document-meta]{align-items:center;display:flex;flex-wrap:wrap;font-size:.875rem;gap:.375rem;line-height:1.75rem;margin-bottom:1.5rem;min-height:1.75rem}.tiptap [data-type=document-meta][data-cursor-in-meta=true][data-empty=true]:after{color:var(--ui-text-dimmed);content:"Type '/' to add a property…";pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.prose-variant .tiptap [data-type=document-meta]{display:none}
1
+ html .cm-editor,html .cm-editor .cm-content,html .cm-editor .cm-gutters,html .cm-editor .cm-scroller,html .cm-editor .cm-tooltip{font-family:var(
2
+ --font-code,ui-monospace,"SF Mono",Menlo,Monaco,Consolas,monospace
3
+ )!important}html.dark .tiptap .shiki,html.dark .tiptap .shiki span{background-color:var(--ui-bg-muted)!important;color:var(--shiki-dark)!important}.collaboration-carets__caret{border-left:1px solid #0d0d0d;border-right:1px solid #0d0d0d;margin-left:-1px;margin-right:-1px;opacity:0;pointer-events:none;position:relative;transition:opacity .3s ease;word-break:normal}.collaboration-carets__caret.is-hidden{display:none}.collaboration-carets__caret.is-active{opacity:1}.collaboration-carets__label{border-radius:3px 3px 3px 0;color:#0d0d0d;font-size:12px;font-style:normal;font-weight:600;left:-1px;line-height:normal;padding:.1rem .3rem;position:absolute;top:-1.4em;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.ProseMirror-yjs-selection,.collaboration-carets__selection{background-color:var(--collaboration-selection-color)!important;pointer-events:none}.search-highlight{background-color:color-mix(in srgb,var(--color-primary-400) 35%,transparent);border-radius:2px;padding:0 1px}.doc-passage-highlight{background-color:color-mix(in srgb,var(--color-success-400) 35%,transparent);border-radius:2px;padding:0 1px}.tiptap{min-height:100%;padding-bottom:8rem}.tiptap.file-drop-active{border-radius:4px;outline:2px dashed var(--ui-primary);outline-offset:4px}.tiptap .document-header{font-size:2.5rem;font-weight:800;letter-spacing:-.025em;line-height:1.2;margin-bottom:1rem}.tiptap [data-type=document-meta]{align-items:center;display:flex;flex-wrap:wrap;font-size:.875rem;gap:.375rem;line-height:1.75rem;margin-bottom:1.5rem;min-height:1.75rem}.tiptap [data-type=document-meta][data-cursor-in-meta=true][data-empty=true]:after{color:var(--ui-text-dimmed);content:"Type '/' to add a property…";pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.prose-variant .tiptap [data-type=document-meta]{display:none}
@@ -57,7 +57,10 @@ function buildTheme(bundle) {
57
57
  ".cm-content": {
58
58
  fontFamily: 'var(--font-code, ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace) !important',
59
59
  caretColor: "var(--ui-text-highlighted)",
60
- padding: "8px 0"
60
+ // Top padding leaves room for a remote caret's name badge on the FIRST
61
+ // line — y-codemirror positions it at `top: -1.4em`, so without headroom
62
+ // it renders above the content box and is clipped by the toolbar above.
63
+ padding: "1.6em 0 8px"
61
64
  },
62
65
  // Local caret — `drawSelection()` hides the native caret and draws this
63
66
  // bar, so style it to read like the native caret used elsewhere in the
@@ -145,7 +148,18 @@ function buildTheme(bundle) {
145
148
  // `user.color`); we restyle shape only. The caret already matches TipTap
146
149
  // (1px border bars, -1px margins); we only fix the label badge + hide the
147
150
  // caret dot.
148
- ".cm-ySelection": { borderRadius: "1px" },
151
+ ".cm-ySelection": {
152
+ borderRadius: "1px",
153
+ // Fallback selection fill. y-codemirror paints the band via an inline
154
+ // `background-color: <user.colorLight>`; a valid inline value wins by
155
+ // specificity and keeps the per-user colour. But peers that broadcast
156
+ // only `user.color` (older builds, native gpui/apertura/aperio clients)
157
+ // leave colorLight as y-codemirror's invalid `color + '33'` default,
158
+ // which the browser drops — making the band invisible while the caret
159
+ // (which uses `color` directly) still shows. This neutral tint then
160
+ // applies, so a remote selection is always visible.
161
+ backgroundColor: "color-mix(in srgb, var(--ui-primary) 30%, transparent)"
162
+ },
149
163
  ".cm-ySelectionCaretDot": { display: "none" },
150
164
  ".cm-ySelectionInfo": {
151
165
  opacity: "1",
@@ -646,11 +646,13 @@ const addFieldMenuItems = computed(
646
646
 
647
647
  <!-- Number -->
648
648
  <template v-else-if="field.type === 'number'">
649
- <UInput
650
- type="number"
651
- :model-value="String(getCustomFieldValue(field) ?? '')"
652
- size="sm"
653
- @update:model-value="setCustomFieldValue(field, $event ? Number($event) : void 0)"
649
+ <AMetaNumberStepper
650
+ :model-value="getCustomFieldValue(field) != null ? Number(getCustomFieldValue(field)) : void 0"
651
+ :min="field.min"
652
+ :max="field.max"
653
+ :step="field.step ?? 1"
654
+ :unit="field.unit"
655
+ @update:model-value="setCustomFieldValue(field, $event)"
654
656
  />
655
657
  </template>
656
658
 
@@ -12,8 +12,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
12
12
  awareness: boolean;
13
13
  tag: "video" | "audio";
14
14
  live: boolean;
15
- controls: boolean;
16
15
  total: boolean;
16
+ controls: boolean;
17
17
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
18
  declare const _default: typeof __VLS_export;
19
19
  export default _default;
@@ -12,8 +12,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
12
12
  awareness: boolean;
13
13
  tag: "video" | "audio";
14
14
  live: boolean;
15
- controls: boolean;
16
15
  total: boolean;
16
+ controls: boolean;
17
17
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
18
  declare const _default: typeof __VLS_export;
19
19
  export default _default;
@@ -11,8 +11,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
11
11
  sync: boolean;
12
12
  peers: boolean;
13
13
  awareness: boolean;
14
- max: number;
15
14
  min: number;
15
+ max: number;
16
16
  total: boolean;
17
17
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
18
  declare const _default: typeof __VLS_export;
@@ -11,8 +11,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
11
11
  sync: boolean;
12
12
  peers: boolean;
13
13
  awareness: boolean;
14
- max: number;
15
14
  min: number;
15
+ max: number;
16
16
  total: boolean;
17
17
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
18
  declare const _default: typeof __VLS_export;
@@ -237,9 +237,9 @@ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<import
237
237
  }>> & Readonly<{}>, {
238
238
  color: any;
239
239
  disabled: boolean;
240
+ block: boolean;
240
241
  square: boolean;
241
242
  viewTransition: boolean;
242
- block: boolean;
243
243
  loading: boolean;
244
244
  collapsed: boolean;
245
245
  tooltip: boolean | Record<string, any>;
@@ -237,9 +237,9 @@ declare const __VLS_export: __VLS_WithSlots<import("vue").DefineComponent<import
237
237
  }>> & Readonly<{}>, {
238
238
  color: any;
239
239
  disabled: boolean;
240
+ block: boolean;
240
241
  square: boolean;
241
242
  viewTransition: boolean;
242
- block: boolean;
243
243
  loading: boolean;
244
244
  collapsed: boolean;
245
245
  tooltip: boolean | Record<string, any>;
@@ -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
  />
@@ -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"
@@ -20,16 +20,16 @@ type __VLS_Props = {
20
20
  };
21
21
  };
22
22
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
23
+ prev: () => any;
23
24
  next: () => any;
24
25
  "update:viewMode": (mode: CalendarViewMode) => any;
25
- prev: () => any;
26
26
  today: () => any;
27
27
  "add-event": () => any;
28
28
  "navigate-to-month": (year: number, month: number) => any;
29
29
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
30
+ onPrev?: (() => any) | undefined;
30
31
  onNext?: (() => any) | undefined;
31
32
  "onUpdate:viewMode"?: ((mode: CalendarViewMode) => any) | undefined;
32
- onPrev?: (() => any) | undefined;
33
33
  onToday?: (() => any) | undefined;
34
34
  "onAdd-event"?: (() => any) | undefined;
35
35
  "onNavigate-to-month"?: ((year: number, month: number) => any) | undefined;
@@ -20,16 +20,16 @@ type __VLS_Props = {
20
20
  };
21
21
  };
22
22
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
23
+ prev: () => any;
23
24
  next: () => any;
24
25
  "update:viewMode": (mode: CalendarViewMode) => any;
25
- prev: () => any;
26
26
  today: () => any;
27
27
  "add-event": () => any;
28
28
  "navigate-to-month": (year: number, month: number) => any;
29
29
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
30
+ onPrev?: (() => any) | undefined;
30
31
  onNext?: (() => any) | undefined;
31
32
  "onUpdate:viewMode"?: ((mode: CalendarViewMode) => any) | undefined;
32
- onPrev?: (() => any) | undefined;
33
33
  onToday?: (() => any) | undefined;
34
34
  "onAdd-event"?: (() => any) | undefined;
35
35
  "onNavigate-to-month"?: ((year: number, month: number) => any) | undefined;
@@ -8,13 +8,13 @@ type __VLS_Props = {
8
8
  isLoading: boolean;
9
9
  };
10
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
- next: () => any;
12
11
  prev: () => any;
12
+ next: () => any;
13
13
  togglePlay: () => any;
14
14
  seek: (position: number) => any;
15
15
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
- onNext?: (() => any) | undefined;
17
16
  onPrev?: (() => any) | undefined;
17
+ onNext?: (() => any) | undefined;
18
18
  onTogglePlay?: (() => any) | undefined;
19
19
  onSeek?: ((position: number) => any) | undefined;
20
20
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -8,13 +8,13 @@ type __VLS_Props = {
8
8
  isLoading: boolean;
9
9
  };
10
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
- next: () => any;
12
11
  prev: () => any;
12
+ next: () => any;
13
13
  togglePlay: () => any;
14
14
  seek: (position: number) => any;
15
15
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
- onNext?: (() => any) | undefined;
17
16
  onPrev?: (() => any) | undefined;
17
+ onNext?: (() => any) | undefined;
18
18
  onTogglePlay?: (() => any) | undefined;
19
19
  onSeek?: ((position: number) => any) | undefined;
20
20
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,3 +1,4 @@
1
+ import type { Ref } from 'vue';
1
2
  /**
2
3
  * Mobile-compatible drag & drop using pointer events.
3
4
  * Works on both touch and mouse devices, replacing HTML5 DragEvent handlers.
@@ -26,11 +27,27 @@ export declare function useTouchDrag(opts: {
26
27
  idAttr?: string;
27
28
  /** Data attribute name for containers (default: 'data-drop-container') */
28
29
  containerAttr?: string;
30
+ /**
31
+ * Navigate by range (prev/next) when the drag ghost dwells near the
32
+ * horizontal edges of `el` — e.g. a calendar dragging an event to the left
33
+ * edge steps to the previous period. Fires on a repeating interval while held
34
+ * at the edge, so a quick pass-through (or a drop on an edge cell) doesn't
35
+ * trigger it; resets the moment the pointer leaves the edge zone.
36
+ */
37
+ edgeNav?: {
38
+ el: Ref<HTMLElement | null>;
39
+ onPrev: () => void;
40
+ onNext: () => void;
41
+ /** px from the edge that counts as "at the edge" (default 48) */
42
+ threshold?: number;
43
+ /** ms between repeated nav steps while held at an edge (default 700) */
44
+ intervalMs?: number;
45
+ };
29
46
  }): {
30
- dragId: import("vue").Ref<string | null, string | null>;
31
- dragOverId: import("vue").Ref<string | null, string | null>;
32
- dragOverContainer: import("vue").Ref<string | null, string | null>;
33
- isDragging: import("vue").Ref<boolean, boolean>;
47
+ dragId: Ref<string | null, string | null>;
48
+ dragOverId: Ref<string | null, string | null>;
49
+ dragOverContainer: Ref<string | null, string | null>;
50
+ isDragging: Ref<boolean, boolean>;
34
51
  handlePointerDown: (e: PointerEvent, id: string) => void;
35
52
  cancel: () => void;
36
53
  };
@@ -7,6 +7,33 @@ export function useTouchDrag(opts) {
7
7
  const dragOverId = ref(null);
8
8
  const dragOverContainer = ref(null);
9
9
  const isDragging = ref(false);
10
+ let edgeNavTimer = null;
11
+ let edgeNavDir = null;
12
+ function clearEdgeNav() {
13
+ if (edgeNavTimer) {
14
+ clearInterval(edgeNavTimer);
15
+ edgeNavTimer = null;
16
+ }
17
+ edgeNavDir = null;
18
+ }
19
+ function handleEdgeNav(clientX) {
20
+ const cfg = opts.edgeNav;
21
+ const el = cfg?.el.value;
22
+ if (!cfg || !el) return;
23
+ const rect = el.getBoundingClientRect();
24
+ const threshold = cfg.threshold ?? 48;
25
+ let dir = null;
26
+ if (clientX <= rect.left + threshold) dir = "prev";
27
+ else if (clientX >= rect.right - threshold) dir = "next";
28
+ if (dir === edgeNavDir) return;
29
+ clearEdgeNav();
30
+ if (!dir) return;
31
+ edgeNavDir = dir;
32
+ edgeNavTimer = setInterval(() => {
33
+ if (edgeNavDir === "prev") cfg.onPrev();
34
+ else if (edgeNavDir === "next") cfg.onNext();
35
+ }, cfg.intervalMs ?? 700);
36
+ }
10
37
  let startX = 0;
11
38
  let startY = 0;
12
39
  let delayTimer = null;
@@ -131,6 +158,7 @@ export function useTouchDrag(opts) {
131
158
  opts.onMoveOverContainer(dragId.value, container, e);
132
159
  }
133
160
  autoScroll(e.clientY);
161
+ handleEdgeNav(e.clientX);
134
162
  }
135
163
  function autoScroll(clientY) {
136
164
  const threshold = 60;
@@ -143,6 +171,7 @@ export function useTouchDrag(opts) {
143
171
  }
144
172
  function onDocPointerUp() {
145
173
  cancelPending();
174
+ clearEdgeNav();
146
175
  document.removeEventListener("pointermove", onDocPointerMove);
147
176
  document.removeEventListener("pointerup", onDocPointerUp);
148
177
  document.removeEventListener("pointercancel", onDocPointerUp);
@@ -175,6 +204,7 @@ export function useTouchDrag(opts) {
175
204
  dragOverContainer.value = null;
176
205
  isDragging.value = false;
177
206
  pendingId = null;
207
+ clearEdgeNav();
178
208
  if (delayTimer) {
179
209
  clearTimeout(delayTimer);
180
210
  delayTimer = null;
@@ -4,6 +4,7 @@ import { NodeViewWrapper } from "@tiptap/vue-3";
4
4
  import AColorPalettePopover from "../../components/editor/AColorPalettePopover.vue";
5
5
  import AIconPickerPopover from "../../components/editor/AIconPickerPopover.vue";
6
6
  import ALocationPickerPopover from "../../components/editor/ALocationPickerPopover.vue";
7
+ import AMetaNumberStepper from "../../components/editor/AMetaNumberStepper.vue";
7
8
  import { useAbraLocale } from "../../composables/useAbraLocale";
8
9
  import {
9
10
  DateFormatter,
@@ -62,6 +63,8 @@ const sliderMin = computed(() => props.node.attrs.sliderMin ?? 0);
62
63
  const sliderMax = computed(() => props.node.attrs.sliderMax ?? 100);
63
64
  const sliderStep = computed(() => props.node.attrs.sliderStep ?? 1);
64
65
  const unit = computed(() => props.node.attrs.unit ?? "");
66
+ const numMin = computed(() => props.node.attrs.sliderMin);
67
+ const numMax = computed(() => props.node.attrs.sliderMax);
65
68
  function getStr(key) {
66
69
  return storage()?.pageMeta?.[key] ?? "";
67
70
  }
@@ -239,16 +242,6 @@ function onRatingWheel(e) {
239
242
  const next = Math.max(0, Math.min(max, current + delta));
240
243
  if (next !== current) patch({ [metaKey.value]: next });
241
244
  }
242
- function onNumberWheel(e) {
243
- const delta = -Math.sign(e.deltaY);
244
- if (!delta) return;
245
- const step = (sliderStep.value || 1) * (e.shiftKey ? 10 : 1);
246
- const current = getNum(metaKey.value);
247
- const min = sliderMin.value;
248
- const max = sliderMax.value;
249
- const next = Math.max(min, Math.min(max, current + delta * step));
250
- if (next !== current) patch({ [metaKey.value]: next });
251
- }
252
245
  function onSliderWheel(e) {
253
246
  const delta = -Math.sign(e.deltaY);
254
247
  if (!delta) return;
@@ -784,13 +777,20 @@ function removeOption(opt) {
784
777
 
785
778
  <!-- ── number ─────────────────────────────────────────────────────────── -->
786
779
  <template v-else-if="fieldType === 'number'">
787
- <div
788
- class="flex items-center gap-1.5 h-7 px-2.5 border border-(--ui-border) rounded-md text-sm"
789
- @mousedown.stop
790
- @touchstart.stop
791
- @wheel.prevent="onNumberWheel"
780
+ <AMetaNumberStepper
781
+ class="min-w-40"
782
+ :model-value="getOptNum(metaKey)"
783
+ :min="numMin"
784
+ :max="numMax"
785
+ :step="sliderStep"
786
+ :unit="unit"
787
+ :disabled="!isEditable"
788
+ @update:model-value="onNumber"
792
789
  >
793
- <template v-if="fieldLabel">
790
+ <template
791
+ v-if="fieldLabel"
792
+ #label
793
+ >
794
794
  <input
795
795
  v-if="editingLabel"
796
796
  ref="labelInputEl"
@@ -807,18 +807,7 @@ function removeOption(opt) {
807
807
  @click.stop="startEditLabel"
808
808
  >{{ fieldLabel }}</span>
809
809
  </template>
810
- <UInputNumber
811
- size="xs"
812
- :model-value="getOptNum(metaKey)"
813
- :step="sliderStep"
814
- class="max-w-32"
815
- @update:model-value="onNumber"
816
- />
817
- <span
818
- v-if="unit"
819
- class="text-(--ui-text-muted) shrink-0 text-xs"
820
- >{{ unit }}</span>
821
- </div>
810
+ </AMetaNumberStepper>
822
811
  </template>
823
812
 
824
813
  <!-- ── toggle ─────────────────────────────────────────────────────────── -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abraca/nuxt",
3
- "version": "2.14.0",
3
+ "version": "2.15.0",
4
4
  "description": "First-class Nuxt module for the Abracadabra CRDT collaboration platform",
5
5
  "repository": "abracadabra/abracadabra-nuxt",
6
6
  "license": "MIT",