@j-solution/components 1.8.0 → 1.9.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 +413 -415
- package/assets/jwms-portal-frontend-Ct2Tc7yj.css +1 -0
- package/assets/styles/j-components.css +1 -1
- package/assets/styles/themes.css +422 -422
- package/components/atoms/JButton.vue.cjs +1 -1
- package/components/atoms/JButton.vue.js +1 -1
- package/components/atoms/JButton.vue2.cjs.map +1 -1
- package/components/atoms/JButton.vue2.js.map +1 -1
- package/components/atoms/JLabel.vue.cjs.map +1 -1
- package/components/atoms/JLabel.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.map +1 -1
- package/components/molecules/JTabs.vue2.js.map +1 -1
- package/components/organisms/JDynamicTabs.vue.cjs.map +1 -1
- package/components/organisms/JDynamicTabs.vue.js.map +1 -1
- package/components/organisms/JFilterBar.vue.cjs +1 -1
- package/components/organisms/JFilterBar.vue.js +2 -2
- package/components/organisms/JFilterBar.vue2.cjs +1 -1
- package/components/organisms/JFilterBar.vue2.cjs.map +1 -1
- package/components/organisms/JFilterBar.vue2.js +14 -12
- package/components/organisms/JFilterBar.vue2.js.map +1 -1
- package/components/organisms/JPageContainer.vue.cjs.map +1 -1
- package/components/organisms/JPageContainer.vue.js.map +1 -1
- package/components/organisms/JSidebar/JSidebar.vue.cjs +2 -0
- package/components/organisms/JSidebar/JSidebar.vue.cjs.map +1 -0
- package/components/organisms/JSidebar/JSidebar.vue.js +189 -0
- package/components/organisms/JSidebar/JSidebar.vue.js.map +1 -0
- package/components/organisms/JSidebar/JSidebar.vue2.cjs +2 -0
- package/components/organisms/JSidebar/JSidebar.vue2.cjs.map +1 -0
- package/components/organisms/JSidebar/JSidebar.vue2.js +5 -0
- package/components/organisms/JSidebar/JSidebar.vue2.js.map +1 -0
- package/components/organisms/JSidebar/JSidebarGroup.vue.cjs +2 -0
- package/components/organisms/JSidebar/JSidebarGroup.vue.cjs.map +1 -0
- package/components/organisms/JSidebar/JSidebarGroup.vue.js +89 -0
- package/components/organisms/JSidebar/JSidebarGroup.vue.js.map +1 -0
- package/components/organisms/JSidebar/JSidebarGroup.vue2.cjs +2 -0
- package/components/organisms/JSidebar/JSidebarGroup.vue2.cjs.map +1 -0
- package/components/organisms/JSidebar/JSidebarGroup.vue2.js +5 -0
- package/components/organisms/JSidebar/JSidebarGroup.vue2.js.map +1 -0
- package/components/organisms/JSidebar/JSidebarItem.vue.cjs +2 -0
- package/components/organisms/JSidebar/JSidebarItem.vue.cjs.map +1 -0
- package/components/organisms/JSidebar/JSidebarItem.vue.js +79 -0
- package/components/organisms/JSidebar/JSidebarItem.vue.js.map +1 -0
- package/components/organisms/JSidebar/JSidebarItem.vue2.cjs +2 -0
- package/components/organisms/JSidebar/JSidebarItem.vue2.cjs.map +1 -0
- package/components/organisms/JSidebar/JSidebarItem.vue2.js +5 -0
- package/components/organisms/JSidebar/JSidebarItem.vue2.js.map +1 -0
- package/components/shadcn/Card.vue.cjs.map +1 -1
- package/components/shadcn/Card.vue.js.map +1 -1
- package/components/shadcn/CardContent.vue.cjs.map +1 -1
- package/components/shadcn/CardContent.vue.js.map +1 -1
- package/components/shadcn/CardHeader.vue.cjs.map +1 -1
- package/components/shadcn/CardHeader.vue.js.map +1 -1
- package/components/shadcn/Input.vue.cjs.map +1 -1
- package/components/shadcn/Input.vue.js.map +1 -1
- package/components/shadcn/SelectTrigger.vue.cjs.map +1 -1
- package/components/shadcn/SelectTrigger.vue.js.map +1 -1
- package/components/shadcn/TabsContent.vue.cjs.map +1 -1
- package/components/shadcn/TabsContent.vue.js.map +1 -1
- package/components/shadcn/Textarea.vue.cjs.map +1 -1
- package/components/shadcn/Textarea.vue.js.map +1 -1
- package/components/shadcn/index.cjs.map +1 -1
- package/components/shadcn/index.js.map +1 -1
- package/components/templates/JLayout.vue.cjs.map +1 -1
- package/components/templates/JLayout.vue.js.map +1 -1
- package/components/templates/JLayoutSimple.vue.cjs +1 -1
- package/components/templates/JLayoutSimple.vue.cjs.map +1 -1
- package/components/templates/JLayoutSimple.vue.js +36 -30
- package/components/templates/JLayoutSimple.vue.js.map +1 -1
- package/index.cjs +1 -1
- package/index.js +22 -20
- package/package.json +1 -1
- package/types/index.d.ts +119 -61
- package/types/sidebar.types.cjs +2 -0
- package/types/sidebar.types.cjs.map +1 -0
- package/types/sidebar.types.js +5 -0
- package/types/sidebar.types.js.map +1 -0
- package/assets/jwms-portal-frontend-BtHTA-UF.css +0 -1
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
for (const [t_key, t_val] of t_opts)
|
|
4
4
|
t_merged[t_key] = t_val;
|
|
5
5
|
return t_merged;
|
|
6
|
-
};,u=t(e.default,[["__scopeId","data-v-
|
|
6
|
+
};,u=t(e.default,[["__scopeId","data-v-9058e7ee"]]);exports.default=u;
|
|
7
7
|
//# sourceMappingURL=JButton.vue.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JButton.vue2.cjs","sources":["../../../../src/components/atoms/JButton.vue"],"sourcesContent":["<script setup lang=\"ts\">\
|
|
1
|
+
{"version":3,"file":"JButton.vue2.cjs","sources":["../../../../src/components/atoms/JButton.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Loader2 } from 'lucide-vue-next'\nimport { Button, type ButtonVariants } from '@/components/shadcn'\n\ntype StyleType =\n | 'default' // 기본 스타일 (primary)\n | 'primary' // 강조 버튼 (파랑)\n | 'secondary' // 보조 버튼 (회색)\n | 'success' // 성공 버튼 (초록)\n | 'warning' // 경고 버튼 (주황)\n | 'danger' // 위험 버튼 (빨강)\n | 'outline' // 아웃라인 버튼\n | 'ghost' // 고스트 버튼\n | 'link' // 링크 스타일\n | 'sm' // 작은 크기\n | 'lg' // 큰 크기\n | 'icon' // 아이콘 전용\n\nconst props = withDefaults(\n defineProps<{\n /** 버튼 타입 */\n type?: 'button' | 'submit' | 'reset'\n /** 비활성화 상태 */\n disabled?: boolean\n /** 로딩 상태 */\n loading?: boolean\n /** shadcn variant */\n variant?: ButtonVariants['variant']\n /** shadcn size */\n size?: ButtonVariants['size']\n /** 추가 CSS 클래스 */\n class?: string\n /** 스타일 프리셋 (variant + size 조합) */\n styletype?: StyleType\n }>(),\n {\n type: 'button',\n disabled: false,\n loading: false,\n styletype: 'default',\n },\n)\n\nconst emit = defineEmits<{\n click: [event: MouseEvent]\n}>()\n\n/**\n * styletype -> variant/size 매핑\n */\nconst STYLE_PRESETS: Record<StyleType, { variant: ButtonVariants['variant']; size?: ButtonVariants['size']; class?: string }> = {\n default: { \n variant: 'default',\n },\n primary: { \n variant: 'default',\n },\n secondary: { \n variant: 'secondary',\n },\n success: { \n variant: 'default',\n class: 'bg-green-600 hover:bg-green-700',\n },\n warning: { \n variant: 'default',\n class: 'bg-amber-500 hover:bg-amber-600',\n },\n danger: { \n variant: 'destructive',\n },\n outline: { \n variant: 'outline',\n },\n ghost: { \n variant: 'ghost',\n },\n link: { \n variant: 'link',\n },\n sm: { \n variant: 'default',\n size: 'sm',\n },\n lg: { \n variant: 'default',\n size: 'lg',\n },\n icon: { \n variant: 'default',\n size: 'icon',\n },\n}\n\nconst preset = computed(() => {\n const styleKey = props.styletype || 'default'\n return STYLE_PRESETS[styleKey] ?? STYLE_PRESETS.default\n})\n\nconst finalVariant = computed(() => props.variant || preset.value?.variant)\nconst finalSize = computed(() => props.size || preset.value?.size || 'sm')\nconst finalClass = computed(() => [preset.value?.class, props.class].filter(Boolean).join(' '))\n\nconst isDisabled = computed(() => props.disabled || props.loading)\n\nconst handleClick = (event: MouseEvent) => {\n if (!isDisabled.value) {\n emit('click', event)\n }\n}\n</script>\n\n<template>\n <Button\n :type=\"type\"\n :variant=\"finalVariant\"\n :size=\"finalSize\"\n :class=\"finalClass\"\n :disabled=\"isDisabled\"\n @click=\"handleClick\"\n >\n <Loader2 v-if=\"loading\" class=\"mr-2 h-4 w-4 animate-spin\" />\n <slot />\n </Button>\n</template>\n\n<style scoped>\n/* ========================================\n 패턴 10: Button 향상 효과\n ======================================== */\n:deep(button) {\n transition: all 0.2s ease;\n}\n\n:deep(button:not(:disabled):hover) {\n transform: translateY(-1px);\n box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n}\n\n:deep(button:not(:disabled):active) {\n transform: translateY(0);\n}\n</style>\n"],"names":["props","__props","emit","__emit","STYLE_PRESETS","preset","computed","styleKey","finalVariant","finalSize","finalClass","isDisabled","handleClick","event","_createBlock","_unref","Button","Loader2","_renderSlot","_ctx"],"mappings":"kdAmBA,MAAMA,EAAQC,EAyBRC,EAAOC,EAOPC,EAA0H,CAC9H,QAAS,CACP,QAAS,SAAA,EAEX,QAAS,CACP,QAAS,SAAA,EAEX,UAAW,CACT,QAAS,WAAA,EAEX,QAAS,CACP,QAAS,UACT,MAAO,iCAAA,EAET,QAAS,CACP,QAAS,UACT,MAAO,iCAAA,EAET,OAAQ,CACN,QAAS,aAAA,EAEX,QAAS,CACP,QAAS,SAAA,EAEX,MAAO,CACL,QAAS,OAAA,EAEX,KAAM,CACJ,QAAS,MAAA,EAEX,GAAI,CACF,QAAS,UACT,KAAM,IAAA,EAER,GAAI,CACF,QAAS,UACT,KAAM,IAAA,EAER,KAAM,CACJ,QAAS,UACT,KAAM,MAAA,CACR,EAGIC,EAASC,EAAAA,SAAS,IAAM,CAC5B,MAAMC,EAAWP,EAAM,WAAa,UACpC,OAAOI,EAAcG,CAAQ,GAAKH,EAAc,OAClD,CAAC,EAEKI,EAAeF,EAAAA,SAAS,IAAMN,EAAM,SAAWK,EAAO,OAAO,OAAO,EACpEI,EAAYH,EAAAA,SAAS,IAAMN,EAAM,MAAQK,EAAO,OAAO,MAAQ,IAAI,EACnEK,EAAaJ,EAAAA,SAAS,IAAM,CAACD,EAAO,OAAO,MAAOL,EAAM,KAAK,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,CAAC,EAExFW,EAAaL,EAAAA,SAAS,IAAMN,EAAM,UAAYA,EAAM,OAAO,EAE3DY,EAAeC,GAAsB,CACpCF,EAAW,OACdT,EAAK,QAASW,CAAK,CAEvB,8BAIEC,EAAAA,YAUSC,EAAAA,MAAAC,EAAAA,OAAA,EAAA,CATN,KAAMf,EAAA,KACN,QAASO,EAAA,MACT,KAAMC,EAAA,MACN,uBAAOC,EAAA,KAAU,EACjB,SAAUC,EAAA,MACV,QAAOC,CAAA,qBAER,IAA4D,CAA7CX,EAAA,uBAAfa,EAAAA,YAA4DC,EAAAA,MAAAE,EAAAA,OAAA,EAAA,OAApC,MAAM,2BAAA,gCAC9BC,EAAAA,WAAQC,EAAA,OAAA,UAAA,CAAA,EAAA,OAAA,EAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JButton.vue2.js","sources":["../../../../src/components/atoms/JButton.vue"],"sourcesContent":["<script setup lang=\"ts\">\
|
|
1
|
+
{"version":3,"file":"JButton.vue2.js","sources":["../../../../src/components/atoms/JButton.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Loader2 } from 'lucide-vue-next'\nimport { Button, type ButtonVariants } from '@/components/shadcn'\n\ntype StyleType =\n | 'default' // 기본 스타일 (primary)\n | 'primary' // 강조 버튼 (파랑)\n | 'secondary' // 보조 버튼 (회색)\n | 'success' // 성공 버튼 (초록)\n | 'warning' // 경고 버튼 (주황)\n | 'danger' // 위험 버튼 (빨강)\n | 'outline' // 아웃라인 버튼\n | 'ghost' // 고스트 버튼\n | 'link' // 링크 스타일\n | 'sm' // 작은 크기\n | 'lg' // 큰 크기\n | 'icon' // 아이콘 전용\n\nconst props = withDefaults(\n defineProps<{\n /** 버튼 타입 */\n type?: 'button' | 'submit' | 'reset'\n /** 비활성화 상태 */\n disabled?: boolean\n /** 로딩 상태 */\n loading?: boolean\n /** shadcn variant */\n variant?: ButtonVariants['variant']\n /** shadcn size */\n size?: ButtonVariants['size']\n /** 추가 CSS 클래스 */\n class?: string\n /** 스타일 프리셋 (variant + size 조합) */\n styletype?: StyleType\n }>(),\n {\n type: 'button',\n disabled: false,\n loading: false,\n styletype: 'default',\n },\n)\n\nconst emit = defineEmits<{\n click: [event: MouseEvent]\n}>()\n\n/**\n * styletype -> variant/size 매핑\n */\nconst STYLE_PRESETS: Record<StyleType, { variant: ButtonVariants['variant']; size?: ButtonVariants['size']; class?: string }> = {\n default: { \n variant: 'default',\n },\n primary: { \n variant: 'default',\n },\n secondary: { \n variant: 'secondary',\n },\n success: { \n variant: 'default',\n class: 'bg-green-600 hover:bg-green-700',\n },\n warning: { \n variant: 'default',\n class: 'bg-amber-500 hover:bg-amber-600',\n },\n danger: { \n variant: 'destructive',\n },\n outline: { \n variant: 'outline',\n },\n ghost: { \n variant: 'ghost',\n },\n link: { \n variant: 'link',\n },\n sm: { \n variant: 'default',\n size: 'sm',\n },\n lg: { \n variant: 'default',\n size: 'lg',\n },\n icon: { \n variant: 'default',\n size: 'icon',\n },\n}\n\nconst preset = computed(() => {\n const styleKey = props.styletype || 'default'\n return STYLE_PRESETS[styleKey] ?? STYLE_PRESETS.default\n})\n\nconst finalVariant = computed(() => props.variant || preset.value?.variant)\nconst finalSize = computed(() => props.size || preset.value?.size || 'sm')\nconst finalClass = computed(() => [preset.value?.class, props.class].filter(Boolean).join(' '))\n\nconst isDisabled = computed(() => props.disabled || props.loading)\n\nconst handleClick = (event: MouseEvent) => {\n if (!isDisabled.value) {\n emit('click', event)\n }\n}\n</script>\n\n<template>\n <Button\n :type=\"type\"\n :variant=\"finalVariant\"\n :size=\"finalSize\"\n :class=\"finalClass\"\n :disabled=\"isDisabled\"\n @click=\"handleClick\"\n >\n <Loader2 v-if=\"loading\" class=\"mr-2 h-4 w-4 animate-spin\" />\n <slot />\n </Button>\n</template>\n\n<style scoped>\n/* ========================================\n 패턴 10: Button 향상 효과\n ======================================== */\n:deep(button) {\n transition: all 0.2s ease;\n}\n\n:deep(button:not(:disabled):hover) {\n transform: translateY(-1px);\n box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);\n}\n\n:deep(button:not(:disabled):active) {\n transform: translateY(0);\n}\n</style>\n"],"names":["props","__props","emit","__emit","STYLE_PRESETS","preset","computed","styleKey","finalVariant","finalSize","finalClass","isDisabled","handleClick","event","_createBlock","_unref","Button","Loader2","_renderSlot","_ctx"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,UAAMA,IAAQC,GAyBRC,IAAOC,GAOPC,IAA0H;AAAA,MAC9H,SAAS;AAAA,QACP,SAAS;AAAA,MAAA;AAAA,MAEX,SAAS;AAAA,QACP,SAAS;AAAA,MAAA;AAAA,MAEX,WAAW;AAAA,QACT,SAAS;AAAA,MAAA;AAAA,MAEX,SAAS;AAAA,QACP,SAAS;AAAA,QACT,OAAO;AAAA,MAAA;AAAA,MAET,SAAS;AAAA,QACP,SAAS;AAAA,QACT,OAAO;AAAA,MAAA;AAAA,MAET,QAAQ;AAAA,QACN,SAAS;AAAA,MAAA;AAAA,MAEX,SAAS;AAAA,QACP,SAAS;AAAA,MAAA;AAAA,MAEX,OAAO;AAAA,QACL,SAAS;AAAA,MAAA;AAAA,MAEX,MAAM;AAAA,QACJ,SAAS;AAAA,MAAA;AAAA,MAEX,IAAI;AAAA,QACF,SAAS;AAAA,QACT,MAAM;AAAA,MAAA;AAAA,MAER,IAAI;AAAA,QACF,SAAS;AAAA,QACT,MAAM;AAAA,MAAA;AAAA,MAER,MAAM;AAAA,QACJ,SAAS;AAAA,QACT,MAAM;AAAA,MAAA;AAAA,IACR,GAGIC,IAASC,EAAS,MAAM;AAC5B,YAAMC,IAAWP,EAAM,aAAa;AACpC,aAAOI,EAAcG,CAAQ,KAAKH,EAAc;AAAA,IAClD,CAAC,GAEKI,IAAeF,EAAS,MAAMN,EAAM,WAAWK,EAAO,OAAO,OAAO,GACpEI,IAAYH,EAAS,MAAMN,EAAM,QAAQK,EAAO,OAAO,QAAQ,IAAI,GACnEK,IAAaJ,EAAS,MAAM,CAACD,EAAO,OAAO,OAAOL,EAAM,KAAK,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG,CAAC,GAExFW,IAAaL,EAAS,MAAMN,EAAM,YAAYA,EAAM,OAAO,GAE3DY,IAAc,CAACC,MAAsB;AACzC,MAAKF,EAAW,SACdT,EAAK,SAASW,CAAK;AAAA,IAEvB;2BAIEC,EAUSC,EAAAC,CAAA,GAAA;AAAA,MATN,MAAMf,EAAA;AAAA,MACN,SAASO,EAAA;AAAA,MACT,MAAMC,EAAA;AAAA,MACN,SAAOC,EAAA,KAAU;AAAA,MACjB,UAAUC,EAAA;AAAA,MACV,SAAOC;AAAA,IAAA;iBAER,MAA4D;AAAA,QAA7CX,EAAA,gBAAfa,EAA4DC,EAAAE,CAAA,GAAA;AAAA;UAApC,OAAM;AAAA,QAAA;QAC9BC,EAAQC,EAAA,QAAA,WAAA,CAAA,GAAA,QAAA,EAAA;AAAA,MAAA;;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JLabel.vue.cjs","sources":["../../../../src/components/atoms/JLabel.vue"],"sourcesContent":["<!-- TODO: 컴포넌트 래핑 초안 - 스타일 및 기능 개선 필요 -->\
|
|
1
|
+
{"version":3,"file":"JLabel.vue.cjs","sources":["../../../../src/components/atoms/JLabel.vue"],"sourcesContent":["<!-- TODO: 컴포넌트 래핑 초안 - 스타일 및 기능 개선 필요 -->\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Label } from '@/components/shadcn'\nimport { cn } from '@/lib/utils'\n\ntype StyleType =\n | 'default' // 기본 스타일\n | 'required' // 필수 필드 표시 (빨간 별표)\n | 'optional' // 선택 필드 표시 (회색 텍스트)\n | 'error' // 에러 상태 (빨간 텍스트)\n | 'success' // 성공 상태 (초록 텍스트)\n | 'warning' // 경고 상태 (주황 텍스트)\n | 'sm' // 작은 크기\n | 'lg' // 큰 크기\n\nconst props = withDefaults(\n defineProps<{\n /** 라벨 텍스트 */\n text?: string\n /** 필수 필드 여부 */\n required?: boolean\n /** 스타일 프리셋 */\n styletype?: StyleType\n /** HTML for 속성 (연결할 input의 id) */\n for?: string\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n }>(),\n {\n text: '',\n required: false,\n styletype: 'default',\n },\n)\n\n/**\n * styletype -> class 매핑\n */\nconst STYLE_PRESETS: Record<StyleType, { class: string }> = {\n default: { class: '' },\n required: { \n class: 'text-red-600',\n },\n optional: { \n class: 'text-gray-500',\n },\n error: { \n class: 'text-red-600',\n },\n success: { \n class: 'text-green-600',\n },\n warning: { \n class: 'text-amber-600',\n },\n sm: { \n class: 'text-xs',\n },\n lg: { \n class: 'text-base',\n },\n}\n\n/** 최종 바인딩: styletype에 따른 클래스 적용 */\nconst mapped = computed(() => {\n const styleKey = props.styletype || 'default'\n const preset = STYLE_PRESETS[styleKey] ?? STYLE_PRESETS.default\n \n return {\n for: props.for,\n class: preset?.class || '',\n }\n})\n\nconst displayText = computed(() => {\n if (props.text) {\n return props.text\n }\n return ''\n})\n</script>\n\n<template>\n <Label\n :for=\"mapped.for\"\n :class=\"cn(\n 'text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',\n mapped.class,\n props.class\n )\"\n >\n <span v-if=\"required && styletype === 'required'\" class=\"text-red-500 mr-1\">*</span>\n <span v-else-if=\"required\" class=\"text-red-500 mr-1\">*</span>\n {{ displayText }}\n <slot />\n </Label>\n</template>\n"],"names":["props","__props","STYLE_PRESETS","mapped","computed","styleKey","preset","displayText","_createBlock","_unref","Label","cn","_createElementBlock","_hoisted_1","_hoisted_2","_toDisplayString","_renderSlot","_ctx"],"mappings":"+cAgBA,MAAMA,EAAQC,EAuBRC,EAAsD,CAC1D,QAAS,CAAE,MAAO,EAAA,EAClB,SAAU,CACR,MAAO,cAAA,EAET,SAAU,CACR,MAAO,eAAA,EAET,MAAO,CACL,MAAO,cAAA,EAET,QAAS,CACP,MAAO,gBAAA,EAET,QAAS,CACP,MAAO,gBAAA,EAET,GAAI,CACF,MAAO,SAAA,EAET,GAAI,CACF,MAAO,WAAA,CACT,EAIIC,EAASC,EAAAA,SAAS,IAAM,CAC5B,MAAMC,EAAWL,EAAM,WAAa,UAC9BM,EAASJ,EAAcG,CAAQ,GAAKH,EAAc,QAExD,MAAO,CACL,IAAKF,EAAM,IACX,MAAOM,GAAQ,OAAS,EAAA,CAE5B,CAAC,EAEKC,EAAcH,EAAAA,SAAS,IACvBJ,EAAM,KACDA,EAAM,KAER,EACR,8BAICQ,EAAAA,YAYQC,EAAAA,MAAAC,EAAAA,OAAA,EAAA,CAXL,IAAKP,EAAA,MAAO,IACZ,uBAAOM,EAAAA,MAAAE,IAAA,+FAA8GR,EAAA,MAAO,MAAaH,EAAM,KAAA,uBAMhJ,IAAoF,CAAxEC,EAAA,UAAYA,EAAA,YAAS,0BAAjCW,EAAAA,mBAAoF,OAApFC,EAA4E,GAAC,GAC5DZ,EAAA,wBAAjBW,EAAAA,mBAA6D,OAA7DE,EAAqD,GAAC,iDAAO,IAC7DC,kBAAGR,EAAA,KAAW,EAAG,IACjB,CAAA,EAAAS,aAAQC,EAAA,OAAA,SAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JLabel.vue.js","sources":["../../../../src/components/atoms/JLabel.vue"],"sourcesContent":["<!-- TODO: 컴포넌트 래핑 초안 - 스타일 및 기능 개선 필요 -->\
|
|
1
|
+
{"version":3,"file":"JLabel.vue.js","sources":["../../../../src/components/atoms/JLabel.vue"],"sourcesContent":["<!-- TODO: 컴포넌트 래핑 초안 - 스타일 및 기능 개선 필요 -->\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Label } from '@/components/shadcn'\nimport { cn } from '@/lib/utils'\n\ntype StyleType =\n | 'default' // 기본 스타일\n | 'required' // 필수 필드 표시 (빨간 별표)\n | 'optional' // 선택 필드 표시 (회색 텍스트)\n | 'error' // 에러 상태 (빨간 텍스트)\n | 'success' // 성공 상태 (초록 텍스트)\n | 'warning' // 경고 상태 (주황 텍스트)\n | 'sm' // 작은 크기\n | 'lg' // 큰 크기\n\nconst props = withDefaults(\n defineProps<{\n /** 라벨 텍스트 */\n text?: string\n /** 필수 필드 여부 */\n required?: boolean\n /** 스타일 프리셋 */\n styletype?: StyleType\n /** HTML for 속성 (연결할 input의 id) */\n for?: string\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n }>(),\n {\n text: '',\n required: false,\n styletype: 'default',\n },\n)\n\n/**\n * styletype -> class 매핑\n */\nconst STYLE_PRESETS: Record<StyleType, { class: string }> = {\n default: { class: '' },\n required: { \n class: 'text-red-600',\n },\n optional: { \n class: 'text-gray-500',\n },\n error: { \n class: 'text-red-600',\n },\n success: { \n class: 'text-green-600',\n },\n warning: { \n class: 'text-amber-600',\n },\n sm: { \n class: 'text-xs',\n },\n lg: { \n class: 'text-base',\n },\n}\n\n/** 최종 바인딩: styletype에 따른 클래스 적용 */\nconst mapped = computed(() => {\n const styleKey = props.styletype || 'default'\n const preset = STYLE_PRESETS[styleKey] ?? STYLE_PRESETS.default\n \n return {\n for: props.for,\n class: preset?.class || '',\n }\n})\n\nconst displayText = computed(() => {\n if (props.text) {\n return props.text\n }\n return ''\n})\n</script>\n\n<template>\n <Label\n :for=\"mapped.for\"\n :class=\"cn(\n 'text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',\n mapped.class,\n props.class\n )\"\n >\n <span v-if=\"required && styletype === 'required'\" class=\"text-red-500 mr-1\">*</span>\n <span v-else-if=\"required\" class=\"text-red-500 mr-1\">*</span>\n {{ displayText }}\n <slot />\n </Label>\n</template>\n"],"names":["props","__props","STYLE_PRESETS","mapped","computed","styleKey","preset","displayText","_createBlock","_unref","Label","cn","_createElementBlock","_hoisted_1","_hoisted_2","_toDisplayString","_renderSlot","_ctx"],"mappings":";;;;;;;;;;;;;;;;;;;;AAgBA,UAAMA,IAAQC,GAuBRC,IAAsD;AAAA,MAC1D,SAAS,EAAE,OAAO,GAAA;AAAA,MAClB,UAAU;AAAA,QACR,OAAO;AAAA,MAAA;AAAA,MAET,UAAU;AAAA,QACR,OAAO;AAAA,MAAA;AAAA,MAET,OAAO;AAAA,QACL,OAAO;AAAA,MAAA;AAAA,MAET,SAAS;AAAA,QACP,OAAO;AAAA,MAAA;AAAA,MAET,SAAS;AAAA,QACP,OAAO;AAAA,MAAA;AAAA,MAET,IAAI;AAAA,QACF,OAAO;AAAA,MAAA;AAAA,MAET,IAAI;AAAA,QACF,OAAO;AAAA,MAAA;AAAA,IACT,GAIIC,IAASC,EAAS,MAAM;AAC5B,YAAMC,IAAWL,EAAM,aAAa,WAC9BM,IAASJ,EAAcG,CAAQ,KAAKH,EAAc;AAExD,aAAO;AAAA,QACL,KAAKF,EAAM;AAAA,QACX,OAAOM,GAAQ,SAAS;AAAA,MAAA;AAAA,IAE5B,CAAC,GAEKC,IAAcH,EAAS,MACvBJ,EAAM,OACDA,EAAM,OAER,EACR;2BAICQ,EAYQC,EAAAC,CAAA,GAAA;AAAA,MAXL,KAAKP,EAAA,MAAO;AAAA,MACZ,SAAOM,EAAAE,CAAA;AAAA;QAA8GR,EAAA,MAAO;AAAA,QAAaH,EAAM;AAAA,MAAA;;iBAMhJ,MAAoF;AAAA,QAAxEC,EAAA,YAAYA,EAAA,cAAS,mBAAjCW,EAAoF,QAApFC,GAA4E,GAAC,KAC5DZ,EAAA,iBAAjBW,EAA6D,QAA7DE,GAAqD,GAAC;UAAO,MAC7DC,EAAGR,EAAA,KAAW,IAAG,KACjB,CAAA;AAAA,QAAAS,EAAQC,EAAA,QAAA,SAAA;AAAA,MAAA;;;;;"}
|
|
@@ -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 return cn('flex flex-col w-full h-full', props.class)\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 return cn('w-full justify-start', preset.value.listPaddingClass, props.listClass)\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}\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</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","cn","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\">\nimport { computed, ref, watch, nextTick } from 'vue'\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/shadcn'\nimport type { JTabsProps, JTabsEmits } from '@/types/dynamic-tabs.types'\nimport { X } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport JIcon from '@/components/atoms/JIcon.vue'\n\n/**\n * JTabs - 기본 탭 UI 컴포넌트 (molecules)\n * Basic Tabs UI Component\n * \n * @description\n * 정적인 탭 목록을 렌더링하는 기본 탭 컴포넌트입니다.\n * 닫기 버튼, 아이콘 등을 지원합니다.\n * \n * @example\n * ```vue\n * <JTabs \n * :tabs=\"tabs\"\n * :active-tab-id=\"activeId\"\n * @tab-change=\"handleChange\"\n * @tab-close=\"handleClose\"\n * />\n * ```\n */\n\ntype StyleType = 'default' | 'minimal'\n\nconst props = withDefaults(defineProps<JTabsProps>(), {\n styletype: 'default',\n})\n\nconst emit = defineEmits<JTabsEmits>()\n\n/**\n * 안전한 tabs 배열 (undefined/null 체크)\n * Safe tabs array (undefined/null check)\n */\nconst safeTabs = computed(() => {\n return Array.isArray(props.tabs) ? props.tabs : []\n})\n\n/**\n * 현재 활성화된 탭 ID (내부 상태)\n * Current active tab ID (internal state)\n */\nconst internalActiveId = ref<string>(\n props.activeTabId || (safeTabs.value.length > 0 ? safeTabs.value[0]?.id : '') || ''\n)\n\n/**\n * 이벤트 처리 중 플래그 (중복 이벤트 방지)\n * Flag to prevent duplicate events\n */\nlet isHandlingEvent = false\n\n/**\n * props.activeTabId가 변경되면 내부 상태 동기화\n * Sync internal state when props.activeTabId changes\n */\nwatch(() => props.activeTabId, (newValue) => {\n if (newValue !== undefined && newValue !== internalActiveId.value) {\n internalActiveId.value = newValue\n }\n}, { immediate: true })\n\n/**\n * props.tabs가 변경되고 activeTabId가 없으면 첫 번째 탭 활성화\n * Activate first tab when tabs change and no activeTabId\n */\nwatch(safeTabs, (newTabs) => {\n if (!props.activeTabId && newTabs.length > 0 && !newTabs.find(t => t.id === internalActiveId.value) && newTabs[0]) {\n internalActiveId.value = newTabs[0].id\n }\n})\n\n/**\n * 탭 값 변경 핸들러 (reka-ui TabsRoot에서 직접 호출됨)\n * Tab value change handler (called directly from reka-ui TabsRoot)\n */\nconst handleTabValueChange = (value: string | number) => {\n if (isHandlingEvent) return\n \n const stringValue = String(value)\n if (stringValue !== internalActiveId.value) {\n isHandlingEvent = true\n internalActiveId.value = stringValue\n emit('update:activeTabId', stringValue)\n emit('tabChange', stringValue)\n // 다음 tick에서 플래그 리셋\n nextTick(() => {\n isHandlingEvent = false\n })\n }\n}\n\n/**\n * 탭 클릭 핸들러 (백업 방안 - reka-ui 이벤트가 작동하지 않을 경우)\n * Tab click handler (backup - in case reka-ui events don't work)\n */\nconst handleTabClick = (tabId: string) => {\n // reka-ui 이벤트가 작동하지 않을 경우 직접 처리\n // handleTabValueChange가 이미 처리했으면 중복 방지\n if (isHandlingEvent || tabId === internalActiveId.value) return\n \n isHandlingEvent = true\n internalActiveId.value = tabId\n emit('update:activeTabId', tabId)\n emit('tabChange', tabId)\n // 다음 tick에서 플래그 리셋\n nextTick(() => {\n isHandlingEvent = false\n })\n}\n\n/**\n * 탭 닫기 핸들러\n * Tab close handler\n */\nconst handleCloseTab = (e: Event, tabId: string) => {\n e.stopPropagation() // 탭 클릭 이벤트 전파 방지\n emit('tabClose', tabId)\n}\n\n/**\n * 루트 클래스\n * Root classes\n */\nconst rootClasses = computed(() => {\n return cn('flex flex-col w-full h-full', props.class)\n})\n\n/**\n * 스타일 프리셋\n */\nconst STYLE_PRESETS: Record<StyleType, {\n tabPaddingClass: string\n tabTextSizeClass: string\n listPaddingClass: string\n}> = {\n default: {\n tabPaddingClass: 'px-2.5 py-1',\n tabTextSizeClass: 'text-xs',\n listPaddingClass: 'p-0.5',\n },\n minimal: {\n tabPaddingClass: 'px-2 py-0.5',\n tabTextSizeClass: 'text-xs',\n listPaddingClass: 'p-0.5',\n },\n}\n\nconst preset = computed(() => {\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\n})\n\n/**\n * 탭 리스트 클래스\n * Tabs list classes\n */\nconst listClasses = computed(() => {\n return cn('w-full justify-start', preset.value.listPaddingClass, props.listClass)\n})\n</script>\n\n<template>\n <Tabs\n :model-value=\"internalActiveId\"\n @update:model-value=\"handleTabValueChange\"\n orientation=\"horizontal\"\n :class=\"rootClasses\"\n >\n <!-- 탭 헤더 영역 / Tab Headers -->\n <TabsList :class=\"listClasses\">\n <TabsTrigger\n v-for=\"tab in safeTabs\"\n :key=\"tab.id\"\n :value=\"tab.id\"\n @click=\"handleTabClick(tab.id)\"\n :class=\"cn('!flex !items-center !gap-2', preset.tabPaddingClass, preset.tabTextSizeClass)\"\n >\n <!-- 탭 아이콘 (있을 경우) / Tab Icon -->\n <JIcon \n v-if=\"tab.icon\" \n :name=\"tab.icon\" \n size=\"sm\"\n class=\"flex-shrink-0\"\n />\n \n <!-- 탭 레이블 / Tab Label -->\n <span class=\"flex-1 truncate\">{{ tab.label }}</span>\n \n <!-- 닫기 버튼 / Close Button (항상 표시) -->\n <button\n v-if=\"tab.closable\"\n type=\"button\"\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\"\n :aria-label=\"`${tab.label} 탭 닫기`\"\n @click=\"(e) => handleCloseTab(e, tab.id)\"\n >\n <X class=\"h-2.5 w-2.5\" />\n </button>\n </TabsTrigger>\n </TabsList>\n\n <!-- 탭 콘텐츠 영역 / Tab Contents -->\n <div class=\"flex-1 w-full overflow-auto\">\n <TabsContent\n v-for=\"tab in safeTabs\"\n :key=\"`content-${tab.id}`\"\n :value=\"tab.id\"\n class=\"h-full mt-0 data-[state=active]:flex data-[state=active]:flex-col\"\n >\n <!-- 슬롯 우선 / Slot First -->\n <slot :name=\"`content-${tab.id}`\" :tab=\"tab\">\n <!-- 동적 컴포넌트 렌더링 / Dynamic Component Rendering -->\n <component\n v-if=\"tab.component\"\n :is=\"tab.component\"\n v-bind=\"tab.props || {}\"\n />\n \n <!-- 기본 콘텐츠 / Default Content -->\n <div v-else class=\"p-4\">\n <p class=\"text-muted-foreground\">{{ tab.label }} 콘텐츠</p>\n </div>\n </slot>\n </TabsContent>\n </div>\n </Tabs>\n</template>\n\n<style scoped>\n/**\n * 탭 리스트 스타일 - 하단 보더 제거\n * Tab list styles without bottom border\n */\n:deep([role=\"tablist\"]:not(.ag-side-buttons)) {\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: thin;\n scrollbar-color: rgba(0, 0, 0, 0.2) transparent;\n background: hsl(var(--background));\n padding: 0 0.5rem;\n padding-top: 0.25rem;\n gap: 0.25rem;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar) {\n height: 6px;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-track) {\n background: transparent;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\n background-color: rgba(0, 0, 0, 0.2);\n border-radius: 3px;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\n background-color: rgba(0, 0, 0, 0.3);\n}\n\n/**\n * 다크모드에서 스크롤바 스타일\n * Scrollbar styles in dark mode\n */\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\n background-color: rgba(255, 255, 255, 0.2);\n}\n\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\n background-color: rgba(255, 255, 255, 0.3);\n}\n\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)) {\n scrollbar-color: rgba(255, 255, 255, 0.2) transparent;\n}\n\n/**\n * 탭 버튼 스타일 - 명확한 구분\n * Tab button styles - clear distinction\n */\n:deep([role=\"tab\"]) {\n position: relative;\n padding: 0.375rem 0.75rem;\n border-radius: 0.375rem 0.375rem 0 0;\n transition: all 0.2s ease;\n border: 1px solid transparent;\n border-bottom: none;\n}\n\n/**\n * Minimal 스타일 탭 버튼 - JSidebarAdvanced와 높이 맞춤\n */\n:deep([role=\"tablist\"][class*=\"p-0.5\"] [role=\"tab\"]) {\n padding: 0.25rem 0.5rem;\n}\n\n/**\n * 비활성 탭 - 명확하게 구분\n * Inactive tabs - clear distinction\n */\n:deep([role=\"tab\"][data-state=\"inactive\"]) {\n background: hsl(var(--muted) / 0.2);\n color: hsl(var(--muted-foreground));\n border-top: 1px solid hsl(var(--border) / 0.4);\n border-left: 1px solid hsl(var(--border) / 0.4);\n border-right: 1px solid hsl(var(--border) / 0.4);\n}\n\n:deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\n background: hsl(var(--muted) / 0.4);\n color: hsl(var(--foreground));\n}\n\n/**\n * 다크모드에서 비활성 탭 - 다크 배경색 사용\n * Inactive tabs in dark mode - use dark background\n */\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]) {\n background: hsl(var(--secondary));\n color: hsl(var(--secondary-foreground));\n border-top: 1px solid hsl(var(--border) / 0.5);\n border-left: 1px solid hsl(var(--border) / 0.5);\n border-right: 1px solid hsl(var(--border) / 0.5);\n}\n\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\n background: hsl(var(--muted));\n color: hsl(var(--muted-foreground));\n}\n\n/**\n * 활성 탭 - 강조된 스타일\n * Active tab - emphasized style\n */\n:deep([role=\"tab\"][data-state=\"active\"]) {\n background: hsl(var(--background));\n color: hsl(var(--foreground));\n font-weight: 500;\n border-top: 2px solid hsl(var(--primary));\n border-left: 1px solid hsl(var(--border));\n border-right: 1px solid hsl(var(--border));\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n z-index: 1;\n}\n\n/**\n * 다크모드에서 활성 탭 - 배경색 명확히 구분\n * Active tab in dark mode - clear background distinction\n */\n.dark :deep([role=\"tab\"][data-state=\"active\"]) {\n background: hsl(var(--card));\n color: hsl(var(--card-foreground));\n border-top: 2px solid hsl(var(--primary));\n border-left: 1px solid hsl(var(--border));\n border-right: 1px solid hsl(var(--border));\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n</style>\n\n"],"names":["props","__props","emit","__emit","safeTabs","computed","internalActiveId","ref","isHandlingEvent","watch","newValue","newTabs","t","handleTabValueChange","value","stringValue","nextTick","handleTabClick","tabId","handleCloseTab","e","rootClasses","cn","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 +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 return cn('flex flex-col w-full h-full', props.class)\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 return cn('w-full justify-start', preset.value.listPaddingClass, props.listClass)\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}\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</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","cn","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":";;;;;;;;;;;;;;;;;;;;;;;AA6BA,UAAMA,IAAQC,GAIRC,IAAOC,GAMPC,IAAWC,EAAS,MACjB,MAAM,QAAQL,EAAM,IAAI,IAAIA,EAAM,OAAO,CAAA,CACjD,GAMKM,IAAmBC;AAAA,MACvBP,EAAM,gBAAgBI,EAAS,MAAM,SAAS,IAAIA,EAAS,MAAM,CAAC,GAAG,KAAK,OAAO;AAAA,IAAA;AAOnF,QAAII,IAAkB;AAMtB,IAAAC,EAAM,MAAMT,EAAM,aAAa,CAACU,MAAa;AAC3C,MAAIA,MAAa,UAAaA,MAAaJ,EAAiB,UAC1DA,EAAiB,QAAQI;AAAA,IAE7B,GAAG,EAAE,WAAW,IAAM,GAMtBD,EAAML,GAAU,CAACO,MAAY;AAC3B,MAAI,CAACX,EAAM,eAAeW,EAAQ,SAAS,KAAK,CAACA,EAAQ,KAAK,CAAAC,MAAKA,EAAE,OAAON,EAAiB,KAAK,KAAKK,EAAQ,CAAC,MAC9GL,EAAiB,QAAQK,EAAQ,CAAC,EAAE;AAAA,IAExC,CAAC;AAMD,UAAME,IAAuB,CAACC,MAA2B;AACvD,UAAIN,EAAiB;AAErB,YAAMO,IAAc,OAAOD,CAAK;AAChC,MAAIC,MAAgBT,EAAiB,UACnCE,IAAkB,IAClBF,EAAiB,QAAQS,GACzBb,EAAK,sBAAsBa,CAAW,GACtCb,EAAK,aAAaa,CAAW,GAE7BC,EAAS,MAAM;AACb,QAAAR,IAAkB;AAAA,MACpB,CAAC;AAAA,IAEL,GAMMS,IAAiB,CAACC,MAAkB;AAGxC,MAAIV,KAAmBU,MAAUZ,EAAiB,UAElDE,IAAkB,IAClBF,EAAiB,QAAQY,GACzBhB,EAAK,sBAAsBgB,CAAK,GAChChB,EAAK,aAAagB,CAAK,GAEvBF,EAAS,MAAM;AACb,QAAAR,IAAkB;AAAA,MACpB,CAAC;AAAA,IACH,GAMMW,IAAiB,CAAC,GAAUD,MAAkB;AAClD,QAAE,gBAAA,GACFhB,EAAK,YAAYgB,CAAK;AAAA,IACxB,GAMME,IAAcf,EAAS,MACpBgB,EAAG,+BAA+BrB,EAAM,KAAK,CACrD,GAKKsB,IAID;AAAA,MACH,SAAS;AAAA,QACP,iBAAiB;AAAA,QACjB,kBAAkB;AAAA,QAClB,kBAAkB;AAAA,MAAA;AAAA,MAEpB,SAAS;AAAA,QACP,iBAAiB;AAAA,QACjB,kBAAkB;AAAA,QAClB,kBAAkB;AAAA,MAAA;AAAA,IACpB,GAGIC,IAASlB,EAAS,MACfiB,EAActB,EAAM,SAAS,KAAKsB,EAAc,OACxD,GAMKE,IAAcnB,EAAS,MACpBgB,EAAG,wBAAwBE,EAAO,MAAM,kBAAkBvB,EAAM,SAAS,CACjF;2BAICyB,EA+DOC,EAAAC,CAAA,GAAA;AAAA,MA9DJ,eAAarB,EAAA;AAAA,MACb,uBAAoBO;AAAA,MACrB,aAAY;AAAA,MACX,SAAOO,EAAA,KAAW;AAAA,IAAA;iBAGnB,MA8BW;AAAA,QA9BXQ,EA8BWF,EAAAG,CAAA,GAAA;AAAA,UA9BA,SAAOL,EAAA,KAAW;AAAA,QAAA;qBAEzB,MAAuB;AAAA,oBADzBM,EA4BcC,GAAA,MAAAC,EA3BE5B,EAAA,OAAQ,CAAf6B,YADTR,EA4BcC,EAAAQ,CAAA,GAAA;AAAA,cA1BX,KAAKD,EAAI;AAAA,cACT,OAAOA,EAAI;AAAA,cACX,SAAK,CAAAE,MAAElB,EAAegB,EAAI,EAAE;AAAA,cAC5B,OAAKG,EAAEV,KAAE,8BAA+BH,EAAA,MAAO,iBAAiBA,EAAA,MAAO,gBAAgB,CAAA;AAAA,YAAA;yBAGxF,MAKE;AAAA,gBAJMU,EAAI,aADZR,EAKEY,GAAA;AAAA;kBAHC,MAAMJ,EAAI;AAAA,kBACX,MAAK;AAAA,kBACL,OAAM;AAAA,gBAAA;gBAIRK,EAAoD,QAApDC,GAAoDC,EAAnBP,EAAI,KAAK,GAAA,CAAA;AAAA,gBAIlCA,EAAI,iBADZH,EAQS,UAAA;AAAA;kBANP,MAAK;AAAA,kBACL,OAAM;AAAA,kBACL,cAAU,GAAKG,EAAI,KAAK;AAAA,kBACxB,SAAK,CAAGQ,MAAMtB,EAAesB,GAAGR,EAAI,EAAE;AAAA,gBAAA;kBAEvCL,EAAyBF,EAAAgB,CAAA,GAAA,EAAtB,OAAM,eAAa;AAAA,gBAAA;;;;;;;QAM5BJ,EAsBM,OAtBNK,GAsBM;AAAA,kBArBJb,EAoBcC,GAAA,MAAAC,EAnBE5B,EAAA,OAAQ,CAAf6B,YADTR,EAoBcC,EAAAkB,CAAA,GAAA;AAAA,YAlBX,KAAG,WAAaX,EAAI,EAAE;AAAA,YACtB,OAAOA,EAAI;AAAA,YACZ,OAAM;AAAA,UAAA;uBAGN,MAYO;AAAA,cAZPY,EAYOC,EAAA,QAAA,WAZiBb,EAAI,EAAE,MAAK,KAAAA,EAAA,GAAnC,MAYO;AAAA,gBATGA,EAAI,aADZc,KAAAtB,EAIEuB,EAFKf,EAAI,SAAS,GAFpBgB,EAIE;AAAA;;mBADQhB,EAAI,SAAK,CAAA,CAAA,GAAA,MAAA,EAAA,MAInBc,EAAA,GAAAjB,EAEM,OAFNoB,GAEM;AAAA,kBADJZ,EAAwD,KAAxDa,GAAwDX,EAApBP,EAAI,KAAK,IAAG,QAAI,CAAA;AAAA,gBAAA;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"JTabs.vue2.js","sources":["../../../../src/components/molecules/JTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, watch, nextTick } from 'vue'\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/shadcn'\nimport type { JTabsProps, JTabsEmits } from '@/types/dynamic-tabs.types'\nimport { X } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport JIcon from '@/components/atoms/JIcon.vue'\n\n/**\n * JTabs - 기본 탭 UI 컴포넌트 (molecules)\n * Basic Tabs UI Component\n * \n * @description\n * 정적인 탭 목록을 렌더링하는 기본 탭 컴포넌트입니다.\n * 닫기 버튼, 아이콘 등을 지원합니다.\n * \n * @example\n * ```vue\n * <JTabs \n * :tabs=\"tabs\"\n * :active-tab-id=\"activeId\"\n * @tab-change=\"handleChange\"\n * @tab-close=\"handleClose\"\n * />\n * ```\n */\n\ntype StyleType = 'default' | 'minimal'\n\nconst props = withDefaults(defineProps<JTabsProps>(), {\n styletype: 'default',\n})\n\nconst emit = defineEmits<JTabsEmits>()\n\n/**\n * 안전한 tabs 배열 (undefined/null 체크)\n * Safe tabs array (undefined/null check)\n */\nconst safeTabs = computed(() => {\n return Array.isArray(props.tabs) ? props.tabs : []\n})\n\n/**\n * 현재 활성화된 탭 ID (내부 상태)\n * Current active tab ID (internal state)\n */\nconst internalActiveId = ref<string>(\n props.activeTabId || (safeTabs.value.length > 0 ? safeTabs.value[0]?.id : '') || ''\n)\n\n/**\n * 이벤트 처리 중 플래그 (중복 이벤트 방지)\n * Flag to prevent duplicate events\n */\nlet isHandlingEvent = false\n\n/**\n * props.activeTabId가 변경되면 내부 상태 동기화\n * Sync internal state when props.activeTabId changes\n */\nwatch(() => props.activeTabId, (newValue) => {\n if (newValue !== undefined && newValue !== internalActiveId.value) {\n internalActiveId.value = newValue\n }\n}, { immediate: true })\n\n/**\n * props.tabs가 변경되고 activeTabId가 없으면 첫 번째 탭 활성화\n * Activate first tab when tabs change and no activeTabId\n */\nwatch(safeTabs, (newTabs) => {\n if (!props.activeTabId && newTabs.length > 0 && !newTabs.find(t => t.id === internalActiveId.value) && newTabs[0]) {\n internalActiveId.value = newTabs[0].id\n }\n})\n\n/**\n * 탭 값 변경 핸들러 (reka-ui TabsRoot에서 직접 호출됨)\n * Tab value change handler (called directly from reka-ui TabsRoot)\n */\nconst handleTabValueChange = (value: string | number) => {\n if (isHandlingEvent) return\n \n const stringValue = String(value)\n if (stringValue !== internalActiveId.value) {\n isHandlingEvent = true\n internalActiveId.value = stringValue\n emit('update:activeTabId', stringValue)\n emit('tabChange', stringValue)\n // 다음 tick에서 플래그 리셋\n nextTick(() => {\n isHandlingEvent = false\n })\n }\n}\n\n/**\n * 탭 클릭 핸들러 (백업 방안 - reka-ui 이벤트가 작동하지 않을 경우)\n * Tab click handler (backup - in case reka-ui events don't work)\n */\nconst handleTabClick = (tabId: string) => {\n // reka-ui 이벤트가 작동하지 않을 경우 직접 처리\n // handleTabValueChange가 이미 처리했으면 중복 방지\n if (isHandlingEvent || tabId === internalActiveId.value) return\n \n isHandlingEvent = true\n internalActiveId.value = tabId\n emit('update:activeTabId', tabId)\n emit('tabChange', tabId)\n // 다음 tick에서 플래그 리셋\n nextTick(() => {\n isHandlingEvent = false\n })\n}\n\n/**\n * 탭 닫기 핸들러\n * Tab close handler\n */\nconst handleCloseTab = (e: Event, tabId: string) => {\n e.stopPropagation() // 탭 클릭 이벤트 전파 방지\n emit('tabClose', tabId)\n}\n\n/**\n * 루트 클래스\n * Root classes\n */\nconst rootClasses = computed(() => {\n return cn('flex flex-col w-full h-full', props.class)\n})\n\n/**\n * 스타일 프리셋\n */\nconst STYLE_PRESETS: Record<StyleType, {\n tabPaddingClass: string\n tabTextSizeClass: string\n listPaddingClass: string\n}> = {\n default: {\n tabPaddingClass: 'px-2.5 py-1',\n tabTextSizeClass: 'text-xs',\n listPaddingClass: 'p-0.5',\n },\n minimal: {\n tabPaddingClass: 'px-2 py-0.5',\n tabTextSizeClass: 'text-xs',\n listPaddingClass: 'p-0.5',\n },\n}\n\nconst preset = computed(() => {\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\n})\n\n/**\n * 탭 리스트 클래스\n * Tabs list classes\n */\nconst listClasses = computed(() => {\n return cn('w-full justify-start', preset.value.listPaddingClass, props.listClass)\n})\n</script>\n\n<template>\n <Tabs\n :model-value=\"internalActiveId\"\n @update:model-value=\"handleTabValueChange\"\n orientation=\"horizontal\"\n :class=\"rootClasses\"\n >\n <!-- 탭 헤더 영역 / Tab Headers -->\n <TabsList :class=\"listClasses\">\n <TabsTrigger\n v-for=\"tab in safeTabs\"\n :key=\"tab.id\"\n :value=\"tab.id\"\n @click=\"handleTabClick(tab.id)\"\n :class=\"cn('!flex !items-center !gap-2', preset.tabPaddingClass, preset.tabTextSizeClass)\"\n >\n <!-- 탭 아이콘 (있을 경우) / Tab Icon -->\n <JIcon \n v-if=\"tab.icon\" \n :name=\"tab.icon\" \n size=\"sm\"\n class=\"flex-shrink-0\"\n />\n \n <!-- 탭 레이블 / Tab Label -->\n <span class=\"flex-1 truncate\">{{ tab.label }}</span>\n \n <!-- 닫기 버튼 / Close Button (항상 표시) -->\n <button\n v-if=\"tab.closable\"\n type=\"button\"\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\"\n :aria-label=\"`${tab.label} 탭 닫기`\"\n @click=\"(e) => handleCloseTab(e, tab.id)\"\n >\n <X class=\"h-2.5 w-2.5\" />\n </button>\n </TabsTrigger>\n </TabsList>\n\n <!-- 탭 콘텐츠 영역 / Tab Contents -->\n <div class=\"flex-1 w-full overflow-auto\">\n <TabsContent\n v-for=\"tab in safeTabs\"\n :key=\"`content-${tab.id}`\"\n :value=\"tab.id\"\n class=\"h-full mt-0 data-[state=active]:flex data-[state=active]:flex-col\"\n >\n <!-- 슬롯 우선 / Slot First -->\n <slot :name=\"`content-${tab.id}`\" :tab=\"tab\">\n <!-- 동적 컴포넌트 렌더링 / Dynamic Component Rendering -->\n <component\n v-if=\"tab.component\"\n :is=\"tab.component\"\n v-bind=\"tab.props || {}\"\n />\n \n <!-- 기본 콘텐츠 / Default Content -->\n <div v-else class=\"p-4\">\n <p class=\"text-muted-foreground\">{{ tab.label }} 콘텐츠</p>\n </div>\n </slot>\n </TabsContent>\n </div>\n </Tabs>\n</template>\n\n<style scoped>\n/**\n * 탭 리스트 스타일 - 하단 보더 제거\n * Tab list styles without bottom border\n */\n:deep([role=\"tablist\"]:not(.ag-side-buttons)) {\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: thin;\n scrollbar-color: rgba(0, 0, 0, 0.2) transparent;\n background: hsl(var(--background));\n padding: 0 0.5rem;\n padding-top: 0.25rem;\n gap: 0.25rem;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar) {\n height: 6px;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-track) {\n background: transparent;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\n background-color: rgba(0, 0, 0, 0.2);\n border-radius: 3px;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\n background-color: rgba(0, 0, 0, 0.3);\n}\n\n/**\n * 다크모드에서 스크롤바 스타일\n * Scrollbar styles in dark mode\n */\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\n background-color: rgba(255, 255, 255, 0.2);\n}\n\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\n background-color: rgba(255, 255, 255, 0.3);\n}\n\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)) {\n scrollbar-color: rgba(255, 255, 255, 0.2) transparent;\n}\n\n/**\n * 탭 버튼 스타일 - 명확한 구분\n * Tab button styles - clear distinction\n */\n:deep([role=\"tab\"]) {\n position: relative;\n padding: 0.375rem 0.75rem;\n border-radius: 0.375rem 0.375rem 0 0;\n transition: all 0.2s ease;\n border: 1px solid transparent;\n border-bottom: none;\n}\n\n/**\n * Minimal 스타일 탭 버튼 - JSidebarAdvanced와 높이 맞춤\n */\n:deep([role=\"tablist\"][class*=\"p-0.5\"] [role=\"tab\"]) {\n padding: 0.25rem 0.5rem;\n}\n\n/**\n * 비활성 탭 - 명확하게 구분\n * Inactive tabs - clear distinction\n */\n:deep([role=\"tab\"][data-state=\"inactive\"]) {\n background: hsl(var(--muted) / 0.2);\n color: hsl(var(--muted-foreground));\n border-top: 1px solid hsl(var(--border) / 0.4);\n border-left: 1px solid hsl(var(--border) / 0.4);\n border-right: 1px solid hsl(var(--border) / 0.4);\n}\n\n:deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\n background: hsl(var(--muted) / 0.4);\n color: hsl(var(--foreground));\n}\n\n/**\n * 다크모드에서 비활성 탭 - 다크 배경색 사용\n * Inactive tabs in dark mode - use dark background\n */\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]) {\n background: hsl(var(--secondary));\n color: hsl(var(--secondary-foreground));\n border-top: 1px solid hsl(var(--border) / 0.5);\n border-left: 1px solid hsl(var(--border) / 0.5);\n border-right: 1px solid hsl(var(--border) / 0.5);\n}\n\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\n background: hsl(var(--muted));\n color: hsl(var(--muted-foreground));\n}\n\n/**\n * 활성 탭 - 강조된 스타일\n * Active tab - emphasized style\n */\n:deep([role=\"tab\"][data-state=\"active\"]) {\n background: hsl(var(--background));\n color: hsl(var(--foreground));\n font-weight: 500;\n border-top: 2px solid hsl(var(--primary));\n border-left: 1px solid hsl(var(--border));\n border-right: 1px solid hsl(var(--border));\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n z-index: 1;\n}\n\n/**\n * 다크모드에서 활성 탭 - 배경색 명확히 구분\n * Active tab in dark mode - clear background distinction\n */\n.dark :deep([role=\"tab\"][data-state=\"active\"]) {\n background: hsl(var(--card));\n color: hsl(var(--card-foreground));\n border-top: 2px solid hsl(var(--primary));\n border-left: 1px solid hsl(var(--border));\n border-right: 1px solid hsl(var(--border));\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n</style>\n\n"],"names":["props","__props","emit","__emit","safeTabs","computed","internalActiveId","ref","isHandlingEvent","watch","newValue","newTabs","t","handleTabValueChange","value","stringValue","nextTick","handleTabClick","tabId","handleCloseTab","rootClasses","cn","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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JDynamicTabs.vue.cjs","sources":["../../../../src/components/organisms/JDynamicTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from 'vue'\r\nimport JTabs from '@/components/molecules/JTabs.vue'\r\nimport { cn } from '@/lib/utils'\r\nimport type {\r\n JDynamicTabsProps, \r\n JDynamicTabsEmits, \r\n JDynamicTabsMethods,\r\n DynamicTab \r\n} from '@/types/dynamic-tabs.types'\r\n\r\n/**\r\n * JDynamicTabs - 동적 탭 컴포넌트 (organisms)\r\n * Dynamic Tabs Component\r\n * \r\n * @description\r\n * 사이드 메뉴 클릭으로 탭을 동적으로 추가/제거할 수 있는 탭 컴포넌트입니다.\r\n * \r\n * Features:\r\n * - 탭 동적 추가/제거\r\n * - 중복 탭 방지 (이미 있으면 활성화)\r\n * - 닫기 버튼으로 탭 제거\r\n * - 활성 탭 자동 재설정\r\n * - 최대 탭 개수 제한\r\n * \r\n * @example\r\n * const tabsRef = ref()\r\n * \r\n * const handleMenuClick = (menuItem) => {\r\n * tabsRef.value?.addTab({\r\n * id: menuItem.id,\r\n * label: menuItem.label,\r\n * component: menuItem.component,\r\n * closable: true\r\n * })\r\n * }\r\n */\r\n\r\nconst props = withDefaults(defineProps<JDynamicTabsProps>(), {\r\n initialTabs: () => [],\r\n maxTabs: 0, // 0 = unlimited\r\n emptyMessage: '탭을 추가해주세요.',\r\n styletype: 'default',\r\n})\r\n\r\nconst emit = defineEmits<JDynamicTabsEmits>()\r\n\r\n/**\r\n * 탭 목록 (내부 상태)\r\n * Tabs list (internal state)\r\n */\r\nconst tabs = ref<DynamicTab[]>(Array.isArray(props.initialTabs) ? [...props.initialTabs] : [])\r\n\r\n/**\r\n * 현재 활성화된 탭 ID (내부 상태)\r\n * Current active tab ID (internal state)\r\n */\r\nconst activeTabId = ref<string>(\r\n props.defaultActiveId || (Array.isArray(props.initialTabs) && props.initialTabs.length > 0 ? props.initialTabs[0]?.id : '') || ''\r\n)\r\n\r\n/**\r\n * initialTabs가 변경되면 내부 상태 업데이트\r\n * Update internal state when initialTabs changes\r\n */\r\nwatch(() => props.initialTabs, (newTabs) => {\r\n if (!newTabs) {\r\n tabs.value = []\r\n return\r\n }\r\n \r\n if (Array.isArray(newTabs) && newTabs.length > 0) {\r\n tabs.value = [...newTabs]\r\n if (!activeTabId.value && newTabs[0]) {\r\n activeTabId.value = newTabs[0].id\r\n }\r\n } else if (Array.isArray(newTabs)) {\r\n // 빈 배열인 경우\r\n tabs.value = []\r\n } else {\r\n // 배열이 아닌 경우\r\n tabs.value = []\r\n }\r\n}, { immediate: true })\r\n\r\n/**\r\n * 탭 추가\r\n * Add tab (if exists, activate it; otherwise, add to array and activate)\r\n * \r\n * @param tab - 추가할 탭 정보\r\n */\r\nconst addTab = (tab: DynamicTab) => {\r\n // tabs.value가 배열이 아니면 초기화\r\n if (!Array.isArray(tabs.value)) {\r\n tabs.value = []\r\n }\r\n \r\n // 이미 존재하는 탭인지 확인\r\n const existingTab = tabs.value.find(t => t.id === tab.id)\r\n \r\n if (existingTab) {\r\n // 이미 있으면 해당 탭 활성화\r\n activateTab(tab.id)\r\n return\r\n }\r\n \r\n // 최대 탭 개수 체크\r\n if (props.maxTabs > 0 && tabs.value.length >= props.maxTabs) {\r\n console.warn(`최대 ${props.maxTabs}개의 탭만 열 수 있습니다.`)\r\n return\r\n }\r\n \r\n // 새 탭 추가\r\n tabs.value.push({\r\n ...tab,\r\n closable: tab.closable !== false, // 기본값 true\r\n })\r\n \r\n // 새로 추가된 탭 활성화\r\n activateTab(tab.id)\r\n \r\n // 이벤트 발생\r\n emit('tabAdd', tab)\r\n}\r\n\r\n/**\r\n * 탭 닫기\r\n * Close tab (remove from array and reset active tab)\r\n * \r\n * @param id - 닫을 탭 ID\r\n */\r\nconst closeTab = (id: string) => {\r\n if (!Array.isArray(tabs.value)) {\r\n return\r\n }\r\n \r\n const tabIndex = tabs.value.findIndex(t => t.id === id)\r\n \r\n if (tabIndex === -1) return\r\n \r\n // 탭 제거\r\n tabs.value.splice(tabIndex, 1)\r\n \r\n // 활성 탭 재설정\r\n if (activeTabId.value === id) {\r\n if (tabs.value.length > 0) {\r\n // 이전 탭이 있으면 이전 탭 활성화, 없으면 다음 탭 활성화\r\n const newIndex = Math.min(tabIndex, tabs.value.length - 1)\r\n activeTabId.value = tabs.value[newIndex]?.id || ''\r\n } else {\r\n activeTabId.value = ''\r\n }\r\n }\r\n \r\n // 이벤트 발생\r\n emit('tabClose', id)\r\n}\r\n\r\n/**\r\n * 탭 활성화\r\n * Activate tab\r\n * \r\n * @param id - 활성화할 탭 ID\r\n */\r\nconst activateTab = (id: string) => {\r\n if (!Array.isArray(tabs.value)) {\r\n return\r\n }\r\n const tab = tabs.value.find(t => t.id === id)\r\n if (tab) {\r\n activeTabId.value = id\r\n }\r\n}\r\n\r\n/**\r\n * 특정 탭 찾기\r\n * Find specific tab\r\n * \r\n * @param id - 찾을 탭 ID\r\n */\r\nconst findTab = (id: string): DynamicTab | undefined => {\r\n if (!Array.isArray(tabs.value)) {\r\n return undefined\r\n }\r\n return tabs.value.find(t => t.id === id)\r\n}\r\n\r\n/**\r\n * 모든 탭 닫기 (closable이 true인 탭만)\r\n * Close all tabs (only closable tabs)\r\n */\r\nconst closeAllTabs = () => {\r\n if (!Array.isArray(tabs.value)) {\r\n return\r\n }\r\n const closableTabs = tabs.value.filter(t => t.closable)\r\n closableTabs.forEach(tab => closeTab(tab.id))\r\n}\r\n\r\n/**\r\n * 탭 변경 핸들러\r\n * Tab change handler\r\n */\r\nconst handleTabChange = (id: string) => {\r\n activeTabId.value = id\r\n emit('tabChange', id)\r\n}\r\n\r\n/**\r\n * 탭 닫기 핸들러\r\n * Tab close handler\r\n */\r\nconst handleTabClose = (id: string) => {\r\n closeTab(id)\r\n}\r\n\r\n/**\r\n * 외부에서 호출 가능한 메서드 노출\r\n * Expose methods for external use\r\n */\r\ndefineExpose<JDynamicTabsMethods>({\r\n addTab,\r\n closeTab,\r\n activateTab,\r\n findTab,\r\n closeAllTabs,\r\n})\r\n\r\n/**\r\n * 안전한 tabs 배열\r\n * Safe tabs array\r\n */\r\nconst safeTabs = computed(() => {\r\n return Array.isArray(tabs.value) ? tabs.value : []\r\n})\r\n\r\n/**\r\n * 탭이 있는지 여부\r\n * Whether there are tabs\r\n */\r\nconst hasTabs = computed(() => safeTabs.value.length > 0)\r\n\r\n/**\r\n * 루트 클래스\r\n * Root classes\r\n */\r\nconst rootClasses = computed(() => {\r\n return cn('w-full h-full', props.class)\r\n})\r\n\r\n/**\r\n * 콘텐츠 클래스\r\n * Content classes\r\n */\r\nconst contentClasses = computed(() => {\r\n return props.contentClass || ''\r\n})\r\n</script>\r\n\r\n<template>\r\n <div :class=\"rootClasses\">\r\n <!-- 탭이 있을 경우 / When there are tabs -->\r\n <JTabs\r\n v-if=\"hasTabs\"\r\n :tabs=\"safeTabs\"\r\n :active-tab-id=\"activeTabId\"\r\n :styletype=\"props.styletype\"\r\n :class=\"contentClasses\"\r\n @tab-change=\"handleTabChange\"\r\n @tab-close=\"handleTabClose\"\r\n >\r\n <!-- 각 탭 콘텐츠 슬롯 전달 / Forward slots for each tab content -->\r\n <template\r\n v-for=\"tab in safeTabs\"\r\n :key=\"tab.id\"\r\n #[`content-${tab.id}`]=\"slotProps\"\r\n >\r\n <slot :name=\"`content-${tab.id}`\" v-bind=\"slotProps\" />\r\n </template>\r\n </JTabs>\r\n \r\n <!-- 탭이 없을 경우 / When there are no tabs -->\r\n <div\r\n v-else\r\n class=\"flex items-center justify-center h-full w-full\"\r\n >\r\n <div class=\"text-center\">\r\n <p class=\"text-muted-foreground text-lg\">{{ emptyMessage }}</p>\r\n <p class=\"text-muted-foreground/60 text-sm mt-2\">\r\n 메뉴를 클릭하여 탭을 추가하세요.\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n"],"names":["props","__props","emit","__emit","tabs","ref","activeTabId","watch","newTabs","addTab","tab","t","activateTab","closeTab","id","tabIndex","newIndex","findTab","closeAllTabs","handleTabChange","handleTabClose","__expose","safeTabs","computed","hasTabs","rootClasses","cn","contentClasses","_createElementBlock","_createBlock","JTabs","_renderList","_withCtx","slotProps","_renderSlot","_ctx","_openBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_toDisplayString","_cache"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JDynamicTabs.vue.cjs","sources":["../../../../src/components/organisms/JDynamicTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, computed, watch } from 'vue'\nimport JTabs from '@/components/molecules/JTabs.vue'\nimport { cn } from '@/lib/utils'\nimport type {\n JDynamicTabsProps, \n JDynamicTabsEmits, \n JDynamicTabsMethods,\n DynamicTab \n} from '@/types/dynamic-tabs.types'\n\n/**\n * JDynamicTabs - 동적 탭 컴포넌트 (organisms)\n * Dynamic Tabs Component\n * \n * @description\n * 사이드 메뉴 클릭으로 탭을 동적으로 추가/제거할 수 있는 탭 컴포넌트입니다.\n * \n * Features:\n * - 탭 동적 추가/제거\n * - 중복 탭 방지 (이미 있으면 활성화)\n * - 닫기 버튼으로 탭 제거\n * - 활성 탭 자동 재설정\n * - 최대 탭 개수 제한\n * \n * @example\n * const tabsRef = ref()\n * \n * const handleMenuClick = (menuItem) => {\n * tabsRef.value?.addTab({\n * id: menuItem.id,\n * label: menuItem.label,\n * component: menuItem.component,\n * closable: true\n * })\n * }\n */\n\nconst props = withDefaults(defineProps<JDynamicTabsProps>(), {\n initialTabs: () => [],\n maxTabs: 0, // 0 = unlimited\n emptyMessage: '탭을 추가해주세요.',\n styletype: 'default',\n})\n\nconst emit = defineEmits<JDynamicTabsEmits>()\n\n/**\n * 탭 목록 (내부 상태)\n * Tabs list (internal state)\n */\nconst tabs = ref<DynamicTab[]>(Array.isArray(props.initialTabs) ? [...props.initialTabs] : [])\n\n/**\n * 현재 활성화된 탭 ID (내부 상태)\n * Current active tab ID (internal state)\n */\nconst activeTabId = ref<string>(\n props.defaultActiveId || (Array.isArray(props.initialTabs) && props.initialTabs.length > 0 ? props.initialTabs[0]?.id : '') || ''\n)\n\n/**\n * initialTabs가 변경되면 내부 상태 업데이트\n * Update internal state when initialTabs changes\n */\nwatch(() => props.initialTabs, (newTabs) => {\n if (!newTabs) {\n tabs.value = []\n return\n }\n \n if (Array.isArray(newTabs) && newTabs.length > 0) {\n tabs.value = [...newTabs]\n if (!activeTabId.value && newTabs[0]) {\n activeTabId.value = newTabs[0].id\n }\n } else if (Array.isArray(newTabs)) {\n // 빈 배열인 경우\n tabs.value = []\n } else {\n // 배열이 아닌 경우\n tabs.value = []\n }\n}, { immediate: true })\n\n/**\n * 탭 추가\n * Add tab (if exists, activate it; otherwise, add to array and activate)\n * \n * @param tab - 추가할 탭 정보\n */\nconst addTab = (tab: DynamicTab) => {\n // tabs.value가 배열이 아니면 초기화\n if (!Array.isArray(tabs.value)) {\n tabs.value = []\n }\n \n // 이미 존재하는 탭인지 확인\n const existingTab = tabs.value.find(t => t.id === tab.id)\n \n if (existingTab) {\n // 이미 있으면 해당 탭 활성화\n activateTab(tab.id)\n return\n }\n \n // 최대 탭 개수 체크\n if (props.maxTabs > 0 && tabs.value.length >= props.maxTabs) {\n console.warn(`최대 ${props.maxTabs}개의 탭만 열 수 있습니다.`)\n return\n }\n \n // 새 탭 추가\n tabs.value.push({\n ...tab,\n closable: tab.closable !== false, // 기본값 true\n })\n \n // 새로 추가된 탭 활성화\n activateTab(tab.id)\n \n // 이벤트 발생\n emit('tabAdd', tab)\n}\n\n/**\n * 탭 닫기\n * Close tab (remove from array and reset active tab)\n * \n * @param id - 닫을 탭 ID\n */\nconst closeTab = (id: string) => {\n if (!Array.isArray(tabs.value)) {\n return\n }\n \n const tabIndex = tabs.value.findIndex(t => t.id === id)\n \n if (tabIndex === -1) return\n \n // 탭 제거\n tabs.value.splice(tabIndex, 1)\n \n // 활성 탭 재설정\n if (activeTabId.value === id) {\n if (tabs.value.length > 0) {\n // 이전 탭이 있으면 이전 탭 활성화, 없으면 다음 탭 활성화\n const newIndex = Math.min(tabIndex, tabs.value.length - 1)\n activeTabId.value = tabs.value[newIndex]?.id || ''\n } else {\n activeTabId.value = ''\n }\n }\n \n // 이벤트 발생\n emit('tabClose', id)\n}\n\n/**\n * 탭 활성화\n * Activate tab\n * \n * @param id - 활성화할 탭 ID\n */\nconst activateTab = (id: string) => {\n if (!Array.isArray(tabs.value)) {\n return\n }\n const tab = tabs.value.find(t => t.id === id)\n if (tab) {\n activeTabId.value = id\n }\n}\n\n/**\n * 특정 탭 찾기\n * Find specific tab\n * \n * @param id - 찾을 탭 ID\n */\nconst findTab = (id: string): DynamicTab | undefined => {\n if (!Array.isArray(tabs.value)) {\n return undefined\n }\n return tabs.value.find(t => t.id === id)\n}\n\n/**\n * 모든 탭 닫기 (closable이 true인 탭만)\n * Close all tabs (only closable tabs)\n */\nconst closeAllTabs = () => {\n if (!Array.isArray(tabs.value)) {\n return\n }\n const closableTabs = tabs.value.filter(t => t.closable)\n closableTabs.forEach(tab => closeTab(tab.id))\n}\n\n/**\n * 탭 변경 핸들러\n * Tab change handler\n */\nconst handleTabChange = (id: string) => {\n activeTabId.value = id\n emit('tabChange', id)\n}\n\n/**\n * 탭 닫기 핸들러\n * Tab close handler\n */\nconst handleTabClose = (id: string) => {\n closeTab(id)\n}\n\n/**\n * 외부에서 호출 가능한 메서드 노출\n * Expose methods for external use\n */\ndefineExpose<JDynamicTabsMethods>({\n addTab,\n closeTab,\n activateTab,\n findTab,\n closeAllTabs,\n})\n\n/**\n * 안전한 tabs 배열\n * Safe tabs array\n */\nconst safeTabs = computed(() => {\n return Array.isArray(tabs.value) ? tabs.value : []\n})\n\n/**\n * 탭이 있는지 여부\n * Whether there are tabs\n */\nconst hasTabs = computed(() => safeTabs.value.length > 0)\n\n/**\n * 루트 클래스\n * Root classes\n */\nconst rootClasses = computed(() => {\n return cn('w-full h-full', props.class)\n})\n\n/**\n * 콘텐츠 클래스\n * Content classes\n */\nconst contentClasses = computed(() => {\n return props.contentClass || ''\n})\n</script>\n\n<template>\n <div :class=\"rootClasses\">\n <!-- 탭이 있을 경우 / When there are tabs -->\n <JTabs\n v-if=\"hasTabs\"\n :tabs=\"safeTabs\"\n :active-tab-id=\"activeTabId\"\n :styletype=\"props.styletype\"\n :class=\"contentClasses\"\n @tab-change=\"handleTabChange\"\n @tab-close=\"handleTabClose\"\n >\n <!-- 각 탭 콘텐츠 슬롯 전달 / Forward slots for each tab content -->\n <template\n v-for=\"tab in safeTabs\"\n :key=\"tab.id\"\n #[`content-${tab.id}`]=\"slotProps\"\n >\n <slot :name=\"`content-${tab.id}`\" v-bind=\"slotProps\" />\n </template>\n </JTabs>\n \n <!-- 탭이 없을 경우 / When there are no tabs -->\n <div\n v-else\n class=\"flex items-center justify-center h-full w-full\"\n >\n <div class=\"text-center\">\n <p class=\"text-muted-foreground text-lg\">{{ emptyMessage }}</p>\n <p class=\"text-muted-foreground/60 text-sm mt-2\">\n 메뉴를 클릭하여 탭을 추가하세요.\n </p>\n </div>\n </div>\n </div>\n</template>\n"],"names":["props","__props","emit","__emit","tabs","ref","activeTabId","watch","newTabs","addTab","tab","t","activateTab","closeTab","id","tabIndex","newIndex","findTab","closeAllTabs","handleTabChange","handleTabClose","__expose","safeTabs","computed","hasTabs","rootClasses","cn","contentClasses","_createElementBlock","_createBlock","JTabs","_renderList","_withCtx","slotProps","_renderSlot","_ctx","_openBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_toDisplayString","_cache"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JDynamicTabs.vue.js","sources":["../../../../src/components/organisms/JDynamicTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\r\nimport { ref, computed, watch } from 'vue'\r\nimport JTabs from '@/components/molecules/JTabs.vue'\r\nimport { cn } from '@/lib/utils'\r\nimport type {\r\n JDynamicTabsProps, \r\n JDynamicTabsEmits, \r\n JDynamicTabsMethods,\r\n DynamicTab \r\n} from '@/types/dynamic-tabs.types'\r\n\r\n/**\r\n * JDynamicTabs - 동적 탭 컴포넌트 (organisms)\r\n * Dynamic Tabs Component\r\n * \r\n * @description\r\n * 사이드 메뉴 클릭으로 탭을 동적으로 추가/제거할 수 있는 탭 컴포넌트입니다.\r\n * \r\n * Features:\r\n * - 탭 동적 추가/제거\r\n * - 중복 탭 방지 (이미 있으면 활성화)\r\n * - 닫기 버튼으로 탭 제거\r\n * - 활성 탭 자동 재설정\r\n * - 최대 탭 개수 제한\r\n * \r\n * @example\r\n * const tabsRef = ref()\r\n * \r\n * const handleMenuClick = (menuItem) => {\r\n * tabsRef.value?.addTab({\r\n * id: menuItem.id,\r\n * label: menuItem.label,\r\n * component: menuItem.component,\r\n * closable: true\r\n * })\r\n * }\r\n */\r\n\r\nconst props = withDefaults(defineProps<JDynamicTabsProps>(), {\r\n initialTabs: () => [],\r\n maxTabs: 0, // 0 = unlimited\r\n emptyMessage: '탭을 추가해주세요.',\r\n styletype: 'default',\r\n})\r\n\r\nconst emit = defineEmits<JDynamicTabsEmits>()\r\n\r\n/**\r\n * 탭 목록 (내부 상태)\r\n * Tabs list (internal state)\r\n */\r\nconst tabs = ref<DynamicTab[]>(Array.isArray(props.initialTabs) ? [...props.initialTabs] : [])\r\n\r\n/**\r\n * 현재 활성화된 탭 ID (내부 상태)\r\n * Current active tab ID (internal state)\r\n */\r\nconst activeTabId = ref<string>(\r\n props.defaultActiveId || (Array.isArray(props.initialTabs) && props.initialTabs.length > 0 ? props.initialTabs[0]?.id : '') || ''\r\n)\r\n\r\n/**\r\n * initialTabs가 변경되면 내부 상태 업데이트\r\n * Update internal state when initialTabs changes\r\n */\r\nwatch(() => props.initialTabs, (newTabs) => {\r\n if (!newTabs) {\r\n tabs.value = []\r\n return\r\n }\r\n \r\n if (Array.isArray(newTabs) && newTabs.length > 0) {\r\n tabs.value = [...newTabs]\r\n if (!activeTabId.value && newTabs[0]) {\r\n activeTabId.value = newTabs[0].id\r\n }\r\n } else if (Array.isArray(newTabs)) {\r\n // 빈 배열인 경우\r\n tabs.value = []\r\n } else {\r\n // 배열이 아닌 경우\r\n tabs.value = []\r\n }\r\n}, { immediate: true })\r\n\r\n/**\r\n * 탭 추가\r\n * Add tab (if exists, activate it; otherwise, add to array and activate)\r\n * \r\n * @param tab - 추가할 탭 정보\r\n */\r\nconst addTab = (tab: DynamicTab) => {\r\n // tabs.value가 배열이 아니면 초기화\r\n if (!Array.isArray(tabs.value)) {\r\n tabs.value = []\r\n }\r\n \r\n // 이미 존재하는 탭인지 확인\r\n const existingTab = tabs.value.find(t => t.id === tab.id)\r\n \r\n if (existingTab) {\r\n // 이미 있으면 해당 탭 활성화\r\n activateTab(tab.id)\r\n return\r\n }\r\n \r\n // 최대 탭 개수 체크\r\n if (props.maxTabs > 0 && tabs.value.length >= props.maxTabs) {\r\n console.warn(`최대 ${props.maxTabs}개의 탭만 열 수 있습니다.`)\r\n return\r\n }\r\n \r\n // 새 탭 추가\r\n tabs.value.push({\r\n ...tab,\r\n closable: tab.closable !== false, // 기본값 true\r\n })\r\n \r\n // 새로 추가된 탭 활성화\r\n activateTab(tab.id)\r\n \r\n // 이벤트 발생\r\n emit('tabAdd', tab)\r\n}\r\n\r\n/**\r\n * 탭 닫기\r\n * Close tab (remove from array and reset active tab)\r\n * \r\n * @param id - 닫을 탭 ID\r\n */\r\nconst closeTab = (id: string) => {\r\n if (!Array.isArray(tabs.value)) {\r\n return\r\n }\r\n \r\n const tabIndex = tabs.value.findIndex(t => t.id === id)\r\n \r\n if (tabIndex === -1) return\r\n \r\n // 탭 제거\r\n tabs.value.splice(tabIndex, 1)\r\n \r\n // 활성 탭 재설정\r\n if (activeTabId.value === id) {\r\n if (tabs.value.length > 0) {\r\n // 이전 탭이 있으면 이전 탭 활성화, 없으면 다음 탭 활성화\r\n const newIndex = Math.min(tabIndex, tabs.value.length - 1)\r\n activeTabId.value = tabs.value[newIndex]?.id || ''\r\n } else {\r\n activeTabId.value = ''\r\n }\r\n }\r\n \r\n // 이벤트 발생\r\n emit('tabClose', id)\r\n}\r\n\r\n/**\r\n * 탭 활성화\r\n * Activate tab\r\n * \r\n * @param id - 활성화할 탭 ID\r\n */\r\nconst activateTab = (id: string) => {\r\n if (!Array.isArray(tabs.value)) {\r\n return\r\n }\r\n const tab = tabs.value.find(t => t.id === id)\r\n if (tab) {\r\n activeTabId.value = id\r\n }\r\n}\r\n\r\n/**\r\n * 특정 탭 찾기\r\n * Find specific tab\r\n * \r\n * @param id - 찾을 탭 ID\r\n */\r\nconst findTab = (id: string): DynamicTab | undefined => {\r\n if (!Array.isArray(tabs.value)) {\r\n return undefined\r\n }\r\n return tabs.value.find(t => t.id === id)\r\n}\r\n\r\n/**\r\n * 모든 탭 닫기 (closable이 true인 탭만)\r\n * Close all tabs (only closable tabs)\r\n */\r\nconst closeAllTabs = () => {\r\n if (!Array.isArray(tabs.value)) {\r\n return\r\n }\r\n const closableTabs = tabs.value.filter(t => t.closable)\r\n closableTabs.forEach(tab => closeTab(tab.id))\r\n}\r\n\r\n/**\r\n * 탭 변경 핸들러\r\n * Tab change handler\r\n */\r\nconst handleTabChange = (id: string) => {\r\n activeTabId.value = id\r\n emit('tabChange', id)\r\n}\r\n\r\n/**\r\n * 탭 닫기 핸들러\r\n * Tab close handler\r\n */\r\nconst handleTabClose = (id: string) => {\r\n closeTab(id)\r\n}\r\n\r\n/**\r\n * 외부에서 호출 가능한 메서드 노출\r\n * Expose methods for external use\r\n */\r\ndefineExpose<JDynamicTabsMethods>({\r\n addTab,\r\n closeTab,\r\n activateTab,\r\n findTab,\r\n closeAllTabs,\r\n})\r\n\r\n/**\r\n * 안전한 tabs 배열\r\n * Safe tabs array\r\n */\r\nconst safeTabs = computed(() => {\r\n return Array.isArray(tabs.value) ? tabs.value : []\r\n})\r\n\r\n/**\r\n * 탭이 있는지 여부\r\n * Whether there are tabs\r\n */\r\nconst hasTabs = computed(() => safeTabs.value.length > 0)\r\n\r\n/**\r\n * 루트 클래스\r\n * Root classes\r\n */\r\nconst rootClasses = computed(() => {\r\n return cn('w-full h-full', props.class)\r\n})\r\n\r\n/**\r\n * 콘텐츠 클래스\r\n * Content classes\r\n */\r\nconst contentClasses = computed(() => {\r\n return props.contentClass || ''\r\n})\r\n</script>\r\n\r\n<template>\r\n <div :class=\"rootClasses\">\r\n <!-- 탭이 있을 경우 / When there are tabs -->\r\n <JTabs\r\n v-if=\"hasTabs\"\r\n :tabs=\"safeTabs\"\r\n :active-tab-id=\"activeTabId\"\r\n :styletype=\"props.styletype\"\r\n :class=\"contentClasses\"\r\n @tab-change=\"handleTabChange\"\r\n @tab-close=\"handleTabClose\"\r\n >\r\n <!-- 각 탭 콘텐츠 슬롯 전달 / Forward slots for each tab content -->\r\n <template\r\n v-for=\"tab in safeTabs\"\r\n :key=\"tab.id\"\r\n #[`content-${tab.id}`]=\"slotProps\"\r\n >\r\n <slot :name=\"`content-${tab.id}`\" v-bind=\"slotProps\" />\r\n </template>\r\n </JTabs>\r\n \r\n <!-- 탭이 없을 경우 / When there are no tabs -->\r\n <div\r\n v-else\r\n class=\"flex items-center justify-center h-full w-full\"\r\n >\r\n <div class=\"text-center\">\r\n <p class=\"text-muted-foreground text-lg\">{{ emptyMessage }}</p>\r\n <p class=\"text-muted-foreground/60 text-sm mt-2\">\r\n 메뉴를 클릭하여 탭을 추가하세요.\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n"],"names":["props","__props","emit","__emit","tabs","ref","activeTabId","watch","newTabs","addTab","tab","t","activateTab","closeTab","id","tabIndex","newIndex","findTab","closeAllTabs","handleTabChange","handleTabClose","__expose","safeTabs","computed","hasTabs","rootClasses","cn","contentClasses","_createElementBlock","_createBlock","JTabs","_renderList","_withCtx","slotProps","_renderSlot","_ctx","_openBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_toDisplayString","_cache"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JDynamicTabs.vue.js","sources":["../../../../src/components/organisms/JDynamicTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, computed, watch } from 'vue'\nimport JTabs from '@/components/molecules/JTabs.vue'\nimport { cn } from '@/lib/utils'\nimport type {\n JDynamicTabsProps, \n JDynamicTabsEmits, \n JDynamicTabsMethods,\n DynamicTab \n} from '@/types/dynamic-tabs.types'\n\n/**\n * JDynamicTabs - 동적 탭 컴포넌트 (organisms)\n * Dynamic Tabs Component\n * \n * @description\n * 사이드 메뉴 클릭으로 탭을 동적으로 추가/제거할 수 있는 탭 컴포넌트입니다.\n * \n * Features:\n * - 탭 동적 추가/제거\n * - 중복 탭 방지 (이미 있으면 활성화)\n * - 닫기 버튼으로 탭 제거\n * - 활성 탭 자동 재설정\n * - 최대 탭 개수 제한\n * \n * @example\n * const tabsRef = ref()\n * \n * const handleMenuClick = (menuItem) => {\n * tabsRef.value?.addTab({\n * id: menuItem.id,\n * label: menuItem.label,\n * component: menuItem.component,\n * closable: true\n * })\n * }\n */\n\nconst props = withDefaults(defineProps<JDynamicTabsProps>(), {\n initialTabs: () => [],\n maxTabs: 0, // 0 = unlimited\n emptyMessage: '탭을 추가해주세요.',\n styletype: 'default',\n})\n\nconst emit = defineEmits<JDynamicTabsEmits>()\n\n/**\n * 탭 목록 (내부 상태)\n * Tabs list (internal state)\n */\nconst tabs = ref<DynamicTab[]>(Array.isArray(props.initialTabs) ? [...props.initialTabs] : [])\n\n/**\n * 현재 활성화된 탭 ID (내부 상태)\n * Current active tab ID (internal state)\n */\nconst activeTabId = ref<string>(\n props.defaultActiveId || (Array.isArray(props.initialTabs) && props.initialTabs.length > 0 ? props.initialTabs[0]?.id : '') || ''\n)\n\n/**\n * initialTabs가 변경되면 내부 상태 업데이트\n * Update internal state when initialTabs changes\n */\nwatch(() => props.initialTabs, (newTabs) => {\n if (!newTabs) {\n tabs.value = []\n return\n }\n \n if (Array.isArray(newTabs) && newTabs.length > 0) {\n tabs.value = [...newTabs]\n if (!activeTabId.value && newTabs[0]) {\n activeTabId.value = newTabs[0].id\n }\n } else if (Array.isArray(newTabs)) {\n // 빈 배열인 경우\n tabs.value = []\n } else {\n // 배열이 아닌 경우\n tabs.value = []\n }\n}, { immediate: true })\n\n/**\n * 탭 추가\n * Add tab (if exists, activate it; otherwise, add to array and activate)\n * \n * @param tab - 추가할 탭 정보\n */\nconst addTab = (tab: DynamicTab) => {\n // tabs.value가 배열이 아니면 초기화\n if (!Array.isArray(tabs.value)) {\n tabs.value = []\n }\n \n // 이미 존재하는 탭인지 확인\n const existingTab = tabs.value.find(t => t.id === tab.id)\n \n if (existingTab) {\n // 이미 있으면 해당 탭 활성화\n activateTab(tab.id)\n return\n }\n \n // 최대 탭 개수 체크\n if (props.maxTabs > 0 && tabs.value.length >= props.maxTabs) {\n console.warn(`최대 ${props.maxTabs}개의 탭만 열 수 있습니다.`)\n return\n }\n \n // 새 탭 추가\n tabs.value.push({\n ...tab,\n closable: tab.closable !== false, // 기본값 true\n })\n \n // 새로 추가된 탭 활성화\n activateTab(tab.id)\n \n // 이벤트 발생\n emit('tabAdd', tab)\n}\n\n/**\n * 탭 닫기\n * Close tab (remove from array and reset active tab)\n * \n * @param id - 닫을 탭 ID\n */\nconst closeTab = (id: string) => {\n if (!Array.isArray(tabs.value)) {\n return\n }\n \n const tabIndex = tabs.value.findIndex(t => t.id === id)\n \n if (tabIndex === -1) return\n \n // 탭 제거\n tabs.value.splice(tabIndex, 1)\n \n // 활성 탭 재설정\n if (activeTabId.value === id) {\n if (tabs.value.length > 0) {\n // 이전 탭이 있으면 이전 탭 활성화, 없으면 다음 탭 활성화\n const newIndex = Math.min(tabIndex, tabs.value.length - 1)\n activeTabId.value = tabs.value[newIndex]?.id || ''\n } else {\n activeTabId.value = ''\n }\n }\n \n // 이벤트 발생\n emit('tabClose', id)\n}\n\n/**\n * 탭 활성화\n * Activate tab\n * \n * @param id - 활성화할 탭 ID\n */\nconst activateTab = (id: string) => {\n if (!Array.isArray(tabs.value)) {\n return\n }\n const tab = tabs.value.find(t => t.id === id)\n if (tab) {\n activeTabId.value = id\n }\n}\n\n/**\n * 특정 탭 찾기\n * Find specific tab\n * \n * @param id - 찾을 탭 ID\n */\nconst findTab = (id: string): DynamicTab | undefined => {\n if (!Array.isArray(tabs.value)) {\n return undefined\n }\n return tabs.value.find(t => t.id === id)\n}\n\n/**\n * 모든 탭 닫기 (closable이 true인 탭만)\n * Close all tabs (only closable tabs)\n */\nconst closeAllTabs = () => {\n if (!Array.isArray(tabs.value)) {\n return\n }\n const closableTabs = tabs.value.filter(t => t.closable)\n closableTabs.forEach(tab => closeTab(tab.id))\n}\n\n/**\n * 탭 변경 핸들러\n * Tab change handler\n */\nconst handleTabChange = (id: string) => {\n activeTabId.value = id\n emit('tabChange', id)\n}\n\n/**\n * 탭 닫기 핸들러\n * Tab close handler\n */\nconst handleTabClose = (id: string) => {\n closeTab(id)\n}\n\n/**\n * 외부에서 호출 가능한 메서드 노출\n * Expose methods for external use\n */\ndefineExpose<JDynamicTabsMethods>({\n addTab,\n closeTab,\n activateTab,\n findTab,\n closeAllTabs,\n})\n\n/**\n * 안전한 tabs 배열\n * Safe tabs array\n */\nconst safeTabs = computed(() => {\n return Array.isArray(tabs.value) ? tabs.value : []\n})\n\n/**\n * 탭이 있는지 여부\n * Whether there are tabs\n */\nconst hasTabs = computed(() => safeTabs.value.length > 0)\n\n/**\n * 루트 클래스\n * Root classes\n */\nconst rootClasses = computed(() => {\n return cn('w-full h-full', props.class)\n})\n\n/**\n * 콘텐츠 클래스\n * Content classes\n */\nconst contentClasses = computed(() => {\n return props.contentClass || ''\n})\n</script>\n\n<template>\n <div :class=\"rootClasses\">\n <!-- 탭이 있을 경우 / When there are tabs -->\n <JTabs\n v-if=\"hasTabs\"\n :tabs=\"safeTabs\"\n :active-tab-id=\"activeTabId\"\n :styletype=\"props.styletype\"\n :class=\"contentClasses\"\n @tab-change=\"handleTabChange\"\n @tab-close=\"handleTabClose\"\n >\n <!-- 각 탭 콘텐츠 슬롯 전달 / Forward slots for each tab content -->\n <template\n v-for=\"tab in safeTabs\"\n :key=\"tab.id\"\n #[`content-${tab.id}`]=\"slotProps\"\n >\n <slot :name=\"`content-${tab.id}`\" v-bind=\"slotProps\" />\n </template>\n </JTabs>\n \n <!-- 탭이 없을 경우 / When there are no tabs -->\n <div\n v-else\n class=\"flex items-center justify-center h-full w-full\"\n >\n <div class=\"text-center\">\n <p class=\"text-muted-foreground text-lg\">{{ emptyMessage }}</p>\n <p class=\"text-muted-foreground/60 text-sm mt-2\">\n 메뉴를 클릭하여 탭을 추가하세요.\n </p>\n </div>\n </div>\n </div>\n</template>\n"],"names":["props","__props","emit","__emit","tabs","ref","activeTabId","watch","newTabs","addTab","tab","t","activateTab","closeTab","id","tabIndex","newIndex","findTab","closeAllTabs","handleTabChange","handleTabClose","__expose","safeTabs","computed","hasTabs","rootClasses","cn","contentClasses","_createElementBlock","_createBlock","JTabs","_renderList","_withCtx","slotProps","_renderSlot","_ctx","_openBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_toDisplayString","_cache"],"mappings":"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"}
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
for (const [t_key, t_val] of t_opts)
|
|
4
4
|
t_merged[t_key] = t_val;
|
|
5
5
|
return t_merged;
|
|
6
|
-
};,r=t(e.default,[["__scopeId","data-v-
|
|
6
|
+
};,r=t(e.default,[["__scopeId","data-v-b054109f"]]);exports.default=r;
|
|
7
7
|
//# sourceMappingURL=JFilterBar.vue.cjs.map
|
|
@@ -6,8 +6,8 @@ const r = (r_comp, r_opts) => {
|
|
|
6
6
|
r_merged[r_key] = r_val;
|
|
7
7
|
return r_merged;
|
|
8
8
|
};
|
|
9
|
-
const
|
|
9
|
+
const m = /* @__PURE__ */ r(o, [["__scopeId", "data-v-b054109f"]]);
|
|
10
10
|
export {
|
|
11
|
-
|
|
11
|
+
m as default
|
|
12
12
|
};
|
|
13
13
|
//# sourceMappingURL=JFilterBar.vue.js.map
|
|
@@ -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"),B=require("../atoms/JBadge.vue.cjs"),f=require("../atoms/JButton.vue.cjs"),
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),d=require("lucide-vue-next"),B=require("../atoms/JBadge.vue.cjs"),f=require("../atoms/JButton.vue.cjs"),v=require("../atoms/JLabel.vue.cjs"),k=require("../../lib/utils.cjs"),x={class:"flex items-center justify-between px-3 py-1.5"},_={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={class:"filter-fields-grid"},D=e.defineComponent({__name:"JFilterBar",props:{class:{},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(s,{emit:p}){const o=s,n=p,c=e.computed(()=>o.collapsible?!o.collapsed:!0);function m(t){return!!(t==null||typeof t=="string"&&t.trim()===""||Array.isArray(t)&&t.length===0)}const i=e.computed(()=>{const t=[];for(const[l,r]of Object.entries(o.filterDisplay)){const a=o.filterValues[l];if(m(a))continue;const u=r.displayValue?r.displayValue(a):String(a);u.trim()!==""&&t.push({key:l,label:r.label,value:u})}return t});function y(){n("update:collapsed",!o.collapsed)}function h(){const t={};for(const l of Object.keys(o.filterValues)){const r=o.filterValues[l];typeof r=="string"?t[l]="":Array.isArray(r)?t[l]=[]:t[l]=null}n("update:filterValues",t),n("reset")}function V(){n("search")}function g(t){const l={...o.filterValues},r=l[t];typeof r=="string"?l[t]="":Array.isArray(r)?l[t]=[]:l[t]=null,n("update:filterValues",l)}return(t,l)=>(e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(e.unref(k.cn)("j-filter-bar w-full rounded-sm border bg-card text-card-foreground",o.class))},[e.createElementVNode("div",x,[e.createElementVNode("div",_,[s.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),s.title?(e.openBlock(),e.createBlock(v.default,{key:1,text:s.title,class:"text-sm font-semibold text-foreground"},null,8,["text"])):e.createCommentVNode("",!0),i.value.length>0?(e.openBlock(),e.createElementBlock("div",b,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(i.value,r=>(e.openBlock(),e.createBlock(B.default,{key:r.key,variant:"secondary",size:"sm",class:"flex items-center gap-1 cursor-default"},{default:e.withCtx(()=>[e.createElementVNode("span",w,e.toDisplayString(r.label)+":",1),e.createElementVNode("span",null,e.toDisplayString(r.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=>g(r.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",{},void 0,!0),s.showResetButton?(e.openBlock(),e.createBlock(f.default,{key:0,variant:"secondary",size:"sm",onClick:h},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(s.resetButtonText),1)]),_:1})):e.createCommentVNode("",!0),s.showSearchButton?(e.openBlock(),e.createBlock(f.default,{key:1,styletype:"primary",size:"sm",onClick:V},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(s.searchButtonText),1)]),_:1})):e.createCommentVNode("",!0)])]),e.withDirectives(e.createElementVNode("div",E,[e.createElementVNode("div",S,[e.renderSlot(t.$slots,"filters",{},void 0,!0)])],512),[[e.vShow,c.value]])],2))}});exports.default=D;
|
|
2
2
|
//# sourceMappingURL=JFilterBar.vue2.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JFilterBar.vue2.cjs","sources":["../../../../src/components/organisms/JFilterBar.vue"],"sourcesContent":["<template>\r\n <div :class=\"cn('j-filter-bar w-full rounded-sm border bg-card text-card-foreground', props.class)\">\r\n <!-- Row 1: toolbar -->\r\n <div class=\"flex items-center justify-between px-3 py-1.5\">\r\n <div class=\"flex items-center gap-2\">\r\n <button\r\n v-if=\"collapsible\"\r\n type=\"button\"\r\n class=\"flex items-center justify-center h-6 w-6 rounded hover:bg-accent hover:text-accent-foreground transition-colors\"\r\n @click=\"toggleCollapsed\"\r\n >\r\n <ChevronDown\r\n :class=\"[\r\n 'h-3.5 w-3.5 transition-transform',\r\n isExpanded ? 'rotate-0' : '-rotate-90',\r\n ]\"\r\n />\r\n </button>\r\n <!-- 타이틀 -->\r\n <JLabel\r\n v-if=\"title\"\r\n :text=\"title\"\r\n class=\"text-sm font-semibold text-foreground\"\r\n />\r\n <!-- 선택된 필터 뱃지 표시 -->\r\n <div v-if=\"activeFilters.length > 0\" class=\"flex items-center gap-1 flex-wrap\">\r\n <JBadge\r\n v-for=\"filter in activeFilters\"\r\n :key=\"filter.key\"\r\n variant=\"secondary\"\r\n size=\"sm\"\r\n class=\"flex items-center gap-1 cursor-default\"\r\n >\r\n <span class=\"text-muted-foreground\">{{ filter.label }}:</span>\r\n <span>{{ filter.value }}</span>\r\n <button\r\n type=\"button\"\r\n class=\"ml-0.5 rounded-full hover:bg-gray-300 p-0.5 transition-colors\"\r\n @click.stop=\"removeFilter(filter.key)\"\r\n >\r\n <X class=\"h-3 w-3\" />\r\n </button>\r\n </JBadge>\r\n </div>\r\n </div>\r\n <div class=\"flex items-center gap-2\">\r\n <slot name=\"actions\" />\r\n <JButton\r\n v-if=\"showResetButton\"\r\n variant=\"secondary\"\r\n size=\"sm\"\r\n @click=\"handleReset\"\r\n >\r\n {{ resetButtonText }}\r\n </JButton>\r\n <JButton\r\n v-if=\"showSearchButton\"\r\n styletype=\"primary\"\r\n size=\"sm\"\r\n @click=\"handleSearch\"\r\n >\r\n {{ searchButtonText }}\r\n </JButton>\r\n </div>\r\n </div>\r\n\r\n <!-- Row 2: filters -->\r\n <div v-show=\"isExpanded\" class=\"px-3 pb-3\">\r\n <slot name=\"filters\" />\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from 'vue'\r\nimport { ChevronDown, X } from 'lucide-vue-next'\r\nimport JBadge from '@/components/atoms/JBadge.vue'\r\nimport JButton from '@/components/atoms/JButton.vue'\r\nimport JLabel from '@/components/atoms/JLabel.vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\n/** 활성 필터 아이템 타입 */\r\nexport interface ActiveFilterItem {\r\n /** 필터 식별 키 */\r\n key: string\r\n /** 표시할 라벨 (필터명) */\r\n label: string\r\n /** 표시할 값 */\r\n value: string\r\n}\r\n\r\n/** 필터 설정 타입 */\r\nexport interface FilterDisplayItem {\r\n /** 표시할 라벨 */\r\n label: string\r\n /** 값을 표시용 문자열로 변환 (예: combo value -> label) */\r\n displayValue?: (value: unknown) => string\r\n}\r\n\r\nexport interface JFilterBarProps {\r\n /** 추가 클래스 (외부 커스터마이징용) */\r\n class?: string\r\n /** 필터바 타이틀 */\r\n title?: string\r\n /** 필터 접힘 상태 (v-model 지원) */\r\n collapsed?: boolean\r\n /** 접기/펼치기 가능 여부. false면 토글 버튼 숨김 & 필터 항상 표시 */\r\n collapsible?: boolean\r\n /** 필터 값 객체 (v-model:filterValues 지원) */\r\n filterValues?: Record<string, unknown>\r\n /** 필터 표시 설정 (label, displayValue 등) */\r\n filterDisplay?: Record<string, FilterDisplayItem>\r\n /** 초기화 버튼 표시 여부 */\r\n showResetButton?: boolean\r\n /** 조회 버튼 표시 여부 */\r\n showSearchButton?: boolean\r\n /** 초기화 버튼 텍스트 */\r\n resetButtonText?: string\r\n /** 조회 버튼 텍스트 */\r\n searchButtonText?: string\r\n}\r\n\r\nconst props = withDefaults(defineProps<JFilterBarProps>(), {\r\n collapsed: true,\r\n collapsible: true,\r\n filterValues: () => ({}),\r\n filterDisplay: () => ({}),\r\n showResetButton: false,\r\n showSearchButton: false,\r\n resetButtonText: '초기화',\r\n searchButtonText: '조회',\r\n})\r\n\r\nconst emit = defineEmits<{\r\n 'update:collapsed': [value: boolean]\r\n 'update:filterValues': [value: Record<string, unknown>]\r\n /** 조회 버튼 클릭 */\r\n search: []\r\n /** 초기화 버튼 클릭 */\r\n reset: []\r\n}>()\r\n\r\nconst isExpanded = computed(() => {\r\n if (!props.collapsible) return true\r\n return !props.collapsed\r\n})\r\n\r\n/** 값이 비어있는지 확인 */\r\nfunction isEmpty(value: unknown): boolean {\r\n if (value === null || value === undefined) return true\r\n if (typeof value === 'string' && value.trim() === '') return true\r\n if (Array.isArray(value) && value.length === 0) return true\r\n return false\r\n}\r\n\r\n/** filterValues + filterDisplay 기반으로 활성 필터 목록 자동 생성 */\r\nconst activeFilters = computed<ActiveFilterItem[]>(() => {\r\n const filters: ActiveFilterItem[] = []\r\n\r\n for (const [key, config] of Object.entries(props.filterDisplay)) {\r\n const value = props.filterValues[key]\r\n if (isEmpty(value)) continue\r\n\r\n const displayValue = config.displayValue ? config.displayValue(value) : String(value)\r\n if (displayValue.trim() === '') continue\r\n\r\n filters.push({\r\n key,\r\n label: config.label,\r\n value: displayValue,\r\n })\r\n }\r\n\r\n return filters\r\n})\r\n\r\nfunction toggleCollapsed() {\r\n emit('update:collapsed', !props.collapsed)\r\n}\r\n\r\nfunction handleReset() {\r\n // filterValues의 모든 값을 초기화\r\n const resetValues: Record<string, unknown> = {}\r\n for (const key of Object.keys(props.filterValues)) {\r\n const currentValue = props.filterValues[key]\r\n if (typeof currentValue === 'string') {\r\n resetValues[key] = ''\r\n } else if (Array.isArray(currentValue)) {\r\n resetValues[key] = []\r\n } else {\r\n resetValues[key] = null\r\n }\r\n }\r\n emit('update:filterValues', resetValues)\r\n emit('reset')\r\n}\r\n\r\nfunction handleSearch() {\r\n emit('search')\r\n}\r\n\r\nfunction removeFilter(key: string) {\r\n // filterValues 업데이트 (해당 키 값을 초기화)\r\n const newValues = { ...props.filterValues }\r\n const currentValue = newValues[key]\r\n\r\n // 타입에 따라 적절한 초기값으로 설정\r\n if (typeof currentValue === 'string') {\r\n newValues[key] = ''\r\n } else if (Array.isArray(currentValue)) {\r\n newValues[key] = []\r\n } else {\r\n newValues[key] = null\r\n }\r\n\r\n emit('update:filterValues', newValues)\r\n}\r\n</script>\r\n\r\n<style scoped>\r\n/* ========================================\r\n 패턴 3: Tabs 아래 배치 시 연결 스타일\r\n ======================================== */\r\n\r\n:deep([data-state=\"active\"]) > .j-filter-bar {\r\n border-top: none;\r\n border-top-left-radius: 0;\r\n border-top-right-radius: 0;\r\n}\r\n\r\n:deep([role=\"tabpanel\"]) .j-filter-bar {\r\n border-top: none;\r\n}\r\n</style>\r\n"],"names":["props","__props","emit","__emit","isExpanded","computed","isEmpty","value","activeFilters","filters","key","config","displayValue","toggleCollapsed","handleReset","resetValues","currentValue","handleSearch","removeFilter","newValues","_createElementBlock","_normalizeClass","_unref","cn","_createElementVNode","_hoisted_1","_hoisted_2","_createVNode","ChevronDown","_createBlock","JLabel","_openBlock","_hoisted_3","_Fragment","_renderList","filter","JBadge","_hoisted_4","_toDisplayString","_withModifiers","$event","X","_hoisted_6","_renderSlot","_ctx","JButton","_withDirectives","_hoisted_7"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JFilterBar.vue2.cjs","sources":["../../../../src/components/organisms/JFilterBar.vue"],"sourcesContent":["<template>\n <div :class=\"cn('j-filter-bar w-full rounded-sm border bg-card text-card-foreground', props.class)\">\n <!-- Row 1: toolbar -->\n <div class=\"flex items-center justify-between px-3 py-1.5\">\n <div class=\"flex items-center gap-2\">\n <button\n v-if=\"collapsible\"\n type=\"button\"\n class=\"flex items-center justify-center h-6 w-6 rounded hover:bg-accent hover:text-accent-foreground transition-colors\"\n @click=\"toggleCollapsed\"\n >\n <ChevronDown\n :class=\"[\n 'h-3.5 w-3.5 transition-transform',\n isExpanded ? 'rotate-0' : '-rotate-90',\n ]\"\n />\n </button>\n <!-- 타이틀 -->\n <JLabel\n v-if=\"title\"\n :text=\"title\"\n class=\"text-sm font-semibold text-foreground\"\n />\n <!-- 선택된 필터 뱃지 표시 -->\n <div v-if=\"activeFilters.length > 0\" class=\"flex items-center gap-1 flex-wrap\">\n <JBadge\n v-for=\"filter in activeFilters\"\n :key=\"filter.key\"\n variant=\"secondary\"\n size=\"sm\"\n class=\"flex items-center gap-1 cursor-default\"\n >\n <span class=\"text-muted-foreground\">{{ filter.label }}:</span>\n <span>{{ filter.value }}</span>\n <button\n type=\"button\"\n class=\"ml-0.5 rounded-full hover:bg-gray-300 p-0.5 transition-colors\"\n @click.stop=\"removeFilter(filter.key)\"\n >\n <X class=\"h-3 w-3\" />\n </button>\n </JBadge>\n </div>\n </div>\n <div class=\"flex items-center gap-2\">\n <slot name=\"actions\" />\n <JButton\n v-if=\"showResetButton\"\n variant=\"secondary\"\n size=\"sm\"\n @click=\"handleReset\"\n >\n {{ resetButtonText }}\n </JButton>\n <JButton\n v-if=\"showSearchButton\"\n styletype=\"primary\"\n size=\"sm\"\n @click=\"handleSearch\"\n >\n {{ searchButtonText }}\n </JButton>\n </div>\n </div>\n\n <!-- Row 2: filters (반응형 그리드: max 4열, 자동 축소) -->\n <div v-show=\"isExpanded\" class=\"px-3 pb-3\">\n <div class=\"filter-fields-grid\">\n <slot name=\"filters\" />\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { ChevronDown, X } from 'lucide-vue-next'\nimport JBadge from '@/components/atoms/JBadge.vue'\nimport JButton from '@/components/atoms/JButton.vue'\nimport JLabel from '@/components/atoms/JLabel.vue'\nimport { cn } from '@/lib/utils'\n\n/** 활성 필터 아이템 타입 */\nexport interface ActiveFilterItem {\n /** 필터 식별 키 */\n key: string\n /** 표시할 라벨 (필터명) */\n label: string\n /** 표시할 값 */\n value: string\n}\n\n/** 필터 설정 타입 */\nexport interface FilterDisplayItem {\n /** 표시할 라벨 */\n label: string\n /** 값을 표시용 문자열로 변환 (예: combo value -> label) */\n displayValue?: (value: unknown) => string\n}\n\nexport interface JFilterBarProps {\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n /** 필터바 타이틀 */\n title?: string\n /** 필터 접힘 상태 (v-model 지원) */\n collapsed?: boolean\n /** 접기/펼치기 가능 여부. false면 토글 버튼 숨김 & 필터 항상 표시 */\n collapsible?: boolean\n /** 필터 값 객체 (v-model:filterValues 지원) */\n filterValues?: Record<string, unknown>\n /** 필터 표시 설정 (label, displayValue 등) */\n filterDisplay?: Record<string, FilterDisplayItem>\n /** 초기화 버튼 표시 여부 */\n showResetButton?: boolean\n /** 조회 버튼 표시 여부 */\n showSearchButton?: boolean\n /** 초기화 버튼 텍스트 */\n resetButtonText?: string\n /** 조회 버튼 텍스트 */\n searchButtonText?: string\n}\n\nconst props = withDefaults(defineProps<JFilterBarProps>(), {\n collapsed: true,\n collapsible: true,\n filterValues: () => ({}),\n filterDisplay: () => ({}),\n showResetButton: false,\n showSearchButton: false,\n resetButtonText: '초기화',\n searchButtonText: '조회',\n})\n\nconst emit = defineEmits<{\n 'update:collapsed': [value: boolean]\n 'update:filterValues': [value: Record<string, unknown>]\n /** 조회 버튼 클릭 */\n search: []\n /** 초기화 버튼 클릭 */\n reset: []\n}>()\n\nconst isExpanded = computed(() => {\n if (!props.collapsible) return true\n return !props.collapsed\n})\n\n/** 값이 비어있는지 확인 */\nfunction isEmpty(value: unknown): boolean {\n if (value === null || value === undefined) return true\n if (typeof value === 'string' && value.trim() === '') return true\n if (Array.isArray(value) && value.length === 0) return true\n return false\n}\n\n/** filterValues + filterDisplay 기반으로 활성 필터 목록 자동 생성 */\nconst activeFilters = computed<ActiveFilterItem[]>(() => {\n const filters: ActiveFilterItem[] = []\n\n for (const [key, config] of Object.entries(props.filterDisplay)) {\n const value = props.filterValues[key]\n if (isEmpty(value)) continue\n\n const displayValue = config.displayValue ? config.displayValue(value) : String(value)\n if (displayValue.trim() === '') continue\n\n filters.push({\n key,\n label: config.label,\n value: displayValue,\n })\n }\n\n return filters\n})\n\nfunction toggleCollapsed() {\n emit('update:collapsed', !props.collapsed)\n}\n\nfunction handleReset() {\n // filterValues의 모든 값을 초기화\n const resetValues: Record<string, unknown> = {}\n for (const key of Object.keys(props.filterValues)) {\n const currentValue = props.filterValues[key]\n if (typeof currentValue === 'string') {\n resetValues[key] = ''\n } else if (Array.isArray(currentValue)) {\n resetValues[key] = []\n } else {\n resetValues[key] = null\n }\n }\n emit('update:filterValues', resetValues)\n emit('reset')\n}\n\nfunction handleSearch() {\n emit('search')\n}\n\nfunction removeFilter(key: string) {\n // filterValues 업데이트 (해당 키 값을 초기화)\n const newValues = { ...props.filterValues }\n const currentValue = newValues[key]\n\n // 타입에 따라 적절한 초기값으로 설정\n if (typeof currentValue === 'string') {\n newValues[key] = ''\n } else if (Array.isArray(currentValue)) {\n newValues[key] = []\n } else {\n newValues[key] = null\n }\n\n emit('update:filterValues', newValues)\n}\n</script>\n\n<style scoped>\n/* 필터 필드 반응형 그리드: max 4열, 자동 축소 (4 → 3 → 2 → 1) */\n.filter-fields-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(max(25% - 0.75rem, 220px), 1fr));\n gap: 0.5rem 0.75rem;\n}\n\n/* ========================================\n 패턴 3: Tabs 아래 배치 시 연결 스타일\n ======================================== */\n\n:deep([data-state=\"active\"]) > .j-filter-bar {\n border-top: none;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n\n:deep([role=\"tabpanel\"]) .j-filter-bar {\n border-top: none;\n}\n</style>\n"],"names":["props","__props","emit","__emit","isExpanded","computed","isEmpty","value","activeFilters","filters","key","config","displayValue","toggleCollapsed","handleReset","resetValues","currentValue","handleSearch","removeFilter","newValues","_createElementBlock","_normalizeClass","_unref","cn","_createElementVNode","_hoisted_1","_hoisted_2","_createVNode","ChevronDown","_createBlock","JLabel","_openBlock","_hoisted_3","_Fragment","_renderList","filter","JBadge","_hoisted_4","_toDisplayString","_withModifiers","$event","X","_hoisted_6","_renderSlot","_ctx","JButton","_withDirectives","_hoisted_7","_hoisted_8"],"mappings":"mgCA4HA,MAAMA,EAAQC,EAWRC,EAAOC,EASPC,EAAaC,EAAAA,SAAS,IACrBL,EAAM,YACJ,CAACA,EAAM,UADiB,EAEhC,EAGD,SAASM,EAAQC,EAAyB,CAGxC,MAFI,GAAAA,GAAU,MACV,OAAOA,GAAU,UAAYA,EAAM,KAAA,IAAW,IAC9C,MAAM,QAAQA,CAAK,GAAKA,EAAM,SAAW,EAE/C,CAGA,MAAMC,EAAgBH,EAAAA,SAA6B,IAAM,CACvD,MAAMI,EAA8B,CAAA,EAEpC,SAAW,CAACC,EAAKC,CAAM,IAAK,OAAO,QAAQX,EAAM,aAAa,EAAG,CAC/D,MAAMO,EAAQP,EAAM,aAAaU,CAAG,EACpC,GAAIJ,EAAQC,CAAK,EAAG,SAEpB,MAAMK,EAAeD,EAAO,aAAeA,EAAO,aAAaJ,CAAK,EAAI,OAAOA,CAAK,EAChFK,EAAa,KAAA,IAAW,IAE5BH,EAAQ,KAAK,CACX,IAAAC,EACA,MAAOC,EAAO,MACd,MAAOC,CAAA,CACR,CACH,CAEA,OAAOH,CACT,CAAC,EAED,SAASI,GAAkB,CACzBX,EAAK,mBAAoB,CAACF,EAAM,SAAS,CAC3C,CAEA,SAASc,GAAc,CAErB,MAAMC,EAAuC,CAAA,EAC7C,UAAWL,KAAO,OAAO,KAAKV,EAAM,YAAY,EAAG,CACjD,MAAMgB,EAAehB,EAAM,aAAaU,CAAG,EACvC,OAAOM,GAAiB,SAC1BD,EAAYL,CAAG,EAAI,GACV,MAAM,QAAQM,CAAY,EACnCD,EAAYL,CAAG,EAAI,CAAA,EAEnBK,EAAYL,CAAG,EAAI,IAEvB,CACAR,EAAK,sBAAuBa,CAAW,EACvCb,EAAK,OAAO,CACd,CAEA,SAASe,GAAe,CACtBf,EAAK,QAAQ,CACf,CAEA,SAASgB,EAAaR,EAAa,CAEjC,MAAMS,EAAY,CAAE,GAAGnB,EAAM,YAAA,EACvBgB,EAAeG,EAAUT,CAAG,EAG9B,OAAOM,GAAiB,SAC1BG,EAAUT,CAAG,EAAI,GACR,MAAM,QAAQM,CAAY,EACnCG,EAAUT,CAAG,EAAI,CAAA,EAEjBS,EAAUT,CAAG,EAAI,KAGnBR,EAAK,sBAAuBiB,CAAS,CACvC,6BAzNEC,EAAAA,mBAuEM,MAAA,CAvEA,MAAKC,EAAAA,eAAEC,QAAAC,EAAAA,EAAA,EAAE,qEAAuEvB,EAAM,KAAK,CAAA,CAAA,GAE/FwB,EAAAA,mBA6DM,MA7DNC,EA6DM,CA5DJD,EAAAA,mBAwCM,MAxCNE,EAwCM,CAtCIzB,EAAA,2BADRmB,EAAAA,mBAYS,SAAA,OAVP,KAAK,SACL,MAAM,kHACL,QAAOP,CAAA,GAERc,cAKEL,EAAAA,MAAAM,EAAAA,WAAA,EAAA,CAJC,MAAKP,EAAAA,eAAA,oCAAoEjB,EAAA,MAAU,WAAA,YAAA,qDAQhFH,EAAA,qBADR4B,EAAAA,YAIEC,EAAAA,QAAA,OAFC,KAAM7B,EAAA,MACP,MAAM,uCAAA,gDAGGO,EAAA,MAAc,OAAM,GAA/BuB,EAAAA,YAAAX,EAAAA,mBAkBM,MAlBNY,EAkBM,kBAjBJZ,EAAAA,mBAgBSa,EAAAA,SAAA,KAAAC,EAAAA,WAfU1B,EAAA,MAAV2B,kBADTN,EAAAA,YAgBSO,UAAA,CAdN,IAAKD,EAAO,IACb,QAAQ,YACR,KAAK,KACL,MAAM,wCAAA,qBAEN,IAA8D,CAA9DX,qBAA8D,OAA9Da,EAA8DC,EAAAA,gBAAvBH,EAAO,KAAK,EAAG,IAAC,CAAA,EACvDX,EAAAA,mBAA+B,OAAA,KAAAc,EAAAA,gBAAtBH,EAAO,KAAK,EAAA,CAAA,EACrBX,EAAAA,mBAMS,SAAA,CALP,KAAK,SACL,MAAM,gEACL,QAAKe,EAAAA,cAAAC,GAAOtB,EAAaiB,EAAO,GAAG,EAAA,CAAA,MAAA,CAAA,CAAA,GAEpCR,EAAAA,YAAqBL,EAAAA,MAAAmB,EAAAA,CAAA,EAAA,CAAlB,MAAM,UAAS,CAAA,6DAK1BjB,EAAAA,mBAkBM,MAlBNkB,EAkBM,CAjBJC,EAAAA,WAAuBC,EAAA,OAAA,UAAA,CAAA,EAAA,OAAA,EAAA,EAEf3C,EAAA,+BADR4B,EAAAA,YAOUgB,EAAAA,QAAA,OALR,QAAQ,YACR,KAAK,KACJ,QAAO/B,CAAA,qBAER,IAAqB,qCAAlBb,EAAA,eAAe,EAAA,CAAA,CAAA,sCAGZA,EAAA,gCADR4B,EAAAA,YAOUgB,EAAAA,QAAA,OALR,UAAU,UACV,KAAK,KACJ,QAAO5B,CAAA,qBAER,IAAsB,qCAAnBhB,EAAA,gBAAgB,EAAA,CAAA,CAAA,0CAMzB6C,iBAAAtB,EAAAA,mBAIM,MAJNuB,EAIM,CAHJvB,EAAAA,mBAEM,MAFNwB,EAEM,CADJL,EAAAA,WAAuBC,EAAA,OAAA,UAAA,CAAA,EAAA,OAAA,EAAA,CAAA,mBAFdxC,EAAA,KAAU,CAAA"}
|