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:
Koshi
2026-03-27 17:33:05 +01:00
parent ec3c7bd8fe
commit b40fb33e7a
7 changed files with 79 additions and 60 deletions

View File

@@ -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"

View File

@@ -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',

View File

@@ -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>

View File

@@ -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"

View File

@@ -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(() => {

View File

@@ -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>

View File

@@ -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>