mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 10:12:11 +00:00
More app fixes (#9432)
- Increased the z-index on app mode outputs so that they display above a zoomed image - The "view job" button on the job queued toast in mobile app mode will take you to outputs instead of assets - Image previews now have a minimum zoom of ~20% and a maximum zoom of ~50x - The enter panel in linear mode now has a minimum size of ~1/5th screen size - In arrange mode, dragging to rearrange inputs will no longer cause a horizontal scrollbar to appear. - Videos will now display the first frame instead of a generic video icon - Muted/Bypassed nodes can no longer be selected as inputs/outputs, or be displayed when in app mode. - Linked input can no longer be selected or displayed - Adds a share workflow button in app mode and wires up the existing context menu ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9432-More-app-fixes-31a6d73d365081509cd0ea74bfdc9b95) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
@@ -3,9 +3,16 @@ import { computed } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
|
import WorkflowActionsDropdown from '@/components/common/WorkflowActionsDropdown.vue'
|
||||||
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { useAppMode } from '@/composables/useAppMode'
|
import { useAppMode } from '@/composables/useAppMode'
|
||||||
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
import { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'
|
||||||
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
|
import {
|
||||||
|
openShareDialog,
|
||||||
|
prefetchShareDialog
|
||||||
|
} from '@/platform/workflow/sharing/composables/lazyShareDialog'
|
||||||
import { useAppModeStore } from '@/stores/appModeStore'
|
import { useAppModeStore } from '@/stores/appModeStore'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||||
@@ -18,6 +25,8 @@ const workspaceStore = useWorkspaceStore()
|
|||||||
const { enableAppBuilder } = useAppMode()
|
const { enableAppBuilder } = useAppMode()
|
||||||
const appModeStore = useAppModeStore()
|
const appModeStore = useAppModeStore()
|
||||||
const { enterBuilder } = appModeStore
|
const { enterBuilder } = appModeStore
|
||||||
|
const { toastErrorHandler } = useErrorHandling()
|
||||||
|
const { flags } = useFeatureFlags()
|
||||||
const { hasNodes } = storeToRefs(appModeStore)
|
const { hasNodes } = storeToRefs(appModeStore)
|
||||||
const tooltipOptions = { showDelay: 300, hideDelay: 300 }
|
const tooltipOptions = { showDelay: 300, hideDelay: 300 }
|
||||||
|
|
||||||
@@ -60,6 +69,21 @@ function openTemplates() {
|
|||||||
>
|
>
|
||||||
<i class="icon-[lucide--hammer] size-4" />
|
<i class="icon-[lucide--hammer] size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="isCloud && flags.workflowSharingEnabled"
|
||||||
|
v-tooltip.right="{
|
||||||
|
value: t('actionbar.shareTooltip'),
|
||||||
|
...tooltipOptions
|
||||||
|
}"
|
||||||
|
variant="secondary"
|
||||||
|
size="unset"
|
||||||
|
:aria-label="t('actionbar.shareTooltip')"
|
||||||
|
class="size-10 rounded-lg"
|
||||||
|
@click="() => openShareDialog().catch(toastErrorHandler)"
|
||||||
|
@pointerenter="prefetchShareDialog"
|
||||||
|
>
|
||||||
|
<i class="icon-[lucide--send] size-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex w-10 flex-col overflow-hidden rounded-lg bg-secondary-background"
|
class="flex w-10 flex-col overflow-hidden rounded-lg bg-secondary-background"
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
|||||||
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||||
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
|
||||||
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
|
||||||
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
|
import {
|
||||||
|
LGraphEventMode,
|
||||||
|
TitleMode
|
||||||
|
} from '@/lib/litegraph/src/types/globalEnums'
|
||||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
|
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
@@ -159,7 +162,8 @@ function handleDown(e: MouseEvent) {
|
|||||||
}
|
}
|
||||||
function handleClick(e: MouseEvent) {
|
function handleClick(e: MouseEvent) {
|
||||||
const [node, widget] = getHovered(e) ?? []
|
const [node, widget] = getHovered(e) ?? []
|
||||||
if (!node) return canvasInteractions.forwardEventToCanvas(e)
|
if (node?.mode !== LGraphEventMode.ALWAYS)
|
||||||
|
return canvasInteractions.forwardEventToCanvas(e)
|
||||||
|
|
||||||
if (!widget) {
|
if (!widget) {
|
||||||
if (!isSelectOutputsMode.value) return
|
if (!isSelectOutputsMode.value) return
|
||||||
@@ -192,7 +196,10 @@ function nodeToDisplayTuple(
|
|||||||
const renderedOutputs = computed(() => {
|
const renderedOutputs = computed(() => {
|
||||||
void appModeStore.selectedOutputs.length
|
void appModeStore.selectedOutputs.length
|
||||||
return canvas
|
return canvas
|
||||||
.graph!.nodes.filter((n) => n.constructor.nodeData?.output_node)
|
.graph!.nodes.filter(
|
||||||
|
(n) =>
|
||||||
|
n.constructor.nodeData?.output_node && n.mode === LGraphEventMode.ALWAYS
|
||||||
|
)
|
||||||
.map(nodeToDisplayTuple)
|
.map(nodeToDisplayTuple)
|
||||||
})
|
})
|
||||||
const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
||||||
@@ -215,6 +222,7 @@ const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
|
|||||||
v-if="isArrangeMode"
|
v-if="isArrangeMode"
|
||||||
v-slot="{ dragClass }"
|
v-slot="{ dragClass }"
|
||||||
v-model="appModeStore.selectedInputs"
|
v-model="appModeStore.selectedInputs"
|
||||||
|
class="overflow-x-clip"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
|
v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const panY = ref(0.0)
|
|||||||
|
|
||||||
function handleWheel(e: WheelEvent) {
|
function handleWheel(e: WheelEvent) {
|
||||||
const zoomPaneEl = zoomPane.value
|
const zoomPaneEl = zoomPane.value
|
||||||
if (!zoomPaneEl) return
|
if (!zoomPaneEl || (e.deltaY < 0 ? zoom.value > 1200 : zoom.value < -500))
|
||||||
|
return
|
||||||
|
|
||||||
zoom.value -= e.deltaY
|
zoom.value -= e.deltaY
|
||||||
const { x, y, width, height } = zoomPaneEl.getBoundingClientRect()
|
const { x, y, width, height } = zoomPaneEl.getBoundingClientRect()
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ vi.mock('@/stores/appModeStore', () => ({
|
|||||||
useAppModeStore: vi.fn(() => mockAppModeStore)
|
useAppModeStore: vi.fn(() => mockAppModeStore)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useErrorHandling', () => ({}))
|
||||||
|
|
||||||
vi.mock('@/composables/useFeatureFlags', () => ({
|
vi.mock('@/composables/useFeatureFlags', () => ({
|
||||||
useFeatureFlags: vi.fn(() => mockFeatureFlags)
|
useFeatureFlags: vi.fn(() => mockFeatureFlags)
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import type { ComputedRef, Ref } from 'vue'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||||
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
|
import { openShareDialog } from '@/platform/workflow/sharing/composables/lazyShareDialog'
|
||||||
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
|
||||||
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
|
||||||
import {
|
import {
|
||||||
@@ -191,8 +194,9 @@ export function useWorkflowActionsMenu(
|
|||||||
id: 'share',
|
id: 'share',
|
||||||
label: t('breadcrumbsMenu.share'),
|
label: t('breadcrumbsMenu.share'),
|
||||||
icon: 'icon-[comfy--send]',
|
icon: 'icon-[comfy--send]',
|
||||||
command: async () => {},
|
command: () =>
|
||||||
visible: false
|
openShareDialog().catch(useErrorHandling().toastErrorHandler),
|
||||||
|
visible: isCloud && flags.workflowSharingEnabled
|
||||||
})
|
})
|
||||||
|
|
||||||
addItem({
|
addItem({
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { cn } from '@/utils/tailwindUtil'
|
|||||||
|
|
||||||
const { id, name } = defineProps<{
|
const { id, name } = defineProps<{
|
||||||
id: string
|
id: string
|
||||||
isSelectInputsMode: boolean
|
enable: boolean
|
||||||
name: string
|
name: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ function togglePromotion() {
|
|||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="isSelectInputsMode"
|
v-if="enable"
|
||||||
class="pointer-events-auto relative col-span-2 flex cursor-pointer flex-row gap-1"
|
class="pointer-events-auto relative col-span-2 flex cursor-pointer flex-row gap-1"
|
||||||
@pointerdown.capture.stop.prevent="togglePromotion"
|
@pointerdown.capture.stop.prevent="togglePromotion"
|
||||||
@click.capture.stop.prevent
|
@click.capture.stop.prevent
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import Popover from '@/components/ui/Popover.vue'
|
|||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||||
|
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||||
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
import { appendCloudResParam } from '@/platform/distribution/cloudPreviewUtil'
|
||||||
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
|
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
|
||||||
@@ -44,7 +45,7 @@ const props = defineProps<{
|
|||||||
mobile?: boolean
|
mobile?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{ navigateAssets: [] }>()
|
defineEmits<{ navigateOutputs: [] }>()
|
||||||
|
|
||||||
//NOTE: due to batching, will never be greater than 2
|
//NOTE: due to batching, will never be greater than 2
|
||||||
const pendingJobQueues = ref(0)
|
const pendingJobQueues = ref(0)
|
||||||
@@ -72,7 +73,7 @@ const mappedSelections = computed(() => {
|
|||||||
).map(([, widgetName]) => widgetName)
|
).map(([, widgetName]) => widgetName)
|
||||||
unprocessedInputs = unprocessedInputs.slice(inputGroup.length)
|
unprocessedInputs = unprocessedInputs.slice(inputGroup.length)
|
||||||
const node = resolveNode(nodeId)
|
const node = resolveNode(nodeId)
|
||||||
if (!node) continue
|
if (node?.mode !== LGraphEventMode.ALWAYS) continue
|
||||||
|
|
||||||
const nodeData = nodeToNodeData(node)
|
const nodeData = nodeToNodeData(node)
|
||||||
remove(nodeData.widgets ?? [], (w) => !inputGroup.includes(w.name))
|
remove(nodeData.widgets ?? [], (w) => !inputGroup.includes(w.name))
|
||||||
@@ -105,6 +106,7 @@ function getDropIndicator(node: LGraphNode) {
|
|||||||
function nodeToNodeData(node: LGraphNode) {
|
function nodeToNodeData(node: LGraphNode) {
|
||||||
const dropIndicator = getDropIndicator(node)
|
const dropIndicator = getDropIndicator(node)
|
||||||
const nodeData = extractVueNodeData(node)
|
const nodeData = extractVueNodeData(node)
|
||||||
|
remove(nodeData.widgets ?? [], (w) => w.slotMetadata?.linked ?? false)
|
||||||
for (const widget of nodeData.widgets ?? []) widget.slotMetadata = undefined
|
for (const widget of nodeData.widgets ?? []) widget.slotMetadata = undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -261,7 +263,7 @@ defineExpose({ runButtonClick })
|
|||||||
<Button
|
<Button
|
||||||
v-if="mobile"
|
v-if="mobile"
|
||||||
variant="inverted"
|
variant="inverted"
|
||||||
@click="$emit('navigateAssets')"
|
@click="$emit('navigateOutputs')"
|
||||||
>
|
>
|
||||||
{{ t('linearMode.viewJob') }}
|
{{ t('linearMode.viewJob') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ async function rerun(e: Event) {
|
|||||||
/>
|
/>
|
||||||
<OutputHistory
|
<OutputHistory
|
||||||
v-if="!isBuilderMode"
|
v-if="!isBuilderMode"
|
||||||
class="min-w-0"
|
class="z-10 min-w-0"
|
||||||
@update-selection="handleSelection"
|
@update-selection="handleSelection"
|
||||||
/>
|
/>
|
||||||
<LinearFeedback
|
<LinearFeedback
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ const menuEntries = computed<MenuItem[]>(() => [
|
|||||||
:style="{ translate }"
|
:style="{ translate }"
|
||||||
>
|
>
|
||||||
<div class="absolute h-full w-screen overflow-y-auto contain-size">
|
<div class="absolute h-full w-screen overflow-y-auto contain-size">
|
||||||
<LinearControls mobile @navigate-assets="activeIndex = 2" />
|
<LinearControls mobile @navigate-outputs="activeIndex = 1" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 left-[100vw] flex h-full w-screen flex-col bg-base-background"
|
class="absolute top-0 left-[100vw] flex h-full w-screen flex-col bg-base-background"
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const queueCount = computed(
|
|||||||
|
|
||||||
const itemClass = cn(
|
const itemClass = cn(
|
||||||
'shrink-0 cursor-pointer rounded-lg border-2 border-transparent p-1 outline-none',
|
'shrink-0 cursor-pointer rounded-lg border-2 border-transparent p-1 outline-none',
|
||||||
'data-[state=checked]:border-interface-panel-job-progress-border'
|
'relative data-[state=checked]:border-interface-panel-job-progress-border'
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasActiveContent = computed(
|
const hasActiveContent = computed(
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
import type { ResultItemImpl } from '@/stores/queueStore'
|
import type { ResultItemImpl } from '@/stores/queueStore'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
import VideoPlayOverlay from '@/platform/assets/components/VideoPlayOverlay.vue'
|
||||||
|
|
||||||
const { output } = defineProps<{
|
const { output } = defineProps<{
|
||||||
output: ResultItemImpl
|
output: ResultItemImpl
|
||||||
}>()
|
}>()
|
||||||
@@ -19,6 +21,16 @@ const { output } = defineProps<{
|
|||||||
height="40"
|
height="40"
|
||||||
:src="output.url"
|
:src="output.url"
|
||||||
/>
|
/>
|
||||||
|
<template v-else-if="getMediaType(output) === 'video'">
|
||||||
|
<video
|
||||||
|
class="pointer-events-none block size-10 rounded-sm bg-secondary-background object-cover"
|
||||||
|
preload="metadata"
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
:src="output.url"
|
||||||
|
/>
|
||||||
|
<VideoPlayOverlay size="sm" />
|
||||||
|
</template>
|
||||||
<i
|
<i
|
||||||
v-else
|
v-else
|
||||||
:class="cn(mediaTypes[getMediaType(output)]?.iconClass, 'block size-10')"
|
:class="cn(mediaTypes[getMediaType(output)]?.iconClass, 'block size-10')"
|
||||||
|
|||||||
@@ -52,7 +52,9 @@
|
|||||||
>
|
>
|
||||||
<AppOutput
|
<AppOutput
|
||||||
v-if="
|
v-if="
|
||||||
lgraphNode?.constructor?.nodeData?.output_node && isSelectOutputsMode
|
lgraphNode?.constructor?.nodeData?.output_node &&
|
||||||
|
isSelectOutputsMode &&
|
||||||
|
nodeData.mode === LGraphEventMode.ALWAYS
|
||||||
"
|
"
|
||||||
:id="nodeData.id"
|
:id="nodeData.id"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -53,7 +53,11 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Widget Component -->
|
<!-- Widget Component -->
|
||||||
<AppInput :id="widget.id" :name="widget.name" :is-select-inputs-mode>
|
<AppInput
|
||||||
|
:id="widget.id"
|
||||||
|
:name="widget.name"
|
||||||
|
:enable="canSelectInputs && !widget.simplified.options?.disabled"
|
||||||
|
>
|
||||||
<component
|
<component
|
||||||
:is="widget.vueComponent"
|
:is="widget.vueComponent"
|
||||||
v-model="widget.value"
|
v-model="widget.value"
|
||||||
@@ -89,6 +93,7 @@ import { useAppMode } from '@/composables/useAppMode'
|
|||||||
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
import { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { st } from '@/i18n'
|
import { st } from '@/i18n'
|
||||||
|
import { LGraphEventMode } from '@/lib/litegraph/src/types/globalEnums'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
|
||||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
@@ -154,6 +159,9 @@ onErrorCaptured((error) => {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const canSelectInputs = computed(
|
||||||
|
() => isSelectInputsMode.value && nodeData?.mode === LGraphEventMode.ALWAYS
|
||||||
|
)
|
||||||
const nodeType = computed(() => nodeData?.type || '')
|
const nodeType = computed(() => nodeData?.type || '')
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
const showAdvanced = computed(
|
const showAdvanced = computed(
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
|||||||
<SplitterPanel
|
<SplitterPanel
|
||||||
id="linearCenterPanel"
|
id="linearCenterPanel"
|
||||||
:size="CENTER_PANEL_SIZE"
|
:size="CENTER_PANEL_SIZE"
|
||||||
class="relative flex min-w-0 flex-col gap-4 text-muted-foreground outline-none"
|
class="relative flex min-w-[20vw] flex-col gap-4 text-muted-foreground outline-none"
|
||||||
>
|
>
|
||||||
<LinearProgressBar
|
<LinearProgressBar
|
||||||
class="absolute top-0 left-0 z-21 w-[calc(100%+16px)]"
|
class="absolute top-0 left-0 z-21 w-[calc(100%+16px)]"
|
||||||
|
|||||||
Reference in New Issue
Block a user