Add Vue linting rules and fix all violations

Enable 12 vue/ rules in oxlint: lifecycle, props, emits, imports.
Convert 9 components from `const props = defineProps()` to
destructured form (Vue 3.5+ reactive destructuring).
This commit is contained in:
Danila Poyarkov 2026-03-08 23:08:27 +03:00
parent 7a334268d0
commit 9c110bff92
10 changed files with 103 additions and 102 deletions

View file

@ -1,6 +1,6 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn"],
"plugins": ["typescript", "import", "unicorn", "vue"],
"env": {
"browser": true,
"es2024": true
@ -17,7 +17,20 @@
"import/no-self-import": "error",
"unicorn/no-instanceof-array": "error",
"unicorn/no-typeof-undefined": "error"
"unicorn/no-typeof-undefined": "error",
"vue/no-arrow-functions-in-watch": "error",
"vue/no-deprecated-destroyed-lifecycle": "error",
"vue/no-export-in-script-setup": "error",
"vue/no-lifecycle-after-await": "error",
"vue/no-import-compiler-macros": "error",
"vue/prefer-import-from-vue": "error",
"vue/valid-define-emits": "error",
"vue/valid-define-props": "error",
"vue/no-required-prop-with-default": "error",
"vue/define-emits-declaration": "error",
"vue/define-props-destructuring": "error",
"vue/require-typed-ref": "error"
},
"overrides": [
{

View file

@ -13,7 +13,7 @@ import {
import { selectContent, selectItem, selectTrigger } from '@/components/ui/select'
defineProps<{
const { options, placeholder } = defineProps<{
options: { value: T; label: string }[]
placeholder?: string
}>()

View file

@ -4,13 +4,10 @@ import { colorToHexRaw, parseColor } from '@open-pencil/core'
import type { Color } from '@/types'
const props = withDefaults(
defineProps<{
color: Color
editable?: boolean
}>(),
{ editable: false }
)
const { color, editable = false } = defineProps<{
color: Color
editable?: boolean
}>()
const emit = defineEmits<{
update: [color: Color]
@ -18,8 +15,8 @@ const emit = defineEmits<{
function onHexChange(e: Event) {
const hex = (e.target as HTMLInputElement).value
const color = parseColor(hex.startsWith('#') ? hex : `#${hex}`)
emit('update', { ...color, a: props.color.a })
const parsed = parseColor(hex.startsWith('#') ? hex : `#${hex}`)
emit('update', { ...parsed, a: color.a })
}
</script>

View file

@ -7,7 +7,7 @@ import HsvColorArea from './HsvColorArea.vue'
import type { Color } from '@/types'
const props = defineProps<{
const { color } = defineProps<{
color: Color
}>()
@ -15,7 +15,7 @@ const emit = defineEmits<{
update: [color: Color]
}>()
const swatchColor = computed(() => colorToCSS(props.color))
const swatchColor = computed(() => colorToCSS(color))
</script>
<template>

View file

@ -44,7 +44,7 @@ const DEFAULT_GRADIENT_TRANSFORMS: Record<GradientSubtype, GradientTransform> =
GRADIENT_DIAMOND: { m00: 0.5, m01: 0, m02: 0.5, m10: 0, m11: 0.5, m12: 0.5 }
}
const props = defineProps<{
const { fill } = defineProps<{
fill: Fill
}>()
@ -55,68 +55,68 @@ const emit = defineEmits<{
const activeStopIndex = ref(0)
const fillCategory = computed<FillCategory>(() => {
if (props.fill.type.startsWith('GRADIENT')) return 'GRADIENT'
if (props.fill.type === 'IMAGE') return 'IMAGE'
if (fill.type.startsWith('GRADIENT')) return 'GRADIENT'
if (fill.type === 'IMAGE') return 'IMAGE'
return 'SOLID'
})
const isGradient = computed(() => fillCategory.value === 'GRADIENT')
const gradientSubtype = computed(() =>
isGradient.value ? (props.fill.type as GradientSubtype) : 'GRADIENT_LINEAR'
isGradient.value ? (fill.type as GradientSubtype) : 'GRADIENT_LINEAR'
)
const activeColor = computed(() => {
if (isGradient.value && props.fill.gradientStops?.length) {
const idx = Math.min(activeStopIndex.value, props.fill.gradientStops.length - 1)
return props.fill.gradientStops[idx].color
if (isGradient.value && fill.gradientStops?.length) {
const idx = Math.min(activeStopIndex.value, fill.gradientStops.length - 1)
return fill.gradientStops[idx].color
}
return props.fill.color
return fill.color
})
function onColorUpdate(color: Color) {
if (isGradient.value && props.fill.gradientStops?.length) {
const stops = [...props.fill.gradientStops]
if (isGradient.value && fill.gradientStops?.length) {
const stops = [...fill.gradientStops]
const idx = Math.min(activeStopIndex.value, stops.length - 1)
stops[idx] = { ...stops[idx], color }
emit('update', { ...props.fill, gradientStops: stops })
emit('update', { ...fill, gradientStops: stops })
} else {
emit('update', { ...props.fill, color })
emit('update', { ...fill, color })
}
}
function setCategory(cat: FillCategory) {
if (cat === fillCategory.value) return
if (cat === 'SOLID') {
const color = props.fill.gradientStops?.length
? { ...props.fill.gradientStops[0].color }
: props.fill.color
emit('update', { ...props.fill, type: 'SOLID', color })
const color = fill.gradientStops?.length
? { ...fill.gradientStops[0].color }
: fill.color
emit('update', { ...fill, type: 'SOLID', color })
} else if (cat === 'GRADIENT') {
const type: GradientSubtype = 'GRADIENT_LINEAR'
const stops = props.fill.gradientStops?.length
? props.fill.gradientStops
const stops = fill.gradientStops?.length
? fill.gradientStops
: [
{ color: { ...props.fill.color }, position: 0 },
{ color: { ...fill.color }, position: 0 },
{ color: { r: 1, g: 1, b: 1, a: 1 }, position: 1 }
]
emit('update', {
...props.fill,
...fill,
type,
gradientStops: stops,
gradientTransform: DEFAULT_GRADIENT_TRANSFORMS[type]
})
activeStopIndex.value = 0
} else {
emit('update', { ...props.fill, type: 'IMAGE' })
emit('update', { ...fill, type: 'IMAGE' })
}
}
function setGradientSubtype(type: string) {
const subtype = type as GradientSubtype
if (subtype === props.fill.type) return
if (subtype === fill.type) return
emit('update', {
...props.fill,
...fill,
type: subtype,
gradientTransform: DEFAULT_GRADIENT_TRANSFORMS[subtype]
})
@ -127,8 +127,8 @@ function selectStop(index: number) {
}
function addStop() {
if (!props.fill.gradientStops) return
const stops = [...props.fill.gradientStops]
if (!fill.gradientStops) return
const stops = [...fill.gradientStops]
const newPos =
stops.length >= 2
? (stops[stops.length - 2].position + stops[stops.length - 1].position) / 2
@ -137,39 +137,39 @@ function addStop() {
stops.sort((a, b) => a.position - b.position)
const newIndex = stops.findIndex((s) => s.position === newPos)
activeStopIndex.value = newIndex
emit('update', { ...props.fill, gradientStops: stops })
emit('update', { ...fill, gradientStops: stops })
}
function removeStop(index: number) {
if (!props.fill.gradientStops || props.fill.gradientStops.length <= 2) return
const stops = props.fill.gradientStops.filter((_, i) => i !== index)
if (!fill.gradientStops || fill.gradientStops.length <= 2) return
const stops = fill.gradientStops.filter((_, i) => i !== index)
activeStopIndex.value = Math.min(activeStopIndex.value, stops.length - 1)
emit('update', { ...props.fill, gradientStops: stops })
emit('update', { ...fill, gradientStops: stops })
}
function updateStopPosition(index: number, value: string) {
if (!props.fill.gradientStops) return
if (!fill.gradientStops) return
const pos = Math.max(0, Math.min(100, Number(value))) / 100
const stops = [...props.fill.gradientStops]
const stops = [...fill.gradientStops]
stops[index] = { ...stops[index], position: pos }
emit('update', { ...props.fill, gradientStops: stops })
emit('update', { ...fill, gradientStops: stops })
}
function updateStopColor(index: number, hex: string) {
if (!props.fill.gradientStops) return
if (!fill.gradientStops) return
const color = parseColor(hex.startsWith('#') ? hex : `#${hex}`)
if (!color) return
const stops = [...props.fill.gradientStops]
const stops = [...fill.gradientStops]
stops[index] = { ...stops[index], color: { ...color, a: stops[index].color.a } }
emit('update', { ...props.fill, gradientStops: stops })
emit('update', { ...fill, gradientStops: stops })
}
function updateStopOpacity(index: number, value: string) {
if (!props.fill.gradientStops) return
if (!fill.gradientStops) return
const a = Math.max(0, Math.min(100, Number(value))) / 100
const stops = [...props.fill.gradientStops]
const stops = [...fill.gradientStops]
stops[index] = { ...stops[index], color: { ...stops[index].color, a } }
emit('update', { ...props.fill, gradientStops: stops })
emit('update', { ...fill, gradientStops: stops })
}
function gradientStops(stops: GradientStop[]): string {
@ -177,15 +177,15 @@ function gradientStops(stops: GradientStop[]): string {
}
const swatchBackground = computed(() => {
if (isGradient.value && props.fill.gradientStops?.length) {
return `linear-gradient(to right, ${gradientStops(props.fill.gradientStops)})`
if (isGradient.value && fill.gradientStops?.length) {
return `linear-gradient(to right, ${gradientStops(fill.gradientStops)})`
}
return colorToCSS(props.fill.color)
return colorToCSS(fill.color)
})
const gradientBarBackground = computed(() => {
if (!props.fill.gradientStops?.length) return ''
return `linear-gradient(to right, ${gradientStops(props.fill.gradientStops)})`
if (!fill.gradientStops?.length) return ''
return `linear-gradient(to right, ${gradientStops(fill.gradientStops)})`
})
const gradientStopBarRef = ref<HTMLDivElement | null>(null)
@ -203,9 +203,9 @@ function onStopBarPointerMove(e: PointerEvent) {
if (!el || draggingStopIndex.value === null || !el.hasPointerCapture(e.pointerId)) return
const rect = el.getBoundingClientRect()
const pos = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
const stops = [...(props.fill.gradientStops ?? [])]
const stops = [...(fill.gradientStops ?? [])]
stops[draggingStopIndex.value] = { ...stops[draggingStopIndex.value], position: pos }
emit('update', { ...props.fill, gradientStops: stops })
emit('update', { ...fill, gradientStops: stops })
}
function onStopBarPointerUp() {

View file

@ -18,7 +18,7 @@ import { colorToHex8, rgba255ToColor } from '@open-pencil/core'
import type { Color } from '@/types'
const props = defineProps<{
const { color } = defineProps<{
color: Color
}>()
@ -26,7 +26,7 @@ const emit = defineEmits<{
update: [color: Color]
}>()
const hexWithAlpha = computed(() => colorToHex8(props.color))
const hexWithAlpha = computed(() => colorToHex8(color))
const rekaColor = computed(() => normalizeColor(hexWithAlpha.value))

View file

@ -28,7 +28,7 @@ import { initials } from '@/utils/text'
import type { Component } from 'vue'
import type { CollabState, RemotePeer } from '@/composables/use-collab'
const props = defineProps<{
const { collabState, collabPeers, pendingRoomId, followingPeer } = defineProps<{
collabState: CollabState
collabPeers: RemotePeer[]
pendingRoomId?: string | null
@ -69,7 +69,7 @@ const menuItems: MenuAction[] = [
{ icon: IconZoomIn, label: 'Zoom to fit', action: () => store.zoomToFit() }
]
const onlineCount = computed(() => props.collabPeers.length + 1)
const onlineCount = computed(() => collabPeers.length + 1)
</script>
<template>
@ -115,7 +115,7 @@ const onlineCount = computed(() => props.collabPeers.length + 1)
<!-- Center: Online badge + action toast -->
<div class="pointer-events-auto relative mx-auto flex flex-col items-center gap-1.5">
<!-- Online badge with peers popover -->
<PopoverRoot v-if="props.collabState.connected">
<PopoverRoot v-if="collabState.connected">
<PopoverTrigger as-child>
<button
class="flex h-8 cursor-pointer items-center gap-1.5 rounded-full border border-white/10 bg-panel/70 px-3 shadow-md backdrop-blur-xl select-none active:bg-hover"
@ -137,33 +137,33 @@ const onlineCount = computed(() => props.collabPeers.length + 1)
<div class="flex items-center gap-2">
<div
class="flex size-7 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white"
:style="{ background: colorToCSS(props.collabState.localColor) }"
:style="{ background: colorToCSS(collabState.localColor) }"
>
{{ initials(props.collabState.localName || 'You') }}
{{ initials(collabState.localName || 'You') }}
</div>
<span class="min-w-0 flex-1 truncate text-xs text-surface">
{{ props.collabState.localName || 'You' }}
{{ collabState.localName || 'You' }}
</span>
<span class="text-[10px] text-muted">you</span>
</div>
<div
v-for="peer in props.collabPeers"
v-for="peer in collabPeers"
:key="peer.clientId"
class="flex cursor-pointer items-center gap-2 rounded-md px-0.5 py-0.5 select-none active:bg-hover"
@click="
emit('follow', props.followingPeer === peer.clientId ? null : peer.clientId)
emit('follow', followingPeer === peer.clientId ? null : peer.clientId)
"
>
<div
class="flex size-7 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white"
:class="props.followingPeer === peer.clientId ? 'ring-2 ring-white/40' : ''"
:class="followingPeer === peer.clientId ? 'ring-2 ring-white/40' : ''"
:style="{ background: colorToCSS(peer.color) }"
>
{{ initials(peer.name) }}
</div>
<span class="min-w-0 flex-1 truncate text-xs text-surface">{{ peer.name }}</span>
<span v-if="props.followingPeer === peer.clientId" class="text-[10px] text-accent"
<span v-if="followingPeer === peer.clientId" class="text-[10px] text-accent"
>following</span
>
</div>

View file

@ -2,28 +2,19 @@
import { ref, computed } from 'vue'
import { useEventListener } from '@vueuse/core'
const props = withDefaults(
defineProps<{
modelValue: number | symbol
min?: number
max?: number
step?: number
icon?: string
label?: string
suffix?: string
sensitivity?: number
placeholder?: string
}>(),
{
min: -Infinity,
max: Infinity,
step: 1,
sensitivity: 1,
placeholder: 'Mixed'
}
)
const { modelValue, min = -Infinity, max = Infinity, step = 1, icon, label, suffix, sensitivity = 1, placeholder = 'Mixed' } = defineProps<{
modelValue: number | symbol
min?: number
max?: number
step?: number
icon?: string
label?: string
suffix?: string
sensitivity?: number
placeholder?: string
}>()
const isMixed = computed(() => typeof props.modelValue === 'symbol')
const isMixed = computed(() => typeof modelValue === 'symbol')
const emit = defineEmits<{
'update:modelValue': [value: number]
@ -37,7 +28,7 @@ const scrubbing = ref(false)
let stopMove: (() => void) | undefined
let stopUp: (() => void) | undefined
const numericValue = computed(() => (isMixed.value ? 0 : (props.modelValue as number)))
const numericValue = computed(() => (isMixed.value ? 0 : (modelValue as number)))
const displayValue = computed(() => (isMixed.value ? '' : Math.round(numericValue.value)))
function startScrub(e: PointerEvent) {
@ -57,9 +48,9 @@ function startScrub(e: PointerEvent) {
document.body.style.cursor = 'ew-resize'
}
if (hasMoved) {
accumulated += dx * props.step * props.sensitivity
const clamped = Math.round(Math.min(props.max, Math.max(props.min, accumulated)))
if (clamped !== props.modelValue) {
accumulated += dx * step * sensitivity
const clamped = Math.round(Math.min(max, Math.max(min, accumulated)))
if (clamped !== modelValue) {
emit('update:modelValue', clamped)
}
}
@ -71,8 +62,8 @@ function startScrub(e: PointerEvent) {
stopMove?.()
stopUp?.()
if (hasMoved) {
if (props.modelValue !== valueBeforeScrub) {
emit('commit', props.modelValue, valueBeforeScrub)
if (modelValue !== valueBeforeScrub) {
emit('commit', modelValue, valueBeforeScrub)
}
} else {
startEdit()
@ -91,7 +82,7 @@ function commitEdit(e: Event) {
const val = +(e.target as HTMLInputElement).value
const previous = numericValue.value
if (!Number.isNaN(val)) {
const clamped = Math.min(props.max, Math.max(props.min, val))
const clamped = Math.min(max, Math.max(min, val))
emit('update:modelValue', clamped)
if (clamped !== previous) {
emit('commit', clamped, previous)

View file

@ -23,7 +23,7 @@ import { useAIChat } from '@/composables/use-chat'
const { providerID, providerDef, modelID, customModelID } = useAIChat()
const props = defineProps<{
const { status } = defineProps<{
status: 'ready' | 'submitted' | 'streaming' | 'error'
}>()
@ -34,7 +34,7 @@ const emit = defineEmits<{
const input = ref('')
const isStreaming = computed(() => props.status === 'streaming' || props.status === 'submitted')
const isStreaming = computed(() => status === 'streaming' || status === 'submitted')
const isCustomProvider = computed(() => providerID.value === 'openai-compatible')
const selectedModelName = computed(() => {

View file

@ -14,7 +14,7 @@ import { AI_PROVIDERS, useAIChat } from '@/composables/use-chat'
const { providerID, providerDef } = useAIChat()
defineProps<{
const { triggerClass, itemClass, testId } = defineProps<{
triggerClass?: string
itemClass?: string
testId?: string