@abraca/nuxt 2.14.0 → 2.16.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 (44) 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/ASlider.d.vue.ts +1 -1
  6. package/dist/runtime/components/aware/ASlider.vue.d.ts +1 -1
  7. package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
  8. package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
  9. package/dist/runtime/components/editor/AColorPalettePopover.vue +97 -5
  10. package/dist/runtime/components/editor/AIconPickerPopover.vue +81 -3
  11. package/dist/runtime/components/editor/AMetaNumberStepper.d.vue.ts +40 -0
  12. package/dist/runtime/components/editor/AMetaNumberStepper.vue +214 -0
  13. package/dist/runtime/components/editor/AMetaNumberStepper.vue.d.ts +40 -0
  14. package/dist/runtime/components/registry/APluginBrowser.vue +18 -2
  15. package/dist/runtime/components/renderers/ACalendarRenderer.vue +7 -1
  16. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
  17. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
  18. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  19. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  20. package/dist/runtime/components/settings/APluginInstallDialog.d.vue.ts +39 -0
  21. package/dist/runtime/components/settings/APluginInstallDialog.vue +254 -0
  22. package/dist/runtime/components/settings/APluginInstallDialog.vue.d.ts +39 -0
  23. package/dist/runtime/components/settings/APluginsTabInstalled.d.vue.ts +7 -0
  24. package/dist/runtime/components/settings/APluginsTabInstalled.vue +413 -0
  25. package/dist/runtime/components/settings/APluginsTabInstalled.vue.d.ts +7 -0
  26. package/dist/runtime/components/settings/APluginsTabPending.d.vue.ts +24 -0
  27. package/dist/runtime/components/settings/APluginsTabPending.vue +248 -0
  28. package/dist/runtime/components/settings/APluginsTabPending.vue.d.ts +24 -0
  29. package/dist/runtime/components/settings/ASettingsPluginsPanel.d.vue.ts +14 -1
  30. package/dist/runtime/components/settings/ASettingsPluginsPanel.vue +34 -80
  31. package/dist/runtime/components/settings/ASettingsPluginsPanel.vue.d.ts +14 -1
  32. package/dist/runtime/composables/useDeclinedSpacePlugins.d.ts +7 -0
  33. package/dist/runtime/composables/useDeclinedSpacePlugins.js +24 -0
  34. package/dist/runtime/composables/useLoadTimePending.d.ts +29 -0
  35. package/dist/runtime/composables/useLoadTimePending.js +37 -0
  36. package/dist/runtime/composables/usePluginCatalog.d.ts +5 -1
  37. package/dist/runtime/composables/usePluginCatalog.js +34 -0
  38. package/dist/runtime/composables/useTouchDrag.d.ts +21 -4
  39. package/dist/runtime/composables/useTouchDrag.js +30 -0
  40. package/dist/runtime/composables/useUploadedPluginStore.d.ts +43 -0
  41. package/dist/runtime/composables/useUploadedPluginStore.js +66 -0
  42. package/dist/runtime/extensions/views/MetaFieldView.vue +17 -28
  43. package/dist/runtime/plugin-abracadabra.client.js +48 -1
  44. 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.16.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
 
@@ -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>