mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
Modal Component & Custom UI Components (#4908)
This commit is contained in:
@@ -42,7 +42,14 @@ const config: KnipConfig = {
|
|||||||
'vite.electron.config.mts',
|
'vite.electron.config.mts',
|
||||||
'vite.types.config.mts',
|
'vite.types.config.mts',
|
||||||
// Auto generated manager types
|
// Auto generated manager types
|
||||||
'src/types/generatedManagerTypes.ts'
|
'src/types/generatedManagerTypes.ts',
|
||||||
|
// Design system components (may not be used immediately)
|
||||||
|
'src/components/button/IconGroup.vue',
|
||||||
|
'src/components/button/MoreButton.vue',
|
||||||
|
'src/components/button/TextButton.vue',
|
||||||
|
'src/components/card/CardTitle.vue',
|
||||||
|
'src/components/card/CardDescription.vue',
|
||||||
|
'src/components/input/SingleSelect.vue'
|
||||||
],
|
],
|
||||||
ignoreExportsUsedInFile: true,
|
ignoreExportsUsedInFile: true,
|
||||||
// Vue-specific configuration
|
// Vue-specific configuration
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -18556,4 +18556,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,4 +120,4 @@
|
|||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zod-validation-error": "^3.3.0"
|
"zod-validation-error": "^3.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
38
src/components/button/IconButton.vue
Normal file
38
src/components/button/IconButton.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<Button unstyled :class="buttonStyle" @click="onClick">
|
||||||
|
<slot></slot>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||||
|
import {
|
||||||
|
getBaseButtonClasses,
|
||||||
|
getButtonTypeClasses,
|
||||||
|
getIconButtonSizeClasses
|
||||||
|
} from '@/types/buttonTypes'
|
||||||
|
|
||||||
|
interface IconButtonProps extends BaseButtonProps {
|
||||||
|
onClick: (event: Event) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
size = 'md',
|
||||||
|
type = 'secondary',
|
||||||
|
class: className,
|
||||||
|
onClick
|
||||||
|
} = defineProps<IconButtonProps>()
|
||||||
|
|
||||||
|
const buttonStyle = computed(() => {
|
||||||
|
const baseClasses = `${getBaseButtonClasses()} p-0`
|
||||||
|
const sizeClasses = getIconButtonSizeClasses(size)
|
||||||
|
const typeClasses = getButtonTypeClasses(type)
|
||||||
|
|
||||||
|
return [baseClasses, sizeClasses, typeClasses, className]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
7
src/components/button/IconGroup.vue
Normal file
7
src/components/button/IconGroup.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex justify-center items-center flex-shrink-0 outline-none border-none p-0 bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white rounded-lg cursor-pointer"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
44
src/components/button/IconTextButton.vue
Normal file
44
src/components/button/IconTextButton.vue
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<template>
|
||||||
|
<Button unstyled :class="buttonStyle" @click="onClick">
|
||||||
|
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
<slot v-if="iconPosition === 'right'" name="icon"></slot>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||||
|
import {
|
||||||
|
getBaseButtonClasses,
|
||||||
|
getButtonSizeClasses,
|
||||||
|
getButtonTypeClasses
|
||||||
|
} from '@/types/buttonTypes'
|
||||||
|
|
||||||
|
interface IconTextButtonProps extends BaseButtonProps {
|
||||||
|
iconPosition?: 'left' | 'right'
|
||||||
|
label: string
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
size = 'md',
|
||||||
|
type = 'primary',
|
||||||
|
class: className,
|
||||||
|
iconPosition = 'left',
|
||||||
|
label,
|
||||||
|
onClick
|
||||||
|
} = defineProps<IconTextButtonProps>()
|
||||||
|
|
||||||
|
const buttonStyle = computed(() => {
|
||||||
|
const baseClasses = `${getBaseButtonClasses()} !justify-start gap-2`
|
||||||
|
const sizeClasses = getButtonSizeClasses(size)
|
||||||
|
const typeClasses = getButtonTypeClasses(type)
|
||||||
|
|
||||||
|
return [baseClasses, sizeClasses, typeClasses, className]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
51
src/components/button/MoreButton.vue
Normal file
51
src/components/button/MoreButton.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative inline-flex items-center">
|
||||||
|
<IconButton @click="toggle">
|
||||||
|
<i-lucide:more-vertical class="text-sm" />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
ref="popover"
|
||||||
|
:append-to="'body'"
|
||||||
|
:auto-z-index="true"
|
||||||
|
:base-z-index="1000"
|
||||||
|
:dismissable="true"
|
||||||
|
:close-on-escape="true"
|
||||||
|
unstyled
|
||||||
|
:pt="pt"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-1 p-2 min-w-40">
|
||||||
|
<slot :close="hide" />
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Popover from 'primevue/popover'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
import IconButton from './IconButton.vue'
|
||||||
|
|
||||||
|
const popover = ref<InstanceType<typeof Popover>>()
|
||||||
|
|
||||||
|
const toggle = (event: Event) => {
|
||||||
|
popover.value?.toggle(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hide = () => {
|
||||||
|
popover.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
const pt = computed(() => ({
|
||||||
|
root: {
|
||||||
|
class: 'absolute z-50'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
class: [
|
||||||
|
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg',
|
||||||
|
'shadow-lg border border-zinc-200 dark-theme:border-zinc-700'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
40
src/components/button/TextButton.vue
Normal file
40
src/components/button/TextButton.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<Button unstyled :class="buttonStyle" role="button" @click="onClick">
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from 'primevue/button'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||||
|
import {
|
||||||
|
getBaseButtonClasses,
|
||||||
|
getButtonSizeClasses,
|
||||||
|
getButtonTypeClasses
|
||||||
|
} from '@/types/buttonTypes'
|
||||||
|
|
||||||
|
interface TextButtonProps extends BaseButtonProps {
|
||||||
|
label: string
|
||||||
|
onClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
size = 'md',
|
||||||
|
type = 'primary',
|
||||||
|
class: className,
|
||||||
|
label,
|
||||||
|
onClick
|
||||||
|
} = defineProps<TextButtonProps>()
|
||||||
|
|
||||||
|
const buttonStyle = computed(() => {
|
||||||
|
const baseClasses = getBaseButtonClasses()
|
||||||
|
const sizeClasses = getButtonSizeClasses(size)
|
||||||
|
const typeClasses = getButtonTypeClasses(type)
|
||||||
|
|
||||||
|
return [baseClasses, sizeClasses, typeClasses, className]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
7
src/components/card/CardBottom.vue
Normal file
7
src/components/card/CardBottom.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex-1 w-full h-full">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
38
src/components/card/CardContainer.vue
Normal file
38
src/components/card/CardContainer.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="containerClasses" :style="containerStyle">
|
||||||
|
<slot name="top"></slot>
|
||||||
|
<slot name="bottom"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const {
|
||||||
|
ratio = 'square',
|
||||||
|
maxWidth,
|
||||||
|
minWidth
|
||||||
|
} = defineProps<{
|
||||||
|
maxWidth: number
|
||||||
|
minWidth: number
|
||||||
|
ratio?: 'square' | 'portrait' | 'tallPortrait'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const containerClasses = computed(() => {
|
||||||
|
const baseClasses =
|
||||||
|
'flex flex-col bg-white dark-theme:bg-zinc-800 rounded-lg shadow-sm border border-zinc-200 dark-theme:border-zinc-700 overflow-hidden'
|
||||||
|
|
||||||
|
const ratioClasses = {
|
||||||
|
square: 'aspect-[256/308]',
|
||||||
|
portrait: 'aspect-[256/325]',
|
||||||
|
tallPortrait: 'aspect-[256/353]'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseClasses} ${ratioClasses[ratio]}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const containerStyle = computed(() => ({
|
||||||
|
maxWidth: `${maxWidth}px`,
|
||||||
|
minWidth: `${minWidth}px`
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
7
src/components/card/CardDescription.vue
Normal file
7
src/components/card/CardDescription.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-zinc-500 dark-theme:text-zinc-400 text-xs line-clamp-2 h-7">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
7
src/components/card/CardTitle.vue
Normal file
7
src/components/card/CardTitle.vue
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div class="text-neutral text-sm">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts"></script>
|
||||||
40
src/components/card/CardTop.vue
Normal file
40
src/components/card/CardTop.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="topStyle">
|
||||||
|
<slot class="absolute top-0 left-0 w-full h-full"></slot>
|
||||||
|
|
||||||
|
<div class="absolute top-2 left-2 flex gap-2">
|
||||||
|
<slot name="top-left"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute top-2 right-2 flex gap-2">
|
||||||
|
<slot name="top-right"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-2 left-2 flex gap-2">
|
||||||
|
<slot name="bottom-left"></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-2 right-2 flex gap-2">
|
||||||
|
<slot name="bottom-right"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const { ratio = 'square' } = defineProps<{
|
||||||
|
ratio?: 'square' | 'landscape'
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const topStyle = computed(() => {
|
||||||
|
const baseClasses = 'relative p-0'
|
||||||
|
|
||||||
|
const ratioClasses = {
|
||||||
|
square: 'aspect-[1/1]',
|
||||||
|
landscape: 'aspect-[48/27]'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${baseClasses} ${ratioClasses[ratio]}`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
13
src/components/chip/SquareChip.vue
Normal file
13
src/components/chip/SquareChip.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="inline-flex justify-center items-center gap-1 flex-shrink-0 py-1 px-2 text-xs bg-[#D9D9D966]/40 rounded font-bold text-white/90"
|
||||||
|
>
|
||||||
|
<slot name="icon" class="text-xs text-white/90"></slot>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { label } = defineProps<{
|
||||||
|
label: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button
|
|
||||||
class="flex justify-center items-center outline-none border-none p-0 bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white w-8 h-8 rounded-lg cursor-pointer"
|
|
||||||
role="button"
|
|
||||||
@click="onClick"
|
|
||||||
>
|
|
||||||
<slot></slot>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
const { onClick } = defineProps<{
|
|
||||||
onClick: () => void
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<BaseWidgetLayout>
|
|
||||||
<template #leftPanel>
|
|
||||||
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
|
|
||||||
<template #header-icon>
|
|
||||||
<i-lucide:puzzle class="text-neutral" />
|
|
||||||
</template>
|
|
||||||
<template #header-title>
|
|
||||||
<span class="text-neutral text-base">{{ t('g.title') }}</span>
|
|
||||||
</template>
|
|
||||||
</LeftSidePanel>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #header>
|
|
||||||
<!-- here -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #content>
|
|
||||||
<!-- here -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #rightPanel>
|
|
||||||
<RightSidePanel></RightSidePanel>
|
|
||||||
</template>
|
|
||||||
</BaseWidgetLayout>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { provide, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
|
|
||||||
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
|
|
||||||
|
|
||||||
import BaseWidgetLayout from './layout/BaseWidgetLayout.vue'
|
|
||||||
import LeftSidePanel from './panel/LeftSidePanel.vue'
|
|
||||||
import RightSidePanel from './panel/RightSidePanel.vue'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const { onClose } = defineProps<{
|
|
||||||
onClose: () => void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
provide(OnCloseKey, onClose)
|
|
||||||
|
|
||||||
const tempNavigation = ref<(NavItemData | NavGroupData)[]>([
|
|
||||||
{ id: 'installed', label: 'Installed' },
|
|
||||||
{
|
|
||||||
title: 'TAGS',
|
|
||||||
items: [
|
|
||||||
{ id: 'tag-sd15', label: 'SD 1.5' },
|
|
||||||
{ id: 'tag-sdxl', label: 'SDXL' },
|
|
||||||
{ id: 'tag-utility', label: 'Utility' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'CATEGORIES',
|
|
||||||
items: [
|
|
||||||
{ id: 'cat-models', label: 'Models' },
|
|
||||||
{ id: 'cat-nodes', label: 'Nodes' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
const selectedNavItem = ref<string | null>('installed')
|
|
||||||
</script>
|
|
||||||
123
src/components/input/MultiSelect.vue
Normal file
123
src/components/input/MultiSelect.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative inline-block">
|
||||||
|
<MultiSelect
|
||||||
|
v-model="selectedItems"
|
||||||
|
:options="options"
|
||||||
|
option-label="name"
|
||||||
|
unstyled
|
||||||
|
:placeholder="label"
|
||||||
|
:max-selected-labels="0"
|
||||||
|
:pt="pt"
|
||||||
|
>
|
||||||
|
<!-- Trigger value (keep text scale identical) -->
|
||||||
|
<template #value>
|
||||||
|
<span class="text-sm text-zinc-700 dark-theme:text-gray-200">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Chevron size identical to current -->
|
||||||
|
<template #dropdownicon>
|
||||||
|
<i-lucide:chevron-down class="text-lg text-neutral-400" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->
|
||||||
|
<template #option="slotProps">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
class="flex h-4 w-4 p-0.5 flex-shrink-0 items-center justify-center rounded border-[3px] transition-all duration-200"
|
||||||
|
:class="
|
||||||
|
slotProps.selected
|
||||||
|
? 'border-blue-400 bg-blue-400 dark-theme:border-blue-500 dark-theme:bg-blue-500'
|
||||||
|
: 'border-neutral-300 dark-theme:border-zinc-600 bg-neutral-100 dark-theme:bg-zinc-700'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i-lucide:check
|
||||||
|
v-if="slotProps.selected"
|
||||||
|
class="text-xs text-bold text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span>{{ slotProps.option.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MultiSelect>
|
||||||
|
|
||||||
|
<!-- Selected count badge (unchanged) -->
|
||||||
|
<div
|
||||||
|
v-if="selectedCount > 0"
|
||||||
|
class="pointer-events-none absolute -right-2 -top-2 z-10 flex h-5 w-5 items-center justify-center rounded-full bg-blue-400 dark-theme:bg-blue-500 text-xs font-semibold text-white"
|
||||||
|
>
|
||||||
|
{{ selectedCount }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import MultiSelect, {
|
||||||
|
MultiSelectPassThroughMethodOptions
|
||||||
|
} from 'primevue/multiselect'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const { label, options } = defineProps<{
|
||||||
|
label?: string
|
||||||
|
options: { name: string; value: string }[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const selectedItems = defineModel<{ name: string; value: string }[]>({
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedCount = computed(() => selectedItems.value.length)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pure unstyled mode using only the PrimeVue PT API.
|
||||||
|
* All PrimeVue built-in checkboxes/headers are hidden via PT (no :deep hacks).
|
||||||
|
* Visual output matches the previous version exactly.
|
||||||
|
*/
|
||||||
|
const pt = computed(() => ({
|
||||||
|
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
|
||||||
|
class: [
|
||||||
|
'relative inline-flex cursor-pointer select-none w-full',
|
||||||
|
'rounded-lg bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white',
|
||||||
|
'transition-all duration-200 ease-in-out',
|
||||||
|
'border-[2.5px] border-solid',
|
||||||
|
selectedCount.value > 0
|
||||||
|
? 'border-blue-400 dark-theme:border-blue-500'
|
||||||
|
: 'border-transparent',
|
||||||
|
{ 'opacity-60 cursor-default': props.disabled }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
labelContainer: {
|
||||||
|
class:
|
||||||
|
'flex-1 flex items-center overflow-hidden whitespace-nowrap pl-4 py-2 '
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
class: 'p-0'
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
class: 'flex shrink-0 cursor-pointer items-center justify-center px-3'
|
||||||
|
},
|
||||||
|
header: { class: 'hidden' },
|
||||||
|
|
||||||
|
// Overlay & list visuals unchanged
|
||||||
|
overlay:
|
||||||
|
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg border border-solid border-zinc-100',
|
||||||
|
list: {
|
||||||
|
class: 'flex flex-col gap-1 p-0 list-none border-none text-xs'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Option row hover tone identical
|
||||||
|
option:
|
||||||
|
'flex gap-1 items-center p-2 hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
|
||||||
|
|
||||||
|
// Hide built-in checkboxes entirely via PT (no :deep)
|
||||||
|
pcHeaderCheckbox: {
|
||||||
|
root: { class: 'hidden' },
|
||||||
|
style: 'display: none !important'
|
||||||
|
},
|
||||||
|
pcOptionCheckbox: {
|
||||||
|
root: { class: 'hidden' },
|
||||||
|
style: 'display: none !important'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
24
src/components/input/SearchBox.vue
Normal file
24
src/components/input/SearchBox.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex w-full items-center rounded-lg px-2 py-1.5 gap-2 bg-white dark-theme:bg-zinc-800"
|
||||||
|
>
|
||||||
|
<i-lucide:search class="text-neutral" />
|
||||||
|
<InputText
|
||||||
|
v-model="searchQuery"
|
||||||
|
:placeholder="placeHolder || 'Search...'"
|
||||||
|
type="text"
|
||||||
|
unstyled
|
||||||
|
class="w-full p-0 border-none outline-none bg-transparent text-xs text-neutral dark-theme:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import InputText from 'primevue/inputtext'
|
||||||
|
import { defineModel } from 'vue'
|
||||||
|
|
||||||
|
const { placeHolder } = defineProps<{
|
||||||
|
placeHolder?: string
|
||||||
|
}>()
|
||||||
|
const searchQuery = defineModel<string>('')
|
||||||
|
</script>
|
||||||
132
src/components/input/SingleSelect.vue
Normal file
132
src/components/input/SingleSelect.vue
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative inline-flex items-center">
|
||||||
|
<Select
|
||||||
|
v-model="selectedItem"
|
||||||
|
:options="options"
|
||||||
|
option-label="name"
|
||||||
|
option-value="value"
|
||||||
|
unstyled
|
||||||
|
:placeholder="label"
|
||||||
|
:pt="pt"
|
||||||
|
>
|
||||||
|
<!-- Trigger value -->
|
||||||
|
<template #value="slotProps">
|
||||||
|
<div class="flex items-center gap-2 text-sm">
|
||||||
|
<slot name="icon" />
|
||||||
|
<span
|
||||||
|
v-if="slotProps.value !== null && slotProps.value !== undefined"
|
||||||
|
class="text-zinc-700 dark-theme:text-gray-200"
|
||||||
|
>
|
||||||
|
{{ getLabel(slotProps.value) }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-zinc-700 dark-theme:text-gray-200">
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Trigger caret -->
|
||||||
|
<template #dropdownicon>
|
||||||
|
<i-lucide:chevron-down
|
||||||
|
class="text-base text-neutral-400 dark-theme:text-gray-300"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Option row -->
|
||||||
|
<template #option="{ option, selected }">
|
||||||
|
<div class="flex items-center justify-between gap-3 w-full">
|
||||||
|
<span class="truncate">{{ option.name }}</span>
|
||||||
|
<i-lucide:check
|
||||||
|
v-if="selected"
|
||||||
|
class="text-neutral-900 dark-theme:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Select, { SelectPassThroughMethodOptions } from 'primevue/select'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const { label, options } = defineProps<{
|
||||||
|
label?: string
|
||||||
|
options: {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
}[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const selectedItem = defineModel<string | null>({ required: true })
|
||||||
|
|
||||||
|
const getLabel = (val: string | null | undefined) => {
|
||||||
|
if (val == null) return label ?? ''
|
||||||
|
const found = options.find((o) => o.value === val)
|
||||||
|
return found ? found.name : label ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unstyled + PT API only
|
||||||
|
* - No background/border (same as page background)
|
||||||
|
* - Text/icon scale: compact size matching MultiSelect
|
||||||
|
*/
|
||||||
|
const pt = computed(() => ({
|
||||||
|
root: ({
|
||||||
|
props
|
||||||
|
}: SelectPassThroughMethodOptions<{ name: string; value: string }>) => ({
|
||||||
|
class: [
|
||||||
|
// container
|
||||||
|
'relative inline-flex w-full cursor-pointer select-none items-center',
|
||||||
|
// trigger surface
|
||||||
|
'rounded-md',
|
||||||
|
'bg-transparent text-neutral dark-theme:text-white',
|
||||||
|
'border-0',
|
||||||
|
// disabled
|
||||||
|
{ 'opacity-60 cursor-default': props.disabled }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
label: {
|
||||||
|
class:
|
||||||
|
// Align with MultiSelect labelContainer spacing
|
||||||
|
'flex-1 flex items-center overflow-hidden whitespace-nowrap pl-4 py-2 outline-none'
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
class:
|
||||||
|
// Right chevron touch area
|
||||||
|
'flex shrink-0 items-center justify-center px-3 py-2'
|
||||||
|
},
|
||||||
|
overlay: {
|
||||||
|
class: [
|
||||||
|
// dropdown panel
|
||||||
|
'mt-2 bg-white dark-theme:bg-zinc-800 text-neutral dark-theme:text-white rounded-lg'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
list: {
|
||||||
|
class:
|
||||||
|
// Same list tone/size as MultiSelect
|
||||||
|
'flex flex-col gap-1 p-0 list-none border-none text-xs'
|
||||||
|
},
|
||||||
|
option: ({
|
||||||
|
context
|
||||||
|
}: SelectPassThroughMethodOptions<{ name: string; value: string }>) => ({
|
||||||
|
class: [
|
||||||
|
// Row layout
|
||||||
|
'flex items-center justify-between gap-3 px-3 py-2',
|
||||||
|
'hover:bg-neutral-100/50 dark-theme:hover:bg-zinc-700/50',
|
||||||
|
// Selected state + check icon
|
||||||
|
{ 'bg-neutral-100/50 dark-theme:bg-zinc-700/50': context.selected }
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
optionLabel: {
|
||||||
|
class: 'truncate'
|
||||||
|
},
|
||||||
|
optionGroupLabel: {
|
||||||
|
class:
|
||||||
|
'px-3 py-2 text-xs uppercase tracking-wide text-zinc-500 dark-theme:text-zinc-400'
|
||||||
|
},
|
||||||
|
emptyMessage: {
|
||||||
|
class: 'px-3 py-2 text-sm text-zinc-500 dark-theme:text-zinc-400'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
205
src/components/widget/ModelSelector.vue
Normal file
205
src/components/widget/ModelSelector.vue
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<template>
|
||||||
|
<BaseWidgetLayout :content-title="$t('Checkpoints')">
|
||||||
|
<template #leftPanel>
|
||||||
|
<LeftSidePanel v-model="selectedNavItem" :nav-items="tempNavigation">
|
||||||
|
<template #header-icon>
|
||||||
|
<i-lucide:puzzle class="text-neutral" />
|
||||||
|
</template>
|
||||||
|
<template #header-title>
|
||||||
|
<span class="text-neutral text-base">{{ t('g.title') }}</span>
|
||||||
|
</template>
|
||||||
|
</LeftSidePanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #header>
|
||||||
|
<SearchBox v-model:="searchQuery" class="max-w-[384px]" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #header-right-area>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<IconTextButton type="primary" label="Upload Model" @click="() => {}">
|
||||||
|
<template #icon>
|
||||||
|
<i-lucide:upload />
|
||||||
|
</template>
|
||||||
|
</IconTextButton>
|
||||||
|
<MoreButton>
|
||||||
|
<template #default="{ close }">
|
||||||
|
<IconTextButton
|
||||||
|
type="secondary"
|
||||||
|
label="Settings"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i-lucide:download />
|
||||||
|
</template>
|
||||||
|
</IconTextButton>
|
||||||
|
<IconTextButton
|
||||||
|
type="primary"
|
||||||
|
label="Profile"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i-lucide:scroll />
|
||||||
|
</template>
|
||||||
|
</IconTextButton>
|
||||||
|
</template>
|
||||||
|
</MoreButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #contentFilter>
|
||||||
|
<div class="relative px-6 pt-2 pb-4 flex gap-2">
|
||||||
|
<MultiSelect
|
||||||
|
v-model="selectedFrameworks"
|
||||||
|
label="Select Frameworks"
|
||||||
|
:options="frameworkOptions"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
v-model="selectedProjects"
|
||||||
|
label="Select Projects"
|
||||||
|
:options="projectOptions"
|
||||||
|
/>
|
||||||
|
<SingleSelect
|
||||||
|
v-model="selectedSort"
|
||||||
|
label="Sorting Type"
|
||||||
|
:options="sortOptions"
|
||||||
|
class="w-[135px]"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<i-lucide:filter />
|
||||||
|
</template>
|
||||||
|
</SingleSelect>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<!-- Card Examples -->
|
||||||
|
<!-- <div class="min-h-0 px-6 py-4 overflow-y-auto scrollbar-hide"> -->
|
||||||
|
<!-- <h2 class="text-xxl py-4 pt-0 m-0">{{ $t('Checkpoints') }}</h2> -->
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<CardContainer
|
||||||
|
v-for="i in 100"
|
||||||
|
:key="i"
|
||||||
|
ratio="square"
|
||||||
|
:max-width="480"
|
||||||
|
:min-width="230"
|
||||||
|
>
|
||||||
|
<template #top>
|
||||||
|
<CardTop ratio="landscape">
|
||||||
|
<template #default>
|
||||||
|
<div class="w-full h-full bg-blue-500"></div>
|
||||||
|
</template>
|
||||||
|
<template #top-right>
|
||||||
|
<IconButton
|
||||||
|
class="!bg-white !text-neutral-900"
|
||||||
|
@click="() => {}"
|
||||||
|
>
|
||||||
|
<i-lucide:info />
|
||||||
|
</IconButton>
|
||||||
|
</template>
|
||||||
|
<template #bottom-right>
|
||||||
|
<SquareChip label="png" />
|
||||||
|
<SquareChip label="1.2 MB" />
|
||||||
|
<SquareChip label="LoRA">
|
||||||
|
<template #icon>
|
||||||
|
<i-lucide:folder />
|
||||||
|
</template>
|
||||||
|
</SquareChip>
|
||||||
|
</template>
|
||||||
|
</CardTop>
|
||||||
|
</template>
|
||||||
|
<template #bottom>
|
||||||
|
<CardBottom></CardBottom>
|
||||||
|
</template>
|
||||||
|
</CardContainer>
|
||||||
|
</div>
|
||||||
|
<!-- </div> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #rightPanel>
|
||||||
|
<RightSidePanel></RightSidePanel>
|
||||||
|
</template>
|
||||||
|
</BaseWidgetLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { provide, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import IconButton from '@/components/button/IconButton.vue'
|
||||||
|
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||||
|
import MoreButton from '@/components/button/MoreButton.vue'
|
||||||
|
import CardBottom from '@/components/card/CardBottom.vue'
|
||||||
|
import CardContainer from '@/components/card/CardContainer.vue'
|
||||||
|
import CardTop from '@/components/card/CardTop.vue'
|
||||||
|
import SquareChip from '@/components/chip/SquareChip.vue'
|
||||||
|
import MultiSelect from '@/components/input/MultiSelect.vue'
|
||||||
|
import SearchBox from '@/components/input/SearchBox.vue'
|
||||||
|
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||||
|
import BaseWidgetLayout from '@/components/widget/layout/BaseWidgetLayout.vue'
|
||||||
|
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||||
|
import RightSidePanel from '@/components/widget/panel/RightSidePanel.vue'
|
||||||
|
import { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||||
|
import { OnCloseKey } from '@/types/widgetTypes'
|
||||||
|
|
||||||
|
const frameworkOptions = ref([
|
||||||
|
{ name: 'Vue', value: 'vue' },
|
||||||
|
{ name: 'React', value: 'react' },
|
||||||
|
{ name: 'Angular', value: 'angular' },
|
||||||
|
{ name: 'Svelte', value: 'svelte' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const projectOptions = ref([
|
||||||
|
{ name: 'Project A', value: 'proj-a' },
|
||||||
|
{ name: 'Project B', value: 'proj-b' },
|
||||||
|
{ name: 'Project C', value: 'proj-c' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const sortOptions = ref([
|
||||||
|
{ name: 'Popular', value: 'popular' },
|
||||||
|
{ name: 'Latest', value: 'latest' },
|
||||||
|
{ name: 'A → Z', value: 'az' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const tempNavigation = ref<(NavItemData | NavGroupData)[]>([
|
||||||
|
{ id: 'installed', label: 'Installed' },
|
||||||
|
{
|
||||||
|
title: 'TAGS',
|
||||||
|
items: [
|
||||||
|
{ id: 'tag-sd15', label: 'SD 1.5' },
|
||||||
|
{ id: 'tag-sdxl', label: 'SDXL' },
|
||||||
|
{ id: 'tag-utility', label: 'Utility' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CATEGORIES',
|
||||||
|
items: [
|
||||||
|
{ id: 'cat-models', label: 'Models' },
|
||||||
|
{ id: 'cat-nodes', label: 'Nodes' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const { onClose } = defineProps<{
|
||||||
|
onClose: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
provide(OnCloseKey, onClose)
|
||||||
|
|
||||||
|
const searchQuery = ref<string>('')
|
||||||
|
const selectedFrameworks = ref([])
|
||||||
|
const selectedProjects = ref([])
|
||||||
|
const selectedSort = ref<string>('popular')
|
||||||
|
|
||||||
|
const selectedNavItem = ref<string | null>('installed')
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="base-widget-layout rounded-2xl overflow-hidden relative bg-zinc-100 dark-theme:bg-zinc-800"
|
class="base-widget-layout rounded-2xl overflow-hidden relative bg-zinc-50 dark-theme:bg-zinc-800"
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
v-show="!isRightPanelOpen && hasRightPanel"
|
v-show="!isRightPanelOpen && hasRightPanel"
|
||||||
@@ -45,8 +45,13 @@
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end gap-2 min-w-20">
|
<slot name="header-right-area"></slot>
|
||||||
<slot name="header-right-area"></slot>
|
<div
|
||||||
|
class="flex justify-end gap-2 w-0"
|
||||||
|
:class="
|
||||||
|
hasRightPanel && !isRightPanelOpen ? 'min-w-18' : 'min-w-8'
|
||||||
|
"
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
v-if="isRightPanelOpen && hasRightPanel"
|
v-if="isRightPanelOpen && hasRightPanel"
|
||||||
@click="toggleRightPanel"
|
@click="toggleRightPanel"
|
||||||
@@ -55,18 +60,24 @@
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="flex-1">
|
|
||||||
<slot name="content"></slot>
|
<main class="flex flex-col flex-1 min-h-0">
|
||||||
|
<!-- Fallback title bar when no leftPanel is provided -->
|
||||||
|
<slot name="contentFilter"></slot>
|
||||||
|
<h2 v-if="!$slots.leftPanel" class="text-xxl px-6 pt-2 pb-6 m-0">
|
||||||
|
{{ contentTitle }}
|
||||||
|
</h2>
|
||||||
|
<div class="min-h-0 px-6 pt-0 pb-10 overflow-y-auto scrollbar-hide">
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<Transition name="slide-panel-right">
|
<aside
|
||||||
<aside
|
v-if="hasRightPanel && isRightPanelOpen"
|
||||||
v-if="hasRightPanel && isRightPanelOpen"
|
class="w-1/4 min-w-40 max-w-80"
|
||||||
class="w-1/4 min-w-40 max-w-80"
|
>
|
||||||
>
|
<slot name="rightPanel"></slot>
|
||||||
<slot name="rightPanel"></slot>
|
</aside>
|
||||||
</aside>
|
|
||||||
</Transition>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,10 +87,14 @@
|
|||||||
import { useBreakpoints } from '@vueuse/core'
|
import { useBreakpoints } from '@vueuse/core'
|
||||||
import { computed, inject, ref, useSlots, watch } from 'vue'
|
import { computed, inject, ref, useSlots, watch } from 'vue'
|
||||||
|
|
||||||
import IconButton from '@/components/custom/button/IconButton.vue'
|
import IconButton from '@/components/button/IconButton.vue'
|
||||||
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
|
import { OnCloseKey } from '@/types/widgetTypes'
|
||||||
|
|
||||||
const BREAKPOINTS = { sm: 480 }
|
const { contentTitle } = defineProps<{
|
||||||
|
contentTitle: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const BREAKPOINTS = { md: 880 }
|
||||||
const PANEL_SIZES = {
|
const PANEL_SIZES = {
|
||||||
width: 'w-1/3',
|
width: 'w-1/3',
|
||||||
minWidth: 'min-w-40',
|
minWidth: 'min-w-40',
|
||||||
@@ -90,7 +105,7 @@ const slots = useSlots()
|
|||||||
const closeDialog = inject(OnCloseKey, () => {})
|
const closeDialog = inject(OnCloseKey, () => {})
|
||||||
|
|
||||||
const breakpoints = useBreakpoints(BREAKPOINTS)
|
const breakpoints = useBreakpoints(BREAKPOINTS)
|
||||||
const notMobile = breakpoints.greater('sm')
|
const notMobile = breakpoints.greater('md')
|
||||||
|
|
||||||
const isLeftPanelOpen = ref<boolean>(true)
|
const isLeftPanelOpen = ref<boolean>(true)
|
||||||
const isRightPanelOpen = ref<boolean>(false)
|
const isRightPanelOpen = ref<boolean>(false)
|
||||||
@@ -160,17 +175,4 @@ const toggleRightPanel = () => {
|
|||||||
.slide-panel-leave-to {
|
.slide-panel-leave-to {
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slide transition for right panel */
|
|
||||||
.slide-panel-right-enter-active,
|
|
||||||
.slide-panel-right-leave-active {
|
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
will-change: transform;
|
|
||||||
backface-visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-panel-right-enter-from,
|
|
||||||
.slide-panel-right-leave-to {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -9,15 +9,20 @@
|
|||||||
role="button"
|
role="button"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<i-lucide:folder class="text-xs text-neutral" />
|
<i-lucide:folder v-if="hasFolderIcon" class="text-xs text-neutral" />
|
||||||
<span>
|
<span class="flex items-center">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { active, onClick } = defineProps<{
|
const {
|
||||||
|
hasFolderIcon = true,
|
||||||
|
active,
|
||||||
|
onClick
|
||||||
|
} = defineProps<{
|
||||||
|
hasFolderIcon?: boolean
|
||||||
active?: boolean
|
active?: boolean
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}>()
|
}>()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<h3
|
<h3
|
||||||
class="m-0 px-3 py-0 pt-5 text-sm font-bold uppercase text-neutral-400 dark-theme:text-neutral-400"
|
class="m-0 px-3 py-0 pt-5 text-xxs font-bold uppercase text-neutral-400 dark-theme:text-neutral-400"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h3>
|
</h3>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<slot name="header-title"></slot>
|
<slot name="header-title"></slot>
|
||||||
</PanelHeader>
|
</PanelHeader>
|
||||||
|
|
||||||
<nav class="flex-1 px-3 py-4 flex flex-col gap-2">
|
<nav class="flex-1 px-3 py-4 flex flex-col gap-1">
|
||||||
<template v-for="(item, index) in navItems" :key="index">
|
<template v-for="(item, index) in navItems" :key="index">
|
||||||
<div v-if="'items' in item" class="flex flex-col gap-2">
|
<div v-if="'items' in item" class="flex flex-col gap-2">
|
||||||
<NavTitle :title="item.title" />
|
<NavTitle :title="item.title" />
|
||||||
@@ -36,10 +36,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
|
import NavItem from '@/components/widget/nav/NavItem.vue'
|
||||||
|
import NavTitle from '@/components/widget/nav/NavTitle.vue'
|
||||||
|
import { NavGroupData, NavItemData } from '@/types/navTypes'
|
||||||
|
|
||||||
import NavItem from '../nav/NavItem.vue'
|
|
||||||
import NavTitle from '../nav/NavTitle.vue'
|
|
||||||
import PanelHeader from './PanelHeader.vue'
|
import PanelHeader from './PanelHeader.vue'
|
||||||
|
|
||||||
const { navItems = [], modelValue } = defineProps<{
|
const { navItems = [], modelValue } = defineProps<{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import ModelSelector from '@/components/custom/widget/ModelSelector.vue'
|
import ModelSelector from '@/components/widget/ModelSelector.vue'
|
||||||
import { useDialogService } from '@/services/dialogService'
|
import { useDialogService } from '@/services/dialogService'
|
||||||
import { useDialogStore } from '@/stores/dialogStore'
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
|
|
||||||
|
|||||||
@@ -439,12 +439,17 @@ export const useDialogService = () => {
|
|||||||
}) {
|
}) {
|
||||||
const layoutDefaultProps: DialogComponentProps = {
|
const layoutDefaultProps: DialogComponentProps = {
|
||||||
headless: true,
|
headless: true,
|
||||||
unstyled: true,
|
|
||||||
modal: true,
|
modal: true,
|
||||||
closable: false,
|
closable: false,
|
||||||
pt: {
|
pt: {
|
||||||
mask: {
|
root: {
|
||||||
class: 'bg-black bg-opacity-40'
|
class: 'rounded-2xl overflow-hidden'
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
class: '!p-0 hidden'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
class: '!p-0 !m-0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/types/buttonTypes.ts
Normal file
42
src/types/buttonTypes.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
|
||||||
|
export interface BaseButtonProps {
|
||||||
|
size?: 'sm' | 'md'
|
||||||
|
type?: 'primary' | 'secondary' | 'transparent'
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getButtonSizeClasses = (size: BaseButtonProps['size'] = 'md') => {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'px-2 py-1.5 text-xs',
|
||||||
|
md: 'px-2.5 py-2 text-sm'
|
||||||
|
}
|
||||||
|
return sizeClasses[size]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getButtonTypeClasses = (
|
||||||
|
type: BaseButtonProps['type'] = 'primary'
|
||||||
|
) => {
|
||||||
|
const typeClasses = {
|
||||||
|
primary:
|
||||||
|
'bg-neutral-900 text-white dark-theme:bg-white dark-theme:text-neutral-900',
|
||||||
|
secondary:
|
||||||
|
'bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white',
|
||||||
|
transparent: 'bg-transparent text-neutral-600 dark-theme:text-neutral-400'
|
||||||
|
}
|
||||||
|
return typeClasses[type]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getIconButtonSizeClasses = (
|
||||||
|
size: BaseButtonProps['size'] = 'md'
|
||||||
|
) => {
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: 'w-6 h-6 text-xs !rounded-md',
|
||||||
|
md: 'w-8 h-8 text-sm'
|
||||||
|
}
|
||||||
|
return sizeClasses[size]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBaseButtonClasses = () => {
|
||||||
|
return 'flex items-center justify-center flex-shrink-0 outline-none border-none rounded-lg cursor-pointer transition-all duration-200'
|
||||||
|
}
|
||||||
2
src/types/custom_components/index.d.ts
vendored
2
src/types/custom_components/index.d.ts
vendored
@@ -1,2 +0,0 @@
|
|||||||
export * from './navTypes'
|
|
||||||
export * from './widgetTypes'
|
|
||||||
@@ -8,6 +8,7 @@ export default {
|
|||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
fontSize: {
|
fontSize: {
|
||||||
|
xxs: '0.625rem',
|
||||||
xs: '0.75rem',
|
xs: '0.75rem',
|
||||||
sm: '0.875rem',
|
sm: '0.875rem',
|
||||||
base: '1rem',
|
base: '1rem',
|
||||||
@@ -82,7 +83,7 @@ export default {
|
|||||||
100: '#8282821a',
|
100: '#8282821a',
|
||||||
200: '#e4e4e7',
|
200: '#e4e4e7',
|
||||||
300: '#d4d4d8',
|
300: '#d4d4d8',
|
||||||
400: '#a1a1aa',
|
400: '#A1A3AE',
|
||||||
500: '#71717a',
|
500: '#71717a',
|
||||||
600: '#52525b',
|
600: '#52525b',
|
||||||
700: '#38393b',
|
700: '#38393b',
|
||||||
|
|||||||
Reference in New Issue
Block a user