Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Prev Previous commit
Next Next commit
[feature] Add model selector dialog integration and custom component …
…types
  • Loading branch information
viva-jinyi committed Aug 18, 2025
commit ad89567b36a3ba57f25c974c6f6174f0a7461493
14 changes: 14 additions & 0 deletions src/components/custom/button/IconButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<button
class="flex justify-center items-center outline-none border-none p-0 bg-white text-neutral-950 dark-theme:bg-neutral-700 dark-theme:text-white w-8 h-8 rounded-lg cursor-pointer"
@click="onClick"
>
<slot></slot>
</button>
</template>

<script setup lang="ts">
const { onClick } = defineProps<{
onClick: () => void
}>()
</script>
64 changes: 64 additions & 0 deletions src/components/custom/widget/ModelSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<template>
<BaseWidgetLayout>
<template #leftPanel>
<LeftSidePanel :nav-items="temp_navigation">
<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 RightSidePanel from './panel/RightSidePanel.vue'
const { t } = useI18n()
const { onClose } = defineProps<{
onClose: () => void
}>()
provide(OnCloseKey, onClose)
const temp_navigation = 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' }
]
}
])
</script>
49 changes: 49 additions & 0 deletions src/components/custom/widget/layout/BaseWidgetLayout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<template>
<div class="base-widget-layout rounded-2xl overflow-hidden relative">
<IconButton class="absolute top-4 right-6" @click="closeDialog">
<i class="pi pi-times text-sm"></i>
</IconButton>
<div class="flex w-full h-full">
<nav v-if="$slots.leftPanel">
<slot name="leftPanel"></slot>
</nav>

<div class="flex-1 flex bg-neutral-50 dark-theme:bg-neutral-900">
<div class="flex-1 flex flex-col">
<header v-if="$slots.header" class="w-full h-16 px-6 py-4">
<slot name="header"> </slot>
</header>
<main class="flex-1">
<slot name="content"></slot>
</main>
</div>

<!-- <aside v-if="$slots.rightPanel">
<slot name="rightPanel"></slot>
</aside> -->
</div>
</div>
</div>
</template>

<script setup lang="ts">
import { inject } from 'vue'
import { OnCloseKey } from '@/types/custom_components/widgetTypes'
const closeDialog = inject(OnCloseKey, () => {})
</script>
<style scoped>
.base-widget-layout {
height: 80vh;
width: 90vw;
max-width: 1280px;
aspect-ratio: 20/13;
}
@media (min-width: 1450px) {
.base-widget-layout {
max-width: 1724px;
}
}
</style>
21 changes: 21 additions & 0 deletions src/components/custom/widget/nav/NavItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<template>
<div
class="flex items-center gap-2 px-4 py-2 text-xs rounded-md transition-colors cursor-pointer"
:class="
active
? 'bg-neutral-100 dark-theme:bg-neutral-600 text-neutral'
: 'text-neutral hover:bg-neutral-50 hover:dark-theme:bg-neutral-700'
"
>
<i-lucide:folder class="text-xs text-neutral" />
<span>
<slot></slot>
</span>
</div>
</template>

<script setup lang="ts">
defineProps<{
active?: boolean
}>()
</script>
13 changes: 13 additions & 0 deletions src/components/custom/widget/nav/NavTitle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<h3
class="m-0 px-3 py-0 pt-5 text-sm font-bold uppercase text-neutral-400 dark-theme:text-neutral-400"
>
{{ title }}
</h3>
</template>

<script setup lang="ts">
const { title } = defineProps<{
title: string
}>()
</script>
67 changes: 67 additions & 0 deletions src/components/custom/widget/panel/LeftSidePanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<template>
<div class="flex flex-col h-full w-56 bg-white dark-theme:bg-neutral-800">
<PanelHeader>
<template #icon>
<slot name="header-icon"></slot>
</template>
<slot name="header-title"></slot>
</PanelHeader>

