feat: schema builder's create flow

This commit is contained in:
Florian Sylvain committed 2026-10-20 00:29:01 +02:00
1 parent f514146fed
commit 083cc9e23e
71 files changed
+7223 -61

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
import { computed, nextTick, ref, shallowRef } from 'vue'
import { t } from '@/i18n'
import { moveItem } from '@/lib/arrayMove'
interface KeyboardReorderOptions<T> {
items: () => readonly T[]
name: (item: T) => string
commit: (next: T[]) => void
root: () => HTMLElement | null
}
export function useKeyboardReorder<T>(options: KeyboardReorderOptions<T>) {
const working = shallowRef<T[] | null>(null)
const grabbed = ref<number | null>(null)
const origin = ref(0)
const announcement = ref('')
const shown = computed<readonly T[]>(() => working.value ?? options.items())
const active = computed(() => grabbed.value !== null)
function focusHandle(index: number) {
nextTick(() =>
options.root()?.querySelector<HTMLElement>(`[data-handle="${index}"]`)?.focus(),
)
}
function movingText(item: T, position: number, total: number) {
return t(
'Moving {0}. Position {1} of {2}. Press Up or Down to move, Space to drop, Esc to cancel.',
[options.name(item), position, total],
)
}
function grab(index: number) {
const items = [...options.items()]
const item = items[index]
if (item === undefined) {
return
}
working.value = items
grabbed.value = index
origin.value = index
announcement.value = movingText(item, index + 1, items.length)
}
function move(delta: number) {
const current = grabbed.value
const items = working.value
if (current === null || !items) {
return
}
const target = current + delta
if (target < 0 || target >= items.length) {
return
}
working.value = moveItem(items, current, target)
grabbed.value = target
announcement.value = movingText(items[current]!, target + 1, items.length)
focusHandle(target)
}
function release() {
working.value = null
grabbed.value = null
}
function drop() {
const current = grabbed.value
const items = working.value
if (current === null || !items) {
return
}
announcement.value = t('{0} dropped. Position {1} of {2}.', [
options.name(items[current]!),
current + 1,
items.length,
])
release()
if (current !== origin.value) {
options.commit(items)
}
focusHandle(current)
}
function cancel() {
const current = grabbed.value
const items = working.value
if (current === null || !items) {
return
}
const index = origin.value
announcement.value = t('Reorder cancelled. {0} is back at position {1} of {2}.', [
options.name(items[current]!),
index + 1,
items.length,
])
release()
focusHandle(index)
}
function onKeydown(event: KeyboardEvent, index: number) {
if (grabbed.value === null) {
if (event.key === ' ' || event.key === 'Enter') {
event.preventDefault()
grab(index)
}
return
}
const actions: Record<string, () => void> = {
ArrowUp: () => move(-1),
ArrowDown: () => move(1),
' ': drop,
Enter: drop,
Escape: cancel,
}
const action = actions[event.key]
if (action) {
event.preventDefault()
event.stopPropagation()
action()
}
}
return { shown, grabbed, active, announcement, onKeydown }
}