fix(primitives): eslint/tsconfig migration, asChild refactor, type fixes

- Migrate to eslint flat config + composite tsconfig.
- Complete the asChild→as="template" refactor (remove asChild prop + :as-child
  bindings across components, matching Primitive's slot model).
- Fix test type errors and source type-safety (useGraceArea hull/point math,
  FocusScope/util ref typing).

Note: ~53 vue-tsc errors remain (HTML attr/event passthrough typing on
transparent wrapper components + a couple of duplicate-export naming
collisions) — not gated by CI (build/lint/test green); pending a
component-attribute-typing design decision.
This commit is contained in:
2026-06-07 16:29:56 +07:00
parent c7644ade69
commit 626fbc70d8
408 changed files with 27367 additions and 154 deletions
@@ -0,0 +1,133 @@
<script lang="ts">
const DIGIT_RE = /\d/;
const NON_DIGIT_G = /\D/g;
</script>
<script setup lang="ts">
import { computed, onBeforeUnmount, useTemplateRef, watch } from 'vue';
import { usePinInputContext } from './context';
interface Props {
index: number;
}
const props = defineProps<Props>();
const ctx = usePinInputContext();
const el = useTemplateRef<HTMLInputElement>('el');
watch(el, (curr, prev) => {
if (prev)
ctx.unregister(prev);
if (curr)
ctx.register(curr);
});
onBeforeUnmount(() => {
if (el.value)
ctx.unregister(el.value);
});
const displayed = computed(() => {
const ch = ctx.value.value[props.index] ?? '';
if (!ch)
return '';
return ctx.mask.value ? '•' : ch;
});
// `inputType` / `inputMode` were thin ternary wrappers — inline in template.
function onInput(e: Event): void {
const target = e.target as HTMLInputElement;
const raw = target.value;
// keep only the last typed character
let ch = raw.length > 0 ? raw[raw.length - 1]! : '';
if (ctx.type.value === 'number' && ch && !DIGIT_RE.test(ch))
ch = '';
ctx.setAt(props.index, ch);
// re-sync DOM input since we overwrite with displayed
target.value = ch ? (ctx.mask.value ? '•' : ch) : '';
if (ch && props.index < ctx.length.value - 1)
ctx.focusIndex(props.index + 1);
}
function onKeyDown(e: KeyboardEvent): void {
const i = props.index;
const n = ctx.length.value;
switch (e.key) {
case 'Backspace': {
const current = ctx.value.value[i] ?? '';
if (current) {
ctx.clearAt(i);
}
else if (i > 0) {
ctx.focusIndex(i - 1);
ctx.clearAt(i - 1);
}
e.preventDefault();
break;
}
case 'Delete': {
ctx.clearAt(i);
e.preventDefault();
break;
}
case 'ArrowLeft': {
if (i > 0)
ctx.focusIndex(i - 1);
e.preventDefault();
break;
}
case 'ArrowRight': {
if (i < n - 1)
ctx.focusIndex(i + 1);
e.preventDefault();
break;
}
case 'Home': {
ctx.focusIndex(0);
e.preventDefault();
break;
}
case 'End': {
ctx.focusIndex(n - 1);
e.preventDefault();
break;
}
}
}
function onPaste(e: ClipboardEvent): void {
const data = e.clipboardData?.getData('text') ?? '';
if (!data)
return;
e.preventDefault();
const chars = ctx.type.value === 'number'
? data.replace(NON_DIGIT_G, '').split('')
: data.split('');
let idx = props.index;
for (const ch of chars) {
if (idx >= ctx.length.value)
break;
ctx.setAt(idx, ch);
idx++;
}
ctx.focusIndex(Math.min(idx, ctx.length.value - 1));
}
</script>
<template>
<input
ref="el"
:type="ctx.mask.value ? 'password' : 'text'"
:inputmode="ctx.type.value === 'number' ? 'numeric' : 'text'"
:value="displayed"
:placeholder="ctx.placeholder.value"
:disabled="ctx.disabled.value"
:autocomplete="ctx.otp.value ? 'one-time-code' : 'off'"
:data-index="props.index"
maxlength="1"
@input="onInput"
@keydown="onKeyDown"
@paste="onPaste"
>
</template>