@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.
- package/dist/module.d.mts +15 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -0
- package/dist/runtime/assets/editor.css +3 -1
- package/dist/runtime/components/ACodeEditor.vue +138 -23
- package/dist/runtime/components/ADocViewToggle.d.vue.ts +40 -0
- package/dist/runtime/components/ADocViewToggle.vue +234 -0
- package/dist/runtime/components/ADocViewToggle.vue.d.ts +40 -0
- package/dist/runtime/components/ADocumentTree.vue +1 -1
- package/dist/runtime/components/AEditor.vue +183 -15
- package/dist/runtime/components/ANodePanel.vue +91 -91
- package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
- package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
- package/dist/runtime/components/aware/ASlider.d.vue.ts +1 -1
- package/dist/runtime/components/aware/ASlider.vue.d.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearchButton.d.vue.ts +1 -1
- package/dist/runtime/components/docs/ADocsSearchButton.vue.d.ts +1 -1
- package/dist/runtime/components/editor/AColorPalettePopover.vue +97 -5
- package/dist/runtime/components/editor/ADocSuggestMenu.d.vue.ts +7 -0
- package/dist/runtime/components/editor/ADocSuggestMenu.vue +68 -0
- package/dist/runtime/components/editor/ADocSuggestMenu.vue.d.ts +7 -0
- package/dist/runtime/components/editor/AIconPickerPopover.vue +81 -3
- package/dist/runtime/components/editor/AMetaNumberStepper.d.vue.ts +40 -0
- package/dist/runtime/components/editor/AMetaNumberStepper.vue +214 -0
- package/dist/runtime/components/editor/AMetaNumberStepper.vue.d.ts +40 -0
- package/dist/runtime/components/renderers/ACalendarRenderer.vue +7 -1
- package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +2 -2
- package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +2 -2
- package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
- package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
- package/dist/runtime/composables/useDocLinkPick.d.ts +9 -8
- package/dist/runtime/composables/useDocLinkPick.js +7 -18
- package/dist/runtime/composables/useDocSuggest.d.ts +34 -0
- package/dist/runtime/composables/useDocSuggest.js +56 -0
- package/dist/runtime/composables/useTouchDrag.d.ts +21 -4
- package/dist/runtime/composables/useTouchDrag.js +30 -0
- package/dist/runtime/extensions/doc-link-drop.js +2 -2
- package/dist/runtime/extensions/doc-suggest.d.ts +28 -0
- package/dist/runtime/extensions/doc-suggest.js +85 -0
- package/dist/runtime/extensions/views/MetaFieldView.vue +17 -28
- package/dist/runtime/utils/codeHighlightStyle.d.ts +15 -0
- package/dist/runtime/utils/codeHighlightStyle.js +34 -0
- package/dist/runtime/utils/loadCodeMirror.d.ts +1 -0
- package/dist/runtime/utils/loadCodeMirror.js +6 -3
- 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
|
|
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
|
|
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
|
|
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="
|
|
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
|
|
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="
|
|
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"
|