mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
feat: linear mode views, mobile display, and UI components
Amp-Thread-ID: https://ampcode.com/threads/T-019d3012-26c4-70ff-984c-913b12217454 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
class="flex h-7 rounded-lg bg-component-node-widget-background text-xs text-component-node-foreground"
|
||||
class="flex h-8 rounded-lg bg-component-node-widget-background text-xs text-component-node-foreground"
|
||||
>
|
||||
<slot name="background" />
|
||||
<Button
|
||||
v-if="!hideButtons"
|
||||
:aria-label="t('g.decrement')"
|
||||
data-testid="decrement"
|
||||
class="aspect-8/7 h-full rounded-r-none hover:bg-base-foreground/20 disabled:opacity-30"
|
||||
class="aspect-square h-full rounded-r-none hover:bg-base-foreground/20 disabled:opacity-30"
|
||||
variant="muted-textonly"
|
||||
:disabled="!canDecrement"
|
||||
tabindex="-1"
|
||||
@@ -16,7 +16,7 @@
|
||||
>
|
||||
<i class="pi pi-minus" />
|
||||
</Button>
|
||||
<div class="relative my-0.25 min-w-[4ch] flex-1 py-1.5">
|
||||
<div class="relative my-0.25 min-w-[2ch] flex-1 py-1.5">
|
||||
<input
|
||||
ref="inputField"
|
||||
v-bind="inputAttrs"
|
||||
@@ -54,7 +54,7 @@
|
||||
v-if="!hideButtons"
|
||||
:aria-label="t('g.increment')"
|
||||
data-testid="increment"
|
||||
class="aspect-8/7 h-full rounded-l-none hover:bg-base-foreground/20 disabled:opacity-30"
|
||||
class="aspect-square h-full rounded-l-none hover:bg-base-foreground/20 disabled:opacity-30"
|
||||
variant="muted-textonly"
|
||||
:disabled="!canIncrement"
|
||||
tabindex="-1"
|
||||
|
||||
@@ -34,6 +34,7 @@ const {
|
||||
node,
|
||||
isDraggable = false,
|
||||
hiddenFavoriteIndicator = false,
|
||||
hiddenLabel = false,
|
||||
hiddenWidgetActions = false,
|
||||
showNodeName = false,
|
||||
parents = [],
|
||||
@@ -43,6 +44,7 @@ const {
|
||||
node: LGraphNode
|
||||
isDraggable?: boolean
|
||||
hiddenFavoriteIndicator?: boolean
|
||||
hiddenLabel?: boolean
|
||||
hiddenWidgetActions?: boolean
|
||||
showNodeName?: boolean
|
||||
parents?: SubgraphNode[]
|
||||
@@ -148,6 +150,7 @@ const displayLabel = customRef((track, trigger) => {
|
||||
>
|
||||
<!-- widget header -->
|
||||
<div
|
||||
v-if="!hiddenLabel"
|
||||
:class="
|
||||
cn(
|
||||
'mb-1.5 flex min-h-8 min-w-0 items-center justify-between gap-1',
|
||||
|
||||
@@ -8,18 +8,24 @@ import {
|
||||
TooltipTrigger
|
||||
} from 'reka-ui'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const {
|
||||
text,
|
||||
side = 'top',
|
||||
sideOffset = 6,
|
||||
sideOffset = 5,
|
||||
delayDuration = 400,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
size = 'sm',
|
||||
keybind
|
||||
} = defineProps<{
|
||||
text?: string
|
||||
side?: 'top' | 'bottom' | 'left' | 'right'
|
||||
sideOffset?: number
|
||||
delayDuration?: number
|
||||
disabled?: boolean
|
||||
size?: 'sm' | 'lg'
|
||||
keybind?: string
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
@@ -36,10 +42,27 @@ const {
|
||||
:side
|
||||
:side-offset="sideOffset"
|
||||
:collision-padding="10"
|
||||
class="z-1700 max-w-75 rounded-md border border-zinc-700 bg-black px-4 py-2 text-sm/tight font-normal text-white shadow-none"
|
||||
:class="
|
||||
cn(
|
||||
'z-1700 border border-border-default bg-base-background font-normal text-base-foreground shadow-[1px_1px_8px_rgba(0,0,0,0.4)]',
|
||||
size === 'sm' &&
|
||||
'flex items-center gap-2 rounded-lg px-4 py-2 text-xs',
|
||||
size === 'lg' && 'max-w-75 rounded-md px-4 py-2 text-sm'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ text }}
|
||||
<TooltipArrow class="fill-black" />
|
||||
<span
|
||||
v-if="keybind && size === 'sm'"
|
||||
class="rounded-sm bg-secondary-background px-1 text-xs/4"
|
||||
>
|
||||
{{ keybind }}
|
||||
</span>
|
||||
<TooltipArrow
|
||||
:width="8"
|
||||
:height="5"
|
||||
class="fill-base-background stroke-border-default"
|
||||
/>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</TooltipRoot>
|
||||
|
||||
@@ -12,11 +12,9 @@ import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import AppTemplateView from '@/renderer/extensions/linearMode/AppTemplateView.vue'
|
||||
import LinearControls from '@/renderer/extensions/linearMode/LinearControls.vue'
|
||||
import LinearPreview from '@/renderer/extensions/linearMode/LinearPreview.vue'
|
||||
import MobileError from '@/renderer/extensions/linearMode/MobileError.vue'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useColorPaletteService } from '@/services/colorPaletteService'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
@@ -30,11 +28,7 @@ const tabs = [
|
||||
['sideToolbar.assets', 'icon-[lucide--images]']
|
||||
]
|
||||
|
||||
const appModeStore = useAppModeStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const useTemplateLayout = computed(
|
||||
() => Object.keys(appModeStore.zoneAssignments).length > 0
|
||||
)
|
||||
const colorPaletteService = useColorPaletteService()
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
@@ -198,8 +192,7 @@ const menuEntries = computed<MenuItem[]>(() => [
|
||||
:style="{ translate }"
|
||||
>
|
||||
<div class="absolute h-full w-screen overflow-y-auto contain-size">
|
||||
<AppTemplateView v-if="useTemplateLayout" />
|
||||
<LinearControls v-else mobile @navigate-outputs="activeIndex = 1" />
|
||||
<LinearControls mobile @navigate-outputs="activeIndex = 1" />
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 left-[100vw] flex h-full w-screen flex-col bg-base-background"
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ScrubableNumberInput from '@/components/common/ScrubableNumberInput.vue'
|
||||
import { evaluateInput } from '@/lib/litegraph/src/utils/widget'
|
||||
import { evaluateInput, getWidgetStep } from '@/lib/litegraph/src/utils/widget'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import {
|
||||
@@ -81,40 +81,8 @@ const precision = computed(() => {
|
||||
return typeof p === 'number' && p >= 0 ? p : undefined
|
||||
})
|
||||
|
||||
// Calculate the step value based on precision or widget options
|
||||
const stepValue = computed(() => {
|
||||
// Use step2 (correct input spec value) if available
|
||||
if (props.widget.options?.step2 !== undefined) {
|
||||
return Number(props.widget.options.step2)
|
||||
}
|
||||
// Use step / 10 for custom large step values (> 10) to match litegraph behavior
|
||||
// This is important for extensions like Impact Pack that use custom step values (e.g., 640)
|
||||
// We skip default step values (1, 10) to avoid affecting normal widgets
|
||||
const step = props.widget.options?.step as number | undefined
|
||||
if (step !== undefined && step > 10) {
|
||||
return Number(step) / 10
|
||||
}
|
||||
// Otherwise, derive from precision
|
||||
if (precision.value !== undefined) {
|
||||
if (precision.value === 0) {
|
||||
return 1
|
||||
}
|
||||
// For precision > 0, step = 1 / (10^precision)
|
||||
// precision 1 → 0.1, precision 2 → 0.01, etc.
|
||||
return Number((1 / Math.pow(10, precision.value)).toFixed(precision.value))
|
||||
}
|
||||
// Infer step from the value range — use 0.01 for small ranges (likely floats),
|
||||
// 1 for integers. This ensures +/- buttons always increment.
|
||||
const { min: rangeMin, max: rangeMax } = filteredProps.value
|
||||
if (
|
||||
rangeMin !== undefined &&
|
||||
rangeMax !== undefined &&
|
||||
rangeMax - rangeMin <= 100
|
||||
) {
|
||||
return rangeMax - rangeMin <= 1 ? 0.01 : 0.1
|
||||
}
|
||||
return 1
|
||||
})
|
||||
// Use the same step calculation as the litegraph graph node widget
|
||||
const stepValue = computed(() => getWidgetStep(props.widget.options ?? {}))
|
||||
|
||||
// Disable grouping separators by default unless explicitly enabled by the node author
|
||||
const useGrouping = computed(() => {
|
||||
|
||||
@@ -13,13 +13,14 @@
|
||||
<GraphCanvas @ready="onGraphReady" />
|
||||
</div>
|
||||
<LinearView v-if="linearMode" />
|
||||
<LayoutTemplateSelector
|
||||
v-if="isBuilderMode"
|
||||
:model-value="appModeStore.layoutTemplateId"
|
||||
@update:model-value="appModeStore.switchTemplate"
|
||||
/>
|
||||
<template v-if="isBuilderMode">
|
||||
<BuilderToolbar />
|
||||
<BuilderMenu />
|
||||
<LayoutTemplateSelector
|
||||
:model-value="appModeStore.layoutTemplateId"
|
||||
@update:model-value="appModeStore.switchTemplate"
|
||||
/>
|
||||
<BuilderFooterToolbar />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,8 @@ import Splitter from 'primevue/splitter'
|
||||
import SplitterPanel from 'primevue/splitterpanel'
|
||||
import { computed, useTemplateRef } from 'vue'
|
||||
|
||||
import ArrangeLayout from '@/components/builder/ArrangeLayout.vue'
|
||||
import AppModeToolbar from '@/components/appMode/AppModeToolbar.vue'
|
||||
import SidebarAppLayout from '@/components/builder/SidebarAppLayout.vue'
|
||||
import LinearPreview from '@/renderer/extensions/linearMode/LinearPreview.vue'
|
||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||
@@ -15,9 +15,11 @@ import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import LinearProgressBar from '@/renderer/extensions/linearMode/LinearProgressBar.vue'
|
||||
import MobileDisplay from '@/renderer/extensions/linearMode/MobileDisplay.vue'
|
||||
import { useAppModeStore } from '@/stores/appModeStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { useAppMode } from '@/composables/useAppMode'
|
||||
import { useStablePrimeVueSplitterSizer } from '@/composables/useStablePrimeVueSplitterSizer'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import {
|
||||
CENTER_PANEL_SIZE,
|
||||
SIDEBAR_MIN_SIZE,
|
||||
@@ -26,7 +28,8 @@ import {
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const { isBuilderMode } = useAppMode()
|
||||
const { isBuilderMode, isAppMode } = useAppMode()
|
||||
const appModeStore = useAppModeStore()
|
||||
|
||||
const mobileDisplay = useBreakpoints(breakpointsTailwind).smaller('md')
|
||||
const activeTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)
|
||||
@@ -35,11 +38,23 @@ const sidebarOnLeft = computed(
|
||||
)
|
||||
const hasLeftPanel = computed(() => sidebarOnLeft.value && activeTab.value)
|
||||
const hasRightPanel = computed(() => !sidebarOnLeft.value && activeTab.value)
|
||||
const hasAppInputsPanel = computed(
|
||||
() => (isAppMode.value && appModeStore.hasOutputs) || isBuilderMode.value
|
||||
)
|
||||
const isDualLayout = computed(() => appModeStore.layoutTemplateId === 'dual')
|
||||
const appInputsPanelSize = computed(() =>
|
||||
isDualLayout.value ? 33 : SIDE_PANEL_SIZE
|
||||
)
|
||||
const appInputsMinSize = computed(() =>
|
||||
isDualLayout.value ? 20 : SIDEBAR_MIN_SIZE
|
||||
)
|
||||
|
||||
const splitterKey = computed(() => {
|
||||
const left = hasLeftPanel.value ? 'L' : ''
|
||||
const right = hasRightPanel.value ? 'R' : ''
|
||||
return `app-${left}${right}`
|
||||
const inputs = hasAppInputsPanel.value ? 'I' : ''
|
||||
const dual = isDualLayout.value ? 'D' : 'S'
|
||||
return `app-${left}${right}${inputs}${dual}`
|
||||
})
|
||||
|
||||
const leftPanelRef = useTemplateRef<MaybeElement>('leftPanel')
|
||||
@@ -90,8 +105,7 @@ const { onResizeEnd } = useStablePrimeVueSplitterSizer(
|
||||
<LinearProgressBar
|
||||
class="absolute top-0 left-0 z-21 h-1 w-[calc(100%+16px)]"
|
||||
/>
|
||||
<ArrangeLayout v-if="isBuilderMode" />
|
||||
<LinearPreview v-else />
|
||||
<LinearPreview />
|
||||
<div class="pointer-events-none absolute top-2 left-4.5 z-21">
|
||||
<AppModeToolbar v-if="!isBuilderMode" class="pointer-events-auto" />
|
||||
</div>
|
||||
@@ -107,6 +121,23 @@ const { onResizeEnd } = useStablePrimeVueSplitterSizer(
|
||||
<ExtensionSlot v-if="activeTab" :extension="activeTab" />
|
||||
</div>
|
||||
</SplitterPanel>
|
||||
<SplitterPanel
|
||||
v-if="hasAppInputsPanel"
|
||||
:size="appInputsPanelSize"
|
||||
:min-size="appInputsMinSize"
|
||||
:class="
|
||||
cn(
|
||||
'overflow-hidden outline-none',
|
||||
isDualLayout
|
||||
? 'max-w-[min(50vw,936px)] min-w-156'
|
||||
: 'max-w-117 min-w-78'
|
||||
)
|
||||
"
|
||||
>
|
||||
<div class="h-full overflow-x-hidden border-l border-border-subtle">
|
||||
<SidebarAppLayout />
|
||||
</div>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user