From 4b49d69601b63ec94bef2e2013b4cd8f7b42692c Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 16 Mar 2026 22:21:21 +0300 Subject: [PATCH] Fix all 135 vue-tsc errors, promote to check gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit vue-tsc now runs as part of 'bun run check' — zero errors. Fixes by category: - TS18047 (73): null guards in LayoutSection, TypographySection - TS18048 (17): collab injection guards in CollabPanel, MobileHud - TS2339 (12): Uint8Array.fromBase64/toBase64 declarations (ES2024) - TS2741 (10): AppMenu discriminated union for separators - TS6133 (7): remove unused declarations - TS2322 (6): FontPicker AcceptableValue, Toolbar motion-v variants - TS2345 (3): FillSection updateArrayItem signature, ScrubInput guard - TS2314 (2): ChatMessage UIMessagePart generic args - Misc (5): PropertiesPanel dead comparison, VariablesDialog onSubmit, NodeContextMenuContent document access, LayerTree arg count --- bun.lock | 14 +- package.json | 9 +- packages/core/src/global.d.ts | 8 + .../vue/src/composables/use-font-status.ts | 4 +- .../vue/src/composables/use-node-props.ts | 4 +- src/components/AppMenu.vue | 26 +- src/components/CollabPanel.vue | 16 +- src/components/FillPicker.vue | 2 +- src/components/FontPicker.vue | 7 +- src/components/LayerTree.vue | 4 +- src/components/MobileHud.vue | 22 +- src/components/NodeContextMenuContent.vue | 14 +- src/components/ScrubInput.vue | 2 +- src/components/Toolbar.vue | 4 +- src/components/VariablesDialog.vue | 4 +- src/components/chat/ChatMessage.vue | 6 +- .../properties/AppearanceSection.vue | 2 +- src/components/properties/FillSection.vue | 3 +- src/components/properties/LayoutSection.vue | 641 +++++++++--------- src/components/properties/StrokeSection.vue | 10 - .../properties/TypographySection.vue | 12 +- src/composables/use-chat.ts | 2 +- src/composables/use-collab.ts | 8 + src/global.d.ts | 8 + 24 files changed, 436 insertions(+), 396 deletions(-) diff --git a/bun.lock b/bun.lock index ea2e5ec8c..400d83a2d 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ "@tauri-apps/plugin-shell": "^2.3.5", "@unhead/vue": "^2.1.10", "@vueuse/core": "^14.2.1", - "ai": "^6.0.105", + "ai": "^6.0.116", "canvaskit-wasm": "^0.40.0", "culori": "^4.0.2", "dedent": "^1.7.1", @@ -162,7 +162,7 @@ "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.59", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.16", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MbtheWHgEFV/8HL1Z6E3hOAsmP73zZlNFg0F0nJAD0Adnjp4J/plqNK00Y896d+dWTw+r0OXzyov9/2wCFjH0Q=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="], "@ai-sdk/google": ["@ai-sdk/google@3.0.43", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-NGCgP5g8HBxrNdxvF8Dhww+UKfqAkZAmyYBvbu9YLoBkzAmGKDBGhVptN/oXPB5Vm0jggMdoLycZ8JReQM8Zqg=="], @@ -1156,7 +1156,7 @@ "agentfmt": ["agentfmt@0.1.3", "", { "dependencies": { "picocolors": "^1.1.1" } }, "sha512-BVSZr8ovigxjYewuBx42Ep4RJj00HURaQYUQ+id41sgb7r0PMUSEWsnD/+h96xj6t6v2WVyPuGPH0K6RtjftKQ=="], - "ai": ["ai@6.0.105", "", { "dependencies": { "@ai-sdk/gateway": "3.0.59", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.16", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rp+exWtZS3J0DDvZIfetpKCIg7D3cCsvBPoFN3I67IDTs9aoBZDbpecoIkmNLT+U9RBkoEial3OGHRvme23HCw=="], + "ai": ["ai@6.0.116", "", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="], "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], @@ -2536,14 +2536,14 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.16", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kBvDqNkt5EwlzF9FujmNhhtl8FYg3e8FO8P5uneKliqfRThWemzBj+wfYr7ZCymAQhTRnwSSz1/SOqhOAwmx9g=="], - "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.16", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kBvDqNkt5EwlzF9FujmNhhtl8FYg3e8FO8P5uneKliqfRThWemzBj+wfYr7ZCymAQhTRnwSSz1/SOqhOAwmx9g=="], "@ai-sdk/valibot/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.16", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kBvDqNkt5EwlzF9FujmNhhtl8FYg3e8FO8P5uneKliqfRThWemzBj+wfYr7ZCymAQhTRnwSSz1/SOqhOAwmx9g=="], "@ai-sdk/vue/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.16", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kBvDqNkt5EwlzF9FujmNhhtl8FYg3e8FO8P5uneKliqfRThWemzBj+wfYr7ZCymAQhTRnwSSz1/SOqhOAwmx9g=="], + "@ai-sdk/vue/ai": ["ai@6.0.105", "", { "dependencies": { "@ai-sdk/gateway": "3.0.59", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.16", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rp+exWtZS3J0DDvZIfetpKCIg7D3cCsvBPoFN3I67IDTs9aoBZDbpecoIkmNLT+U9RBkoEial3OGHRvme23HCw=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -2618,8 +2618,6 @@ "@waku/utils/chai": ["chai@4.5.0", "", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="], - "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.16", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kBvDqNkt5EwlzF9FujmNhhtl8FYg3e8FO8P5uneKliqfRThWemzBj+wfYr7ZCymAQhTRnwSSz1/SOqhOAwmx9g=="], - "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "c12/rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], @@ -2682,6 +2680,8 @@ "workbox-build/pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], + "@ai-sdk/vue/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.59", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.16", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MbtheWHgEFV/8HL1Z6E3hOAsmP73zZlNFg0F0nJAD0Adnjp4J/plqNK00Y896d+dWTw+r0OXzyov9/2wCFjH0Q=="], + "@libp2p/crypto/@libp2p/interface/@multiformats/multiaddr": ["@multiformats/multiaddr@13.0.1", "", { "dependencies": { "@chainsafe/is-ip": "^2.0.1", "multiformats": "^13.0.0", "uint8-varint": "^2.0.1", "uint8arrays": "^5.0.0" } }, "sha512-XToN915cnfr6Lr9EdGWakGJbPT0ghpg/850HvdC+zFX8XvpLZElwa8synCiwa8TuvKNnny6m8j8NVBNCxhIO3g=="], "@multiformats/dns/@libp2p/interface/@multiformats/multiaddr": ["@multiformats/multiaddr@13.0.1", "", { "dependencies": { "@chainsafe/is-ip": "^2.0.1", "multiformats": "^13.0.0", "uint8-varint": "^2.0.1", "uint8arrays": "^5.0.0" } }, "sha512-XToN915cnfr6Lr9EdGWakGJbPT0ghpg/850HvdC+zFX8XvpLZElwa8synCiwa8TuvKNnny6m8j8NVBNCxhIO3g=="], diff --git a/package.json b/package.json index d5d742400..f03ceb06c 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,7 @@ "tauri": "tauri", "lint": "oxlint -c oxlint.json --type-aware --type-check src/ packages/core/src/ packages/cli/src/ packages/mcp/src/", "format": "oxfmt --write src/", - "check": "bun run lint && tsgo --noEmit", - "check:vue": "vue-tsc --noEmit", + "check": "bun run lint && tsgo --noEmit && vue-tsc --noEmit", "test": "playwright test --project=openpencil", "test:update": "playwright test --project=openpencil --update-snapshots", "test:figma": "playwright test --project=figma", @@ -48,7 +47,7 @@ "@tauri-apps/plugin-shell": "^2.3.5", "@unhead/vue": "^2.1.10", "@vueuse/core": "^14.2.1", - "ai": "^6.0.105", + "ai": "^6.0.116", "canvaskit-wasm": "^0.40.0", "culori": "^4.0.2", "dedent": "^1.7.1", @@ -65,12 +64,12 @@ "valibot": "^1.2.0", "vue": "^3.5.29", "vue-router": "^5.0.3", + "vue-stream-markdown": "^0.6.5", "y-indexeddb": "^9.0.12", "y-protocols": "^1.0.7", "yjs": "^13.6.29", "yoga-layout": "npm:@open-pencil/yoga-layout@3.3.0-grid.2", - "zod": "^4.3.6", - "vue-stream-markdown": "^0.6.5" + "zod": "^4.3.6" }, "devDependencies": { "@agentclientprotocol/sdk": "^0.14.1", diff --git a/packages/core/src/global.d.ts b/packages/core/src/global.d.ts index 587425001..4ba8b5ced 100644 --- a/packages/core/src/global.d.ts +++ b/packages/core/src/global.d.ts @@ -9,3 +9,11 @@ interface FontData { interface Window { queryLocalFonts?(): Promise } + +interface Uint8ArrayConstructor { + fromBase64(base64: string, options?: { alphabet?: 'base64' | 'base64url' }): Uint8Array +} + +interface Uint8Array { + toBase64(options?: { alphabet?: 'base64' | 'base64url' }): string +} diff --git a/packages/vue/src/composables/use-font-status.ts b/packages/vue/src/composables/use-font-status.ts index 2d2c90f98..5d4a58ef9 100644 --- a/packages/vue/src/composables/use-font-status.ts +++ b/packages/vue/src/composables/use-font-status.ts @@ -4,10 +4,10 @@ import { isFontLoaded, DEFAULT_FONT_FAMILY } from '@open-pencil/core' import type { SceneNode } from '@open-pencil/core' -export function useNodeFontStatus(node: () => SceneNode) { +export function useNodeFontStatus(node: () => SceneNode | null | undefined) { const missingFonts = computed(() => { const n = node() - if (n.type !== 'TEXT') return [] + if (!n || n.type !== 'TEXT') return [] const families = new Set() families.add(n.fontFamily || DEFAULT_FONT_FAMILY) diff --git a/packages/vue/src/composables/use-node-props.ts b/packages/vue/src/composables/use-node-props.ts index b68b8468c..fdbf86f3c 100644 --- a/packages/vue/src/composables/use-node-props.ts +++ b/packages/vue/src/composables/use-node-props.ts @@ -2,7 +2,7 @@ import { computed } from 'vue' import { useEditor } from '../context' -import type { SceneNode } from '@open-pencil/core' +import type { Fill, SceneNode, Stroke } from '@open-pencil/core' export const MIXED = Symbol('mixed') export type MixedValue = T | typeof MIXED @@ -52,7 +52,7 @@ export function useNodeProps() { function updateArrayItem( key: ArrayPropKey, index: number, - patch: Record, + patch: Record | Fill | Stroke, label: string ) { for (const n of targetNodes()) { diff --git a/src/components/AppMenu.vue b/src/components/AppMenu.vue index 0baafc2f4..d91839083 100644 --- a/src/components/AppMenu.vue +++ b/src/components/AppMenu.vue @@ -16,7 +16,7 @@ import { import IconChevronRight from '~icons/lucide/chevron-right' -import { computed, ref } from 'vue' +import { computed } from 'vue' import { useInlineRename } from '@/composables/use-inline-rename' import { menuContent, menuItem, menuSeparator } from '@/components/ui/menu' @@ -47,18 +47,24 @@ function commitRename(input: HTMLInputElement) { const isMac = navigator.platform.includes('Mac') const mod = isMac ? '⌘' : 'Ctrl+' -interface MenuItem { +interface MenuAction { + separator?: false label: string shortcut?: string action?: () => void - separator?: boolean disabled?: boolean checked?: boolean onCheckedChange?: (checked: boolean) => void - sub?: MenuItem[] + sub?: MenuEntry[] } -const fileMenu: MenuItem[] = [ +interface MenuSeparator { + separator: true +} + +type MenuEntry = MenuAction | MenuSeparator + +const fileMenu: MenuEntry[] = [ { label: 'New', shortcut: `${mod}N`, @@ -89,7 +95,7 @@ const fileMenu: MenuItem[] = [ } ] -const editMenu: MenuItem[] = [ +const editMenu: MenuEntry[] = [ { label: 'Undo', shortcut: `${mod}Z`, action: () => store.undoAction() }, { label: 'Redo', shortcut: `${mod}⇧Z`, action: () => store.redoAction() }, { separator: true }, @@ -101,7 +107,7 @@ const editMenu: MenuItem[] = [ { label: 'Select all', shortcut: `${mod}A`, action: () => store.selectAll() } ] -const viewMenu: MenuItem[] = [ +const viewMenu: MenuEntry[] = [ { label: 'Zoom to 100%', shortcut: `${mod}0`, action: () => store.zoomTo100() }, { label: 'Zoom to fit', shortcut: `${mod}1`, action: () => store.zoomToFit() }, { label: 'Zoom to selection', shortcut: `${mod}2`, action: () => store.zoomToSelection() }, @@ -127,7 +133,7 @@ const viewMenu: MenuItem[] = [ } ] -const objectMenu: MenuItem[] = [ +const objectMenu: MenuEntry[] = [ { label: 'Group', shortcut: `${mod}G`, action: () => store.groupSelected() }, { label: 'Ungroup', shortcut: `${mod}⇧G`, action: () => store.ungroupSelected() }, { separator: true }, @@ -146,13 +152,13 @@ const objectMenu: MenuItem[] = [ { label: 'Send to back', shortcut: '[', action: () => store.sendToBack() } ] -const textMenu: MenuItem[] = [ +const textMenu: MenuEntry[] = [ { label: 'Bold', shortcut: `${mod}B` }, { label: 'Italic', shortcut: `${mod}I` }, { label: 'Underline', shortcut: `${mod}U` } ] -const arrangeMenu: MenuItem[] = [ +const arrangeMenu: MenuEntry[] = [ { label: 'Add auto layout', shortcut: '⇧A', action: () => store.wrapInAutoLayout() }, { separator: true }, { label: 'Align left', shortcut: '⌥A' }, diff --git a/src/components/CollabPanel.vue b/src/components/CollabPanel.vue index 2502115ac..51c6a00eb 100644 --- a/src/components/CollabPanel.vue +++ b/src/components/CollabPanel.vue @@ -14,7 +14,7 @@ import { } from 'reka-ui' import { colorToCSS } from '@open-pencil/core' -import { useCollabInjected } from '@/composables/use-collab' +import { DEFAULT_COLLAB_STATE, useCollabInjected } from '@/composables/use-collab' import { toast } from '@/composables/use-toast' import { initials } from '@/utils/text' @@ -23,14 +23,14 @@ const router = useRouter() const collab = useCollabInjected() const joinInput = ref('') -const nameDraft = ref(collab.state.value.localName) +const nameDraft = ref(collab?.state.value.localName ?? '') const copied = ref(false) const pendingRoomId = (route.params.roomId as string) || null const popoverOpen = ref(!!pendingRoomId) -const state = computed(() => collab.state.value) -const peers = computed(() => collab.remotePeers.value) -const followingPeer = computed(() => collab.followingPeer.value) +const state = computed(() => collab?.state.value ?? DEFAULT_COLLAB_STATE) +const peers = computed(() => collab?.remotePeers.value ?? []) +const followingPeer = computed(() => collab?.followingPeer.value ?? null) const shareUrl = computed(() => { if (!state.value.roomId) return '' @@ -50,7 +50,7 @@ function copyLink() { } function onShare() { - if (!nameDraft.value.trim()) return + if (!collab || !nameDraft.value.trim()) return collab.setLocalName(nameDraft.value.trim()) const roomId = collab.shareCurrentDoc() router.push(`/share/${roomId}`) @@ -60,6 +60,7 @@ function onShare() { } function onJoin() { + if (!collab) return const roomId = pendingRoomId || joinInput.value.trim().replace(/.*\/share\//, '') if (!roomId || !nameDraft.value.trim()) return collab.setLocalName(nameDraft.value.trim()) @@ -69,6 +70,7 @@ function onJoin() { } function onDisconnect() { + if (!collab) return collab.disconnect() router.push('/') } @@ -110,7 +112,7 @@ function onDisconnect() { : 'border-panel' " :style="{ background: colorToCSS(peer.color) }" - @click="collab.followPeer(followingPeer === peer.clientId ? null : peer.clientId)" + @click="collab?.followPeer(followingPeer === peer.clientId ? null : peer.clientId)" > {{ initials(peer.name) }} diff --git a/src/components/FillPicker.vue b/src/components/FillPicker.vue index 17457d0b4..f45c9e91a 100644 --- a/src/components/FillPicker.vue +++ b/src/components/FillPicker.vue @@ -455,7 +455,7 @@ function setScaleMode(mode: string) {