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:
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { demos } from './router';
|
||||
|
||||
const route = useRoute();
|
||||
const query = ref('');
|
||||
|
||||
const filtered = computed(() => {
|
||||
const q = query.value.trim().toLowerCase();
|
||||
return q ? demos.filter(d => d.name.toLowerCase().includes(q)) : demos;
|
||||
});
|
||||
|
||||
const currentDemoName = computed(() => {
|
||||
const n = route.meta.demoName;
|
||||
return typeof n === 'string' ? n : '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid h-screen grid-cols-[15rem_1fr]">
|
||||
<aside class="flex min-h-0 flex-col border-r border-neutral-200 bg-white dark:border-neutral-800 dark:bg-neutral-900">
|
||||
<header class="border-b border-neutral-200 px-4 py-3.5 font-semibold dark:border-neutral-800">
|
||||
<RouterLink to="/" class="no-underline">
|
||||
@robonen/primitives
|
||||
</RouterLink>
|
||||
<small class="mt-0.5 block font-normal text-neutral-500 dark:text-neutral-400">playground</small>
|
||||
</header>
|
||||
|
||||
<input
|
||||
v-model="query"
|
||||
type="search"
|
||||
placeholder="Filter demos…"
|
||||
aria-label="Filter demos"
|
||||
class="mx-3 my-2.5 rounded-md border border-neutral-200 bg-neutral-50 px-2 py-1.5 outline-none focus:border-blue-600 dark:border-neutral-800 dark:bg-neutral-950 dark:focus:border-blue-400"
|
||||
>
|
||||
|
||||
<nav class="overflow-y-auto px-1.5 pb-3 pt-1">
|
||||
<div class="px-2.5 pb-1 pt-3 text-[0.6875rem] uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
|
||||
Demos ({{ filtered.length }})
|
||||
</div>
|
||||
<RouterLink
|
||||
v-for="demo in filtered"
|
||||
:key="demo.name"
|
||||
:to="demo.routePath"
|
||||
custom
|
||||
>
|
||||
<template #default="{ href, navigate, isActive }">
|
||||
<a
|
||||
:href="href"
|
||||
:aria-current="isActive ? 'page' : undefined"
|
||||
class="block rounded-md px-2.5 py-1.5 no-underline hover:bg-neutral-900/5 aria-[current=page]:bg-blue-600 aria-[current=page]:text-white dark:hover:bg-neutral-100/5 dark:aria-[current=page]:bg-blue-500"
|
||||
@click="navigate"
|
||||
>
|
||||
{{ demo.name }}
|
||||
</a>
|
||||
</template>
|
||||
</RouterLink>
|
||||
|
||||
<div v-if="!demos.length" class="px-2.5 pb-1 pt-3 text-[0.6875rem] uppercase tracking-wider text-neutral-500 dark:text-neutral-400">
|
||||
No demos yet
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main class="min-w-0 overflow-auto px-7 py-6">
|
||||
<header
|
||||
v-if="currentDemoName"
|
||||
class="mb-4 flex items-baseline gap-3 border-b border-neutral-200 pb-3 dark:border-neutral-800"
|
||||
>
|
||||
<h1 class="m-0 text-lg font-semibold">
|
||||
{{ currentDemoName }}
|
||||
</h1>
|
||||
<code class="text-xs text-neutral-500 dark:text-neutral-400">src/demos/{{ currentDemoName }}.vue</code>
|
||||
</header>
|
||||
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Suspense>
|
||||
<template #default>
|
||||
<component :is="Component" v-if="Component" />
|
||||
</template>
|
||||
<template #fallback>
|
||||
<div class="text-neutral-500 dark:text-neutral-400">
|
||||
Loading…
|
||||
</div>
|
||||
</template>
|
||||
</Suspense>
|
||||
</RouterView>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import {
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionRoot,
|
||||
AccordionTrigger,
|
||||
} from '@primitives/accordion';
|
||||
|
||||
const value = ref<string | string[] | undefined>('a');
|
||||
const type = ref<'single' | 'multiple'>('single');
|
||||
const collapsible = ref(true);
|
||||
const disabled = ref(false);
|
||||
|
||||
const items = [
|
||||
{ value: 'a', title: 'Item A', body: 'First panel content.' },
|
||||
{ value: 'b', title: 'Item B', body: 'Second panel content.' },
|
||||
{ value: 'c', title: 'Item C', body: 'Third panel content.' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="grid max-w-xl gap-4">
|
||||
<div class="flex flex-wrap items-center gap-3 rounded-lg border border-neutral-200 bg-white px-3 py-2.5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||
<label class="inline-flex items-center gap-2">
|
||||
type
|
||||
<select v-model="type" class="rounded border border-neutral-200 bg-neutral-50 px-1.5 py-0.5 dark:border-neutral-800 dark:bg-neutral-950">
|
||||
<option value="single">single</option>
|
||||
<option value="multiple">multiple</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1.5">
|
||||
<input v-model="collapsible" type="checkbox"> collapsible
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-1.5">
|
||||
<input v-model="disabled" type="checkbox"> disabled
|
||||
</label>
|
||||
<output class="ml-auto font-mono text-xs text-neutral-500 dark:text-neutral-400">value = {{ JSON.stringify(value) }}</output>
|
||||
</div>
|
||||
|
||||
<AccordionRoot
|
||||
v-model="value"
|
||||
class="grid gap-1"
|
||||
:type="type"
|
||||
:collapsible="collapsible"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<AccordionItem
|
||||
v-for="item in items"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
class="overflow-hidden rounded-md border border-neutral-200 dark:border-neutral-800"
|
||||
>
|
||||
<AccordionTrigger
|
||||
class="block w-full cursor-pointer bg-white px-3 py-2.5 text-left data-[state=open]:bg-neutral-900/5 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-blue-600 dark:bg-neutral-900 dark:data-[state=open]:bg-neutral-100/5 dark:focus-visible:outline-blue-400"
|
||||
>
|
||||
{{ item.title }}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent class="px-3 py-2.5 text-neutral-500 dark:text-neutral-400">
|
||||
{{ item.body }}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</AccordionRoot>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { CheckedState } from '@primitives/checkbox';
|
||||
import { CheckboxIndicator, CheckboxRoot } from '@primitives/checkbox';
|
||||
|
||||
const checked = ref<CheckedState>(false);
|
||||
const disabled = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="grid max-w-md gap-4">
|
||||
<div class="flex flex-wrap items-center gap-3 rounded-lg border border-neutral-200 bg-white px-3 py-2.5 dark:border-neutral-800 dark:bg-neutral-900">
|
||||
<label class="inline-flex items-center gap-1.5">
|
||||
<input v-model="disabled" type="checkbox"> disabled
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded border border-neutral-200 bg-neutral-50 px-2 py-1 text-xs hover:border-blue-600 dark:border-neutral-800 dark:bg-neutral-950 dark:hover:border-blue-400"
|
||||
@click="checked = 'indeterminate'"
|
||||
>
|
||||
set indeterminate
|
||||
</button>
|
||||
<output class="ml-auto font-mono text-xs text-neutral-500 dark:text-neutral-400">checked = {{ JSON.stringify(checked) }}</output>
|
||||
</div>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center gap-2.5">
|
||||
<CheckboxRoot
|
||||
v-model:checked="checked"
|
||||
:disabled="disabled"
|
||||
class="inline-grid h-5 w-5 place-items-center rounded border border-neutral-200 bg-white data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[state=checked]:border-blue-600 data-[state=checked]:bg-blue-600 data-[state=checked]:text-white data-[state=indeterminate]:border-blue-600 data-[state=indeterminate]:bg-blue-600 data-[state=indeterminate]:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-neutral-800 dark:bg-neutral-900 dark:data-[state=checked]:border-blue-500 dark:data-[state=checked]:bg-blue-500 dark:data-[state=indeterminate]:border-blue-500 dark:data-[state=indeterminate]:bg-blue-500 dark:focus-visible:outline-blue-400"
|
||||
>
|
||||
<CheckboxIndicator class="text-[0.8125rem] leading-none">
|
||||
<span v-if="checked === 'indeterminate'">–</span>
|
||||
<span v-else-if="checked">✓</span>
|
||||
</CheckboxIndicator>
|
||||
</CheckboxRoot>
|
||||
Accept terms and conditions
|
||||
</label>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import { router } from './router';
|
||||
import './styles.css';
|
||||
|
||||
const app = createApp(App).use(router);
|
||||
|
||||
app.config.performance = true;
|
||||
|
||||
app.mount('#app');
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Component } from 'vue';
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import HomeView from './views/Home.vue';
|
||||
import NotFoundView from './views/NotFound.vue';
|
||||
|
||||
// Eager paths, lazy components → each demo ships as its own chunk.
|
||||
const demoModules = import.meta.glob<{ default: Component }>('./demos/*.vue');
|
||||
|
||||
export interface DemoEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
routePath: string;
|
||||
}
|
||||
|
||||
export const demos: DemoEntry[] = Object.keys(demoModules)
|
||||
.map((path) => {
|
||||
const name = path.replace(/^.*\/demos\//, '').replace(/\.vue$/, '');
|
||||
return { name, path, routePath: `/demo/${encodeURIComponent(name)}` };
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const demoRoutes: RouteRecordRaw[] = demos.map(demo => ({
|
||||
path: demo.routePath,
|
||||
name: `demo:${demo.name}`,
|
||||
component: demoModules[demo.path]!,
|
||||
meta: { demoName: demo.name },
|
||||
}));
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', name: 'home', component: HomeView },
|
||||
...demoRoutes,
|
||||
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFoundView },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
@apply m-0 h-full bg-neutral-50 text-sm text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100;
|
||||
color-scheme: light dark;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { demos } from '../router';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="max-w-3xl">
|
||||
<h1 class="mb-2 mt-0 text-2xl font-semibold">
|
||||
@robonen/primitives playground
|
||||
</h1>
|
||||
<p class="text-neutral-500 dark:text-neutral-400">
|
||||
Pick a demo from the sidebar, or drop a new <code class="rounded border border-neutral-200 bg-white px-1.5 py-px text-xs dark:border-neutral-800 dark:bg-neutral-900">.vue</code> file into
|
||||
<code class="rounded border border-neutral-200 bg-white px-1.5 py-px text-xs dark:border-neutral-800 dark:bg-neutral-900">src/demos/</code> — it will be picked up automatically and become
|
||||
addressable at <code class="rounded border border-neutral-200 bg-white px-1.5 py-px text-xs dark:border-neutral-800 dark:bg-neutral-900">/demo/<FileName></code>.
|
||||
</p>
|
||||
|
||||
<div class="mt-4 grid gap-2 grid-cols-[repeat(auto-fill,minmax(11.25rem,1fr))]">
|
||||
<RouterLink
|
||||
v-for="demo in demos"
|
||||
:key="demo.name"
|
||||
:to="demo.routePath"
|
||||
class="grid gap-1 rounded-lg border border-neutral-200 bg-white p-3 no-underline hover:border-blue-600 dark:border-neutral-800 dark:bg-neutral-900 dark:hover:border-blue-400"
|
||||
>
|
||||
<strong>{{ demo.name }}</strong>
|
||||
<code class="text-neutral-500 dark:text-neutral-400">{{ demo.routePath }}</code>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<p v-if="!demos.length" class="text-neutral-500 dark:text-neutral-400">
|
||||
No demos found yet.
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="grid max-w-md gap-2">
|
||||
<h1 class="m-0 text-3xl font-semibold">
|
||||
404
|
||||
</h1>
|
||||
<p>
|
||||
No demo matches <code class="rounded border border-neutral-200 bg-white px-1.5 py-px dark:border-neutral-800 dark:bg-neutral-900">{{ route.fullPath }}</code>.
|
||||
</p>
|
||||
<RouterLink to="/" class="text-blue-600 hover:underline dark:text-blue-400">
|
||||
← Back to index
|
||||
</RouterLink>
|
||||
</section>
|
||||
</template>
|
||||
Reference in New Issue
Block a user