Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
c77f0cb
[backport cloud/1.37] fix: version mismatch warning appearing in Play…
comfy-pr-bot Jan 14, 2026
5ec29f6
[backport cloud/1.37] linear v2: Simple Mode (#8047)
AustinMroz Jan 14, 2026
3c99e75
[backport cloud/1.37] [API Nodes] add price badges for Meshy 3D nodes…
comfy-pr-bot Jan 14, 2026
cd6047f
[backport cloud/1.37] Fix: Update for Image Widget test (#8051)
comfy-pr-bot Jan 14, 2026
d85c469
[backport cloud/1.37] feat(price-badges): add ByteDance SeeDance 1.5 …
comfy-pr-bot Jan 15, 2026
9132f87
[backport cloud/1.37] Linear mode bug fixes (#8072)
comfy-pr-bot Jan 15, 2026
a3cd630
[backport cloud/1.37] fix: prevent Record Audio waveform from overflo…
comfy-pr-bot Jan 15, 2026
40b0954
[backport cloud/1.37] Further linear fixes (#8084)
AustinMroz Jan 15, 2026
5a276f2
[backport cloud/1.37] Make sure toggle visibility checks remote confi…
AustinMroz Jan 16, 2026
ac6adb0
[backport cloud/1.37] Fix copypasted primitives inside subgraphs (#8096)
comfy-pr-bot Jan 16, 2026
3eb8c6a
[backport cloud/1.37] Improve linear compatibility with Safari, run b…
AustinMroz Jan 16, 2026
e036d76
[backport cloud/1.37] Fix asset selection in litegraph (#8119)
comfy-pr-bot Jan 17, 2026
a55cae5
[backport cloud/1.37] Update beta message in linear mode (#8109)
comfy-pr-bot Jan 17, 2026
05cbcce
[backport cloud/1.37] feat: make subgraphs blueprints appear higher i…
comfy-pr-bot Jan 18, 2026
995906a
[backport cloud/1.37] control widget fixes (#8163)
AustinMroz Jan 19, 2026
def9b55
Remove hamburger menu from tabs (#8067)
pythongosssss Jan 15, 2026
b7ddd50
[cloud/1.37] Regenerate expectations (#8198)
DrJKL Jan 21, 2026
f074243
[backport cloud/1.37] feat: When a list of strings is received, show …
comfy-pr-bot Jan 21, 2026
abb2b15
[backport cloud/1.37] feat: add per-tab workspace authentication infr…
comfy-pr-bot Jan 22, 2026
751253f
[backport cloud/1.37] feat: add maxColumns prop to VirtualGrid for re…
comfy-pr-bot Jan 22, 2026
14a2208
[backport cloud/1.37] feat(StatusBadge): add dot mode with CVA varian…
comfy-pr-bot Jan 22, 2026
06bc103
[backport cloud/1.37] feat(ui): add shadcn-vue Select components (#8234)
comfy-pr-bot Jan 22, 2026
a6da367
[backport cloud/1.37] feat(ui): add TagsInput component with click-to…
comfy-pr-bot Jan 22, 2026
32ce523
[backport cloud/1.37] feat: add isCloud guard to team workspaces feat…
simula-r Jan 22, 2026
b8a103b
[backport cloud/1.37] feat: add workspace session, auth, and store in…
simula-r Jan 22, 2026
7faf8e0
[backport cloud/1.37] fix: Consistent keydown handling for EditableTe…
DrJKL Jan 22, 2026
0c3d569
[backport cloud/1.37] feat: add badge support to NavItem component (#…
DrJKL Jan 22, 2026
5d94c11
[backport cloud/1.37] feat: add session download tracking to assetDow…
comfy-pr-bot Jan 22, 2026
2d0f3d6
[backport cloud/1.37] refactor: restructure BaseModalLayout from flex…
DrJKL Jan 22, 2026
eb7d0c7
[backport cloud/1.37] feat: implement progressive pagination for Asse…
DrJKL Jan 22, 2026
b98d53e
[backport cloud/1.37] feat(assets): add ModelInfoPanel for asset brow…
DrJKL Jan 22, 2026
88f7886
[backport cloud/1.37] Updates: Model Management (#8255)
comfy-pr-bot Jan 23, 2026
9db2fd8
[backport cloud/1.37] Workspaces 3 create a workspace (#8221) (#8252)
simula-r Jan 23, 2026
9b7f20c
[backport cloud/1.37] Add telemetry for entering linear mode (#8265)
comfy-pr-bot Jan 23, 2026
7a5fb57
[backport cloud/1.37] fix: use authenticated API for remote config po…
comfy-pr-bot Jan 24, 2026
12761f8
[backport cloud/1.37] Linear: progressbar, tooltips, and output fixes…
comfy-pr-bot Jan 24, 2026
4c3c61f
[backport cloud/1.37] [bugfix] Fix inconsistent menu icon sizes in Co…
comfy-pr-bot Jan 24, 2026
113a6a7
[backport cloud/1.37] fix: fallback to asset metadata/name when filen…
comfy-pr-bot Jan 25, 2026
745ea0a
[backport cloud/1.37] [refactor] Manager dialog simplification (#8306)
DrJKL Jan 25, 2026
ec91aa8
[backport cloud/1.37] Updates: More Modal Modification (#8308)
DrJKL Jan 25, 2026
3eb15bb
[backport cloud/1.37] feat: add getAssetFilename util with fallback c…
comfy-pr-bot Jan 25, 2026
3b7e102
[backport cloud/1.37] Add 3d control buttons to linear mode (#8289)
comfy-pr-bot Jan 25, 2026
1963f28
[backport cloud/1.37] Workspaces 4 members invites (#8301)
simula-r Jan 25, 2026
e82c692
[backport cloud/1.37] fix: workspace icon flash and credits showing 0…
comfy-pr-bot Jan 27, 2026
e751cf4
[backport cloud/1.37] perf: remove autoplay from assets cards (#8327)
comfy-pr-bot Jan 27, 2026
a5f50ac
[backport cloud/1.37] feat: add Hugging Face model source support (#8…
comfy-pr-bot Jan 27, 2026
4c2edae
[backport cloud/1.37] Fix dragging Vue nodes into canvas from library…
comfy-pr-bot Jan 28, 2026
8e5a037
[backport cloud/1.37] Feat/workspaces 5 auth gate check (#8357)
simula-r Jan 28, 2026
72add79
[backport cloud/1.37] fix: move WorkspaceAuthGate to LayoutDefault fo…
comfy-pr-bot Jan 29, 2026
2f9ede1
[backport cloud/1.37] Templates: Search speed (#8397)
comfy-pr-bot Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion browser_tests/tests/widget.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,10 @@ test.describe('Image widget', () => {
const comboEntry = comfyPage.page.getByRole('menuitem', {
name: 'image32x32.webp'
})
await comboEntry.click({ noWaitAfter: true })
await comboEntry.click()

// Stabilization for the image swap
await comfyPage.nextFrame()

// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/design-system/src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@
--inverted-background-hover: var(--color-charcoal-600);
--warning-background: var(--color-gold-400);
--warning-background-hover: var(--color-gold-500);
--success-background: var(--color-jade-600);
--border-default: var(--color-smoke-600);
--border-subtle: var(--color-smoke-400);
--muted-background: var(--color-smoke-700);
Expand Down Expand Up @@ -372,6 +373,7 @@
--inverted-background-hover: var(--color-smoke-200);
--warning-background: var(--color-gold-600);
--warning-background-hover: var(--color-gold-500);
--success-background: var(--color-jade-600);
--border-default: var(--color-charcoal-200);
--border-subtle: var(--color-charcoal-300);
--muted-background: var(--color-charcoal-100);
Expand Down Expand Up @@ -516,6 +518,7 @@
--color-inverted-background-hover: var(--inverted-background-hover);
--color-warning-background: var(--warning-background);
--color-warning-background-hover: var(--warning-background-hover);
--color-success-background: var(--success-background);
--color-border-default: var(--border-default);
--color-border-subtle: var(--border-subtle);
--color-muted-background: var(--muted-background);
Expand Down
28 changes: 28 additions & 0 deletions src/components/sidebar/ModeToggle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue'
import { t } from '@/i18n'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCommandStore } from '@/stores/commandStore'

const canvasStore = useCanvasStore()
</script>
<template>
<div class="p-1 bg-secondary-background rounded-lg w-10">
<Button
size="icon"
:title="t('linearMode.linearMode')"
:variant="canvasStore.linearMode ? 'inverted' : 'secondary'"
@click="useCommandStore().execute('Comfy.ToggleLinear')"
>
<i class="icon-[lucide--panels-top-left]" />
</Button>
<Button
size="icon"
:title="t('linearMode.graphMode')"
:variant="canvasStore.linearMode ? 'secondary' : 'inverted'"
@click="useCommandStore().execute('Comfy.ToggleLinear')"
>
<i class="icon-[comfy--workflow]" />
</Button>
Comment on lines +16 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add aria-label to icon-only buttons for accessibility.

These buttons have no visible text—only icons. Screen reader users need aria-label to understand the button's purpose. The tooltip alone doesn't provide accessible labeling.

♿ Proposed accessibility fix
     <Button
       v-tooltip="{
         value: t('linearMode.linearMode'),
         showDelay: 300,
         hideDelay: 300
       }"
       size="icon"
       :variant="canvasStore.linearMode ? 'inverted' : 'secondary'"
+      :aria-label="t('linearMode.linearMode')"
       `@click`="toggleLinearMode"
     >
       <i class="icon-[lucide--panels-top-left]" />
     </Button>
     <Button
       v-tooltip="{
         value: t('linearMode.graphMode'),
         showDelay: 300,
         hideDelay: 300
       }"
       size="icon"
       :variant="canvasStore.linearMode ? 'secondary' : 'inverted'"
+      :aria-label="t('linearMode.graphMode')"
       `@click`="toggleLinearMode"
     >
       <i class="icon-[comfy--workflow]" />
     </Button>

Based on learnings: "Use aria-label only for elements without visible labels (e.g., icon-only buttons)."

🤖 Prompt for AI Agents
In `@src/components/sidebar/ModeToggle.vue` around lines 16 - 39, The icon-only
Button components in ModeToggle.vue lack accessible labels; update both Button
elements to include descriptive aria-label attributes (e.g., aria-label="Switch
to linear mode" and aria-label="Switch to graph mode") that reflect their
actions, keeping existing props and the `@click`="toggleLinearMode" handler and
canvasStore.linearMode variants unchanged so screen readers can announce the
purpose of the buttons.

</div>
Comment on lines +1 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "linearMode" src/locales/ -A 2 -B 2

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3470


🏁 Script executed:

# Check how t() is imported and used in other components
rg "from '@/i18n'" src/components/ | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1377


🏁 Script executed:

# Check if useI18n is used elsewhere
rg "useI18n" src/components/ | head -20

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 1552


Use useI18n() composable instead of direct t import to follow composition API conventions.

While the i18n keys linearMode.linearMode and linearMode.graphMode exist and are correct, the component should follow the established pattern used throughout the sidebar (ComfyMenuButton.vue, SidebarIcon.vue, etc.) and the coding guidelines:

import { useI18n } from 'vue-i18n'

const { t } = useI18n()

Replace the direct import import { t } from '@/i18n' with the VueUse pattern above. This is consistent with other sidebar components and aligns with the coding guideline: "Use VueUse function for useI18n in composition API for string literals."

The rest of the component implementation is clean: proper use of Composition API, no unnecessary style blocks, correct command binding, and clear toggle logic.

🤖 Prompt for AI Agents
In `@src/components/sidebar/ModeToggle.vue` around lines 1 - 27, The component
imports t directly from '@/i18n' instead of using the Vue i18n composable;
replace the top-level import "import { t } from '@/i18n'" with "import { useI18n
} from 'vue-i18n'" and inside the <script setup> call const { t } = useI18n() so
the template uses the composable t; keep existing canvasStore and
useCommandStore usage (symbols: ModeToggle.vue, useI18n, const { t } =
useI18n(), canvasStore, useCommandStore) and remove the old direct import.

</template>
8 changes: 8 additions & 0 deletions src/components/sidebar/SideToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@
<SidebarBottomPanelToggleButton :is-small="isSmall" />
<SidebarShortcutsToggleButton :is-small="isSmall" />
<SidebarSettingsButton :is-small="isSmall" />
<ModeToggle
v-if="menuItemStore.hasSeenLinear || flags.linearToggleEnabled"
/>
</div>
</div>
<HelpCenterPopups :is-small="isSmall" />
Expand All @@ -57,14 +60,17 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'

import HelpCenterPopups from '@/components/helpcenter/HelpCenterPopups.vue'
import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarSettingsButton from '@/components/sidebar/SidebarSettingsButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCommandStore } from '@/stores/commandStore'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useUserStore } from '@/stores/userStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import type { SidebarTabExtension } from '@/types/extensionTypes'
Expand All @@ -80,9 +86,11 @@ const settingStore = useSettingStore()
const userStore = useUserStore()
const commandStore = useCommandStore()
const canvasStore = useCanvasStore()
const menuItemStore = useMenuItemStore()
const sideToolbarRef = ref<HTMLElement>()
const topToolbarRef = ref<HTMLElement>()
const bottomToolbarRef = ref<HTMLElement>()
const { flags } = useFeatureFlags()

const isSmall = computed(
() => settingStore.get('Comfy.Sidebar.Size') === 'small'
Expand Down
1 change: 1 addition & 0 deletions src/components/sidebar/tabs/WorkflowsSidebarTab.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<SidebarTabTemplate
:title="$t('sideToolbar.workflows')"
v-bind="$attrs"
class="workflows-sidebar-tab"
>
<template #tool-buttons>
Expand Down
2 changes: 1 addition & 1 deletion src/components/topbar/TopMenuHelpButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
variant="textonly"
@click="toggleHelpCenter"
>
{{ $t('menu.helpAndFeedback') }}
<div class="not-md:hidden">{{ $t('menu.helpAndFeedback') }}</div>
<i class="icon-[lucide--circle-help] ml-0.5" />
<span
v-if="shouldShowRedDot"
Expand Down
4 changes: 4 additions & 0 deletions src/components/topbar/WorkflowTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
>
<i class="pi pi-bars" />
</Button>
<i
v-else-if="workflowOption.workflow.activeState?.extra?.linearMode"
class="icon-[lucide--panels-top-left] bg-primary-background"
/>
Comment on lines +10 to +13

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "linearMode" src/locales/en/main.json

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 130


🏁 Script executed:

cat -n src/components/topbar/WorkflowTab.vue | head -100

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3677


🏁 Script executed:

cat -n src/components/topbar/WorkflowTab.vue | tail -150

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 5359


Improve icon accessibility with title attribute, but fix aria-hidden value.

The icon is purely decorative and lacks an accessible label. Add a title attribute for tooltip support. However, if the icon is decorative (which it is), it should have aria-hidden="true" rather than aria-hidden="false", or omit the aria-hidden attribute entirely if only the title is needed.

🔧 Suggested fix
     <i
       v-else-if="workflowOption.workflow.activeState?.extra?.linearMode"
       class="icon-[lucide--panels-top-left] bg-primary-background"
+      :title="t('linearMode.linearMode')"
+      aria-hidden="true"
     />
🤖 Prompt for AI Agents
In `@src/components/topbar/WorkflowTab.vue` around lines 19 - 22, Add a
descriptive title to the decorative icon in WorkflowTab.vue and correct its
accessibility flag: update the <i> element rendered by the v-else-if branch (the
element with class "icon-[lucide--panels-top-left] bg-primary-background") to
include a title attribute like title="Linear mode" and ensure it uses
aria-hidden="true" (or omit aria-hidden entirely) since it is decorative.

<span class="workflow-label inline-block max-w-[150px] truncate text-sm">
{{ workflowOption.workflow.filename }}
</span>
Expand Down
76 changes: 76 additions & 0 deletions src/components/ui/Popover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<script setup lang="ts">
import {
PopoverArrow,
PopoverContent,
PopoverPortal,
PopoverRoot,
PopoverTrigger
} from 'reka-ui'

import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'

defineOptions({
inheritAttrs: false
})

defineProps<{
entries?: { label: string; action?: () => void; icon?: string }[][]
icon?: string
to?: string | HTMLElement
}>()
</script>

<template>
<PopoverRoot v-slot="{ close }">
<PopoverTrigger as-child>
<slot name="button">
<Button size="icon">
<i :class="icon ?? 'icon-[lucide--ellipsis]'" />
</Button>
</slot>
</PopoverTrigger>
Comment on lines +26 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add aria-label for icon-only button accessibility.

The default trigger button contains only an icon without visible text. Per accessibility guidelines, icon-only buttons need an aria-label to provide an accessible name for screen readers.

♻️ Suggested fix
       <slot name="button">
-        <Button size="icon">
+        <Button size="icon" aria-label="Open menu">
           <i :class="icon ?? 'icon-[lucide--ellipsis]'" />
         </Button>
       </slot>

Note: Consider using vue-i18n for the aria-label string if this component is user-facing.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PopoverTrigger as-child>
<slot name="button">
<Button size="icon">
<i :class="icon ?? 'icon-[lucide--ellipsis]'" />
</Button>
</slot>
</PopoverTrigger>
<PopoverTrigger as-child>
<slot name="button">
<Button size="icon" aria-label="Open menu">
<i :class="icon ?? 'icon-[lucide--ellipsis]'" />
</Button>
</slot>
</PopoverTrigger>
🤖 Prompt for AI Agents
In `@src/components/ui/Popover.vue` around lines 26 - 32, The icon-only default
trigger lacks an accessible name—add an aria-label to the <Button> in the
default slot: introduce a prop (e.g., ariaLabel) on this Popover component
(props: ariaLabel: { type: String, default: null }) and bind it to the Button as
:aria-label="ariaLabel ?? $t('popover.openOptions')" (or a sensible default
string if i18n isn't available); keep the binding next to size="icon" inside the
slot so <PopoverTrigger> / slot="button" / Button / icon remain unchanged
otherwise.

<PopoverPortal :to>
<PopoverContent
side="bottom"
:side-offset="5"
:collision-padding="10"
v-bind="$attrs"
class="rounded-lg p-2 bg-base-background shadow-sm border border-border-subtle will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
>
<slot>
<div class="flex flex-col p-1">
<section
v-for="(entryGroup, index) in entries ?? []"
:key="index"
class="flex flex-col border-b-2 last:border-none border-border-subtle"
>
<div
v-for="{ label, action, icon } in entryGroup"
:key="label"
:class="
cn(
'flex flex-row gap-4 p-2 rounded-sm my-1',
action &&
'cursor-pointer hover:bg-secondary-background-hover'
)
"
@click="
() => {
if (!action) return
action()
close()
}
"
>
<i v-if="icon" :class="icon" />
{{ label }}
</div>
</section>
</div>
</slot>
<PopoverArrow class="fill-base-background stroke-border-subtle" />
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>
29 changes: 29 additions & 0 deletions src/components/ui/TypeformPopoverButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { whenever } from '@vueuse/core'
import { useTemplateRef } from 'vue'

import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'

defineProps<{
dataTfWidget: string
}>()

const feedbackRef = useTemplateRef('feedbackRef')

whenever(feedbackRef, () => {
const scriptEl = document.createElement('script')
scriptEl.src = '//embed.typeform.com/next/embed.js'
feedbackRef.value?.appendChild(scriptEl)
})
Comment on lines +14 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential multiple script injections on repeated popover opens.

The whenever callback runs each time feedbackRef transitions from falsy to truthy. If the popover is opened multiple times, this could inject multiple <script> elements. Consider guarding against duplicate injections:

♻️ Suggested fix
+const scriptInjected = ref(false)
+
 whenever(feedbackRef, () => {
+  if (scriptInjected.value) return
   const scriptEl = document.createElement('script')
   scriptEl.src = '//embed.typeform.com/next/embed.js'
   feedbackRef.value?.appendChild(scriptEl)
+  scriptInjected.value = true
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
whenever(feedbackRef, () => {
const scriptEl = document.createElement('script')
scriptEl.src = '//embed.typeform.com/next/embed.js'
feedbackRef.value?.appendChild(scriptEl)
})
const scriptInjected = ref(false)
whenever(feedbackRef, () => {
if (scriptInjected.value) return
const scriptEl = document.createElement('script')
scriptEl.src = '//embed.typeform.com/next/embed.js'
feedbackRef.value?.appendChild(scriptEl)
scriptInjected.value = true
})
🤖 Prompt for AI Agents
In `@src/components/ui/TypeformPopoverButton.vue` around lines 14 - 18, The
whenever callback currently appends a new script every time feedbackRef becomes
truthy, risking duplicate injections; modify the callback in whenever to first
check for an existing script with the same src (or a unique id) inside
feedbackRef.value (or document) before creating/appending scriptEl (e.g., use
feedbackRef.value.querySelector('script[src="//embed.typeform.com/next/embed.js"]')
or document.getElementById) and only create/append the script if that query
returns null, ensuring the Typeform embed script is injected once.

</script>
<template>
<Popover>
<template #button>
<Button variant="inverted" class="rounded-full size-12">
<i class="icon-[lucide--circle-question-mark] size-6" />
</Button>
</template>
Comment on lines +22 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add aria-label for icon-only help button.

The button contains only an icon without visible text. For screen reader accessibility, add an aria-label. As per coding guidelines, use vue-i18n for the label.

♻️ Suggested fix
+<script setup lang="ts">
+import { useI18n } from 'vue-i18n'
+// ... existing imports
+
+const { t } = useI18n()
+// ... rest of script
+</script>
+
     <template `#button`>
-      <Button variant="inverted" class="rounded-full size-12">
+      <Button variant="inverted" class="rounded-full size-12" :aria-label="t('feedback.openFeedback')">
         <i class="icon-[lucide--circle-question-mark] size-6" />
       </Button>
     </template>
🤖 Prompt for AI Agents
In `@src/components/ui/TypeformPopoverButton.vue` around lines 22 - 26, The
icon-only help Button in the template (`#button`) lacks an accessible name; update
the Button element in TypeformPopoverButton.vue to include an aria-label bound
to a vue-i18n translation (e.g., $t('typeform.help_button') or similar key) so
screen readers get a descriptive label; add the i18n key to your locale messages
and use the aria-label binding on the Button component instance.

<div ref="feedbackRef" data-tf-auto-resize :data-tf-widget />
</Popover>
</template>
59 changes: 59 additions & 0 deletions src/components/ui/ZoomPane.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue'

const zoomPane = useTemplateRef('zoomPane')

const zoom = ref(1.0)
const panX = ref(0.0)
const panY = ref(0.0)

function handleWheel(e: WheelEvent) {
const zoomPaneEl = zoomPane.value
if (!zoomPaneEl) return

zoom.value -= e.deltaY
const { x, y, width, height } = zoomPaneEl.getBoundingClientRect()
const offsetX = e.clientX - x - width / 2
const offsetY = e.clientY - y - height / 2
const scaler = 1.1 ** (e.deltaY / -30)

panY.value = panY.value * scaler - offsetY * (scaler - 1)
panX.value = panX.value * scaler - offsetX * (scaler - 1)
}
Comment on lines +10 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Zoom calculation produces erratic behavior across devices.

Line 14 applies raw deltaY directly to zoom.value, but deltaY varies significantly across browsers and input devices (100-1000+ per scroll event). Combined with the exponential scale calculation on line 42 (1.1 ** (zoom.value / 30)), this produces unpredictable zoom behavior.

🐛 Proposed fix: Normalize deltaY and use consistent scaling
 function handleWheel(e: WheelEvent) {
   const zoomPaneEl = zoomPane.value
   if (!zoomPaneEl) return

-  zoom.value -= e.deltaY
+  // Normalize deltaY to consistent step size
+  const delta = Math.sign(e.deltaY) * Math.min(Math.abs(e.deltaY), 100)
+  zoom.value -= delta
   const { x, y, width, height } = zoomPaneEl.getBoundingClientRect()
   const offsetX = e.clientX - x - width / 2
   const offsetY = e.clientY - y - height / 2
-  const scaler = 1.1 ** (e.deltaY / -30)
+  const scaler = 1.1 ** (delta / -30)

   panY.value = panY.value * scaler - offsetY * (scaler - 1)
   panX.value = panX.value * scaler - offsetX * (scaler - 1)
 }
🤖 Prompt for AI Agents
In `@src/components/ui/ZoomPane.vue` around lines 10 - 22, handleWheel uses raw
e.deltaY causing wildly different zoom steps across devices; normalize and clamp
deltaY, then compute scaler from that normalized value and apply it to zoom and
pan. Specifically, in handleWheel normalize deltaY (e.g., let delta =
Math.sign(e.deltaY) * Math.min(Math.abs(e.deltaY), 100) or divide by a constant
like 100 to get a bounded delta in [-1,1]), use that normalized delta in the
scaler formula (replace 1.1 ** (e.deltaY / -30) with 1.1 ** (normalizedDelta /
-30) or similar), update zoom.value using the scaler (e.g., multiply or add
using the normalized delta consistently) and compute panX/panY adjustments using
the same scaler so zoom behavior is consistent across devices; make these
changes inside handleWheel and keep references to zoom.value, panX.value,
panY.value, and the scaler calculation.


let dragging = false
function handleDown(e: PointerEvent) {
if (e.button !== 0) return

const zoomPaneEl = zoomPane.value
if (!zoomPaneEl) return
zoomPaneEl.parentElement?.focus()

zoomPaneEl.setPointerCapture(e.pointerId)
dragging = true
}
function handleMove(e: PointerEvent) {
if (!dragging) return
panX.value += e.movementX
panY.value += e.movementY
Comment on lines +24 to +38

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Shared dragging state causes cross-instance interference.

The dragging variable is declared at module scope without reactivity. If multiple ZoomPane components are mounted, they share this state, causing one pane's drag to affect others.

🐛 Proposed fix: Use ref for instance isolation
+const dragging = ref(false)
-let dragging = false
 function handleDown(e: PointerEvent) {
   if (e.button !== 0) return

   const zoomPaneEl = zoomPane.value
   if (!zoomPaneEl) return
   zoomPaneEl.parentElement?.focus()

   zoomPaneEl.setPointerCapture(e.pointerId)
-  dragging = true
+  dragging.value = true
 }
 function handleMove(e: PointerEvent) {
-  if (!dragging) return
+  if (!dragging.value) return
   panX.value += e.movementX
   panY.value += e.movementY
 }

And update template events:

-    `@pointerup`="dragging = false"
-    `@pointercancel`="dragging = false"
+    `@pointerup`="dragging = false"
+    `@pointercancel`="dragging = false"
🤖 Prompt for AI Agents
In `@src/components/ui/ZoomPane.vue` around lines 24 - 38, The module-scoped plain
`dragging` causes cross-instance interference; replace it with a component-local
Vue ref (e.g., `const dragging = ref(false)`) and import ref from 'vue', then
update the handlers `handleDown`, `handleMove` and the corresponding
pointerup/pointercancel handler to read/write `dragging.value` instead of
`dragging`, ensure you call/release pointer capture on the `zoomPane` element
within `handleDown`/`handleUp` using the same logic, and update any template
event bindings to use the revised handlers that rely on the instance-local
`dragging` ref so multiple ZoomPane components no longer share state.

}

const transform = computed(() => {
const scale = 1.1 ** (zoom.value / 30)
const matrix = [scale, 0, 0, scale, panX.value, panY.value]
return `matrix(${matrix.join(',')})`
})
</script>
<template>
<div
ref="zoomPane"
class="contain-size flex place-content-center"
@wheel="handleWheel"
@pointerdown.prevent="handleDown"
@pointermove="handleMove"
@pointerup="dragging = false"
@pointercancel="dragging = false"
>
<slot :style="{ transform }" />
</div>
</template>
Loading