<nav class="flex-1 px-3 py-4 flex flex-col gap-2">
<template v-for="(item, index) in navItems" :key="index">
<div v-if="'items' in item" class="flex flex-col gap-2">
<NavTitle :title="item.title" />
<NavItem
v-for="subItem in item.items"
:key="subItem.id"
:active="activeItem === subItem.id"
@click="activeItem = subItem.id"
>
{{ subItem.label }}
</NavItem>
</div>
<div v-else class="flex flex-col gap-2">
<NavItem
:active="activeItem === item.id"
@click="activeItem = item.id"
>
{{ item.label }}
</NavItem>
</div>
</template>
</nav>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { NavGroupData, NavItemData } from '@/types/custom_components/navTypes'
import NavItem from '../nav/NavItem.vue'
import NavTitle from '../nav/NavTitle.vue'
import PanelHeader from './PanelHeader.vue'
const { navItems = [] } = defineProps<{
navItems: (NavItemData | NavGroupData)[]
}>()
const getFirstItemId = () => {
if (!navItems || navItems.length === 0) {
return null
}
const firstEntry = navItems[0]
if ('items' in firstEntry && firstEntry.items.length > 0) {
return firstEntry.items[0].id
}
if ('id' in firstEntry) {
return firstEntry.id
}
return null // 해당하는 아이템이 없는 경우
}
const activeItem = ref(getFirstItemId())
</script>
12 changes: 12 additions & 0 deletions src/components/custom/widget/panel/PanelHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<header class="flex items-center justify-between h-16 px-6">
<div class="flex items-center gap-2 pl-1">
<slot name="icon">
<i-lucide:puzzle class="text-neutral text-base" />
</slot>
<h2 class="font-bold text-base text-neutral">
<slot></slot>
</h2>
</div>
</header>
</template>
7 changes: 7 additions & 0 deletions src/components/custom/widget/panel/RightSidePanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<template>
<div class="w-80 h-full pl-4 pr-6 pb-8 bg-white dark-theme:bg-neutral-800">
<slot></slot>
</div>
</template>

<script setup lang="ts"></script>
18 changes: 10 additions & 8 deletions src/components/dialog/GlobalDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
:aria-labelledby="item.key"
>
<template #header>
<component
:is="item.headerComponent"
v-if="item.headerComponent"
:id="item.key"
/>
<h3 v-else :id="item.key">
{{ item.title || ' ' }}
</h3>
<div v-if="!item.dialogComponentProps?.headless">
<component
:is="item.headerComponent"
v-if="item.headerComponent"
:id="item.key"
/>
<h3 v-else :id="item.key">
{{ item.title || ' ' }}
</h3>
</div>
</template>

<component
Expand Down
28 changes: 28 additions & 0 deletions src/composables/useModelSelectorDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ModelSelector from '@/components/custom/widget/ModelSelector.vue'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'

const DIALOG_KEY = 'global-model-selector'

export const useModelSelectorDialog = () => {
const dialogService = useDialogService()
const dialogStore = useDialogStore()

function hide() {
dialogStore.closeDialog({ key: DIALOG_KEY })
}

function show() {
dialogService.showLayoutDialog({
key: DIALOG_KEY,
component: ModelSelector,
props: {
onClose: hide
}
})
}

return {
show
}
}
12 changes: 9 additions & 3 deletions src/services/dialogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,18 +434,24 @@ export const useDialogService = () => {
function showLayoutDialog(options: {
key: string
component: Component
props?: Record<string, any>
props: { onClose: () => void }
dialogComponentProps?: DialogComponentProps
}) {
const layoutDefaultProps: DialogComponentProps = {
headless: true,
unstyled: true,
modal: true
modal: true,
closable: false,
pt: {
mask: {
class: 'bg-black bg-opacity-40'
}
}
}

return dialogStore.showDialog({
...options,
dialogComponentProps: merge(
{},
layoutDefaultProps,
options.dialogComponentProps
)
Expand Down
1 change: 1 addition & 0 deletions src/stores/dialogStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface CustomDialogComponentProps {
closeOnEscape?: boolean
dismissableMask?: boolean
unstyled?: boolean
headless?: true
}

export type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
Expand Down
2 changes: 2 additions & 0 deletions src/types/custom_components/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './navTypes'
export * from './widgetTypes'
9 changes: 9 additions & 0 deletions src/types/custom_components/navTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface NavItemData {
id: string
label: string
}

export interface NavGroupData {
title: string
items: NavItemData[]
}
3 changes: 3 additions & 0 deletions src/types/custom_components/widgetTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { InjectionKey } from 'vue'

export const OnCloseKey: InjectionKey<() => void> = Symbol()