@j-solution/components 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/assets/jwms-portal-frontend-CwxPfHfa.css +1 -0
- package/assets/styles/j-components.css +1 -1
- package/assets/styles/themes.css +107 -0
- package/components/atoms/JAvatar.vue.cjs +1 -1
- package/components/atoms/JAvatar.vue.cjs.map +1 -1
- package/components/atoms/JAvatar.vue.js +10 -7
- package/components/atoms/JAvatar.vue.js.map +1 -1
- package/components/atoms/JBadge.vue.cjs +1 -1
- package/components/atoms/JBadge.vue.cjs.map +1 -1
- package/components/atoms/JBadge.vue.js +7 -6
- package/components/atoms/JBadge.vue.js.map +1 -1
- package/components/atoms/JButton.vue.cjs +1 -1
- package/components/atoms/JButton.vue.cjs.map +1 -1
- package/components/atoms/JButton.vue.js +5 -5
- package/components/atoms/JButton.vue.js.map +1 -1
- package/components/atoms/JDatepicker.vue.cjs +1 -1
- package/components/atoms/JDatepicker.vue.cjs.map +1 -1
- package/components/atoms/JDatepicker.vue.js +10 -10
- package/components/atoms/JDatepicker.vue.js.map +1 -1
- package/components/atoms/JEditor.vue.cjs +1 -1
- package/components/atoms/JEditor.vue.js +1 -1
- package/components/atoms/JEditor.vue2.cjs +1 -1
- package/components/atoms/JEditor.vue2.cjs.map +1 -1
- package/components/atoms/JEditor.vue2.js +31 -17
- package/components/atoms/JEditor.vue2.js.map +1 -1
- package/components/atoms/JGrid.vue.cjs +1 -1
- package/components/atoms/JGrid.vue.js +2 -2
- package/components/atoms/JGrid.vue2.cjs +1 -1
- package/components/atoms/JGrid.vue2.cjs.map +1 -1
- package/components/atoms/JGrid.vue2.js +45 -33
- package/components/atoms/JGrid.vue2.js.map +1 -1
- package/components/atoms/JIcon.vue.cjs +1 -1
- package/components/atoms/JIcon.vue.cjs.map +1 -1
- package/components/atoms/JIcon.vue.js +14 -13
- package/components/atoms/JIcon.vue.js.map +1 -1
- package/components/atoms/JKbd.vue.cjs +1 -1
- package/components/atoms/JKbd.vue.cjs.map +1 -1
- package/components/atoms/JKbd.vue.js +13 -10
- package/components/atoms/JKbd.vue.js.map +1 -1
- package/components/atoms/JLabel.vue.cjs +1 -1
- package/components/atoms/JLabel.vue.cjs.map +1 -1
- package/components/atoms/JLabel.vue.js +4 -4
- package/components/atoms/JLabel.vue.js.map +1 -1
- package/components/atoms/JLink.vue.cjs +1 -1
- package/components/atoms/JLink.vue.cjs.map +1 -1
- package/components/atoms/JLink.vue.js +5 -5
- package/components/atoms/JLink.vue.js.map +1 -1
- package/components/atoms/JPreview.vue.cjs +1 -1
- package/components/atoms/JPreview.vue.js +2 -2
- package/components/atoms/JPreview.vue2.cjs +1 -1
- package/components/atoms/JPreview.vue2.cjs.map +1 -1
- package/components/atoms/JPreview.vue2.js +33 -20
- package/components/atoms/JPreview.vue2.js.map +1 -1
- package/components/atoms/JProgress.vue.cjs +1 -1
- package/components/atoms/JProgress.vue.cjs.map +1 -1
- package/components/atoms/JProgress.vue.js +15 -9
- package/components/atoms/JProgress.vue.js.map +1 -1
- package/components/atoms/JRadio.vue.cjs +1 -1
- package/components/atoms/JRadio.vue.cjs.map +1 -1
- package/components/atoms/JRadio.vue.js +1 -1
- package/components/atoms/JRadio.vue.js.map +1 -1
- package/components/atoms/JSearchCombo.vue.cjs +1 -1
- package/components/atoms/JSearchCombo.vue.cjs.map +1 -1
- package/components/atoms/JSearchCombo.vue.js +38 -37
- package/components/atoms/JSearchCombo.vue.js.map +1 -1
- package/components/atoms/JSpinner.vue.cjs +1 -1
- package/components/atoms/JSpinner.vue.cjs.map +1 -1
- package/components/atoms/JSpinner.vue.js +8 -7
- package/components/atoms/JSpinner.vue.js.map +1 -1
- package/components/atoms/JSplitter.vue.cjs +1 -1
- package/components/atoms/JSplitter.vue.cjs.map +1 -1
- package/components/atoms/JSplitter.vue.js +32 -27
- package/components/atoms/JSplitter.vue.js.map +1 -1
- package/components/atoms/JTooltip.vue.cjs +1 -1
- package/components/atoms/JTooltip.vue.cjs.map +1 -1
- package/components/atoms/JTooltip.vue.js +18 -15
- package/components/atoms/JTooltip.vue.js.map +1 -1
- package/components/examples/ExampleCrudPage.vue.cjs +2 -0
- package/components/examples/ExampleCrudPage.vue.cjs.map +1 -0
- package/components/examples/ExampleCrudPage.vue.js +358 -0
- package/components/examples/ExampleCrudPage.vue.js.map +1 -0
- package/components/examples/ExampleCrudPage.vue2.cjs +2 -0
- package/components/examples/ExampleCrudPage.vue2.cjs.map +1 -0
- package/components/examples/ExampleCrudPage.vue2.js +5 -0
- package/components/examples/ExampleCrudPage.vue2.js.map +1 -0
- package/components/examples/ExampleTabMappingPage.vue.cjs +2 -0
- package/components/examples/ExampleTabMappingPage.vue.cjs.map +1 -0
- package/components/examples/ExampleTabMappingPage.vue.js +522 -0
- package/components/examples/ExampleTabMappingPage.vue.js.map +1 -0
- package/components/examples/ExampleTabMappingPage.vue2.cjs +2 -0
- package/components/examples/ExampleTabMappingPage.vue2.cjs.map +1 -0
- package/components/examples/ExampleTabMappingPage.vue2.js +5 -0
- package/components/examples/ExampleTabMappingPage.vue2.js.map +1 -0
- package/components/molecules/JBreadcrumb.vue.cjs +1 -1
- package/components/molecules/JBreadcrumb.vue.cjs.map +1 -1
- package/components/molecules/JBreadcrumb.vue.js +3 -3
- package/components/molecules/JBreadcrumb.vue.js.map +1 -1
- package/components/molecules/JFormField.vue.cjs +1 -1
- package/components/molecules/JFormField.vue.cjs.map +1 -1
- package/components/molecules/JFormField.vue.js +26 -24
- package/components/molecules/JFormField.vue.js.map +1 -1
- package/components/molecules/JTabs.vue.cjs +1 -1
- package/components/molecules/JTabs.vue.js +1 -1
- package/components/molecules/JTabs.vue2.cjs +1 -1
- package/components/molecules/JTabs.vue2.cjs.map +1 -1
- package/components/molecules/JTabs.vue2.js +7 -7
- package/components/molecules/JTabs.vue2.js.map +1 -1
- package/components/molecules/JTitlebar.vue.cjs +1 -1
- package/components/molecules/JTitlebar.vue.cjs.map +1 -1
- package/components/molecules/JTitlebar.vue.js +35 -36
- package/components/molecules/JTitlebar.vue.js.map +1 -1
- package/components/organisms/JFilterBar.vue.cjs +1 -1
- package/components/organisms/JFilterBar.vue.cjs.map +1 -1
- package/components/organisms/JFilterBar.vue.js +5 -5
- package/components/organisms/JFilterBar.vue.js.map +1 -1
- package/components/organisms/JHeader.vue.cjs +1 -1
- package/components/organisms/JHeader.vue.cjs.map +1 -1
- package/components/organisms/JHeader.vue.js +25 -23
- package/components/organisms/JHeader.vue.js.map +1 -1
- package/components/organisms/JModal.vue.cjs +1 -1
- package/components/organisms/JModal.vue.cjs.map +1 -1
- package/components/organisms/JModal.vue.js +30 -27
- package/components/organisms/JModal.vue.js.map +1 -1
- package/components/organisms/JSidebarAdvanced.vue.cjs +1 -1
- package/components/organisms/JSidebarAdvanced.vue.js +7 -7
- package/components/organisms/JSidebarAdvanced.vue2.cjs +1 -1
- package/components/organisms/JSidebarAdvanced.vue2.cjs.map +1 -1
- package/components/organisms/JSidebarAdvanced.vue2.js +40 -40
- package/components/organisms/JSidebarAdvanced.vue2.js.map +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.cjs +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.cjs.map +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.js +83 -63
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.js.map +1 -1
- package/components/organisms/JSidebarSimple.vue.cjs +1 -1
- package/components/organisms/JSidebarSimple.vue.js +2 -2
- package/components/organisms/JSidebarSimple.vue2.cjs +1 -1
- package/components/organisms/JSidebarSimple.vue2.cjs.map +1 -1
- package/components/organisms/JSidebarSimple.vue2.js +2 -2
- package/components/organisms/JSidebarSimple.vue2.js.map +1 -1
- package/components/shadcn/AccordionTrigger.vue.cjs +1 -1
- package/components/shadcn/AccordionTrigger.vue.cjs.map +1 -1
- package/components/shadcn/AccordionTrigger.vue.js +3 -3
- package/components/shadcn/AccordionTrigger.vue.js.map +1 -1
- package/components/shadcn/CardContent.vue.cjs +1 -1
- package/components/shadcn/CardContent.vue.cjs.map +1 -1
- package/components/shadcn/CardContent.vue.js +1 -1
- package/components/shadcn/CardContent.vue.js.map +1 -1
- package/components/shadcn/CardDescription.vue.cjs +1 -1
- package/components/shadcn/CardDescription.vue.cjs.map +1 -1
- package/components/shadcn/CardDescription.vue.js +1 -1
- package/components/shadcn/CardDescription.vue.js.map +1 -1
- package/components/shadcn/CardFooter.vue.cjs +1 -1
- package/components/shadcn/CardFooter.vue.cjs.map +1 -1
- package/components/shadcn/CardFooter.vue.js +7 -7
- package/components/shadcn/CardFooter.vue.js.map +1 -1
- package/components/shadcn/CardHeader.vue.cjs +1 -1
- package/components/shadcn/CardHeader.vue.cjs.map +1 -1
- package/components/shadcn/CardHeader.vue.js +8 -8
- package/components/shadcn/CardHeader.vue.js.map +1 -1
- package/components/shadcn/CardTitle.vue.cjs +1 -1
- package/components/shadcn/CardTitle.vue.cjs.map +1 -1
- package/components/shadcn/CardTitle.vue.js +5 -5
- package/components/shadcn/CardTitle.vue.js.map +1 -1
- package/components/shadcn/Input.vue.cjs +1 -1
- package/components/shadcn/Input.vue.cjs.map +1 -1
- package/components/shadcn/Input.vue.js +1 -1
- package/components/shadcn/Input.vue.js.map +1 -1
- package/components/shadcn/SelectTrigger.vue.cjs +1 -1
- package/components/shadcn/SelectTrigger.vue.cjs.map +1 -1
- package/components/shadcn/SelectTrigger.vue.js +2 -2
- package/components/shadcn/SelectTrigger.vue.js.map +1 -1
- package/components/shadcn/Switch.vue.cjs +1 -1
- package/components/shadcn/Switch.vue.cjs.map +1 -1
- package/components/shadcn/Switch.vue.js +2 -2
- package/components/shadcn/Switch.vue.js.map +1 -1
- package/components/shadcn/TabsList.vue.cjs +1 -1
- package/components/shadcn/TabsList.vue.cjs.map +1 -1
- package/components/shadcn/TabsList.vue.js +1 -1
- package/components/shadcn/TabsList.vue.js.map +1 -1
- package/components/shadcn/TabsTrigger.vue.cjs +1 -1
- package/components/shadcn/TabsTrigger.vue.cjs.map +1 -1
- package/components/shadcn/TabsTrigger.vue.js +4 -4
- package/components/shadcn/TabsTrigger.vue.js.map +1 -1
- package/components/shadcn/Textarea.vue.cjs +1 -1
- package/components/shadcn/Textarea.vue.cjs.map +1 -1
- package/components/shadcn/Textarea.vue.js +2 -2
- package/components/shadcn/Textarea.vue.js.map +1 -1
- package/components/shadcn/index.cjs +1 -1
- package/components/shadcn/index.cjs.map +1 -1
- package/components/shadcn/index.js +8 -7
- package/components/shadcn/index.js.map +1 -1
- package/index.cjs +1 -1
- package/index.js +76 -72
- package/package.json +1 -1
- package/types/index.d.ts +742 -15
- package/assets/jwms-portal-frontend-DntSIcYt.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JTabs.vue2.cjs","sources":["../../../../src/components/molecules/JTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed, ref, watch, nextTick } from 'vue'\r\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/shadcn'\r\nimport type { JTabsProps, JTabsEmits } from '@/types/dynamic-tabs.types'\r\nimport { X } from 'lucide-vue-next'\r\nimport { cn } from '@/lib/utils'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\n\r\n/**\r\n * JTabs - 기본 탭 UI 컴포넌트 (molecules)\r\n * Basic Tabs UI Component\r\n * \r\n * @description\r\n * 정적인 탭 목록을 렌더링하는 기본 탭 컴포넌트입니다.\r\n * 닫기 버튼, 아이콘 등을 지원합니다.\r\n * \r\n * @example\r\n * ```vue\r\n * <JTabs \r\n * :tabs=\"tabs\"\r\n * :active-tab-id=\"activeId\"\r\n * @tab-change=\"handleChange\"\r\n * @tab-close=\"handleClose\"\r\n * />\r\n * ```\r\n */\r\n\r\ntype StyleType = 'default' | 'minimal'\r\n\r\nconst props = withDefaults(defineProps<JTabsProps>(), {\r\n styletype: 'default',\r\n})\r\n\r\nconst emit = defineEmits<JTabsEmits>()\r\n\r\n/**\r\n * 안전한 tabs 배열 (undefined/null 체크)\r\n * Safe tabs array (undefined/null check)\r\n */\r\nconst safeTabs = computed(() => {\r\n return Array.isArray(props.tabs) ? props.tabs : []\r\n})\r\n\r\n/**\r\n * 현재 활성화된 탭 ID (내부 상태)\r\n * Current active tab ID (internal state)\r\n */\r\nconst internalActiveId = ref<string>(\r\n props.activeTabId || (safeTabs.value.length > 0 ? safeTabs.value[0]?.id : '') || ''\r\n)\r\n\r\n/**\r\n * 이벤트 처리 중 플래그 (중복 이벤트 방지)\r\n * Flag to prevent duplicate events\r\n */\r\nlet isHandlingEvent = false\r\n\r\n/**\r\n * props.activeTabId가 변경되면 내부 상태 동기화\r\n * Sync internal state when props.activeTabId changes\r\n */\r\nwatch(() => props.activeTabId, (newValue) => {\r\n if (newValue !== undefined && newValue !== internalActiveId.value) {\r\n internalActiveId.value = newValue\r\n }\r\n}, { immediate: true })\r\n\r\n/**\r\n * props.tabs가 변경되고 activeTabId가 없으면 첫 번째 탭 활성화\r\n * Activate first tab when tabs change and no activeTabId\r\n */\r\nwatch(safeTabs, (newTabs) => {\r\n if (!props.activeTabId && newTabs.length > 0 && !newTabs.find(t => t.id === internalActiveId.value) && newTabs[0]) {\r\n internalActiveId.value = newTabs[0].id\r\n }\r\n})\r\n\r\n/**\r\n * 탭 값 변경 핸들러 (reka-ui TabsRoot에서 직접 호출됨)\r\n * Tab value change handler (called directly from reka-ui TabsRoot)\r\n */\r\nconst handleTabValueChange = (value: string | number) => {\r\n if (isHandlingEvent) return\r\n \r\n const stringValue = String(value)\r\n if (stringValue !== internalActiveId.value) {\r\n isHandlingEvent = true\r\n internalActiveId.value = stringValue\r\n emit('update:activeTabId', stringValue)\r\n emit('tabChange', stringValue)\r\n // 다음 tick에서 플래그 리셋\r\n nextTick(() => {\r\n isHandlingEvent = false\r\n })\r\n }\r\n}\r\n\r\n/**\r\n * 탭 클릭 핸들러 (백업 방안 - reka-ui 이벤트가 작동하지 않을 경우)\r\n * Tab click handler (backup - in case reka-ui events don't work)\r\n */\r\nconst handleTabClick = (tabId: string) => {\r\n // reka-ui 이벤트가 작동하지 않을 경우 직접 처리\r\n // handleTabValueChange가 이미 처리했으면 중복 방지\r\n if (isHandlingEvent || tabId === internalActiveId.value) return\r\n \r\n isHandlingEvent = true\r\n internalActiveId.value = tabId\r\n emit('update:activeTabId', tabId)\r\n emit('tabChange', tabId)\r\n // 다음 tick에서 플래그 리셋\r\n nextTick(() => {\r\n isHandlingEvent = false\r\n })\r\n}\r\n\r\n/**\r\n * 탭 닫기 핸들러\r\n * Tab close handler\r\n */\r\nconst handleCloseTab = (e: Event, tabId: string) => {\r\n e.stopPropagation() // 탭 클릭 이벤트 전파 방지\r\n emit('tabClose', tabId)\r\n}\r\n\r\n/**\r\n * 루트 클래스\r\n * Root classes\r\n */\r\nconst rootClasses = computed(() => {\r\n const classes = ['flex', 'flex-col', 'w-full', 'h-full']\r\n \r\n if (props.className) {\r\n classes.push(props.className)\r\n }\r\n \r\n return classes.join(' ')\r\n})\r\n\r\n/**\r\n * 스타일 프리셋\r\n */\r\nconst STYLE_PRESETS: Record<StyleType, {\r\n tabPaddingClass: string\r\n tabTextSizeClass: string\r\n listPaddingClass: string\r\n}> = {\r\n default: {\r\n tabPaddingClass: 'px-3 py-2',\r\n tabTextSizeClass: 'text-sm',\r\n listPaddingClass: 'p-2',\r\n },\r\n minimal: {\r\n tabPaddingClass: 'px-2 py-1',\r\n tabTextSizeClass: 'text-xs',\r\n listPaddingClass: 'p-1',\r\n },\r\n}\r\n\r\nconst preset = computed(() => {\r\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\r\n})\r\n\r\n/**\r\n * 탭 리스트 클래스\r\n * Tabs list classes\r\n */\r\nconst listClasses = computed(() => {\r\n const classes = ['w-full', 'justify-start', preset.value.listPaddingClass]\r\n \r\n if (props.listClassName) {\r\n classes.push(props.listClassName)\r\n }\r\n \r\n return classes.join(' ')\r\n})\r\n</script>\r\n\r\n<template>\r\n <Tabs\r\n :model-value=\"internalActiveId\"\r\n @update:model-value=\"handleTabValueChange\"\r\n orientation=\"horizontal\"\r\n :class=\"rootClasses\"\r\n >\r\n <!-- 탭 헤더 영역 / Tab Headers -->\r\n <TabsList :class=\"listClasses\">\r\n <TabsTrigger\r\n v-for=\"tab in safeTabs\"\r\n :key=\"tab.id\"\r\n :value=\"tab.id\"\r\n @click=\"handleTabClick(tab.id)\"\r\n :class=\"cn('!flex !items-center !gap-2', preset.tabPaddingClass, preset.tabTextSizeClass)\"\r\n >\r\n <!-- 탭 아이콘 (있을 경우) / Tab Icon -->\r\n <JIcon \r\n v-if=\"tab.icon\" \r\n :name=\"tab.icon\" \r\n size=\"sm\"\r\n class=\"flex-shrink-0\"\r\n />\r\n \r\n <!-- 탭 레이블 / Tab Label -->\r\n <span class=\"flex-1 truncate\">{{ tab.label }}</span>\r\n \r\n <!-- 닫기 버튼 / Close Button (항상 표시) -->\r\n <button\r\n v-if=\"tab.closable\"\r\n type=\"button\"\r\n class=\"flex-shrink-0 h-4 w-4 rounded-sm hover:bg-destructive/10 hover:text-destructive transition-colors focus:outline-none focus:ring-2 focus:ring-ring flex items-center justify-center\"\r\n :aria-label=\"`${tab.label} 탭 닫기`\"\r\n @click=\"(e) => handleCloseTab(e, tab.id)\"\r\n >\r\n <X class=\"h-3 w-3\" />\r\n </button>\r\n </TabsTrigger>\r\n </TabsList>\r\n\r\n <!-- 탭 콘텐츠 영역 / Tab Contents -->\r\n <div class=\"flex-1 w-full overflow-auto\">\r\n <TabsContent\r\n v-for=\"tab in safeTabs\"\r\n :key=\"`content-${tab.id}`\"\r\n :value=\"tab.id\"\r\n class=\"h-full mt-0 data-[state=active]:flex data-[state=active]:flex-col\"\r\n >\r\n <!-- 슬롯 우선 / Slot First -->\r\n <slot :name=\"`content-${tab.id}`\" :tab=\"tab\">\r\n <!-- 동적 컴포넌트 렌더링 / Dynamic Component Rendering -->\r\n <component\r\n v-if=\"tab.component\"\r\n :is=\"tab.component\"\r\n v-bind=\"tab.props || {}\"\r\n />\r\n \r\n <!-- 기본 콘텐츠 / Default Content -->\r\n <div v-else class=\"p-4\">\r\n <p class=\"text-muted-foreground\">{{ tab.label }} 콘텐츠</p>\r\n </div>\r\n </slot>\r\n </TabsContent>\r\n </div>\r\n </Tabs>\r\n</template>\r\n\r\n<style scoped>\r\n/**\r\n * 탭 리스트 스타일 - 하단 보더 제거\r\n * Tab list styles without bottom border\r\n */\r\n:deep([role=\"tablist\"]) {\r\n overflow-x: auto;\r\n overflow-y: hidden;\r\n scrollbar-width: thin;\r\n scrollbar-color: rgba(0, 0, 0, 0.2) transparent;\r\n background: hsl(var(--background));\r\n padding: 0 0.5rem;\r\n padding-top: 0.5rem;\r\n gap: 0.25rem;\r\n border-bottom: none;\r\n}\r\n\r\n:deep([role=\"tablist\"]::-webkit-scrollbar) {\r\n height: 6px;\r\n}\r\n\r\n:deep([role=\"tablist\"]::-webkit-scrollbar-track) {\r\n background: transparent;\r\n}\r\n\r\n:deep([role=\"tablist\"]::-webkit-scrollbar-thumb) {\r\n background-color: rgba(0, 0, 0, 0.2);\r\n border-radius: 3px;\r\n}\r\n\r\n:deep([role=\"tablist\"]::-webkit-scrollbar-thumb:hover) {\r\n background-color: rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n/**\r\n * 다크모드에서 스크롤바 스타일\r\n * Scrollbar styles in dark mode\r\n */\r\n.dark :deep([role=\"tablist\"]::-webkit-scrollbar-thumb) {\r\n background-color: rgba(255, 255, 255, 0.2);\r\n}\r\n\r\n.dark :deep([role=\"tablist\"]::-webkit-scrollbar-thumb:hover) {\r\n background-color: rgba(255, 255, 255, 0.3);\r\n}\r\n\r\n.dark :deep([role=\"tablist\"]) {\r\n scrollbar-color: rgba(255, 255, 255, 0.2) transparent;\r\n}\r\n\r\n/**\r\n * 탭 버튼 스타일 - 명확한 구분\r\n * Tab button styles - clear distinction\r\n */\r\n:deep([role=\"tab\"]) {\r\n position: relative;\r\n padding: 0.625rem 1rem;\r\n border-radius: 0.375rem 0.375rem 0 0;\r\n transition: all 0.2s ease;\r\n border: 1px solid transparent;\r\n border-bottom: none;\r\n}\r\n\r\n/**\r\n * Minimal 스타일 탭 버튼 - JSidebarAdvanced와 높이 맞춤\r\n */\r\n:deep([role=\"tablist\"][class*=\"p-1\"] [role=\"tab\"]) {\r\n padding: 0.625rem 0.5rem;\r\n}\r\n\r\n/**\r\n * 비활성 탭 - 명확하게 구분\r\n * Inactive tabs - clear distinction\r\n */\r\n:deep([role=\"tab\"][data-state=\"inactive\"]) {\r\n background: hsl(var(--muted) / 0.2);\r\n color: hsl(var(--muted-foreground));\r\n border-top: 1px solid hsl(var(--border) / 0.4);\r\n border-left: 1px solid hsl(var(--border) / 0.4);\r\n border-right: 1px solid hsl(var(--border) / 0.4);\r\n}\r\n\r\n:deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\r\n background: hsl(var(--muted) / 0.4);\r\n color: hsl(var(--foreground));\r\n}\r\n\r\n/**\r\n * 다크모드에서 비활성 탭 - 다크 배경색 사용\r\n * Inactive tabs in dark mode - use dark background\r\n */\r\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]) {\r\n background: hsl(var(--secondary));\r\n color: hsl(var(--secondary-foreground));\r\n border-top: 1px solid hsl(var(--border) / 0.5);\r\n border-left: 1px solid hsl(var(--border) / 0.5);\r\n border-right: 1px solid hsl(var(--border) / 0.5);\r\n}\r\n\r\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\r\n background: hsl(var(--muted));\r\n color: hsl(var(--muted-foreground));\r\n}\r\n\r\n/**\r\n * 활성 탭 - 강조된 스타일\r\n * Active tab - emphasized style\r\n */\r\n:deep([role=\"tab\"][data-state=\"active\"]) {\r\n background: hsl(var(--background));\r\n color: hsl(var(--foreground));\r\n font-weight: 500;\r\n border-top: 2px solid hsl(var(--primary));\r\n border-left: 1px solid hsl(var(--border));\r\n border-right: 1px solid hsl(var(--border));\r\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\r\n z-index: 1;\r\n}\r\n\r\n/**\r\n * 다크모드에서 활성 탭 - 배경색 명확히 구분\r\n * Active tab in dark mode - clear background distinction\r\n */\r\n.dark :deep([role=\"tab\"][data-state=\"active\"]) {\r\n background: hsl(var(--card));\r\n color: hsl(var(--card-foreground));\r\n border-top: 2px solid hsl(var(--primary));\r\n border-left: 1px solid hsl(var(--border));\r\n border-right: 1px solid hsl(var(--border));\r\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n/**\r\n * 콘텐츠 영역에 상단 보더 추가\r\n * Add top border to content area\r\n */\r\n:deep([role=\"tabpanel\"]) {\r\n border-top: 1px solid hsl(var(--border));\r\n}\r\n\r\n/**\r\n * 다크모드에서 콘텐츠 영역 보더 - 더 부드러운 구분\r\n * Content area border in dark mode - subtler separation\r\n */\r\n.dark :deep([role=\"tabpanel\"]) {\r\n border-top: 1px solid hsl(var(--border) / 0.6);\r\n}\r\n</style>\r\n\r\n"],"names":["props","__props","emit","__emit","safeTabs","computed","internalActiveId","ref","isHandlingEvent","watch","newValue","newTabs","t","handleTabValueChange","value","stringValue","nextTick","handleTabClick","tabId","handleCloseTab","e","rootClasses","classes","STYLE_PRESETS","preset","listClasses","_createBlock","_unref","Tabs","_createVNode","TabsList","_createElementBlock","_Fragment","_renderList","tab","TabsTrigger","$event","_normalizeClass","JIcon","_createElementVNode","_hoisted_1","_toDisplayString","X","_hoisted_3","TabsContent","_renderSlot","_ctx","_openBlock","_resolveDynamicComponent","_mergeProps","_hoisted_4","_hoisted_5"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JTabs.vue2.cjs","sources":["../../../../src/components/molecules/JTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed, ref, watch, nextTick } from 'vue'\r\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/shadcn'\r\nimport type { JTabsProps, JTabsEmits } from '@/types/dynamic-tabs.types'\r\nimport { X } from 'lucide-vue-next'\r\nimport { cn } from '@/lib/utils'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\n\r\n/**\r\n * JTabs - 기본 탭 UI 컴포넌트 (molecules)\r\n * Basic Tabs UI Component\r\n * \r\n * @description\r\n * 정적인 탭 목록을 렌더링하는 기본 탭 컴포넌트입니다.\r\n * 닫기 버튼, 아이콘 등을 지원합니다.\r\n * \r\n * @example\r\n * ```vue\r\n * <JTabs \r\n * :tabs=\"tabs\"\r\n * :active-tab-id=\"activeId\"\r\n * @tab-change=\"handleChange\"\r\n * @tab-close=\"handleClose\"\r\n * />\r\n * ```\r\n */\r\n\r\ntype StyleType = 'default' | 'minimal'\r\n\r\nconst props = withDefaults(defineProps<JTabsProps>(), {\r\n styletype: 'default',\r\n})\r\n\r\nconst emit = defineEmits<JTabsEmits>()\r\n\r\n/**\r\n * 안전한 tabs 배열 (undefined/null 체크)\r\n * Safe tabs array (undefined/null check)\r\n */\r\nconst safeTabs = computed(() => {\r\n return Array.isArray(props.tabs) ? props.tabs : []\r\n})\r\n\r\n/**\r\n * 현재 활성화된 탭 ID (내부 상태)\r\n * Current active tab ID (internal state)\r\n */\r\nconst internalActiveId = ref<string>(\r\n props.activeTabId || (safeTabs.value.length > 0 ? safeTabs.value[0]?.id : '') || ''\r\n)\r\n\r\n/**\r\n * 이벤트 처리 중 플래그 (중복 이벤트 방지)\r\n * Flag to prevent duplicate events\r\n */\r\nlet isHandlingEvent = false\r\n\r\n/**\r\n * props.activeTabId가 변경되면 내부 상태 동기화\r\n * Sync internal state when props.activeTabId changes\r\n */\r\nwatch(() => props.activeTabId, (newValue) => {\r\n if (newValue !== undefined && newValue !== internalActiveId.value) {\r\n internalActiveId.value = newValue\r\n }\r\n}, { immediate: true })\r\n\r\n/**\r\n * props.tabs가 변경되고 activeTabId가 없으면 첫 번째 탭 활성화\r\n * Activate first tab when tabs change and no activeTabId\r\n */\r\nwatch(safeTabs, (newTabs) => {\r\n if (!props.activeTabId && newTabs.length > 0 && !newTabs.find(t => t.id === internalActiveId.value) && newTabs[0]) {\r\n internalActiveId.value = newTabs[0].id\r\n }\r\n})\r\n\r\n/**\r\n * 탭 값 변경 핸들러 (reka-ui TabsRoot에서 직접 호출됨)\r\n * Tab value change handler (called directly from reka-ui TabsRoot)\r\n */\r\nconst handleTabValueChange = (value: string | number) => {\r\n if (isHandlingEvent) return\r\n \r\n const stringValue = String(value)\r\n if (stringValue !== internalActiveId.value) {\r\n isHandlingEvent = true\r\n internalActiveId.value = stringValue\r\n emit('update:activeTabId', stringValue)\r\n emit('tabChange', stringValue)\r\n // 다음 tick에서 플래그 리셋\r\n nextTick(() => {\r\n isHandlingEvent = false\r\n })\r\n }\r\n}\r\n\r\n/**\r\n * 탭 클릭 핸들러 (백업 방안 - reka-ui 이벤트가 작동하지 않을 경우)\r\n * Tab click handler (backup - in case reka-ui events don't work)\r\n */\r\nconst handleTabClick = (tabId: string) => {\r\n // reka-ui 이벤트가 작동하지 않을 경우 직접 처리\r\n // handleTabValueChange가 이미 처리했으면 중복 방지\r\n if (isHandlingEvent || tabId === internalActiveId.value) return\r\n \r\n isHandlingEvent = true\r\n internalActiveId.value = tabId\r\n emit('update:activeTabId', tabId)\r\n emit('tabChange', tabId)\r\n // 다음 tick에서 플래그 리셋\r\n nextTick(() => {\r\n isHandlingEvent = false\r\n })\r\n}\r\n\r\n/**\r\n * 탭 닫기 핸들러\r\n * Tab close handler\r\n */\r\nconst handleCloseTab = (e: Event, tabId: string) => {\r\n e.stopPropagation() // 탭 클릭 이벤트 전파 방지\r\n emit('tabClose', tabId)\r\n}\r\n\r\n/**\r\n * 루트 클래스\r\n * Root classes\r\n */\r\nconst rootClasses = computed(() => {\r\n const classes = ['flex', 'flex-col', 'w-full', 'h-full']\r\n \r\n if (props.className) {\r\n classes.push(props.className)\r\n }\r\n \r\n return classes.join(' ')\r\n})\r\n\r\n/**\r\n * 스타일 프리셋\r\n */\r\nconst STYLE_PRESETS: Record<StyleType, {\r\n tabPaddingClass: string\r\n tabTextSizeClass: string\r\n listPaddingClass: string\r\n}> = {\r\n default: {\r\n tabPaddingClass: 'px-2.5 py-1',\r\n tabTextSizeClass: 'text-xs',\r\n listPaddingClass: 'p-0.5',\r\n },\r\n minimal: {\r\n tabPaddingClass: 'px-2 py-0.5',\r\n tabTextSizeClass: 'text-xs',\r\n listPaddingClass: 'p-0.5',\r\n },\r\n}\r\n\r\nconst preset = computed(() => {\r\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\r\n})\r\n\r\n/**\r\n * 탭 리스트 클래스\r\n * Tabs list classes\r\n */\r\nconst listClasses = computed(() => {\r\n const classes = ['w-full', 'justify-start', preset.value.listPaddingClass]\r\n \r\n if (props.listClassName) {\r\n classes.push(props.listClassName)\r\n }\r\n \r\n return classes.join(' ')\r\n})\r\n</script>\r\n\r\n<template>\r\n <Tabs\r\n :model-value=\"internalActiveId\"\r\n @update:model-value=\"handleTabValueChange\"\r\n orientation=\"horizontal\"\r\n :class=\"rootClasses\"\r\n >\r\n <!-- 탭 헤더 영역 / Tab Headers -->\r\n <TabsList :class=\"listClasses\">\r\n <TabsTrigger\r\n v-for=\"tab in safeTabs\"\r\n :key=\"tab.id\"\r\n :value=\"tab.id\"\r\n @click=\"handleTabClick(tab.id)\"\r\n :class=\"cn('!flex !items-center !gap-2', preset.tabPaddingClass, preset.tabTextSizeClass)\"\r\n >\r\n <!-- 탭 아이콘 (있을 경우) / Tab Icon -->\r\n <JIcon \r\n v-if=\"tab.icon\" \r\n :name=\"tab.icon\" \r\n size=\"sm\"\r\n class=\"flex-shrink-0\"\r\n />\r\n \r\n <!-- 탭 레이블 / Tab Label -->\r\n <span class=\"flex-1 truncate\">{{ tab.label }}</span>\r\n \r\n <!-- 닫기 버튼 / Close Button (항상 표시) -->\r\n <button\r\n v-if=\"tab.closable\"\r\n type=\"button\"\r\n class=\"flex-shrink-0 h-3.5 w-3.5 rounded-sm hover:bg-destructive/10 hover:text-destructive transition-colors focus:outline-none focus:ring-2 focus:ring-ring flex items-center justify-center\"\r\n :aria-label=\"`${tab.label} 탭 닫기`\"\r\n @click=\"(e) => handleCloseTab(e, tab.id)\"\r\n >\r\n <X class=\"h-2.5 w-2.5\" />\r\n </button>\r\n </TabsTrigger>\r\n </TabsList>\r\n\r\n <!-- 탭 콘텐츠 영역 / Tab Contents -->\r\n <div class=\"flex-1 w-full overflow-auto\">\r\n <TabsContent\r\n v-for=\"tab in safeTabs\"\r\n :key=\"`content-${tab.id}`\"\r\n :value=\"tab.id\"\r\n class=\"h-full mt-0 data-[state=active]:flex data-[state=active]:flex-col\"\r\n >\r\n <!-- 슬롯 우선 / Slot First -->\r\n <slot :name=\"`content-${tab.id}`\" :tab=\"tab\">\r\n <!-- 동적 컴포넌트 렌더링 / Dynamic Component Rendering -->\r\n <component\r\n v-if=\"tab.component\"\r\n :is=\"tab.component\"\r\n v-bind=\"tab.props || {}\"\r\n />\r\n \r\n <!-- 기본 콘텐츠 / Default Content -->\r\n <div v-else class=\"p-4\">\r\n <p class=\"text-muted-foreground\">{{ tab.label }} 콘텐츠</p>\r\n </div>\r\n </slot>\r\n </TabsContent>\r\n </div>\r\n </Tabs>\r\n</template>\r\n\r\n<style scoped>\r\n/**\r\n * 탭 리스트 스타일 - 하단 보더 제거\r\n * Tab list styles without bottom border\r\n */\r\n:deep([role=\"tablist\"]:not(.ag-side-buttons)) {\r\n overflow-x: auto;\r\n overflow-y: hidden;\r\n scrollbar-width: thin;\r\n scrollbar-color: rgba(0, 0, 0, 0.2) transparent;\r\n background: hsl(var(--background));\r\n padding: 0 0.5rem;\r\n padding-top: 0.25rem;\r\n gap: 0.25rem;\r\n border-bottom: none;\r\n}\r\n\r\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar) {\r\n height: 6px;\r\n}\r\n\r\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-track) {\r\n background: transparent;\r\n}\r\n\r\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\r\n background-color: rgba(0, 0, 0, 0.2);\r\n border-radius: 3px;\r\n}\r\n\r\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\r\n background-color: rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n/**\r\n * 다크모드에서 스크롤바 스타일\r\n * Scrollbar styles in dark mode\r\n */\r\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\r\n background-color: rgba(255, 255, 255, 0.2);\r\n}\r\n\r\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\r\n background-color: rgba(255, 255, 255, 0.3);\r\n}\r\n\r\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)) {\r\n scrollbar-color: rgba(255, 255, 255, 0.2) transparent;\r\n}\r\n\r\n/**\r\n * 탭 버튼 스타일 - 명확한 구분\r\n * Tab button styles - clear distinction\r\n */\r\n:deep([role=\"tab\"]) {\r\n position: relative;\r\n padding: 0.375rem 0.75rem;\r\n border-radius: 0.375rem 0.375rem 0 0;\r\n transition: all 0.2s ease;\r\n border: 1px solid transparent;\r\n border-bottom: none;\r\n}\r\n\r\n/**\r\n * Minimal 스타일 탭 버튼 - JSidebarAdvanced와 높이 맞춤\r\n */\r\n:deep([role=\"tablist\"][class*=\"p-0.5\"] [role=\"tab\"]) {\r\n padding: 0.25rem 0.5rem;\r\n}\r\n\r\n/**\r\n * 비활성 탭 - 명확하게 구분\r\n * Inactive tabs - clear distinction\r\n */\r\n:deep([role=\"tab\"][data-state=\"inactive\"]) {\r\n background: hsl(var(--muted) / 0.2);\r\n color: hsl(var(--muted-foreground));\r\n border-top: 1px solid hsl(var(--border) / 0.4);\r\n border-left: 1px solid hsl(var(--border) / 0.4);\r\n border-right: 1px solid hsl(var(--border) / 0.4);\r\n}\r\n\r\n:deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\r\n background: hsl(var(--muted) / 0.4);\r\n color: hsl(var(--foreground));\r\n}\r\n\r\n/**\r\n * 다크모드에서 비활성 탭 - 다크 배경색 사용\r\n * Inactive tabs in dark mode - use dark background\r\n */\r\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]) {\r\n background: hsl(var(--secondary));\r\n color: hsl(var(--secondary-foreground));\r\n border-top: 1px solid hsl(var(--border) / 0.5);\r\n border-left: 1px solid hsl(var(--border) / 0.5);\r\n border-right: 1px solid hsl(var(--border) / 0.5);\r\n}\r\n\r\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\r\n background: hsl(var(--muted));\r\n color: hsl(var(--muted-foreground));\r\n}\r\n\r\n/**\r\n * 활성 탭 - 강조된 스타일\r\n * Active tab - emphasized style\r\n */\r\n:deep([role=\"tab\"][data-state=\"active\"]) {\r\n background: hsl(var(--background));\r\n color: hsl(var(--foreground));\r\n font-weight: 500;\r\n border-top: 2px solid hsl(var(--primary));\r\n border-left: 1px solid hsl(var(--border));\r\n border-right: 1px solid hsl(var(--border));\r\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\r\n z-index: 1;\r\n}\r\n\r\n/**\r\n * 다크모드에서 활성 탭 - 배경색 명확히 구분\r\n * Active tab in dark mode - clear background distinction\r\n */\r\n.dark :deep([role=\"tab\"][data-state=\"active\"]) {\r\n background: hsl(var(--card));\r\n color: hsl(var(--card-foreground));\r\n border-top: 2px solid hsl(var(--primary));\r\n border-left: 1px solid hsl(var(--border));\r\n border-right: 1px solid hsl(var(--border));\r\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n/**\r\n * 콘텐츠 영역에 상단 보더 추가\r\n * Add top border to content area\r\n */\r\n:deep([role=\"tabpanel\"]) {\r\n border-top: 1px solid hsl(var(--border));\r\n}\r\n\r\n/**\r\n * 다크모드에서 콘텐츠 영역 보더 - 더 부드러운 구분\r\n * Content area border in dark mode - subtler separation\r\n */\r\n.dark :deep([role=\"tabpanel\"]) {\r\n border-top: 1px solid hsl(var(--border) / 0.6);\r\n}\r\n</style>\r\n\r\n"],"names":["props","__props","emit","__emit","safeTabs","computed","internalActiveId","ref","isHandlingEvent","watch","newValue","newTabs","t","handleTabValueChange","value","stringValue","nextTick","handleTabClick","tabId","handleCloseTab","e","rootClasses","classes","STYLE_PRESETS","preset","listClasses","_createBlock","_unref","Tabs","_createVNode","TabsList","_createElementBlock","_Fragment","_renderList","tab","TabsTrigger","$event","_normalizeClass","JIcon","_createElementVNode","_hoisted_1","_toDisplayString","X","_hoisted_3","TabsContent","_renderSlot","_ctx","_openBlock","_resolveDynamicComponent","_mergeProps","_hoisted_4","_hoisted_5"],"mappings":"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"}
|
|
@@ -47,14 +47,14 @@ const Y = { class: "flex-1 truncate" }, q = ["aria-label", "onClick"], G = { cla
|
|
|
47
47
|
return s.className && e.push(s.className), e.join(" ");
|
|
48
48
|
}), _ = {
|
|
49
49
|
default: {
|
|
50
|
-
tabPaddingClass: "px-
|
|
51
|
-
tabTextSizeClass: "text-
|
|
52
|
-
listPaddingClass: "p-
|
|
50
|
+
tabPaddingClass: "px-2.5 py-1",
|
|
51
|
+
tabTextSizeClass: "text-xs",
|
|
52
|
+
listPaddingClass: "p-0.5"
|
|
53
53
|
},
|
|
54
54
|
minimal: {
|
|
55
|
-
tabPaddingClass: "px-2 py-
|
|
55
|
+
tabPaddingClass: "px-2 py-0.5",
|
|
56
56
|
tabTextSizeClass: "text-xs",
|
|
57
|
-
listPaddingClass: "p-
|
|
57
|
+
listPaddingClass: "p-0.5"
|
|
58
58
|
}
|
|
59
59
|
}, p = d(() => _[s.styletype] ?? _.default), j = d(() => {
|
|
60
60
|
const e = ["w-full", "justify-start", p.value.listPaddingClass];
|
|
@@ -88,11 +88,11 @@ const Y = { class: "flex-1 truncate" }, q = ["aria-label", "onClick"], G = { cla
|
|
|
88
88
|
t.closable ? (n(), m("button", {
|
|
89
89
|
key: 1,
|
|
90
90
|
type: "button",
|
|
91
|
-
class: "flex-shrink-0 h-
|
|
91
|
+
class: "flex-shrink-0 h-3.5 w-3.5 rounded-sm hover:bg-destructive/10 hover:text-destructive transition-colors focus:outline-none focus:ring-2 focus:ring-ring flex items-center justify-center",
|
|
92
92
|
"aria-label": `${t.label} 탭 닫기`,
|
|
93
93
|
onClick: (h) => E(h, t.id)
|
|
94
94
|
}, [
|
|
95
|
-
x(o(L), { class: "h-
|
|
95
|
+
x(o(L), { class: "h-2.5 w-2.5" })
|
|
96
96
|
], 8, q)) : T("", !0)
|
|
97
97
|
]),
|
|
98
98
|
_: 2
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JTabs.vue2.js","sources":["../../../../src/components/molecules/JTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed, ref, watch, nextTick } from 'vue'\r\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/shadcn'\r\nimport type { JTabsProps, JTabsEmits } from '@/types/dynamic-tabs.types'\r\nimport { X } from 'lucide-vue-next'\r\nimport { cn } from '@/lib/utils'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\n\r\n/**\r\n * JTabs - 기본 탭 UI 컴포넌트 (molecules)\r\n * Basic Tabs UI Component\r\n * \r\n * @description\r\n * 정적인 탭 목록을 렌더링하는 기본 탭 컴포넌트입니다.\r\n * 닫기 버튼, 아이콘 등을 지원합니다.\r\n * \r\n * @example\r\n * ```vue\r\n * <JTabs \r\n * :tabs=\"tabs\"\r\n * :active-tab-id=\"activeId\"\r\n * @tab-change=\"handleChange\"\r\n * @tab-close=\"handleClose\"\r\n * />\r\n * ```\r\n */\r\n\r\ntype StyleType = 'default' | 'minimal'\r\n\r\nconst props = withDefaults(defineProps<JTabsProps>(), {\r\n styletype: 'default',\r\n})\r\n\r\nconst emit = defineEmits<JTabsEmits>()\r\n\r\n/**\r\n * 안전한 tabs 배열 (undefined/null 체크)\r\n * Safe tabs array (undefined/null check)\r\n */\r\nconst safeTabs = computed(() => {\r\n return Array.isArray(props.tabs) ? props.tabs : []\r\n})\r\n\r\n/**\r\n * 현재 활성화된 탭 ID (내부 상태)\r\n * Current active tab ID (internal state)\r\n */\r\nconst internalActiveId = ref<string>(\r\n props.activeTabId || (safeTabs.value.length > 0 ? safeTabs.value[0]?.id : '') || ''\r\n)\r\n\r\n/**\r\n * 이벤트 처리 중 플래그 (중복 이벤트 방지)\r\n * Flag to prevent duplicate events\r\n */\r\nlet isHandlingEvent = false\r\n\r\n/**\r\n * props.activeTabId가 변경되면 내부 상태 동기화\r\n * Sync internal state when props.activeTabId changes\r\n */\r\nwatch(() => props.activeTabId, (newValue) => {\r\n if (newValue !== undefined && newValue !== internalActiveId.value) {\r\n internalActiveId.value = newValue\r\n }\r\n}, { immediate: true })\r\n\r\n/**\r\n * props.tabs가 변경되고 activeTabId가 없으면 첫 번째 탭 활성화\r\n * Activate first tab when tabs change and no activeTabId\r\n */\r\nwatch(safeTabs, (newTabs) => {\r\n if (!props.activeTabId && newTabs.length > 0 && !newTabs.find(t => t.id === internalActiveId.value) && newTabs[0]) {\r\n internalActiveId.value = newTabs[0].id\r\n }\r\n})\r\n\r\n/**\r\n * 탭 값 변경 핸들러 (reka-ui TabsRoot에서 직접 호출됨)\r\n * Tab value change handler (called directly from reka-ui TabsRoot)\r\n */\r\nconst handleTabValueChange = (value: string | number) => {\r\n if (isHandlingEvent) return\r\n \r\n const stringValue = String(value)\r\n if (stringValue !== internalActiveId.value) {\r\n isHandlingEvent = true\r\n internalActiveId.value = stringValue\r\n emit('update:activeTabId', stringValue)\r\n emit('tabChange', stringValue)\r\n // 다음 tick에서 플래그 리셋\r\n nextTick(() => {\r\n isHandlingEvent = false\r\n })\r\n }\r\n}\r\n\r\n/**\r\n * 탭 클릭 핸들러 (백업 방안 - reka-ui 이벤트가 작동하지 않을 경우)\r\n * Tab click handler (backup - in case reka-ui events don't work)\r\n */\r\nconst handleTabClick = (tabId: string) => {\r\n // reka-ui 이벤트가 작동하지 않을 경우 직접 처리\r\n // handleTabValueChange가 이미 처리했으면 중복 방지\r\n if (isHandlingEvent || tabId === internalActiveId.value) return\r\n \r\n isHandlingEvent = true\r\n internalActiveId.value = tabId\r\n emit('update:activeTabId', tabId)\r\n emit('tabChange', tabId)\r\n // 다음 tick에서 플래그 리셋\r\n nextTick(() => {\r\n isHandlingEvent = false\r\n })\r\n}\r\n\r\n/**\r\n * 탭 닫기 핸들러\r\n * Tab close handler\r\n */\r\nconst handleCloseTab = (e: Event, tabId: string) => {\r\n e.stopPropagation() // 탭 클릭 이벤트 전파 방지\r\n emit('tabClose', tabId)\r\n}\r\n\r\n/**\r\n * 루트 클래스\r\n * Root classes\r\n */\r\nconst rootClasses = computed(() => {\r\n const classes = ['flex', 'flex-col', 'w-full', 'h-full']\r\n \r\n if (props.className) {\r\n classes.push(props.className)\r\n }\r\n \r\n return classes.join(' ')\r\n})\r\n\r\n/**\r\n * 스타일 프리셋\r\n */\r\nconst STYLE_PRESETS: Record<StyleType, {\r\n tabPaddingClass: string\r\n tabTextSizeClass: string\r\n listPaddingClass: string\r\n}> = {\r\n default: {\r\n tabPaddingClass: 'px-3 py-2',\r\n tabTextSizeClass: 'text-sm',\r\n listPaddingClass: 'p-2',\r\n },\r\n minimal: {\r\n tabPaddingClass: 'px-2 py-1',\r\n tabTextSizeClass: 'text-xs',\r\n listPaddingClass: 'p-1',\r\n },\r\n}\r\n\r\nconst preset = computed(() => {\r\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\r\n})\r\n\r\n/**\r\n * 탭 리스트 클래스\r\n * Tabs list classes\r\n */\r\nconst listClasses = computed(() => {\r\n const classes = ['w-full', 'justify-start', preset.value.listPaddingClass]\r\n \r\n if (props.listClassName) {\r\n classes.push(props.listClassName)\r\n }\r\n \r\n return classes.join(' ')\r\n})\r\n</script>\r\n\r\n<template>\r\n <Tabs\r\n :model-value=\"internalActiveId\"\r\n @update:model-value=\"handleTabValueChange\"\r\n orientation=\"horizontal\"\r\n :class=\"rootClasses\"\r\n >\r\n <!-- 탭 헤더 영역 / Tab Headers -->\r\n <TabsList :class=\"listClasses\">\r\n <TabsTrigger\r\n v-for=\"tab in safeTabs\"\r\n :key=\"tab.id\"\r\n :value=\"tab.id\"\r\n @click=\"handleTabClick(tab.id)\"\r\n :class=\"cn('!flex !items-center !gap-2', preset.tabPaddingClass, preset.tabTextSizeClass)\"\r\n >\r\n <!-- 탭 아이콘 (있을 경우) / Tab Icon -->\r\n <JIcon \r\n v-if=\"tab.icon\" \r\n :name=\"tab.icon\" \r\n size=\"sm\"\r\n class=\"flex-shrink-0\"\r\n />\r\n \r\n <!-- 탭 레이블 / Tab Label -->\r\n <span class=\"flex-1 truncate\">{{ tab.label }}</span>\r\n \r\n <!-- 닫기 버튼 / Close Button (항상 표시) -->\r\n <button\r\n v-if=\"tab.closable\"\r\n type=\"button\"\r\n class=\"flex-shrink-0 h-4 w-4 rounded-sm hover:bg-destructive/10 hover:text-destructive transition-colors focus:outline-none focus:ring-2 focus:ring-ring flex items-center justify-center\"\r\n :aria-label=\"`${tab.label} 탭 닫기`\"\r\n @click=\"(e) => handleCloseTab(e, tab.id)\"\r\n >\r\n <X class=\"h-3 w-3\" />\r\n </button>\r\n </TabsTrigger>\r\n </TabsList>\r\n\r\n <!-- 탭 콘텐츠 영역 / Tab Contents -->\r\n <div class=\"flex-1 w-full overflow-auto\">\r\n <TabsContent\r\n v-for=\"tab in safeTabs\"\r\n :key=\"`content-${tab.id}`\"\r\n :value=\"tab.id\"\r\n class=\"h-full mt-0 data-[state=active]:flex data-[state=active]:flex-col\"\r\n >\r\n <!-- 슬롯 우선 / Slot First -->\r\n <slot :name=\"`content-${tab.id}`\" :tab=\"tab\">\r\n <!-- 동적 컴포넌트 렌더링 / Dynamic Component Rendering -->\r\n <component\r\n v-if=\"tab.component\"\r\n :is=\"tab.component\"\r\n v-bind=\"tab.props || {}\"\r\n />\r\n \r\n <!-- 기본 콘텐츠 / Default Content -->\r\n <div v-else class=\"p-4\">\r\n <p class=\"text-muted-foreground\">{{ tab.label }} 콘텐츠</p>\r\n </div>\r\n </slot>\r\n </TabsContent>\r\n </div>\r\n </Tabs>\r\n</template>\r\n\r\n<style scoped>\r\n/**\r\n * 탭 리스트 스타일 - 하단 보더 제거\r\n * Tab list styles without bottom border\r\n */\r\n:deep([role=\"tablist\"]) {\r\n overflow-x: auto;\r\n overflow-y: hidden;\r\n scrollbar-width: thin;\r\n scrollbar-color: rgba(0, 0, 0, 0.2) transparent;\r\n background: hsl(var(--background));\r\n padding: 0 0.5rem;\r\n padding-top: 0.5rem;\r\n gap: 0.25rem;\r\n border-bottom: none;\r\n}\r\n\r\n:deep([role=\"tablist\"]::-webkit-scrollbar) {\r\n height: 6px;\r\n}\r\n\r\n:deep([role=\"tablist\"]::-webkit-scrollbar-track) {\r\n background: transparent;\r\n}\r\n\r\n:deep([role=\"tablist\"]::-webkit-scrollbar-thumb) {\r\n background-color: rgba(0, 0, 0, 0.2);\r\n border-radius: 3px;\r\n}\r\n\r\n:deep([role=\"tablist\"]::-webkit-scrollbar-thumb:hover) {\r\n background-color: rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n/**\r\n * 다크모드에서 스크롤바 스타일\r\n * Scrollbar styles in dark mode\r\n */\r\n.dark :deep([role=\"tablist\"]::-webkit-scrollbar-thumb) {\r\n background-color: rgba(255, 255, 255, 0.2);\r\n}\r\n\r\n.dark :deep([role=\"tablist\"]::-webkit-scrollbar-thumb:hover) {\r\n background-color: rgba(255, 255, 255, 0.3);\r\n}\r\n\r\n.dark :deep([role=\"tablist\"]) {\r\n scrollbar-color: rgba(255, 255, 255, 0.2) transparent;\r\n}\r\n\r\n/**\r\n * 탭 버튼 스타일 - 명확한 구분\r\n * Tab button styles - clear distinction\r\n */\r\n:deep([role=\"tab\"]) {\r\n position: relative;\r\n padding: 0.625rem 1rem;\r\n border-radius: 0.375rem 0.375rem 0 0;\r\n transition: all 0.2s ease;\r\n border: 1px solid transparent;\r\n border-bottom: none;\r\n}\r\n\r\n/**\r\n * Minimal 스타일 탭 버튼 - JSidebarAdvanced와 높이 맞춤\r\n */\r\n:deep([role=\"tablist\"][class*=\"p-1\"] [role=\"tab\"]) {\r\n padding: 0.625rem 0.5rem;\r\n}\r\n\r\n/**\r\n * 비활성 탭 - 명확하게 구분\r\n * Inactive tabs - clear distinction\r\n */\r\n:deep([role=\"tab\"][data-state=\"inactive\"]) {\r\n background: hsl(var(--muted) / 0.2);\r\n color: hsl(var(--muted-foreground));\r\n border-top: 1px solid hsl(var(--border) / 0.4);\r\n border-left: 1px solid hsl(var(--border) / 0.4);\r\n border-right: 1px solid hsl(var(--border) / 0.4);\r\n}\r\n\r\n:deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\r\n background: hsl(var(--muted) / 0.4);\r\n color: hsl(var(--foreground));\r\n}\r\n\r\n/**\r\n * 다크모드에서 비활성 탭 - 다크 배경색 사용\r\n * Inactive tabs in dark mode - use dark background\r\n */\r\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]) {\r\n background: hsl(var(--secondary));\r\n color: hsl(var(--secondary-foreground));\r\n border-top: 1px solid hsl(var(--border) / 0.5);\r\n border-left: 1px solid hsl(var(--border) / 0.5);\r\n border-right: 1px solid hsl(var(--border) / 0.5);\r\n}\r\n\r\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\r\n background: hsl(var(--muted));\r\n color: hsl(var(--muted-foreground));\r\n}\r\n\r\n/**\r\n * 활성 탭 - 강조된 스타일\r\n * Active tab - emphasized style\r\n */\r\n:deep([role=\"tab\"][data-state=\"active\"]) {\r\n background: hsl(var(--background));\r\n color: hsl(var(--foreground));\r\n font-weight: 500;\r\n border-top: 2px solid hsl(var(--primary));\r\n border-left: 1px solid hsl(var(--border));\r\n border-right: 1px solid hsl(var(--border));\r\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\r\n z-index: 1;\r\n}\r\n\r\n/**\r\n * 다크모드에서 활성 탭 - 배경색 명확히 구분\r\n * Active tab in dark mode - clear background distinction\r\n */\r\n.dark :deep([role=\"tab\"][data-state=\"active\"]) {\r\n background: hsl(var(--card));\r\n color: hsl(var(--card-foreground));\r\n border-top: 2px solid hsl(var(--primary));\r\n border-left: 1px solid hsl(var(--border));\r\n border-right: 1px solid hsl(var(--border));\r\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n/**\r\n * 콘텐츠 영역에 상단 보더 추가\r\n * Add top border to content area\r\n */\r\n:deep([role=\"tabpanel\"]) {\r\n border-top: 1px solid hsl(var(--border));\r\n}\r\n\r\n/**\r\n * 다크모드에서 콘텐츠 영역 보더 - 더 부드러운 구분\r\n * Content area border in dark mode - subtler separation\r\n */\r\n.dark :deep([role=\"tabpanel\"]) {\r\n border-top: 1px solid hsl(var(--border) / 0.6);\r\n}\r\n</style>\r\n\r\n"],"names":["props","__props","emit","__emit","safeTabs","computed","internalActiveId","ref","isHandlingEvent","watch","newValue","newTabs","t","handleTabValueChange","value","stringValue","nextTick","handleTabClick","tabId","handleCloseTab","rootClasses","classes","STYLE_PRESETS","preset","listClasses","_createBlock","_unref","Tabs","_createVNode","TabsList","_createElementBlock","_Fragment","_renderList","tab","TabsTrigger","$event","_normalizeClass","JIcon","_createElementVNode","_hoisted_1","_toDisplayString","e","X","_hoisted_3","TabsContent","_renderSlot","_ctx","_openBlock","_resolveDynamicComponent","_mergeProps","_hoisted_4","_hoisted_5"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JTabs.vue2.js","sources":["../../../../src/components/molecules/JTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { computed, ref, watch, nextTick } from 'vue'\r\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/shadcn'\r\nimport type { JTabsProps, JTabsEmits } from '@/types/dynamic-tabs.types'\r\nimport { X } from 'lucide-vue-next'\r\nimport { cn } from '@/lib/utils'\r\nimport JIcon from '@/components/atoms/JIcon.vue'\r\n\r\n/**\r\n * JTabs - 기본 탭 UI 컴포넌트 (molecules)\r\n * Basic Tabs UI Component\r\n * \r\n * @description\r\n * 정적인 탭 목록을 렌더링하는 기본 탭 컴포넌트입니다.\r\n * 닫기 버튼, 아이콘 등을 지원합니다.\r\n * \r\n * @example\r\n * ```vue\r\n * <JTabs \r\n * :tabs=\"tabs\"\r\n * :active-tab-id=\"activeId\"\r\n * @tab-change=\"handleChange\"\r\n * @tab-close=\"handleClose\"\r\n * />\r\n * ```\r\n */\r\n\r\ntype StyleType = 'default' | 'minimal'\r\n\r\nconst props = withDefaults(defineProps<JTabsProps>(), {\r\n styletype: 'default',\r\n})\r\n\r\nconst emit = defineEmits<JTabsEmits>()\r\n\r\n/**\r\n * 안전한 tabs 배열 (undefined/null 체크)\r\n * Safe tabs array (undefined/null check)\r\n */\r\nconst safeTabs = computed(() => {\r\n return Array.isArray(props.tabs) ? props.tabs : []\r\n})\r\n\r\n/**\r\n * 현재 활성화된 탭 ID (내부 상태)\r\n * Current active tab ID (internal state)\r\n */\r\nconst internalActiveId = ref<string>(\r\n props.activeTabId || (safeTabs.value.length > 0 ? safeTabs.value[0]?.id : '') || ''\r\n)\r\n\r\n/**\r\n * 이벤트 처리 중 플래그 (중복 이벤트 방지)\r\n * Flag to prevent duplicate events\r\n */\r\nlet isHandlingEvent = false\r\n\r\n/**\r\n * props.activeTabId가 변경되면 내부 상태 동기화\r\n * Sync internal state when props.activeTabId changes\r\n */\r\nwatch(() => props.activeTabId, (newValue) => {\r\n if (newValue !== undefined && newValue !== internalActiveId.value) {\r\n internalActiveId.value = newValue\r\n }\r\n}, { immediate: true })\r\n\r\n/**\r\n * props.tabs가 변경되고 activeTabId가 없으면 첫 번째 탭 활성화\r\n * Activate first tab when tabs change and no activeTabId\r\n */\r\nwatch(safeTabs, (newTabs) => {\r\n if (!props.activeTabId && newTabs.length > 0 && !newTabs.find(t => t.id === internalActiveId.value) && newTabs[0]) {\r\n internalActiveId.value = newTabs[0].id\r\n }\r\n})\r\n\r\n/**\r\n * 탭 값 변경 핸들러 (reka-ui TabsRoot에서 직접 호출됨)\r\n * Tab value change handler (called directly from reka-ui TabsRoot)\r\n */\r\nconst handleTabValueChange = (value: string | number) => {\r\n if (isHandlingEvent) return\r\n \r\n const stringValue = String(value)\r\n if (stringValue !== internalActiveId.value) {\r\n isHandlingEvent = true\r\n internalActiveId.value = stringValue\r\n emit('update:activeTabId', stringValue)\r\n emit('tabChange', stringValue)\r\n // 다음 tick에서 플래그 리셋\r\n nextTick(() => {\r\n isHandlingEvent = false\r\n })\r\n }\r\n}\r\n\r\n/**\r\n * 탭 클릭 핸들러 (백업 방안 - reka-ui 이벤트가 작동하지 않을 경우)\r\n * Tab click handler (backup - in case reka-ui events don't work)\r\n */\r\nconst handleTabClick = (tabId: string) => {\r\n // reka-ui 이벤트가 작동하지 않을 경우 직접 처리\r\n // handleTabValueChange가 이미 처리했으면 중복 방지\r\n if (isHandlingEvent || tabId === internalActiveId.value) return\r\n \r\n isHandlingEvent = true\r\n internalActiveId.value = tabId\r\n emit('update:activeTabId', tabId)\r\n emit('tabChange', tabId)\r\n // 다음 tick에서 플래그 리셋\r\n nextTick(() => {\r\n isHandlingEvent = false\r\n })\r\n}\r\n\r\n/**\r\n * 탭 닫기 핸들러\r\n * Tab close handler\r\n */\r\nconst handleCloseTab = (e: Event, tabId: string) => {\r\n e.stopPropagation() // 탭 클릭 이벤트 전파 방지\r\n emit('tabClose', tabId)\r\n}\r\n\r\n/**\r\n * 루트 클래스\r\n * Root classes\r\n */\r\nconst rootClasses = computed(() => {\r\n const classes = ['flex', 'flex-col', 'w-full', 'h-full']\r\n \r\n if (props.className) {\r\n classes.push(props.className)\r\n }\r\n \r\n return classes.join(' ')\r\n})\r\n\r\n/**\r\n * 스타일 프리셋\r\n */\r\nconst STYLE_PRESETS: Record<StyleType, {\r\n tabPaddingClass: string\r\n tabTextSizeClass: string\r\n listPaddingClass: string\r\n}> = {\r\n default: {\r\n tabPaddingClass: 'px-2.5 py-1',\r\n tabTextSizeClass: 'text-xs',\r\n listPaddingClass: 'p-0.5',\r\n },\r\n minimal: {\r\n tabPaddingClass: 'px-2 py-0.5',\r\n tabTextSizeClass: 'text-xs',\r\n listPaddingClass: 'p-0.5',\r\n },\r\n}\r\n\r\nconst preset = computed(() => {\r\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\r\n})\r\n\r\n/**\r\n * 탭 리스트 클래스\r\n * Tabs list classes\r\n */\r\nconst listClasses = computed(() => {\r\n const classes = ['w-full', 'justify-start', preset.value.listPaddingClass]\r\n \r\n if (props.listClassName) {\r\n classes.push(props.listClassName)\r\n }\r\n \r\n return classes.join(' ')\r\n})\r\n</script>\r\n\r\n<template>\r\n <Tabs\r\n :model-value=\"internalActiveId\"\r\n @update:model-value=\"handleTabValueChange\"\r\n orientation=\"horizontal\"\r\n :class=\"rootClasses\"\r\n >\r\n <!-- 탭 헤더 영역 / Tab Headers -->\r\n <TabsList :class=\"listClasses\">\r\n <TabsTrigger\r\n v-for=\"tab in safeTabs\"\r\n :key=\"tab.id\"\r\n :value=\"tab.id\"\r\n @click=\"handleTabClick(tab.id)\"\r\n :class=\"cn('!flex !items-center !gap-2', preset.tabPaddingClass, preset.tabTextSizeClass)\"\r\n >\r\n <!-- 탭 아이콘 (있을 경우) / Tab Icon -->\r\n <JIcon \r\n v-if=\"tab.icon\" \r\n :name=\"tab.icon\" \r\n size=\"sm\"\r\n class=\"flex-shrink-0\"\r\n />\r\n \r\n <!-- 탭 레이블 / Tab Label -->\r\n <span class=\"flex-1 truncate\">{{ tab.label }}</span>\r\n \r\n <!-- 닫기 버튼 / Close Button (항상 표시) -->\r\n <button\r\n v-if=\"tab.closable\"\r\n type=\"button\"\r\n class=\"flex-shrink-0 h-3.5 w-3.5 rounded-sm hover:bg-destructive/10 hover:text-destructive transition-colors focus:outline-none focus:ring-2 focus:ring-ring flex items-center justify-center\"\r\n :aria-label=\"`${tab.label} 탭 닫기`\"\r\n @click=\"(e) => handleCloseTab(e, tab.id)\"\r\n >\r\n <X class=\"h-2.5 w-2.5\" />\r\n </button>\r\n </TabsTrigger>\r\n </TabsList>\r\n\r\n <!-- 탭 콘텐츠 영역 / Tab Contents -->\r\n <div class=\"flex-1 w-full overflow-auto\">\r\n <TabsContent\r\n v-for=\"tab in safeTabs\"\r\n :key=\"`content-${tab.id}`\"\r\n :value=\"tab.id\"\r\n class=\"h-full mt-0 data-[state=active]:flex data-[state=active]:flex-col\"\r\n >\r\n <!-- 슬롯 우선 / Slot First -->\r\n <slot :name=\"`content-${tab.id}`\" :tab=\"tab\">\r\n <!-- 동적 컴포넌트 렌더링 / Dynamic Component Rendering -->\r\n <component\r\n v-if=\"tab.component\"\r\n :is=\"tab.component\"\r\n v-bind=\"tab.props || {}\"\r\n />\r\n \r\n <!-- 기본 콘텐츠 / Default Content -->\r\n <div v-else class=\"p-4\">\r\n <p class=\"text-muted-foreground\">{{ tab.label }} 콘텐츠</p>\r\n </div>\r\n </slot>\r\n </TabsContent>\r\n </div>\r\n </Tabs>\r\n</template>\r\n\r\n<style scoped>\r\n/**\r\n * 탭 리스트 스타일 - 하단 보더 제거\r\n * Tab list styles without bottom border\r\n */\r\n:deep([role=\"tablist\"]:not(.ag-side-buttons)) {\r\n overflow-x: auto;\r\n overflow-y: hidden;\r\n scrollbar-width: thin;\r\n scrollbar-color: rgba(0, 0, 0, 0.2) transparent;\r\n background: hsl(var(--background));\r\n padding: 0 0.5rem;\r\n padding-top: 0.25rem;\r\n gap: 0.25rem;\r\n border-bottom: none;\r\n}\r\n\r\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar) {\r\n height: 6px;\r\n}\r\n\r\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-track) {\r\n background: transparent;\r\n}\r\n\r\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\r\n background-color: rgba(0, 0, 0, 0.2);\r\n border-radius: 3px;\r\n}\r\n\r\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\r\n background-color: rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n/**\r\n * 다크모드에서 스크롤바 스타일\r\n * Scrollbar styles in dark mode\r\n */\r\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\r\n background-color: rgba(255, 255, 255, 0.2);\r\n}\r\n\r\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\r\n background-color: rgba(255, 255, 255, 0.3);\r\n}\r\n\r\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)) {\r\n scrollbar-color: rgba(255, 255, 255, 0.2) transparent;\r\n}\r\n\r\n/**\r\n * 탭 버튼 스타일 - 명확한 구분\r\n * Tab button styles - clear distinction\r\n */\r\n:deep([role=\"tab\"]) {\r\n position: relative;\r\n padding: 0.375rem 0.75rem;\r\n border-radius: 0.375rem 0.375rem 0 0;\r\n transition: all 0.2s ease;\r\n border: 1px solid transparent;\r\n border-bottom: none;\r\n}\r\n\r\n/**\r\n * Minimal 스타일 탭 버튼 - JSidebarAdvanced와 높이 맞춤\r\n */\r\n:deep([role=\"tablist\"][class*=\"p-0.5\"] [role=\"tab\"]) {\r\n padding: 0.25rem 0.5rem;\r\n}\r\n\r\n/**\r\n * 비활성 탭 - 명확하게 구분\r\n * Inactive tabs - clear distinction\r\n */\r\n:deep([role=\"tab\"][data-state=\"inactive\"]) {\r\n background: hsl(var(--muted) / 0.2);\r\n color: hsl(var(--muted-foreground));\r\n border-top: 1px solid hsl(var(--border) / 0.4);\r\n border-left: 1px solid hsl(var(--border) / 0.4);\r\n border-right: 1px solid hsl(var(--border) / 0.4);\r\n}\r\n\r\n:deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\r\n background: hsl(var(--muted) / 0.4);\r\n color: hsl(var(--foreground));\r\n}\r\n\r\n/**\r\n * 다크모드에서 비활성 탭 - 다크 배경색 사용\r\n * Inactive tabs in dark mode - use dark background\r\n */\r\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]) {\r\n background: hsl(var(--secondary));\r\n color: hsl(var(--secondary-foreground));\r\n border-top: 1px solid hsl(var(--border) / 0.5);\r\n border-left: 1px solid hsl(var(--border) / 0.5);\r\n border-right: 1px solid hsl(var(--border) / 0.5);\r\n}\r\n\r\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\r\n background: hsl(var(--muted));\r\n color: hsl(var(--muted-foreground));\r\n}\r\n\r\n/**\r\n * 활성 탭 - 강조된 스타일\r\n * Active tab - emphasized style\r\n */\r\n:deep([role=\"tab\"][data-state=\"active\"]) {\r\n background: hsl(var(--background));\r\n color: hsl(var(--foreground));\r\n font-weight: 500;\r\n border-top: 2px solid hsl(var(--primary));\r\n border-left: 1px solid hsl(var(--border));\r\n border-right: 1px solid hsl(var(--border));\r\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\r\n z-index: 1;\r\n}\r\n\r\n/**\r\n * 다크모드에서 활성 탭 - 배경색 명확히 구분\r\n * Active tab in dark mode - clear background distinction\r\n */\r\n.dark :deep([role=\"tab\"][data-state=\"active\"]) {\r\n background: hsl(var(--card));\r\n color: hsl(var(--card-foreground));\r\n border-top: 2px solid hsl(var(--primary));\r\n border-left: 1px solid hsl(var(--border));\r\n border-right: 1px solid hsl(var(--border));\r\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\r\n}\r\n\r\n/**\r\n * 콘텐츠 영역에 상단 보더 추가\r\n * Add top border to content area\r\n */\r\n:deep([role=\"tabpanel\"]) {\r\n border-top: 1px solid hsl(var(--border));\r\n}\r\n\r\n/**\r\n * 다크모드에서 콘텐츠 영역 보더 - 더 부드러운 구분\r\n * Content area border in dark mode - subtler separation\r\n */\r\n.dark :deep([role=\"tabpanel\"]) {\r\n border-top: 1px solid hsl(var(--border) / 0.6);\r\n}\r\n</style>\r\n\r\n"],"names":["props","__props","emit","__emit","safeTabs","computed","internalActiveId","ref","isHandlingEvent","watch","newValue","newTabs","t","handleTabValueChange","value","stringValue","nextTick","handleTabClick","tabId","handleCloseTab","rootClasses","classes","STYLE_PRESETS","preset","listClasses","_createBlock","_unref","Tabs","_createVNode","TabsList","_createElementBlock","_Fragment","_renderList","tab","TabsTrigger","$event","_normalizeClass","JIcon","_createElementVNode","_hoisted_1","_toDisplayString","e","X","_hoisted_3","TabsContent","_renderSlot","_ctx","_openBlock","_resolveDynamicComponent","_mergeProps","_hoisted_4","_hoisted_5"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),x=require("../atoms/JButton.vue.cjs");require("../shadcn/index.cjs");require("lucide-vue-next");const n=require("../../lib/utils.cjs");require("@internationalized/date");require("md-editor-v3");;/* empty css */;/* empty css */require("../shadcn/badge-variants.cjs");require("@vueuse/core");require("reka-ui");;/* empty css */require("../shadcn/avatar-variants.cjs");const o=require("../atoms/JIcon.vue.cjs"),p=require("../atoms/JLabel.vue.cjs"),b=require("../atoms/JPopover.vue.cjs");require("dompurify");;/* empty css */require("ag-grid-vue3");require("ag-grid-community");require("ag-grid-enterprise");;/* empty css */;/* empty css */;/* empty css */;/* empty css */require("vue-sonner");const y={class:"flex items-center gap-3 flex-1"},C={class:"flex items-center gap-2"},g={class:"p-2"},k={class:"text-xs text-muted-foreground whitespace-normal break-words"},v={key:0,class:"flex items-center gap-2"},h={key:1},_=e.defineComponent({__name:"JTitlebar",props:{styletype:{default:"default"},icon:{},title:{},description:{},showHelp:{type:Boolean},buttons:{default:()=>[]}},emits:["buttonClick","help"],setup(r,{emit:u}){const d=r,i={default:{class:"flex items-center justify-between w-full h-8 px-3 border-b border-border bg-background",iconClass:"text-primary",titleClass:"text-foreground text-md",infoIconClass:"text-muted-foreground hover:text-primary"},primary:{class:"flex items-center justify-between w-full h-8 px-3 border-b border-blue-400/30 bg-blue-500",iconClass:"text-white",titleClass:"text-white font-semibold text-md",infoIconClass:"text-white/80 hover:text-white"},accent:{class:"flex items-center justify-between w-full h-8 px-3 border-b border-blue-200 bg-blue-50",iconClass:"text-blue-600",titleClass:"text-blue-700 font-semibold text-md",infoIconClass:"text-blue-600/70 hover:text-blue-700"},neutral:{class:"flex items-center justify-between w-full h-8 px-3 border-b border-gray-300 bg-gray-100",iconClass:"text-gray-600",titleClass:"text-gray-700 text-md",infoIconClass:"text-gray-500 hover:text-gray-700"},elevated:{class:"flex items-center justify-between w-full h-8 px-3 border-b border-border bg-background shadow-md",iconClass:"text-primary",titleClass:"text-foreground font-semibold text-md",infoIconClass:"text-muted-foreground hover:text-primary"}},l=e.computed(()=>i[d.styletype]??i.default),a=u,m=s=>{s.onClick?.(),a("buttonClick",s)};return(s,c)=>(e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(e.unref(n.cn)(l.value.class))},[e.createElementVNode("div",y,[r.icon?(e.openBlock(),e.createBlock(e.unref(o.default),{key:0,name:r.icon,class:e.normalizeClass(l.value.iconClass)},null,8,["name","class"])):e.createCommentVNode("",!0),e.createElementVNode("div",C,[e.createVNode(e.unref(p.default),{text:r.title||"",class:e.normalizeClass(e.unref(n.cn)("font-semibold",l.value.titleClass))},null,8,["text","class"]),r.description?(e.openBlock(),e.createBlock(e.unref(b.default),{key:0,position:"bottom",align:"center","side-offset":8},{trigger:e.withCtx(()=>[e.createVNode(e.unref(o.default),{name:"info",size:"sm",class:e.normalizeClass(e.unref(n.cn)("cursor-help transition-colors inline-flex",l.value.infoIconClass))},null,8,["class"])]),default:e.withCtx(()=>[e.createElementVNode("div",g,[e.createElementVNode("p",k,e.toDisplayString(r.description),1)])]),_:1})):e.createCommentVNode("",!0),r.showHelp?(e.openBlock(),e.createBlock(e.unref(o.default),{key:1,name:"circleQuestionMark",size:"sm",class:e.normalizeClass(e.unref(n.cn)("cursor-pointer transition-colors inline-flex",l.value.infoIconClass)),onClick:c[0]||(c[0]=t=>a("help"))},null,8,["class"])):e.createCommentVNode("",!0)])]),r.buttons&&r.buttons.length>0?(e.openBlock(),e.createElementBlock("div",v,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(r.buttons,(t,f)=>(e.openBlock(),e.createBlock(e.unref(x.default),{key:f,variant:t.variant,styletype:t.styletype,disabled:t.disabled,loading:t.loading,size:t.size,onClick:q=>m(t)},{default:e.withCtx(()=>[t.icon?(e.openBlock(),e.createBlock(e.unref(o.default),{key:0,name:t.icon,size:"sm",class:"mr-1.5"},null,8,["name"])):e.createCommentVNode("",!0),t.text?(e.openBlock(),e.createElementBlock("span",h,e.toDisplayString(t.text),1)):e.createCommentVNode("",!0)]),_:2},1032,["variant","styletype","disabled","loading","size","onClick"]))),128))])):e.createCommentVNode("",!0),e.renderSlot(s.$slots,"buttons")],2))}});exports.default=_;
|
|
2
2
|
//# sourceMappingURL=JTitlebar.vue.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JTitlebar.vue.cjs","sources":["../../../../src/components/molecules/JTitlebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from \"vue\"\nimport { JIcon, JLabel, JPopover, JButton } from '@/components/atoms'\nimport type { ButtonVariants } from '@/components/shadcn'\nimport { cn } from \"@/lib/utils\"\n\nexport type TitlebarButton = {\n /** 버튼 아이콘 */\n icon?: string\n /** 버튼 텍스트 */\n text?: string\n /** 버튼 클릭 핸들러 */\n onClick?: () => void\n /** 버튼 variant */\n variant?: ButtonVariants['variant']\n /** 버튼 스타일 타입 */\n styletype?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'outline' | 'ghost' | 'link' | 'sm' | 'lg' | 'icon'\n /** 버튼 size */\n size?: 'sm' | 'md' | 'lg'\n /** 버튼 비활성화 */\n disabled?: boolean\n /** 버튼 로딩 상태 */\n loading?: boolean\n}\n\ntype StyleType =\n | 'default' // 기본 스타일: 일반 페이지용 (흰 배경, 얇은 보더)\n | 'primary' // 강조 스타일: 주요 기능용 (밝은 파란 배경, 흰 텍스트)\n | 'accent' // 액센트 스타일: 부드러운 강조용 (연한 파란 배경, 진한 텍스트)\n | 'neutral' // 중립 스타일: 서브 페이지용 (회색 배경, 회색 텍스트)\n | 'elevated' // 강조 스타일: 깊이감 있는 구분 (흰 배경, 그림자, 보더)\n\nconst props = withDefaults(\n defineProps<{\n /** Titlebar 스타일 타입 */\n styletype?: StyleType\n /** 프로그램 아이콘 */\n icon?: string\n /** 프로그램명 */\n title?: string\n /** 프로그램 설명 (Popover에 표시) */\n description?: string\n /** 도움말 아이콘(?) 표시 여부 — 클릭 시 help 이벤트 emit */\n showHelp?: boolean\n /** 메인 버튼 목록 */\n buttons?: TitlebarButton[]\n }>(),\n {\n styletype: 'default',\n buttons: () => [],\n }\n)\n\n/**\n * styletype -> class 매핑 (배경, 보더, 그림자 등)\n */\nconst STYLE_PRESETS: Record<StyleType, { \n class: string\n iconClass: string // 아이콘 색상 클래스\n titleClass: string // 제목 텍스트 색상 클래스\n infoIconClass: string // 정보 아이콘 색상 클래스\n}> = {\n default: {\n // 기본: 흰 배경 + 얇은 보더 (가장 일반적)\n class: 'flex items-center justify-between w-full h-
|
|
1
|
+
{"version":3,"file":"JTitlebar.vue.cjs","sources":["../../../../src/components/molecules/JTitlebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from \"vue\"\nimport { JIcon, JLabel, JPopover, JButton } from '@/components/atoms'\nimport type { ButtonVariants } from '@/components/shadcn'\nimport { cn } from \"@/lib/utils\"\n\nexport type TitlebarButton = {\n /** 버튼 아이콘 */\n icon?: string\n /** 버튼 텍스트 */\n text?: string\n /** 버튼 클릭 핸들러 */\n onClick?: () => void\n /** 버튼 variant */\n variant?: ButtonVariants['variant']\n /** 버튼 스타일 타입 */\n styletype?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'outline' | 'ghost' | 'link' | 'sm' | 'lg' | 'icon'\n /** 버튼 size */\n size?: 'sm' | 'md' | 'lg'\n /** 버튼 비활성화 */\n disabled?: boolean\n /** 버튼 로딩 상태 */\n loading?: boolean\n}\n\ntype StyleType =\n | 'default' // 기본 스타일: 일반 페이지용 (흰 배경, 얇은 보더)\n | 'primary' // 강조 스타일: 주요 기능용 (밝은 파란 배경, 흰 텍스트)\n | 'accent' // 액센트 스타일: 부드러운 강조용 (연한 파란 배경, 진한 텍스트)\n | 'neutral' // 중립 스타일: 서브 페이지용 (회색 배경, 회색 텍스트)\n | 'elevated' // 강조 스타일: 깊이감 있는 구분 (흰 배경, 그림자, 보더)\n\nconst props = withDefaults(\n defineProps<{\n /** Titlebar 스타일 타입 */\n styletype?: StyleType\n /** 프로그램 아이콘 */\n icon?: string\n /** 프로그램명 */\n title?: string\n /** 프로그램 설명 (Popover에 표시) */\n description?: string\n /** 도움말 아이콘(?) 표시 여부 — 클릭 시 help 이벤트 emit */\n showHelp?: boolean\n /** 메인 버튼 목록 */\n buttons?: TitlebarButton[]\n }>(),\n {\n styletype: 'default',\n buttons: () => [],\n }\n)\n\n/**\n * styletype -> class 매핑 (배경, 보더, 그림자 등)\n */\nconst STYLE_PRESETS: Record<StyleType, { \n class: string\n iconClass: string // 아이콘 색상 클래스\n titleClass: string // 제목 텍스트 색상 클래스\n infoIconClass: string // 정보 아이콘 색상 클래스\n}> = {\n default: {\n // 기본: 흰 배경 + 얇은 보더 (가장 일반적)\n class: 'flex items-center justify-between w-full h-8 px-3 border-b border-border bg-background',\n iconClass: 'text-primary',\n titleClass: 'text-foreground text-md',\n infoIconClass: 'text-muted-foreground hover:text-primary',\n },\n primary: {\n // 강조: 밝은 파란 배경 + 흰 텍스트 (명확한 강조)\n // text-white를 제거하여 버튼이 색상 상속을 받지 않도록 함\n class: 'flex items-center justify-between w-full h-8 px-3 border-b border-blue-400/30 bg-blue-500',\n iconClass: 'text-white',\n titleClass: 'text-white font-semibold text-md',\n infoIconClass: 'text-white/80 hover:text-white',\n },\n accent: {\n // 액센트: 연한 파란 배경 + 진한 파란 텍스트 (부드러운 강조)\n // text-blue-700을 제거하여 버튼이 색상 상속을 받지 않도록 함\n class: 'flex items-center justify-between w-full h-8 px-3 border-b border-blue-200 bg-blue-50',\n iconClass: 'text-blue-600',\n titleClass: 'text-blue-700 font-semibold text-md',\n infoIconClass: 'text-blue-600/70 hover:text-blue-700',\n },\n neutral: {\n // 중립: 회색 배경 + 회색 텍스트 (확실한 구분)\n // text-gray-700을 제거하여 버튼이 색상 상속을 받지 않도록 함\n class: 'flex items-center justify-between w-full h-8 px-3 border-b border-gray-300 bg-gray-100',\n iconClass: 'text-gray-600',\n titleClass: 'text-gray-700 text-md',\n infoIconClass: 'text-gray-500 hover:text-gray-700',\n },\n elevated: {\n // 강조: 흰 배경 + 그림자 + 보더 (깊이감 있는 구분)\n class: 'flex items-center justify-between w-full h-8 px-3 border-b border-border bg-background shadow-md',\n iconClass: 'text-primary',\n titleClass: 'text-foreground font-semibold text-md',\n infoIconClass: 'text-muted-foreground hover:text-primary',\n },\n}\n\nconst preset = computed(() => {\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\n})\n\nconst emit = defineEmits<{\n /** 버튼 클릭 이벤트 */\n buttonClick: [button: TitlebarButton]\n /** 도움말 아이콘 클릭 이벤트 */\n help: []\n}>()\n\nconst handleButtonClick = (button: TitlebarButton) => {\n // 버튼 클릭 핸들러 패턴:\n // 1. button.onClick이 있으면 실행 (인라인 핸들러)\n // 2. 항상 emit('buttonClick', button) 실행 (부모 컴포넌트로 전달)\n // 주의: onClick과 emit이 모두 실행되므로 중복 처리 가능성 있음\n button.onClick?.()\n emit('buttonClick', button)\n}\n</script>\n\n<template>\n <div :class=\"cn(preset.class)\">\n <!-- 왼쪽: 아이콘 + 프로그램명 -->\n <div class=\"flex items-center gap-3 flex-1\">\n <!-- 아이콘 -->\n <JIcon \n v-if=\"icon\" \n :name=\"icon\" \n :class=\"preset.iconClass\"\n />\n \n <!-- 프로그램명 + 정보 아이콘 + Popover -->\n <div class=\"flex items-center gap-2\">\n <!-- 프로그램명 -->\n <JLabel \n :text=\"title || ''\" \n :class=\"cn('font-semibold', preset.titleClass)\"\n />\n \n <!-- 정보 아이콘 (description이 있을 때만 표시) -->\n <JPopover \n v-if=\"description\" \n position=\"bottom\"\n align=\"center\"\n :side-offset=\"8\"\n >\n <template #trigger>\n <JIcon \n name=\"info\" \n size=\"sm\" \n :class=\"cn('cursor-help transition-colors inline-flex', preset.infoIconClass)\"\n />\n </template>\n <div class=\"p-2\">\n <p class=\"text-xs text-muted-foreground whitespace-normal break-words\">{{ description }}</p>\n </div>\n </JPopover>\n\n <!-- 도움말 아이콘 (showHelp일 때 표시, 클릭 시 help 이벤트) -->\n <JIcon\n v-if=\"showHelp\"\n name=\"circleQuestionMark\"\n size=\"sm\"\n :class=\"cn('cursor-pointer transition-colors inline-flex', preset.infoIconClass)\"\n @click=\"emit('help')\"\n />\n </div>\n </div>\n\n <!-- 오른쪽: 메인 버튼들 -->\n <!-- buttons prop으로 정의된 버튼들 (v-if로 조건부 렌더링) -->\n <div v-if=\"buttons && buttons.length > 0\" class=\"flex items-center gap-2\">\n <JButton\n v-for=\"(button, index) in buttons\"\n :key=\"index\"\n :variant=\"button.variant\"\n :styletype=\"button.styletype\"\n :disabled=\"button.disabled\"\n :loading=\"button.loading\"\n :size=\"button.size\"\n @click=\"handleButtonClick(button)\"\n >\n <JIcon v-if=\"button.icon\" :name=\"button.icon\" size=\"sm\" class=\"mr-1.5\" />\n <span v-if=\"button.text\">{{ button.text }}</span>\n </JButton>\n </div>\n\n <!-- 버튼 슬롯 레이아웃 설명:\n - buttons prop과 buttons slot이 모두 제공되면 함께 표시됨\n - buttons prop: 구조화된 버튼 정의 (JButton 컴포넌트 사용)\n - buttons slot: 완전한 커스텀 버튼 제어 가능\n - 레이아웃: buttons prop이 먼저 렌더링되고, 그 다음 slot이 렌더링됨 -->\n <slot name=\"buttons\" />\n </div>\n</template>\n\n"],"names":["props","__props","STYLE_PRESETS","preset","computed","emit","__emit","handleButtonClick","button","_createElementBlock","_normalizeClass","_unref","cn","_createElementVNode","_hoisted_1","_createBlock","JIcon","_hoisted_2","_createVNode","JLabel","JPopover","_hoisted_3","_hoisted_4","_toDisplayString","_openBlock","_hoisted_5","_Fragment","_renderList","index","JButton","$event","_hoisted_6","_renderSlot","_ctx"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent as w, computed as _, createElementBlock as c, openBlock as
|
|
1
|
+
import { defineComponent as w, computed as _, createElementBlock as c, openBlock as o, normalizeClass as r, unref as t, createElementVNode as m, createCommentVNode as l, renderSlot as z, createBlock as n, createVNode as y, withCtx as f, toDisplayString as g, Fragment as I, renderList as $ } from "vue";
|
|
2
2
|
import j from "../atoms/JButton.vue.js";
|
|
3
3
|
import "../shadcn/index.js";
|
|
4
4
|
import "lucide-vue-next";
|
|
@@ -25,7 +25,7 @@ import "ag-grid-enterprise";
|
|
|
25
25
|
/* empty css */
|
|
26
26
|
/* empty css */
|
|
27
27
|
import "vue-sonner";
|
|
28
|
-
const S = { class: "flex items-center gap-3 flex-1" }, N = { class: "flex items-center gap-2" }, T = { class: "p-2" }, V = { class: "text-
|
|
28
|
+
const S = { class: "flex items-center gap-3 flex-1" }, N = { class: "flex items-center gap-2" }, T = { class: "p-2" }, V = { class: "text-xs text-muted-foreground whitespace-normal break-words" }, H = {
|
|
29
29
|
key: 0,
|
|
30
30
|
class: "flex items-center gap-2"
|
|
31
31
|
}, L = { key: 1 }, de = /* @__PURE__ */ w({
|
|
@@ -39,65 +39,64 @@ const S = { class: "flex items-center gap-3 flex-1" }, N = { class: "flex items-
|
|
|
39
39
|
buttons: { default: () => [] }
|
|
40
40
|
},
|
|
41
41
|
emits: ["buttonClick", "help"],
|
|
42
|
-
setup(s, { emit:
|
|
43
|
-
const
|
|
42
|
+
setup(s, { emit: C }) {
|
|
43
|
+
const h = s, p = {
|
|
44
44
|
default: {
|
|
45
45
|
// 기본: 흰 배경 + 얇은 보더 (가장 일반적)
|
|
46
|
-
class: "flex items-center justify-between w-full h-
|
|
46
|
+
class: "flex items-center justify-between w-full h-8 px-3 border-b border-border bg-background",
|
|
47
47
|
iconClass: "text-primary",
|
|
48
|
-
titleClass: "text-foreground",
|
|
48
|
+
titleClass: "text-foreground text-md",
|
|
49
49
|
infoIconClass: "text-muted-foreground hover:text-primary"
|
|
50
50
|
},
|
|
51
51
|
primary: {
|
|
52
52
|
// 강조: 밝은 파란 배경 + 흰 텍스트 (명확한 강조)
|
|
53
53
|
// text-white를 제거하여 버튼이 색상 상속을 받지 않도록 함
|
|
54
|
-
class: "flex items-center justify-between w-full h-
|
|
54
|
+
class: "flex items-center justify-between w-full h-8 px-3 border-b border-blue-400/30 bg-blue-500",
|
|
55
55
|
iconClass: "text-white",
|
|
56
|
-
titleClass: "text-white font-semibold",
|
|
56
|
+
titleClass: "text-white font-semibold text-md",
|
|
57
57
|
infoIconClass: "text-white/80 hover:text-white"
|
|
58
58
|
},
|
|
59
59
|
accent: {
|
|
60
60
|
// 액센트: 연한 파란 배경 + 진한 파란 텍스트 (부드러운 강조)
|
|
61
61
|
// text-blue-700을 제거하여 버튼이 색상 상속을 받지 않도록 함
|
|
62
|
-
class: "flex items-center justify-between w-full h-
|
|
62
|
+
class: "flex items-center justify-between w-full h-8 px-3 border-b border-blue-200 bg-blue-50",
|
|
63
63
|
iconClass: "text-blue-600",
|
|
64
|
-
titleClass: "text-blue-700 font-semibold",
|
|
64
|
+
titleClass: "text-blue-700 font-semibold text-md",
|
|
65
65
|
infoIconClass: "text-blue-600/70 hover:text-blue-700"
|
|
66
66
|
},
|
|
67
67
|
neutral: {
|
|
68
68
|
// 중립: 회색 배경 + 회색 텍스트 (확실한 구분)
|
|
69
69
|
// text-gray-700을 제거하여 버튼이 색상 상속을 받지 않도록 함
|
|
70
|
-
class: "flex items-center justify-between w-full h-
|
|
70
|
+
class: "flex items-center justify-between w-full h-8 px-3 border-b border-gray-300 bg-gray-100",
|
|
71
71
|
iconClass: "text-gray-600",
|
|
72
|
-
titleClass: "text-gray-700",
|
|
72
|
+
titleClass: "text-gray-700 text-md",
|
|
73
73
|
infoIconClass: "text-gray-500 hover:text-gray-700"
|
|
74
74
|
},
|
|
75
75
|
elevated: {
|
|
76
76
|
// 강조: 흰 배경 + 그림자 + 보더 (깊이감 있는 구분)
|
|
77
|
-
class: "flex items-center justify-between w-full h-
|
|
77
|
+
class: "flex items-center justify-between w-full h-8 px-3 border-b border-border bg-background shadow-md",
|
|
78
78
|
iconClass: "text-primary",
|
|
79
|
-
titleClass: "text-foreground font-semibold",
|
|
79
|
+
titleClass: "text-foreground font-semibold text-md",
|
|
80
80
|
infoIconClass: "text-muted-foreground hover:text-primary"
|
|
81
81
|
}
|
|
82
|
-
},
|
|
82
|
+
}, i = _(() => p[h.styletype] ?? p.default), x = C, k = (a) => {
|
|
83
83
|
a.onClick?.(), x("buttonClick", a);
|
|
84
84
|
};
|
|
85
|
-
return (a, b) => (
|
|
86
|
-
class:
|
|
85
|
+
return (a, b) => (o(), c("div", {
|
|
86
|
+
class: r(t(d)(i.value.class))
|
|
87
87
|
}, [
|
|
88
88
|
m("div", S, [
|
|
89
|
-
s.icon ? (
|
|
89
|
+
s.icon ? (o(), n(t(u), {
|
|
90
90
|
key: 0,
|
|
91
91
|
name: s.icon,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}, null, 8, ["name", "class"])) : i("", !0),
|
|
92
|
+
class: r(i.value.iconClass)
|
|
93
|
+
}, null, 8, ["name", "class"])) : l("", !0),
|
|
95
94
|
m("div", N, [
|
|
96
95
|
y(t(B), {
|
|
97
96
|
text: s.title || "",
|
|
98
|
-
class:
|
|
97
|
+
class: r(t(d)("font-semibold", i.value.titleClass))
|
|
99
98
|
}, null, 8, ["text", "class"]),
|
|
100
|
-
s.description ? (
|
|
99
|
+
s.description ? (o(), n(t(E), {
|
|
101
100
|
key: 0,
|
|
102
101
|
position: "bottom",
|
|
103
102
|
align: "center",
|
|
@@ -107,7 +106,7 @@ const S = { class: "flex items-center gap-3 flex-1" }, N = { class: "flex items-
|
|
|
107
106
|
y(t(u), {
|
|
108
107
|
name: "info",
|
|
109
108
|
size: "sm",
|
|
110
|
-
class:
|
|
109
|
+
class: r(t(d)("cursor-help transition-colors inline-flex", i.value.infoIconClass))
|
|
111
110
|
}, null, 8, ["class"])
|
|
112
111
|
]),
|
|
113
112
|
default: f(() => [
|
|
@@ -116,38 +115,38 @@ const S = { class: "flex items-center gap-3 flex-1" }, N = { class: "flex items-
|
|
|
116
115
|
])
|
|
117
116
|
]),
|
|
118
117
|
_: 1
|
|
119
|
-
})) :
|
|
120
|
-
s.showHelp ? (
|
|
118
|
+
})) : l("", !0),
|
|
119
|
+
s.showHelp ? (o(), n(t(u), {
|
|
121
120
|
key: 1,
|
|
122
121
|
name: "circleQuestionMark",
|
|
123
122
|
size: "sm",
|
|
124
|
-
class:
|
|
123
|
+
class: r(t(d)("cursor-pointer transition-colors inline-flex", i.value.infoIconClass)),
|
|
125
124
|
onClick: b[0] || (b[0] = (e) => x("help"))
|
|
126
|
-
}, null, 8, ["class"])) :
|
|
125
|
+
}, null, 8, ["class"])) : l("", !0)
|
|
127
126
|
])
|
|
128
127
|
]),
|
|
129
|
-
s.buttons && s.buttons.length > 0 ? (
|
|
130
|
-
(
|
|
128
|
+
s.buttons && s.buttons.length > 0 ? (o(), c("div", H, [
|
|
129
|
+
(o(!0), c(I, null, $(s.buttons, (e, v) => (o(), n(t(j), {
|
|
131
130
|
key: v,
|
|
132
131
|
variant: e.variant,
|
|
133
132
|
styletype: e.styletype,
|
|
134
133
|
disabled: e.disabled,
|
|
135
134
|
loading: e.loading,
|
|
136
|
-
|
|
135
|
+
size: e.size,
|
|
137
136
|
onClick: (D) => k(e)
|
|
138
137
|
}, {
|
|
139
138
|
default: f(() => [
|
|
140
|
-
e.icon ? (
|
|
139
|
+
e.icon ? (o(), n(t(u), {
|
|
141
140
|
key: 0,
|
|
142
141
|
name: e.icon,
|
|
143
142
|
size: "sm",
|
|
144
143
|
class: "mr-1.5"
|
|
145
|
-
}, null, 8, ["name"])) :
|
|
146
|
-
e.text ? (
|
|
144
|
+
}, null, 8, ["name"])) : l("", !0),
|
|
145
|
+
e.text ? (o(), c("span", L, g(e.text), 1)) : l("", !0)
|
|
147
146
|
]),
|
|
148
147
|
_: 2
|
|
149
|
-
}, 1032, ["variant", "styletype", "disabled", "loading", "
|
|
150
|
-
])) :
|
|
148
|
+
}, 1032, ["variant", "styletype", "disabled", "loading", "size", "onClick"]))), 128))
|
|
149
|
+
])) : l("", !0),
|
|
151
150
|
z(a.$slots, "buttons")
|
|
152
151
|
], 2));
|
|
153
152
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JTitlebar.vue.js","sources":["../../../../src/components/molecules/JTitlebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from \"vue\"\nimport { JIcon, JLabel, JPopover, JButton } from '@/components/atoms'\nimport type { ButtonVariants } from '@/components/shadcn'\nimport { cn } from \"@/lib/utils\"\n\nexport type TitlebarButton = {\n /** 버튼 아이콘 */\n icon?: string\n /** 버튼 텍스트 */\n text?: string\n /** 버튼 클릭 핸들러 */\n onClick?: () => void\n /** 버튼 variant */\n variant?: ButtonVariants['variant']\n /** 버튼 스타일 타입 */\n styletype?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'outline' | 'ghost' | 'link' | 'sm' | 'lg' | 'icon'\n /** 버튼 size */\n size?: 'sm' | 'md' | 'lg'\n /** 버튼 비활성화 */\n disabled?: boolean\n /** 버튼 로딩 상태 */\n loading?: boolean\n}\n\ntype StyleType =\n | 'default' // 기본 스타일: 일반 페이지용 (흰 배경, 얇은 보더)\n | 'primary' // 강조 스타일: 주요 기능용 (밝은 파란 배경, 흰 텍스트)\n | 'accent' // 액센트 스타일: 부드러운 강조용 (연한 파란 배경, 진한 텍스트)\n | 'neutral' // 중립 스타일: 서브 페이지용 (회색 배경, 회색 텍스트)\n | 'elevated' // 강조 스타일: 깊이감 있는 구분 (흰 배경, 그림자, 보더)\n\nconst props = withDefaults(\n defineProps<{\n /** Titlebar 스타일 타입 */\n styletype?: StyleType\n /** 프로그램 아이콘 */\n icon?: string\n /** 프로그램명 */\n title?: string\n /** 프로그램 설명 (Popover에 표시) */\n description?: string\n /** 도움말 아이콘(?) 표시 여부 — 클릭 시 help 이벤트 emit */\n showHelp?: boolean\n /** 메인 버튼 목록 */\n buttons?: TitlebarButton[]\n }>(),\n {\n styletype: 'default',\n buttons: () => [],\n }\n)\n\n/**\n * styletype -> class 매핑 (배경, 보더, 그림자 등)\n */\nconst STYLE_PRESETS: Record<StyleType, { \n class: string\n iconClass: string // 아이콘 색상 클래스\n titleClass: string // 제목 텍스트 색상 클래스\n infoIconClass: string // 정보 아이콘 색상 클래스\n}> = {\n default: {\n // 기본: 흰 배경 + 얇은 보더 (가장 일반적)\n class: 'flex items-center justify-between w-full h-
|
|
1
|
+
{"version":3,"file":"JTitlebar.vue.js","sources":["../../../../src/components/molecules/JTitlebar.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from \"vue\"\nimport { JIcon, JLabel, JPopover, JButton } from '@/components/atoms'\nimport type { ButtonVariants } from '@/components/shadcn'\nimport { cn } from \"@/lib/utils\"\n\nexport type TitlebarButton = {\n /** 버튼 아이콘 */\n icon?: string\n /** 버튼 텍스트 */\n text?: string\n /** 버튼 클릭 핸들러 */\n onClick?: () => void\n /** 버튼 variant */\n variant?: ButtonVariants['variant']\n /** 버튼 스타일 타입 */\n styletype?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'outline' | 'ghost' | 'link' | 'sm' | 'lg' | 'icon'\n /** 버튼 size */\n size?: 'sm' | 'md' | 'lg'\n /** 버튼 비활성화 */\n disabled?: boolean\n /** 버튼 로딩 상태 */\n loading?: boolean\n}\n\ntype StyleType =\n | 'default' // 기본 스타일: 일반 페이지용 (흰 배경, 얇은 보더)\n | 'primary' // 강조 스타일: 주요 기능용 (밝은 파란 배경, 흰 텍스트)\n | 'accent' // 액센트 스타일: 부드러운 강조용 (연한 파란 배경, 진한 텍스트)\n | 'neutral' // 중립 스타일: 서브 페이지용 (회색 배경, 회색 텍스트)\n | 'elevated' // 강조 스타일: 깊이감 있는 구분 (흰 배경, 그림자, 보더)\n\nconst props = withDefaults(\n defineProps<{\n /** Titlebar 스타일 타입 */\n styletype?: StyleType\n /** 프로그램 아이콘 */\n icon?: string\n /** 프로그램명 */\n title?: string\n /** 프로그램 설명 (Popover에 표시) */\n description?: string\n /** 도움말 아이콘(?) 표시 여부 — 클릭 시 help 이벤트 emit */\n showHelp?: boolean\n /** 메인 버튼 목록 */\n buttons?: TitlebarButton[]\n }>(),\n {\n styletype: 'default',\n buttons: () => [],\n }\n)\n\n/**\n * styletype -> class 매핑 (배경, 보더, 그림자 등)\n */\nconst STYLE_PRESETS: Record<StyleType, { \n class: string\n iconClass: string // 아이콘 색상 클래스\n titleClass: string // 제목 텍스트 색상 클래스\n infoIconClass: string // 정보 아이콘 색상 클래스\n}> = {\n default: {\n // 기본: 흰 배경 + 얇은 보더 (가장 일반적)\n class: 'flex items-center justify-between w-full h-8 px-3 border-b border-border bg-background',\n iconClass: 'text-primary',\n titleClass: 'text-foreground text-md',\n infoIconClass: 'text-muted-foreground hover:text-primary',\n },\n primary: {\n // 강조: 밝은 파란 배경 + 흰 텍스트 (명확한 강조)\n // text-white를 제거하여 버튼이 색상 상속을 받지 않도록 함\n class: 'flex items-center justify-between w-full h-8 px-3 border-b border-blue-400/30 bg-blue-500',\n iconClass: 'text-white',\n titleClass: 'text-white font-semibold text-md',\n infoIconClass: 'text-white/80 hover:text-white',\n },\n accent: {\n // 액센트: 연한 파란 배경 + 진한 파란 텍스트 (부드러운 강조)\n // text-blue-700을 제거하여 버튼이 색상 상속을 받지 않도록 함\n class: 'flex items-center justify-between w-full h-8 px-3 border-b border-blue-200 bg-blue-50',\n iconClass: 'text-blue-600',\n titleClass: 'text-blue-700 font-semibold text-md',\n infoIconClass: 'text-blue-600/70 hover:text-blue-700',\n },\n neutral: {\n // 중립: 회색 배경 + 회색 텍스트 (확실한 구분)\n // text-gray-700을 제거하여 버튼이 색상 상속을 받지 않도록 함\n class: 'flex items-center justify-between w-full h-8 px-3 border-b border-gray-300 bg-gray-100',\n iconClass: 'text-gray-600',\n titleClass: 'text-gray-700 text-md',\n infoIconClass: 'text-gray-500 hover:text-gray-700',\n },\n elevated: {\n // 강조: 흰 배경 + 그림자 + 보더 (깊이감 있는 구분)\n class: 'flex items-center justify-between w-full h-8 px-3 border-b border-border bg-background shadow-md',\n iconClass: 'text-primary',\n titleClass: 'text-foreground font-semibold text-md',\n infoIconClass: 'text-muted-foreground hover:text-primary',\n },\n}\n\nconst preset = computed(() => {\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\n})\n\nconst emit = defineEmits<{\n /** 버튼 클릭 이벤트 */\n buttonClick: [button: TitlebarButton]\n /** 도움말 아이콘 클릭 이벤트 */\n help: []\n}>()\n\nconst handleButtonClick = (button: TitlebarButton) => {\n // 버튼 클릭 핸들러 패턴:\n // 1. button.onClick이 있으면 실행 (인라인 핸들러)\n // 2. 항상 emit('buttonClick', button) 실행 (부모 컴포넌트로 전달)\n // 주의: onClick과 emit이 모두 실행되므로 중복 처리 가능성 있음\n button.onClick?.()\n emit('buttonClick', button)\n}\n</script>\n\n<template>\n <div :class=\"cn(preset.class)\">\n <!-- 왼쪽: 아이콘 + 프로그램명 -->\n <div class=\"flex items-center gap-3 flex-1\">\n <!-- 아이콘 -->\n <JIcon \n v-if=\"icon\" \n :name=\"icon\" \n :class=\"preset.iconClass\"\n />\n \n <!-- 프로그램명 + 정보 아이콘 + Popover -->\n <div class=\"flex items-center gap-2\">\n <!-- 프로그램명 -->\n <JLabel \n :text=\"title || ''\" \n :class=\"cn('font-semibold', preset.titleClass)\"\n />\n \n <!-- 정보 아이콘 (description이 있을 때만 표시) -->\n <JPopover \n v-if=\"description\" \n position=\"bottom\"\n align=\"center\"\n :side-offset=\"8\"\n >\n <template #trigger>\n <JIcon \n name=\"info\" \n size=\"sm\" \n :class=\"cn('cursor-help transition-colors inline-flex', preset.infoIconClass)\"\n />\n </template>\n <div class=\"p-2\">\n <p class=\"text-xs text-muted-foreground whitespace-normal break-words\">{{ description }}</p>\n </div>\n </JPopover>\n\n <!-- 도움말 아이콘 (showHelp일 때 표시, 클릭 시 help 이벤트) -->\n <JIcon\n v-if=\"showHelp\"\n name=\"circleQuestionMark\"\n size=\"sm\"\n :class=\"cn('cursor-pointer transition-colors inline-flex', preset.infoIconClass)\"\n @click=\"emit('help')\"\n />\n </div>\n </div>\n\n <!-- 오른쪽: 메인 버튼들 -->\n <!-- buttons prop으로 정의된 버튼들 (v-if로 조건부 렌더링) -->\n <div v-if=\"buttons && buttons.length > 0\" class=\"flex items-center gap-2\">\n <JButton\n v-for=\"(button, index) in buttons\"\n :key=\"index\"\n :variant=\"button.variant\"\n :styletype=\"button.styletype\"\n :disabled=\"button.disabled\"\n :loading=\"button.loading\"\n :size=\"button.size\"\n @click=\"handleButtonClick(button)\"\n >\n <JIcon v-if=\"button.icon\" :name=\"button.icon\" size=\"sm\" class=\"mr-1.5\" />\n <span v-if=\"button.text\">{{ button.text }}</span>\n </JButton>\n </div>\n\n <!-- 버튼 슬롯 레이아웃 설명:\n - buttons prop과 buttons slot이 모두 제공되면 함께 표시됨\n - buttons prop: 구조화된 버튼 정의 (JButton 컴포넌트 사용)\n - buttons slot: 완전한 커스텀 버튼 제어 가능\n - 레이아웃: buttons prop이 먼저 렌더링되고, 그 다음 slot이 렌더링됨 -->\n <slot name=\"buttons\" />\n </div>\n</template>\n\n"],"names":["props","__props","STYLE_PRESETS","preset","computed","emit","__emit","handleButtonClick","button","_createElementBlock","_normalizeClass","_unref","cn","_createElementVNode","_hoisted_1","_createBlock","JIcon","_hoisted_2","_createVNode","JLabel","JPopover","_hoisted_3","_hoisted_4","_toDisplayString","_openBlock","_hoisted_5","_Fragment","_renderList","index","JButton","$event","_hoisted_6","_renderSlot","_ctx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,UAAMA,IAAQC,GAwBRC,IAKD;AAAA,MACH,SAAS;AAAA;AAAA,QAEP,OAAO;AAAA,QACP,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,eAAe;AAAA,MAAA;AAAA,MAEjB,SAAS;AAAA;AAAA;AAAA,QAGP,OAAO;AAAA,QACP,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,eAAe;AAAA,MAAA;AAAA,MAEjB,QAAQ;AAAA;AAAA;AAAA,QAGN,OAAO;AAAA,QACP,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,eAAe;AAAA,MAAA;AAAA,MAEjB,SAAS;AAAA;AAAA;AAAA,QAGP,OAAO;AAAA,QACP,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,eAAe;AAAA,MAAA;AAAA,MAEjB,UAAU;AAAA;AAAA,QAER,OAAO;AAAA,QACP,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,eAAe;AAAA,MAAA;AAAA,IACjB,GAGIC,IAASC,EAAS,MACfF,EAAcF,EAAM,SAAS,KAAKE,EAAc,OACxD,GAEKG,IAAOC,GAOPC,IAAoB,CAACC,MAA2B;AAKpD,MAAAA,EAAO,UAAA,GACPH,EAAK,eAAeG,CAAM;AAAA,IAC5B;2BAIEC,EAwEM,OAAA;AAAA,MAxEA,OAAKC,EAAEC,EAAAC,CAAA,EAAGT,EAAA,MAAO,KAAK,CAAA;AAAA,IAAA;MAE1BU,EA4CM,OA5CNC,GA4CM;AAAA,QAzCIb,EAAA,aADRc,EAIEJ,EAAAK,CAAA,GAAA;AAAA;UAFC,MAAMf,EAAA;AAAA,UACN,OAAKS,EAAEP,EAAA,MAAO,SAAS;AAAA,QAAA;QAI1BU,EAkCM,OAlCNI,GAkCM;AAAA,UAhCJC,EAGEP,EAAAQ,CAAA,GAAA;AAAA,YAFC,MAAMlB,EAAA,SAAK;AAAA,YACX,OAAKS,EAAEC,EAAAC,CAAA,EAAE,iBAAkBT,EAAA,MAAO,UAAU,CAAA;AAAA,UAAA;UAKvCF,EAAA,oBADRc,EAgBWJ,EAAAS,CAAA,GAAA;AAAA;YAdT,UAAS;AAAA,YACT,OAAM;AAAA,YACL,eAAa;AAAA,UAAA;YAEH,WACT,MAIE;AAAA,cAJFF,EAIEP,EAAAK,CAAA,GAAA;AAAA,gBAHA,MAAK;AAAA,gBACL,MAAK;AAAA,gBACJ,OAAKN,EAAEC,EAAAC,CAAA,EAAE,6CAA8CT,EAAA,MAAO,aAAa,CAAA;AAAA,cAAA;;uBAGhF,MAEM;AAAA,cAFNU,EAEM,OAFNQ,GAEM;AAAA,gBADJR,EAA4F,KAA5FS,GAA4FC,EAAlBtB,EAAA,WAAW,GAAA,CAAA;AAAA,cAAA;;;;UAMjFA,EAAA,iBADRc,EAMEJ,EAAAK,CAAA,GAAA;AAAA;YAJA,MAAK;AAAA,YACL,MAAK;AAAA,YACJ,OAAKN,EAAEC,EAAAC,CAAA,EAAE,gDAAiDT,EAAA,MAAO,aAAa,CAAA;AAAA,YAC9E,gCAAOE,EAAI,MAAA;AAAA,UAAA;;;MAOPJ,EAAA,WAAWA,EAAA,QAAQ,SAAM,KAApCuB,KAAAf,EAcM,OAdNgB,GAcM;AAAA,SAbJD,EAAA,EAAA,GAAAf,EAYUiB,GAAA,MAAAC,EAXkB1B,EAAA,SAAO,CAAzBO,GAAQoB,YADlBb,EAYUJ,EAAAkB,CAAA,GAAA;AAAA,UAVP,KAAKD;AAAA,UACL,SAASpB,EAAO;AAAA,UAChB,WAAWA,EAAO;AAAA,UAClB,UAAUA,EAAO;AAAA,UACjB,SAASA,EAAO;AAAA,UAChB,MAAMA,EAAO;AAAA,UACb,SAAK,CAAAsB,MAAEvB,EAAkBC,CAAM;AAAA,QAAA;qBAEhC,MAAyE;AAAA,YAA5DA,EAAO,aAApBO,EAAyEJ,EAAAK,CAAA,GAAA;AAAA;cAA9C,MAAMR,EAAO;AAAA,cAAM,MAAK;AAAA,cAAK,OAAM;AAAA,YAAA;YAClDA,EAAO,aAAnBC,EAAiD,QAAAsB,GAAAR,EAArBf,EAAO,IAAI,GAAA,CAAA;;;;;MAS3CwB,EAAuBC,EAAA,QAAA,SAAA;AAAA,IAAA;;;"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),d=require("lucide-vue-next"),_=require("../atoms/JBadge.vue.cjs"),f=require("../atoms/JButton.vue.cjs"),B=require("../atoms/JLabel.vue.cjs"),k={class:"w-full rounded-lg border bg-card text-card-foreground"},v={class:"flex items-center justify-between px-
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),d=require("lucide-vue-next"),_=require("../atoms/JBadge.vue.cjs"),f=require("../atoms/JButton.vue.cjs"),B=require("../atoms/JLabel.vue.cjs"),k={class:"w-full rounded-lg border bg-card text-card-foreground"},v={class:"flex items-center justify-between px-3 py-1.5"},x={class:"flex items-center gap-2"},b={key:2,class:"flex items-center gap-1 flex-wrap"},w={class:"text-muted-foreground"},C=["onClick"],N={class:"flex items-center gap-2"},E={class:"px-3 pb-3"},S=e.defineComponent({__name:"JFilterBar",props:{title:{},collapsed:{type:Boolean,default:!0},collapsible:{type:Boolean,default:!0},filterValues:{default:()=>({})},filterDisplay:{default:()=>({})},showResetButton:{type:Boolean,default:!1},showSearchButton:{type:Boolean,default:!1},resetButtonText:{default:"초기화"},searchButtonText:{default:"조회"}},emits:["update:collapsed","update:filterValues","search","reset"],setup(r,{emit:p}){const s=r,n=p,c=e.computed(()=>s.collapsible?!s.collapsed:!0);function m(t){return!!(t==null||typeof t=="string"&&t.trim()===""||Array.isArray(t)&&t.length===0)}const u=e.computed(()=>{const t=[];for(const[l,o]of Object.entries(s.filterDisplay)){const a=s.filterValues[l];if(m(a))continue;const i=o.displayValue?o.displayValue(a):String(a);i.trim()!==""&&t.push({key:l,label:o.label,value:i})}return t});function y(){n("update:collapsed",!s.collapsed)}function h(){const t={};for(const l of Object.keys(s.filterValues)){const o=s.filterValues[l];typeof o=="string"?t[l]="":Array.isArray(o)?t[l]=[]:t[l]=null}n("update:filterValues",t),n("reset")}function g(){n("search")}function V(t){const l={...s.filterValues},o=l[t];typeof o=="string"?l[t]="":Array.isArray(o)?l[t]=[]:l[t]=null,n("update:filterValues",l)}return(t,l)=>(e.openBlock(),e.createElementBlock("div",k,[e.createElementVNode("div",v,[e.createElementVNode("div",x,[r.collapsible?(e.openBlock(),e.createElementBlock("button",{key:0,type:"button",class:"flex items-center justify-center h-6 w-6 rounded hover:bg-accent hover:text-accent-foreground transition-colors",onClick:y},[e.createVNode(e.unref(d.ChevronDown),{class:e.normalizeClass(["h-3.5 w-3.5 transition-transform",c.value?"rotate-0":"-rotate-90"])},null,8,["class"])])):e.createCommentVNode("",!0),r.title?(e.openBlock(),e.createBlock(B.default,{key:1,text:r.title,class:"text-sm font-semibold text-foreground"},null,8,["text"])):e.createCommentVNode("",!0),u.value.length>0?(e.openBlock(),e.createElementBlock("div",b,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(u.value,o=>(e.openBlock(),e.createBlock(_.default,{key:o.key,variant:"secondary",size:"sm",class:"flex items-center gap-1 cursor-default"},{default:e.withCtx(()=>[e.createElementVNode("span",w,e.toDisplayString(o.label)+":",1),e.createElementVNode("span",null,e.toDisplayString(o.value),1),e.createElementVNode("button",{type:"button",class:"ml-0.5 rounded-full hover:bg-gray-300 p-0.5 transition-colors",onClick:e.withModifiers(a=>V(o.key),["stop"])},[e.createVNode(e.unref(d.X),{class:"h-3 w-3"})],8,C)]),_:2},1024))),128))])):e.createCommentVNode("",!0)]),e.createElementVNode("div",N,[e.renderSlot(t.$slots,"actions"),r.showResetButton?(e.openBlock(),e.createBlock(f.default,{key:0,variant:"secondary",size:"sm",onClick:h},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(r.resetButtonText),1)]),_:1})):e.createCommentVNode("",!0),r.showSearchButton?(e.openBlock(),e.createBlock(f.default,{key:1,styletype:"primary",size:"sm",onClick:g},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(r.searchButtonText),1)]),_:1})):e.createCommentVNode("",!0)])]),e.withDirectives(e.createElementVNode("div",E,[e.renderSlot(t.$slots,"filters")],512),[[e.vShow,c.value]])]))}});exports.default=S;
|
|
2
2
|
//# sourceMappingURL=JFilterBar.vue.cjs.map
|