@j-solution/components 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/assets/jwms-portal-frontend-CwxPfHfa.css +1 -0
- package/assets/styles/j-components.css +1 -1
- package/assets/styles/themes.css +107 -0
- package/components/atoms/JAvatar.vue.cjs +1 -1
- package/components/atoms/JAvatar.vue.cjs.map +1 -1
- package/components/atoms/JAvatar.vue.js +10 -7
- package/components/atoms/JAvatar.vue.js.map +1 -1
- package/components/atoms/JBadge.vue.cjs +1 -1
- package/components/atoms/JBadge.vue.cjs.map +1 -1
- package/components/atoms/JBadge.vue.js +7 -6
- package/components/atoms/JBadge.vue.js.map +1 -1
- package/components/atoms/JButton.vue.cjs +1 -1
- package/components/atoms/JButton.vue.cjs.map +1 -1
- package/components/atoms/JButton.vue.js +5 -5
- package/components/atoms/JButton.vue.js.map +1 -1
- package/components/atoms/JDatepicker.vue.cjs +1 -1
- package/components/atoms/JDatepicker.vue.cjs.map +1 -1
- package/components/atoms/JDatepicker.vue.js +10 -10
- package/components/atoms/JDatepicker.vue.js.map +1 -1
- package/components/atoms/JEditor.vue.cjs +1 -1
- package/components/atoms/JEditor.vue.js +1 -1
- package/components/atoms/JEditor.vue2.cjs +1 -1
- package/components/atoms/JEditor.vue2.cjs.map +1 -1
- package/components/atoms/JEditor.vue2.js +31 -17
- package/components/atoms/JEditor.vue2.js.map +1 -1
- package/components/atoms/JGrid.vue.cjs +1 -1
- package/components/atoms/JGrid.vue.js +2 -2
- package/components/atoms/JGrid.vue2.cjs +1 -1
- package/components/atoms/JGrid.vue2.cjs.map +1 -1
- package/components/atoms/JGrid.vue2.js +45 -33
- package/components/atoms/JGrid.vue2.js.map +1 -1
- package/components/atoms/JIcon.vue.cjs +1 -1
- package/components/atoms/JIcon.vue.cjs.map +1 -1
- package/components/atoms/JIcon.vue.js +14 -13
- package/components/atoms/JIcon.vue.js.map +1 -1
- package/components/atoms/JKbd.vue.cjs +1 -1
- package/components/atoms/JKbd.vue.cjs.map +1 -1
- package/components/atoms/JKbd.vue.js +13 -10
- package/components/atoms/JKbd.vue.js.map +1 -1
- package/components/atoms/JLabel.vue.cjs +1 -1
- package/components/atoms/JLabel.vue.cjs.map +1 -1
- package/components/atoms/JLabel.vue.js +4 -4
- package/components/atoms/JLabel.vue.js.map +1 -1
- package/components/atoms/JLink.vue.cjs +1 -1
- package/components/atoms/JLink.vue.cjs.map +1 -1
- package/components/atoms/JLink.vue.js +5 -5
- package/components/atoms/JLink.vue.js.map +1 -1
- package/components/atoms/JPreview.vue.cjs +1 -1
- package/components/atoms/JPreview.vue.js +2 -2
- package/components/atoms/JPreview.vue2.cjs +1 -1
- package/components/atoms/JPreview.vue2.cjs.map +1 -1
- package/components/atoms/JPreview.vue2.js +33 -20
- package/components/atoms/JPreview.vue2.js.map +1 -1
- package/components/atoms/JProgress.vue.cjs +1 -1
- package/components/atoms/JProgress.vue.cjs.map +1 -1
- package/components/atoms/JProgress.vue.js +15 -9
- package/components/atoms/JProgress.vue.js.map +1 -1
- package/components/atoms/JRadio.vue.cjs +1 -1
- package/components/atoms/JRadio.vue.cjs.map +1 -1
- package/components/atoms/JRadio.vue.js +1 -1
- package/components/atoms/JRadio.vue.js.map +1 -1
- package/components/atoms/JSearchCombo.vue.cjs +1 -1
- package/components/atoms/JSearchCombo.vue.cjs.map +1 -1
- package/components/atoms/JSearchCombo.vue.js +38 -37
- package/components/atoms/JSearchCombo.vue.js.map +1 -1
- package/components/atoms/JSpinner.vue.cjs +1 -1
- package/components/atoms/JSpinner.vue.cjs.map +1 -1
- package/components/atoms/JSpinner.vue.js +8 -7
- package/components/atoms/JSpinner.vue.js.map +1 -1
- package/components/atoms/JSplitter.vue.cjs +1 -1
- package/components/atoms/JSplitter.vue.cjs.map +1 -1
- package/components/atoms/JSplitter.vue.js +32 -27
- package/components/atoms/JSplitter.vue.js.map +1 -1
- package/components/atoms/JTooltip.vue.cjs +1 -1
- package/components/atoms/JTooltip.vue.cjs.map +1 -1
- package/components/atoms/JTooltip.vue.js +18 -15
- package/components/atoms/JTooltip.vue.js.map +1 -1
- package/components/examples/ExampleCrudPage.vue.cjs +2 -0
- package/components/examples/ExampleCrudPage.vue.cjs.map +1 -0
- package/components/examples/ExampleCrudPage.vue.js +358 -0
- package/components/examples/ExampleCrudPage.vue.js.map +1 -0
- package/components/examples/ExampleCrudPage.vue2.cjs +2 -0
- package/components/examples/ExampleCrudPage.vue2.cjs.map +1 -0
- package/components/examples/ExampleCrudPage.vue2.js +5 -0
- package/components/examples/ExampleCrudPage.vue2.js.map +1 -0
- package/components/examples/ExampleTabMappingPage.vue.cjs +2 -0
- package/components/examples/ExampleTabMappingPage.vue.cjs.map +1 -0
- package/components/examples/ExampleTabMappingPage.vue.js +522 -0
- package/components/examples/ExampleTabMappingPage.vue.js.map +1 -0
- package/components/examples/ExampleTabMappingPage.vue2.cjs +2 -0
- package/components/examples/ExampleTabMappingPage.vue2.cjs.map +1 -0
- package/components/examples/ExampleTabMappingPage.vue2.js +5 -0
- package/components/examples/ExampleTabMappingPage.vue2.js.map +1 -0
- package/components/molecules/JBreadcrumb.vue.cjs +1 -1
- package/components/molecules/JBreadcrumb.vue.cjs.map +1 -1
- package/components/molecules/JBreadcrumb.vue.js +3 -3
- package/components/molecules/JBreadcrumb.vue.js.map +1 -1
- package/components/molecules/JFormField.vue.cjs +1 -1
- package/components/molecules/JFormField.vue.cjs.map +1 -1
- package/components/molecules/JFormField.vue.js +26 -24
- package/components/molecules/JFormField.vue.js.map +1 -1
- package/components/molecules/JTabs.vue.cjs +1 -1
- package/components/molecules/JTabs.vue.js +1 -1
- package/components/molecules/JTabs.vue2.cjs +1 -1
- package/components/molecules/JTabs.vue2.cjs.map +1 -1
- package/components/molecules/JTabs.vue2.js +7 -7
- package/components/molecules/JTabs.vue2.js.map +1 -1
- package/components/molecules/JTitlebar.vue.cjs +1 -1
- package/components/molecules/JTitlebar.vue.cjs.map +1 -1
- package/components/molecules/JTitlebar.vue.js +35 -36
- package/components/molecules/JTitlebar.vue.js.map +1 -1
- package/components/organisms/JFilterBar.vue.cjs +1 -1
- package/components/organisms/JFilterBar.vue.cjs.map +1 -1
- package/components/organisms/JFilterBar.vue.js +5 -5
- package/components/organisms/JFilterBar.vue.js.map +1 -1
- package/components/organisms/JHeader.vue.cjs +1 -1
- package/components/organisms/JHeader.vue.cjs.map +1 -1
- package/components/organisms/JHeader.vue.js +25 -23
- package/components/organisms/JHeader.vue.js.map +1 -1
- package/components/organisms/JModal.vue.cjs +1 -1
- package/components/organisms/JModal.vue.cjs.map +1 -1
- package/components/organisms/JModal.vue.js +30 -27
- package/components/organisms/JModal.vue.js.map +1 -1
- package/components/organisms/JSidebarAdvanced.vue.cjs +1 -1
- package/components/organisms/JSidebarAdvanced.vue.js +7 -7
- package/components/organisms/JSidebarAdvanced.vue2.cjs +1 -1
- package/components/organisms/JSidebarAdvanced.vue2.cjs.map +1 -1
- package/components/organisms/JSidebarAdvanced.vue2.js +40 -40
- package/components/organisms/JSidebarAdvanced.vue2.js.map +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.cjs +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.cjs.map +1 -1
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.js +83 -63
- package/components/organisms/JSidebarSimple/JDynamicMenuItem.vue.js.map +1 -1
- package/components/organisms/JSidebarSimple.vue.cjs +1 -1
- package/components/organisms/JSidebarSimple.vue.js +2 -2
- package/components/organisms/JSidebarSimple.vue2.cjs +1 -1
- package/components/organisms/JSidebarSimple.vue2.cjs.map +1 -1
- package/components/organisms/JSidebarSimple.vue2.js +2 -2
- package/components/organisms/JSidebarSimple.vue2.js.map +1 -1
- package/components/shadcn/AccordionTrigger.vue.cjs +1 -1
- package/components/shadcn/AccordionTrigger.vue.cjs.map +1 -1
- package/components/shadcn/AccordionTrigger.vue.js +3 -3
- package/components/shadcn/AccordionTrigger.vue.js.map +1 -1
- package/components/shadcn/CardContent.vue.cjs +1 -1
- package/components/shadcn/CardContent.vue.cjs.map +1 -1
- package/components/shadcn/CardContent.vue.js +1 -1
- package/components/shadcn/CardContent.vue.js.map +1 -1
- package/components/shadcn/CardDescription.vue.cjs +1 -1
- package/components/shadcn/CardDescription.vue.cjs.map +1 -1
- package/components/shadcn/CardDescription.vue.js +1 -1
- package/components/shadcn/CardDescription.vue.js.map +1 -1
- package/components/shadcn/CardFooter.vue.cjs +1 -1
- package/components/shadcn/CardFooter.vue.cjs.map +1 -1
- package/components/shadcn/CardFooter.vue.js +7 -7
- package/components/shadcn/CardFooter.vue.js.map +1 -1
- package/components/shadcn/CardHeader.vue.cjs +1 -1
- package/components/shadcn/CardHeader.vue.cjs.map +1 -1
- package/components/shadcn/CardHeader.vue.js +8 -8
- package/components/shadcn/CardHeader.vue.js.map +1 -1
- package/components/shadcn/CardTitle.vue.cjs +1 -1
- package/components/shadcn/CardTitle.vue.cjs.map +1 -1
- package/components/shadcn/CardTitle.vue.js +5 -5
- package/components/shadcn/CardTitle.vue.js.map +1 -1
- package/components/shadcn/Input.vue.cjs +1 -1
- package/components/shadcn/Input.vue.cjs.map +1 -1
- package/components/shadcn/Input.vue.js +1 -1
- package/components/shadcn/Input.vue.js.map +1 -1
- package/components/shadcn/SelectTrigger.vue.cjs +1 -1
- package/components/shadcn/SelectTrigger.vue.cjs.map +1 -1
- package/components/shadcn/SelectTrigger.vue.js +2 -2
- package/components/shadcn/SelectTrigger.vue.js.map +1 -1
- package/components/shadcn/Switch.vue.cjs +1 -1
- package/components/shadcn/Switch.vue.cjs.map +1 -1
- package/components/shadcn/Switch.vue.js +2 -2
- package/components/shadcn/Switch.vue.js.map +1 -1
- package/components/shadcn/TabsList.vue.cjs +1 -1
- package/components/shadcn/TabsList.vue.cjs.map +1 -1
- package/components/shadcn/TabsList.vue.js +1 -1
- package/components/shadcn/TabsList.vue.js.map +1 -1
- package/components/shadcn/TabsTrigger.vue.cjs +1 -1
- package/components/shadcn/TabsTrigger.vue.cjs.map +1 -1
- package/components/shadcn/TabsTrigger.vue.js +4 -4
- package/components/shadcn/TabsTrigger.vue.js.map +1 -1
- package/components/shadcn/Textarea.vue.cjs +1 -1
- package/components/shadcn/Textarea.vue.cjs.map +1 -1
- package/components/shadcn/Textarea.vue.js +2 -2
- package/components/shadcn/Textarea.vue.js.map +1 -1
- package/components/shadcn/index.cjs +1 -1
- package/components/shadcn/index.cjs.map +1 -1
- package/components/shadcn/index.js +8 -7
- package/components/shadcn/index.js.map +1 -1
- package/index.cjs +1 -1
- package/index.js +76 -72
- package/package.json +1 -1
- package/types/index.d.ts +742 -15
- package/assets/jwms-portal-frontend-DntSIcYt.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JFilterBar.vue.cjs","sources":["../../../../src/components/organisms/JFilterBar.vue"],"sourcesContent":["<template>\r\n <div class=\"w-full rounded-lg border bg-card text-card-foreground\">\r\n <!-- Row 1: toolbar -->\r\n <div class=\"flex items-center justify-between px-
|
|
1
|
+
{"version":3,"file":"JFilterBar.vue.cjs","sources":["../../../../src/components/organisms/JFilterBar.vue"],"sourcesContent":["<template>\r\n <div class=\"w-full rounded-lg border bg-card text-card-foreground\">\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\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 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"],"names":["props","__props","emit","__emit","isExpanded","computed","isEmpty","value","activeFilters","filters","key","config","displayValue","toggleCollapsed","handleReset","resetValues","currentValue","handleSearch","removeFilter","newValues","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_unref","ChevronDown","_normalizeClass","_createBlock","JLabel","_hoisted_4","_Fragment","_renderList","filter","JBadge","_hoisted_5","_toDisplayString","_withModifiers","$event","X","_hoisted_7","_renderSlot","_ctx","JButton","_withDirectives","_hoisted_8"],"mappings":"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"}
|
|
@@ -3,14 +3,14 @@ import { ChevronDown as N, X as R } from "lucide-vue-next";
|
|
|
3
3
|
import O from "../atoms/JBadge.vue.js";
|
|
4
4
|
import k from "../atoms/JButton.vue.js";
|
|
5
5
|
import J from "../atoms/JLabel.vue.js";
|
|
6
|
-
const L = { class: "w-full rounded-lg border bg-card text-card-foreground" }, M = { class: "flex items-center justify-between px-
|
|
6
|
+
const L = { class: "w-full rounded-lg border bg-card text-card-foreground" }, M = { class: "flex items-center justify-between px-3 py-1.5" }, X = { class: "flex items-center gap-2" }, q = {
|
|
7
7
|
key: 2,
|
|
8
8
|
class: "flex items-center gap-1 flex-wrap"
|
|
9
|
-
}, G = { class: "text-muted-foreground" }, H = ["onClick"], I = { class: "flex items-center gap-2" }, K = { class: "px-
|
|
9
|
+
}, G = { class: "text-muted-foreground" }, H = ["onClick"], I = { class: "flex items-center gap-2" }, K = { class: "px-3 pb-3" }, Z = /* @__PURE__ */ $({
|
|
10
10
|
__name: "JFilterBar",
|
|
11
11
|
props: {
|
|
12
12
|
title: {},
|
|
13
|
-
collapsed: { type: Boolean, default: !
|
|
13
|
+
collapsed: { type: Boolean, default: !0 },
|
|
14
14
|
collapsible: { type: Boolean, default: !0 },
|
|
15
15
|
filterValues: { default: () => ({}) },
|
|
16
16
|
filterDisplay: { default: () => ({}) },
|
|
@@ -63,12 +63,12 @@ const L = { class: "w-full rounded-lg border bg-card text-card-foreground" }, M
|
|
|
63
63
|
l.collapsible ? (o(), u("button", {
|
|
64
64
|
key: 0,
|
|
65
65
|
type: "button",
|
|
66
|
-
class: "flex items-center justify-center h-
|
|
66
|
+
class: "flex items-center justify-center h-6 w-6 rounded hover:bg-accent hover:text-accent-foreground transition-colors",
|
|
67
67
|
onClick: _
|
|
68
68
|
}, [
|
|
69
69
|
x(v(N), {
|
|
70
70
|
class: T([
|
|
71
|
-
"h-
|
|
71
|
+
"h-3.5 w-3.5 transition-transform",
|
|
72
72
|
m.value ? "rotate-0" : "-rotate-90"
|
|
73
73
|
])
|
|
74
74
|
}, null, 8, ["class"])
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JFilterBar.vue.js","sources":["../../../../src/components/organisms/JFilterBar.vue"],"sourcesContent":["<template>\r\n <div class=\"w-full rounded-lg border bg-card text-card-foreground\">\r\n <!-- Row 1: toolbar -->\r\n <div class=\"flex items-center justify-between px-
|
|
1
|
+
{"version":3,"file":"JFilterBar.vue.js","sources":["../../../../src/components/organisms/JFilterBar.vue"],"sourcesContent":["<template>\r\n <div class=\"w-full rounded-lg border bg-card text-card-foreground\">\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\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 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"],"names":["props","__props","emit","__emit","isExpanded","computed","isEmpty","value","activeFilters","filters","key","config","displayValue","toggleCollapsed","handleReset","resetValues","currentValue","handleSearch","removeFilter","newValues","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_hoisted_3","_createVNode","_unref","ChevronDown","_normalizeClass","_createBlock","JLabel","_hoisted_4","_Fragment","_renderList","filter","JBadge","_hoisted_5","_toDisplayString","_withModifiers","$event","X","_hoisted_7","_renderSlot","_ctx","JButton","_withDirectives","_hoisted_8"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),u=require("../atoms/JIcon.vue.cjs"),K=require("../atoms/JAvatar.vue.cjs"),g=require("../atoms/JButton.vue.cjs"),N=require("../atoms/JPopover.vue.cjs"),v=require("../../lib/utils.cjs"),r=require("../../lib/theme-utils.cjs"),z=require("../../assets/images/logo-fallback.png.cjs"),W={class:"flex items-center gap-6 flex-1"},H=["src"],Q={key:1,class:"text-lg font-bold text-foreground"},X={key:2,class:"flex items-center gap-1"},Z={class:"flex items-center gap-3"},ee={class:"p-2 min-w-[200px]"},te={class:"space-y-1"},oe=["onClick"],le={key:1,class:"w-4"},ne={class:"flex-1 capitalize"},ae={key:0,class:"absolute top-0 right-0 h-4 w-4 rounded-full bg-destructive text-destructive-foreground text-xs flex items-center justify-center"},se={class:"p-2"},re={key:0,class:"max-h-96 overflow-y-auto space-y-1"},ce=["onClick"],ie={class:"flex items-start gap-2"},de={class:"flex-1 min-w-0"},ue={class:"text-sm font-medium text-foreground"},me={key:0,class:"text-xs text-muted-foreground mt-1"},fe={key:1,class:"text-xs text-muted-foreground/60 mt-1"},he={key:1,class:"p-4 text-center text-sm text-muted-foreground"},ke={class:"flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity"},ge={class:"text-sm text-foreground hidden sm:inline"},ve={class:"w-full rounded-md overflow-hidden"},pe={class:"px-3 py-2 border-b border-border"},xe={class:"text-sm font-medium text-foreground"},ye={key:0,class:"text-xs text-muted-foreground mt-0.5"},be=["disabled","onClick"],Be={class:"flex-1 text-left truncate"},_e={key:1,class:"h-px bg-muted my-1"},Ce={key:0,class:"h-px bg-muted my-1"},Te=e.defineComponent({__name:"JHeader",props:{logo:{},logoText:{default:"JWMS Portal"},navItems:{},showNotifications:{type:Boolean,default:!1},notifications:{default:()=>[]},userAvatar:{},userName:{},isLoggedIn:{type:Boolean},userEmail:{},userId:{},styletype:{default:"default"},showSidebarToggle:{type:Boolean,default:!0},isSidebarOpen:{type:Boolean,default:!0},showThemeSelector:{type:Boolean,default:!0},defaultTheme:{default:void 0},availableThemes:{default:void 0}},emits:["logoClick","navClick","notificationClick","userMenuSelect","sidebarToggle","login"],setup(l,{emit:L}){const d=l,f=L,S={default:{containerClass:"h-14 px-4 border-b border-border bg-background",navItemClass:"text-sm text-muted-foreground hover:text-foreground transition-colors px-3 py-2 rounded-md hover:bg-accent",navItemActiveClass:"text-foreground font-medium bg-accent"},minimal:{containerClass:"h-12 px-3 border-b border-border bg-background",navItemClass:"text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded-md hover:bg-accent",navItemActiveClass:"text-foreground font-medium bg-accent"}},B=e.computed(()=>S[d.styletype]??S.default),h=e.ref(0),D=e.computed(()=>{if(d.logo)return d.logo}),_=e.computed(()=>{if(d.logo)return h.value>=1&&z.default?z.default:D.value}),O=()=>{h.value===0?h.value=1:h.value===1&&(h.value=2)},C=e.computed(()=>d.notifications?.filter(o=>!o.read).length||0),M=[{id:"profile",label:"프로필",icon:"user"},{id:"settings",label:"설정",icon:"settings"},{id:"separator",label:"",separator:!0},{id:"logout",label:"로그아웃",icon:"logOut"}],V=e.computed(()=>[{items:M}]),A=()=>{f("logoClick")},F=(o,a)=>{o.onClick?.(),f("navClick",o,a)},q=o=>{o.onClick?.(),f("notificationClick",o)},G=o=>{f("userMenuSelect",o)},J=()=>{f("login")},U=()=>{f("sidebarToggle")},m=e.ref(!1),k=e.ref(r.getDefaultTheme()),p=e.ref([]);let x=!1,y=null,b=null;const I=()=>typeof window>"u"?!1:!!(window.location?.href?.includes("storybook")||window.__STORYBOOK_GLOBALS__),T=()=>{const o=r.detectThemeClasses(),a=r.ensureDefaultTheme(o);if(d.availableThemes&&d.availableThemes.length>0){const t=a.filter(n=>d.availableThemes.includes(n));p.value=r.ensureDefaultTheme(t)}else p.value=a},$=()=>{if(typeof window>"u")return"light";const o=localStorage.getItem("theme");return o==="dark"||o==="light"?o:window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"},E=o=>{x=!0;const a=document.documentElement;o==="dark"?(a.classList.add("dark"),m.value=!0):(a.classList.remove("dark"),m.value=!1),localStorage.setItem("theme",o),x=!1},P=()=>{const o=m.value?"light":"dark";E(o)},R=()=>{if(d.defaultTheme)return r.validateThemeExists(d.defaultTheme);const o=r.getStoredTheme("tweakcn-theme");return o?r.validateThemeExists(o):r.getDefaultTheme()},w=o=>{x=!0;const a=r.validateThemeExists(o),t=r.applyTheme(a);if(t){if(k.value=a,r.setStoredTheme(a,"tweakcn-theme"),I())try{const n=window.__STORYBOOK_GLOBALS__;n&&(n.theme=a)}catch{}}else{const n=r.getDefaultTheme();r.applyTheme(n),k.value=n,r.setStoredTheme(n,"tweakcn-theme")}return x=!1,t},Y=o=>{w(String(o))},j=e.computed(()=>m.value?"sun":"moon");return e.onMounted(()=>{T();const o=$();E(o);const a=R();w(a);const t=document.documentElement;m.value=t.classList.contains("dark"),y=new MutationObserver(()=>{if(x)return;const s=t.classList.contains("dark");s!==m.value&&(m.value=s,localStorage.setItem("theme",s?"dark":"light")),T();const c=Array.from(t.classList).find(i=>i.startsWith("theme-"));if(c){const i=c.replace("theme-","");p.value.includes(i)&&(k.value=i)}}),y.observe(document.documentElement,{attributes:!0,attributeFilter:["class"]}),b=new MutationObserver(()=>{T()}),b.observe(document.head,{childList:!0,subtree:!0});let n=null;if(I()){const s=()=>{try{const c=window.__STORYBOOK_GLOBALS__;if(c){if(typeof c.darkMode<"u"){const i=c.darkMode;i!==m.value&&E(i?"dark":"light")}if(c.theme&&c.theme!==k.value){const i=String(c.theme);p.value.includes(i)&&w(i)}}}catch{}};s(),n=setInterval(s,500)}e.onUnmounted(()=>{y&&(y.disconnect(),y=null),b&&(b.disconnect(),b=null),n&&clearInterval(n)})}),(o,a)=>(e.openBlock(),e.createElementBlock("header",{class:e.normalizeClass(e.unref(v.cn)("flex items-center justify-between w-full",B.value.containerClass))},[e.createElementVNode("div",W,[l.showSidebarToggle?(e.openBlock(),e.createBlock(g.default,{key:0,variant:"ghost",size:"icon",class:"flex-shrink-0","aria-label":"사이드바 토글",onClick:U},{default:e.withCtx(()=>[e.createVNode(u.default,{name:"menu",size:"md"})]),_:1})):e.createCommentVNode("",!0),_.value||l.logoText?(e.openBlock(),e.createElementBlock("div",{key:1,class:"flex items-center cursor-pointer",onClick:A},[_.value&&h.value<2?(e.openBlock(),e.createElementBlock("img",{key:0,src:_.value,alt:"로고",class:"h-8 w-auto",onError:O},null,40,H)):l.logoText?(e.openBlock(),e.createElementBlock("span",Q,e.toDisplayString(l.logoText),1)):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0),l.navItems&&l.navItems.length>0?(e.openBlock(),e.createElementBlock("nav",X,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(l.navItems,(t,n)=>(e.openBlock(),e.createBlock(g.default,{key:n,variant:"ghost",class:e.normalizeClass(e.unref(v.cn)(B.value.navItemClass,t.active&&B.value.navItemActiveClass)),onClick:s=>F(t,n)},{default:e.withCtx(()=>[t.icon?(e.openBlock(),e.createBlock(u.default,{key:0,name:t.icon,size:"sm",class:"mr-1.5"},null,8,["name"])):e.createCommentVNode("",!0),e.createTextVNode(" "+e.toDisplayString(t.label),1)]),_:2},1032,["class","onClick"]))),128))])):e.createCommentVNode("",!0)]),e.createElementVNode("div",Z,[l.showThemeSelector?(e.openBlock(),e.createBlock(N.default,{key:0,position:"bottom",align:"end",styletype:"default-sm"},{trigger:e.withCtx(()=>[e.createVNode(g.default,{variant:"ghost",size:"icon","aria-label":"테마 선택"},{default:e.withCtx(()=>[e.createVNode(u.default,{name:"palette",size:"md"})]),_:1})]),default:e.withCtx(()=>[e.createElementVNode("div",ee,[a[0]||(a[0]=e.createElementVNode("div",{class:"text-xs font-medium text-muted-foreground px-2 py-1.5 mb-1"}," 테마 선택 ",-1)),e.createElementVNode("div",te,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(p.value,t=>(e.openBlock(),e.createElementBlock("button",{key:t,class:e.normalizeClass(e.unref(v.cn)("w-full flex items-center gap-2 px-2 py-1.5 text-sm rounded-md transition-colors","hover:bg-accent hover:text-accent-foreground","border-0 outline-none text-left",k.value===t&&"bg-accent text-accent-foreground font-medium")),onClick:n=>Y(t)},[k.value===t?(e.openBlock(),e.createBlock(u.default,{key:0,name:"check",size:"sm",class:"flex-shrink-0"})):(e.openBlock(),e.createElementBlock("span",le)),e.createElementVNode("span",ne,e.toDisplayString(t),1)],10,oe))),128))])])]),_:1})):e.createCommentVNode("",!0),l.showNotifications?(e.openBlock(),e.createBlock(N.default,{key:1,position:"bottom",align:"end",styletype:"default-sm"},{trigger:e.withCtx(()=>[e.createVNode(g.default,{variant:"ghost",size:"icon",class:"relative","aria-label":"알림"},{default:e.withCtx(()=>[e.createVNode(u.default,{name:"circleAlert",size:"md"}),C.value>0?(e.openBlock(),e.createElementBlock("span",ae,e.toDisplayString(C.value>9?"9+":C.value),1)):e.createCommentVNode("",!0)]),_:1})]),default:e.withCtx(()=>[e.createElementVNode("div",se,[l.notifications&&l.notifications.length>0?(e.openBlock(),e.createElementBlock("div",re,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(l.notifications,t=>(e.openBlock(),e.createElementBlock("div",{key:t.id,class:e.normalizeClass(e.unref(v.cn)("p-3 rounded-md cursor-pointer transition-colors",t.read?"hover:bg-accent/50":"bg-accent")),onClick:n=>q(t)},[e.createElementVNode("div",ie,[t.icon?(e.openBlock(),e.createBlock(u.default,{key:0,name:t.icon,size:"sm",class:"mt-0.5 flex-shrink-0"},null,8,["name"])):e.createCommentVNode("",!0),e.createElementVNode("div",de,[e.createElementVNode("p",ue,e.toDisplayString(t.title),1),t.message?(e.openBlock(),e.createElementBlock("p",me,e.toDisplayString(t.message),1)):e.createCommentVNode("",!0),t.time?(e.openBlock(),e.createElementBlock("p",fe,e.toDisplayString(t.time),1)):e.createCommentVNode("",!0)])])],10,ce))),128))])):(e.openBlock(),e.createElementBlock("div",he," 알림이 없습니다. "))])]),_:1})):e.createCommentVNode("",!0),e.createVNode(g.default,{variant:"ghost",size:"icon","aria-label":m.value?"라이트 모드로 전환":"다크 모드로 전환",onClick:P},{default:e.withCtx(()=>[e.createVNode(u.default,{name:j.value,size:"md"},null,8,["name"])]),_:1},8,["aria-label"]),l.userName?(e.openBlock(),e.createBlock(N.default,{key:2,position:"bottom",align:"end",styletype:"default-sm"},{trigger:e.withCtx(()=>[e.createElementVNode("div",ke,[e.createVNode(K.default,{src:l.userAvatar,fallback:l.userName?l.userName[0]:"U",size:"sm"},null,8,["src","fallback"]),e.createElementVNode("span",ge,e.toDisplayString(l.userName),1),e.createVNode(u.default,{name:"chevronDown",size:"sm",class:"text-muted-foreground hidden sm:inline"})])]),default:e.withCtx(()=>[e.createElementVNode("div",ve,[e.createElementVNode("div",pe,[e.createElementVNode("p",xe,e.toDisplayString(l.userName),1),l.userEmail?(e.openBlock(),e.createElementBlock("p",ye,e.toDisplayString(l.userEmail),1)):e.createCommentVNode("",!0)]),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(V.value,(t,n)=>(e.openBlock(),e.createElementBlock(e.Fragment,{key:n},[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.items,(s,c)=>(e.openBlock(),e.createElementBlock(e.Fragment,{key:s.id},[s.separator?s.separator&&c>0?(e.openBlock(),e.createElementBlock("div",_e)):e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("button",{key:0,disabled:s.disabled,class:e.normalizeClass(e.unref(v.cn)("w-full flex items-center gap-2 px-3 py-1.5 text-sm transition-colors",s.id==="logout"?"text-destructive hover:bg-destructive/10 hover:text-destructive":"hover:bg-accent hover:text-accent-foreground","disabled:opacity-50 disabled:pointer-events-none","border-0 outline-none",t.items[c+1]?.separator&&"border-b-0")),onClick:i=>G(s.id)},[s.icon?(e.openBlock(),e.createBlock(u.default,{key:0,name:s.icon,size:"sm",class:e.normalizeClass(e.unref(v.cn)("flex-shrink-0",s.id==="logout"&&"text-destructive"))},null,8,["name","class"])):e.createCommentVNode("",!0),e.createElementVNode("span",Be,e.toDisplayString(s.label),1)],10,be))],64))),128)),n<V.value.length-1?(e.openBlock(),e.createElementBlock("div",Ce)):e.createCommentVNode("",!0)],64))),128))])]),_:1})):(e.openBlock(),e.createBlock(g.default,{key:3,variant:"ghost",size:"sm","aria-label":"로그인",onClick:J},{default:e.withCtx(()=>[e.createVNode(u.default,{name:"logIn",size:"sm",class:"sm:mr-1.5"}),a[1]||(a[1]=e.createElementVNode("span",{class:"hidden sm:inline"},"로그인",-1))]),_:1})),e.renderSlot(o.$slots,"actions")])],2))}});exports.default=Te;
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue"),u=require("../atoms/JIcon.vue.cjs"),K=require("../atoms/JAvatar.vue.cjs"),g=require("../atoms/JButton.vue.cjs"),N=require("../atoms/JPopover.vue.cjs"),v=require("../../lib/utils.cjs"),r=require("../../lib/theme-utils.cjs"),z=require("../../assets/images/logo-fallback.png.cjs"),W={class:"flex items-center gap-6 flex-1"},H=["src"],Q={key:1,class:"text-sm font-bold text-foreground"},X={key:2,class:"flex items-center gap-1"},Z={class:"flex items-center gap-3"},ee={class:"p-2 min-w-[200px]"},te={class:"space-y-1"},oe=["onClick"],le={key:1,class:"w-4"},ne={class:"flex-1 capitalize"},ae={key:0,class:"absolute top-0 right-0 h-4 w-4 rounded-full bg-destructive text-destructive-foreground text-xs flex items-center justify-center"},se={class:"p-2"},re={key:0,class:"max-h-96 overflow-y-auto space-y-1"},ce=["onClick"],ie={class:"flex items-start gap-2"},de={class:"flex-1 min-w-0"},ue={class:"text-xs font-medium text-foreground"},me={key:0,class:"text-xs text-muted-foreground mt-1"},fe={key:1,class:"text-xs text-muted-foreground/60 mt-1"},he={key:1,class:"p-4 text-center text-xs text-muted-foreground"},ke={class:"flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity"},ge={class:"text-xs text-foreground hidden sm:inline"},ve={class:"w-full rounded-md overflow-hidden"},pe={class:"px-3 py-1.5 border-b border-border"},xe={class:"text-xs font-medium text-foreground"},be={key:0,class:"text-xs text-muted-foreground mt-0.5"},ye=["disabled","onClick"],Be={class:"flex-1 text-left truncate"},_e={key:1,class:"h-px bg-muted my-1"},Ce={key:0,class:"h-px bg-muted my-1"},we=e.defineComponent({__name:"JHeader",props:{logo:{},logoText:{default:"JWMS Portal"},navItems:{},showNotifications:{type:Boolean,default:!1},notifications:{default:()=>[]},userAvatar:{},userName:{},isLoggedIn:{type:Boolean},userEmail:{},userId:{},styletype:{default:"default"},showSidebarToggle:{type:Boolean,default:!0},isSidebarOpen:{type:Boolean,default:!0},showThemeSelector:{type:Boolean,default:!0},defaultTheme:{default:void 0},availableThemes:{default:void 0}},emits:["logoClick","navClick","notificationClick","userMenuSelect","sidebarToggle","login"],setup(l,{emit:L}){const d=l,f=L,S={default:{containerClass:"h-10 px-4 border-b border-border bg-background",navItemClass:"text-xs text-muted-foreground hover:text-foreground transition-colors px-3 py-1.5 rounded-md hover:bg-accent",navItemActiveClass:"text-foreground font-medium bg-accent"},minimal:{containerClass:"h-8 px-3 border-b border-border bg-background",navItemClass:"text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded-md hover:bg-accent",navItemActiveClass:"text-foreground font-medium bg-accent"}},B=e.computed(()=>S[d.styletype]??S.default),h=e.ref(0),D=e.computed(()=>{if(d.logo)return d.logo}),_=e.computed(()=>{if(d.logo)return h.value>=1&&z.default?z.default:D.value}),O=()=>{h.value===0?h.value=1:h.value===1&&(h.value=2)},C=e.computed(()=>d.notifications?.filter(o=>!o.read).length||0),M=[{id:"profile",label:"프로필",icon:"user"},{id:"settings",label:"설정",icon:"settings"},{id:"separator",label:"",separator:!0},{id:"logout",label:"로그아웃",icon:"logOut"}],V=e.computed(()=>[{items:M}]),A=()=>{f("logoClick")},F=(o,a)=>{o.onClick?.(),f("navClick",o,a)},q=o=>{o.onClick?.(),f("notificationClick",o)},G=o=>{f("userMenuSelect",o)},J=()=>{f("login")},U=()=>{f("sidebarToggle")},m=e.ref(!1),k=e.ref(r.getDefaultTheme()),p=e.ref([]);let x=!1,b=null,y=null;const I=()=>typeof window>"u"?!1:!!(window.location?.href?.includes("storybook")||window.__STORYBOOK_GLOBALS__),w=()=>{const o=r.detectThemeClasses(),a=r.ensureDefaultTheme(o);if(d.availableThemes&&d.availableThemes.length>0){const t=a.filter(n=>d.availableThemes.includes(n));p.value=r.ensureDefaultTheme(t)}else p.value=a},$=()=>{if(typeof window>"u")return"light";const o=localStorage.getItem("theme");return o==="dark"||o==="light"?o:window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"},T=o=>{x=!0;const a=document.documentElement;o==="dark"?(a.classList.add("dark"),m.value=!0):(a.classList.remove("dark"),m.value=!1),localStorage.setItem("theme",o),x=!1},P=()=>{const o=m.value?"light":"dark";T(o)},R=()=>{if(d.defaultTheme)return r.validateThemeExists(d.defaultTheme);const o=r.getStoredTheme("tweakcn-theme");return o?r.validateThemeExists(o):r.getDefaultTheme()},E=o=>{x=!0;const a=r.validateThemeExists(o),t=r.applyTheme(a);if(t){if(k.value=a,r.setStoredTheme(a,"tweakcn-theme"),I())try{const n=window.__STORYBOOK_GLOBALS__;n&&(n.theme=a)}catch{}}else{const n=r.getDefaultTheme();r.applyTheme(n),k.value=n,r.setStoredTheme(n,"tweakcn-theme")}return x=!1,t},Y=o=>{E(String(o))},j=e.computed(()=>m.value?"sun":"moon");return e.onMounted(()=>{w();const o=$();T(o);const a=R();E(a);const t=document.documentElement;m.value=t.classList.contains("dark"),b=new MutationObserver(()=>{if(x)return;const s=t.classList.contains("dark");s!==m.value&&(m.value=s,localStorage.setItem("theme",s?"dark":"light")),w();const c=Array.from(t.classList).find(i=>i.startsWith("theme-"));if(c){const i=c.replace("theme-","");p.value.includes(i)&&(k.value=i)}}),b.observe(document.documentElement,{attributes:!0,attributeFilter:["class"]}),y=new MutationObserver(()=>{w()}),y.observe(document.head,{childList:!0,subtree:!0});let n=null;if(I()){const s=()=>{try{const c=window.__STORYBOOK_GLOBALS__;if(c){if(typeof c.darkMode<"u"){const i=c.darkMode;i!==m.value&&T(i?"dark":"light")}if(c.theme&&c.theme!==k.value){const i=String(c.theme);p.value.includes(i)&&E(i)}}}catch{}};s(),n=setInterval(s,500)}e.onUnmounted(()=>{b&&(b.disconnect(),b=null),y&&(y.disconnect(),y=null),n&&clearInterval(n)})}),(o,a)=>(e.openBlock(),e.createElementBlock("header",{class:e.normalizeClass(e.unref(v.cn)("flex items-center justify-between w-full",B.value.containerClass))},[e.createElementVNode("div",W,[l.showSidebarToggle?(e.openBlock(),e.createBlock(g.default,{key:0,variant:"ghost",size:"sm",class:"flex-shrink-0 w-8 h-8 p-0","aria-label":"사이드바 토글",onClick:U},{default:e.withCtx(()=>[e.createVNode(u.default,{name:"menu",size:"sm"})]),_:1})):e.createCommentVNode("",!0),_.value||l.logoText?(e.openBlock(),e.createElementBlock("div",{key:1,class:"flex items-center cursor-pointer",onClick:A},[_.value&&h.value<2?(e.openBlock(),e.createElementBlock("img",{key:0,src:_.value,alt:"로고",class:"h-6 w-auto",onError:O},null,40,H)):l.logoText?(e.openBlock(),e.createElementBlock("span",Q,e.toDisplayString(l.logoText),1)):e.createCommentVNode("",!0)])):e.createCommentVNode("",!0),l.navItems&&l.navItems.length>0?(e.openBlock(),e.createElementBlock("nav",X,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(l.navItems,(t,n)=>(e.openBlock(),e.createBlock(g.default,{key:n,variant:"ghost",class:e.normalizeClass(e.unref(v.cn)(B.value.navItemClass,t.active&&B.value.navItemActiveClass)),onClick:s=>F(t,n)},{default:e.withCtx(()=>[t.icon?(e.openBlock(),e.createBlock(u.default,{key:0,name:t.icon,size:"sm",class:"mr-1.5"},null,8,["name"])):e.createCommentVNode("",!0),e.createTextVNode(" "+e.toDisplayString(t.label),1)]),_:2},1032,["class","onClick"]))),128))])):e.createCommentVNode("",!0)]),e.createElementVNode("div",Z,[l.showThemeSelector?(e.openBlock(),e.createBlock(N.default,{key:0,position:"bottom",align:"end",styletype:"default-sm"},{trigger:e.withCtx(()=>[e.createVNode(g.default,{variant:"ghost",size:"sm",class:"w-8 h-8 p-0","aria-label":"테마 선택"},{default:e.withCtx(()=>[e.createVNode(u.default,{name:"palette",size:"sm"})]),_:1})]),default:e.withCtx(()=>[e.createElementVNode("div",ee,[a[0]||(a[0]=e.createElementVNode("div",{class:"text-xs font-medium text-muted-foreground px-2 py-1.5 mb-1"}," 테마 선택 ",-1)),e.createElementVNode("div",te,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(p.value,t=>(e.openBlock(),e.createElementBlock("button",{key:t,class:e.normalizeClass(e.unref(v.cn)("w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded-md transition-colors","hover:bg-accent hover:text-accent-foreground","border-0 outline-none text-left",k.value===t&&"bg-accent text-accent-foreground font-medium")),onClick:n=>Y(t)},[k.value===t?(e.openBlock(),e.createBlock(u.default,{key:0,name:"check",size:"sm",class:"flex-shrink-0"})):(e.openBlock(),e.createElementBlock("span",le)),e.createElementVNode("span",ne,e.toDisplayString(t),1)],10,oe))),128))])])]),_:1})):e.createCommentVNode("",!0),l.showNotifications?(e.openBlock(),e.createBlock(N.default,{key:1,position:"bottom",align:"end",styletype:"default-sm"},{trigger:e.withCtx(()=>[e.createVNode(g.default,{variant:"ghost",size:"sm",class:"relative w-8 h-8 p-0","aria-label":"알림"},{default:e.withCtx(()=>[e.createVNode(u.default,{name:"circleAlert",size:"sm"}),C.value>0?(e.openBlock(),e.createElementBlock("span",ae,e.toDisplayString(C.value>9?"9+":C.value),1)):e.createCommentVNode("",!0)]),_:1})]),default:e.withCtx(()=>[e.createElementVNode("div",se,[l.notifications&&l.notifications.length>0?(e.openBlock(),e.createElementBlock("div",re,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(l.notifications,t=>(e.openBlock(),e.createElementBlock("div",{key:t.id,class:e.normalizeClass(e.unref(v.cn)("p-3 rounded-md cursor-pointer transition-colors",t.read?"hover:bg-accent/50":"bg-accent")),onClick:n=>q(t)},[e.createElementVNode("div",ie,[t.icon?(e.openBlock(),e.createBlock(u.default,{key:0,name:t.icon,size:"sm",class:"mt-0.5 flex-shrink-0"},null,8,["name"])):e.createCommentVNode("",!0),e.createElementVNode("div",de,[e.createElementVNode("p",ue,e.toDisplayString(t.title),1),t.message?(e.openBlock(),e.createElementBlock("p",me,e.toDisplayString(t.message),1)):e.createCommentVNode("",!0),t.time?(e.openBlock(),e.createElementBlock("p",fe,e.toDisplayString(t.time),1)):e.createCommentVNode("",!0)])])],10,ce))),128))])):(e.openBlock(),e.createElementBlock("div",he," 알림이 없습니다. "))])]),_:1})):e.createCommentVNode("",!0),e.createVNode(g.default,{variant:"ghost",size:"sm",class:"w-8 h-8 p-0","aria-label":m.value?"라이트 모드로 전환":"다크 모드로 전환",onClick:P},{default:e.withCtx(()=>[e.createVNode(u.default,{name:j.value,size:"sm"},null,8,["name"])]),_:1},8,["aria-label"]),e.renderSlot(o.$slots,"toolbar"),l.userName?(e.openBlock(),e.createBlock(N.default,{key:2,position:"bottom",align:"end",styletype:"default-sm"},{trigger:e.withCtx(()=>[e.createElementVNode("div",ke,[e.createVNode(K.default,{src:l.userAvatar,fallback:l.userName?l.userName[0]:"U",size:"xs"},null,8,["src","fallback"]),e.createElementVNode("span",ge,e.toDisplayString(l.userName),1),e.createVNode(u.default,{name:"chevronDown",size:"sm",class:"text-muted-foreground hidden sm:inline"})])]),default:e.withCtx(()=>[e.createElementVNode("div",ve,[e.createElementVNode("div",pe,[e.createElementVNode("p",xe,e.toDisplayString(l.userName),1),l.userEmail?(e.openBlock(),e.createElementBlock("p",be,e.toDisplayString(l.userEmail),1)):e.createCommentVNode("",!0)]),(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(V.value,(t,n)=>(e.openBlock(),e.createElementBlock(e.Fragment,{key:n},[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(t.items,(s,c)=>(e.openBlock(),e.createElementBlock(e.Fragment,{key:s.id},[s.separator?s.separator&&c>0?(e.openBlock(),e.createElementBlock("div",_e)):e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("button",{key:0,disabled:s.disabled,class:e.normalizeClass(e.unref(v.cn)("w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",s.id==="logout"?"text-destructive hover:bg-destructive/10 hover:text-destructive":"hover:bg-accent hover:text-accent-foreground","disabled:opacity-50 disabled:pointer-events-none","border-0 outline-none",t.items[c+1]?.separator&&"border-b-0")),onClick:i=>G(s.id)},[s.icon?(e.openBlock(),e.createBlock(u.default,{key:0,name:s.icon,size:"sm",class:e.normalizeClass(e.unref(v.cn)("flex-shrink-0",s.id==="logout"&&"text-destructive"))},null,8,["name","class"])):e.createCommentVNode("",!0),e.createElementVNode("span",Be,e.toDisplayString(s.label),1)],10,ye))],64))),128)),n<V.value.length-1?(e.openBlock(),e.createElementBlock("div",Ce)):e.createCommentVNode("",!0)],64))),128))])]),_:1})):(e.openBlock(),e.createBlock(g.default,{key:3,variant:"ghost",size:"sm","aria-label":"로그인",onClick:J},{default:e.withCtx(()=>[e.createVNode(u.default,{name:"logIn",size:"sm",class:"sm:mr-1.5"}),a[1]||(a[1]=e.createElementVNode("span",{class:"hidden sm:inline"},"로그인",-1))]),_:1}))])],2))}});exports.default=we;
|
|
2
2
|
//# sourceMappingURL=JHeader.vue.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JHeader.vue.cjs","sources":["../../../../src/components/organisms/JHeader.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, onMounted, onUnmounted } from 'vue'\nimport JIcon from '@/components/atoms/JIcon.vue'\nimport JAvatar from '@/components/atoms/JAvatar.vue'\nimport JButton from '@/components/atoms/JButton.vue'\nimport JPopover from '@/components/atoms/JPopover.vue'\nimport { cn } from '@/lib/utils'\nimport type { ContextMenuItem, ContextMenuGroup } from '@/types/context-menu.types'\nimport {\n detectThemeClasses,\n ensureDefaultTheme,\n validateThemeExists,\n getDefaultTheme,\n getStoredTheme,\n setStoredTheme,\n applyTheme,\n} from '@/lib/theme-utils'\n\n// Fallback 로고 이미지 import (logo prop이 없거나 로드 실패 시 사용)\nimport logoFallback from '@/assets/images/logo-fallback.png'\n\n/**\n * JHeader - 상단 헤더 컴포넌트 (organisms)\n * Header Component\n * \n * @description\n * 애플리케이션의 상단 헤더 영역을 담당하는 컴포넌트입니다.\n * 로고, 네비게이션, 검색, 알림, 사용자 메뉴 등을 포함할 수 있습니다.\n * \n * @example\n * ```vue\n * <JHeader\n * logo=\"/logo.png\"\n * :user-menu-items=\"userMenuItems\"\n * @logo-click=\"handleLogoClick\"\n * />\n * ```\n */\n\nexport type HeaderNavItem = {\n /** 라벨 */\n label: string\n /** 링크 URL */\n href?: string\n /** 아이콘 */\n icon?: string\n /** 활성 상태 */\n active?: boolean\n /** 클릭 핸들러 */\n onClick?: () => void\n}\n\nexport type NotificationItem = {\n /** 알림 ID */\n id: string\n /** 알림 제목 */\n title: string\n /** 알림 내용 */\n message?: string\n /** 알림 시간 */\n time?: string\n /** 읽음 상태 */\n read?: boolean\n /** 아이콘 */\n icon?: string\n /** 클릭 핸들러 */\n onClick?: () => void\n}\n\ntype StyleType =\n | 'default' // 기본 스타일\n | 'minimal' // 최소 스타일\n\nconst props = withDefaults(\n defineProps<{\n /** 로고 이미지 URL */\n logo?: string\n /** 로고 텍스트 (기본값, 이미지가 없을 때) */\n logoText?: string\n /** 네비게이션 아이템 목록 */\n navItems?: HeaderNavItem[]\n /** 알림 표시 여부 */\n showNotifications?: boolean\n /** 알림 목록 */\n notifications?: NotificationItem[]\n /** 사용자 아바타 이미지 */\n userAvatar?: string\n /** 사용자 이름 */\n userName?: string\n /** 로그인 상태 */\n isLoggedIn?: boolean\n /** 사용자 이메일 */\n userEmail?: string\n /** 사용자 ID */\n userId?: string\n /** 스타일 타입 */\n styletype?: StyleType\n /** 사이드바 토글 버튼 표시 여부 */\n showSidebarToggle?: boolean\n /** 사이드바 열림 상태 */\n isSidebarOpen?: boolean\n /** 테마 선택기 표시 여부 */\n showThemeSelector?: boolean\n /** 초기 테마 (기본값: 'default' 또는 저장된 테마) */\n defaultTheme?: string\n /** 선택 가능한 테마 목록 (지정하지 않으면 모든 감지된 테마 사용) */\n availableThemes?: string[]\n }>(),\n {\n logoText: 'JWMS Portal', // 기본값: 텍스트 로고\n showNotifications: false,\n notifications: () => [],\n styletype: 'default',\n showSidebarToggle: true,\n isSidebarOpen: true,\n showThemeSelector: true,\n defaultTheme: undefined,\n availableThemes: undefined,\n }\n)\n\nconst emit = defineEmits<{\n /** 로고 클릭 이벤트 */\n logoClick: []\n /** 네비게이션 아이템 클릭 이벤트 */\n navClick: [item: HeaderNavItem, index: number]\n /** 알림 클릭 이벤트 */\n notificationClick: [item: NotificationItem]\n /** 사용자 메뉴 아이템 선택 이벤트 */\n userMenuSelect: [itemId: string]\n /** 사이드바 토글 이벤트 */\n sidebarToggle: []\n /** 로그인 버튼 클릭 이벤트 */\n login: []\n}>()\n\n/**\n * 스타일 프리셋\n */\nconst STYLE_PRESETS: Record<StyleType, {\n containerClass: string\n navItemClass: string\n navItemActiveClass: string\n}> = {\n default: {\n containerClass: 'h-14 px-4 border-b border-border bg-background',\n navItemClass: 'text-sm text-muted-foreground hover:text-foreground transition-colors px-3 py-2 rounded-md hover:bg-accent',\n navItemActiveClass: 'text-foreground font-medium bg-accent',\n },\n minimal: {\n containerClass: 'h-12 px-3 border-b border-border bg-background',\n navItemClass: 'text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded-md hover:bg-accent',\n navItemActiveClass: 'text-foreground font-medium bg-accent',\n },\n}\n\nconst preset = computed(() => {\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\n})\n\n/**\n * 로고 이미지 로드 에러 상태\n * 0: 에러 없음, 1: 첫 번째 이미지(logo) 에러, 2: fallback 이미지도 에러\n */\nconst logoImageError = ref(0)\n\n/**\n * 로고 이미지 경로 계산\n */\nconst logoImageSrc = computed(() => {\n // props.logo가 있으면 사용\n if (props.logo) {\n return props.logo\n }\n return undefined\n})\n\n/**\n * 실제 표시할 로고 이미지 경로\n * 1. props.logo가 있으면 사용\n * 2. logo 에러 시 fallback 이미지\n * 3. fallback도 에러면 undefined (텍스트로 대체)\n */\nconst displayLogoImage = computed(() => {\n // logo prop이 없으면 이미지 표시 안함 (텍스트 사용)\n if (!props.logo) {\n return undefined\n }\n \n // logo 에러가 발생했으면 fallback 사용\n if (logoImageError.value >= 1 && logoFallback) {\n return logoFallback\n }\n \n // 정상적으로 logo 사용\n return logoImageSrc.value\n})\n\n/**\n * 로고 이미지 로드 에러 핸들러\n */\nconst handleLogoError = () => {\n // 첫 번째 이미지(logo) 에러\n if (logoImageError.value === 0) {\n logoImageError.value = 1\n }\n // fallback 이미지도 에러면 2로 설정 (텍스트로 대체)\n else if (logoImageError.value === 1) {\n logoImageError.value = 2\n }\n}\n\n/**\n * 읽지 않은 알림 개수\n */\nconst unreadCount = computed(() => {\n return props.notifications?.filter(n => !n.read).length || 0\n})\n\n/**\n * 고정 사용자 메뉴 아이템\n */\nconst fixedUserMenuItems: ContextMenuItem[] = [\n { id: 'profile', label: '프로필', icon: 'user' },\n { id: 'settings', label: '설정', icon: 'settings' },\n { id: 'separator', label: '', separator: true },\n { id: 'logout', label: '로그아웃', icon: 'logOut' },\n]\n\n/**\n * 사용자 메뉴 아이템을 ContextMenuGroup 형식으로 변환\n */\nconst userMenuGroups = computed<ContextMenuGroup[]>(() => {\n return [{\n items: fixedUserMenuItems\n }]\n})\n\n/**\n * 로고 클릭 핸들러\n */\nconst handleLogoClick = () => {\n emit('logoClick')\n}\n\n/**\n * 네비게이션 아이템 클릭 핸들러\n */\nconst handleNavClick = (item: HeaderNavItem, index: number) => {\n item.onClick?.()\n emit('navClick', item, index)\n}\n\n/**\n * 알림 클릭 핸들러\n */\nconst handleNotificationClick = (item: NotificationItem) => {\n item.onClick?.()\n emit('notificationClick', item)\n}\n\n/**\n * 사용자 메뉴 선택 핸들러\n */\nconst handleUserMenuSelect = (itemId: string) => {\n emit('userMenuSelect', itemId)\n}\n\n/**\n * 로그인 버튼 클릭 핸들러\n */\nconst handleLogin = () => {\n emit('login')\n}\n\n/**\n * 사이드바 토글 핸들러\n */\nconst handleSidebarToggle = () => {\n emit('sidebarToggle')\n}\n\n/**\n * tweakcn 테마 타입 정의 (동적 감지 지원)\n */\ntype ThemeName = string\n\n/**\n * 다크모드 상태를 추적하는 반응형 변수\n * - false: 라이트 모드\n * - true: 다크 모드\n * 초기값은 false(라이트 모드)이며, 컴포넌트 마운트 시 실제 테마로 업데이트됩니다.\n */\nconst isDarkMode = ref(false)\n\n/**\n * 현재 선택된 tweakcn 테마 (디폴트 테마로 초기화)\n */\nconst currentTheme = ref<ThemeName>(getDefaultTheme())\n\n/**\n * 테마 옵션 목록 (동적으로 감지)\n */\nconst themeOptions = ref<ThemeName[]>([])\n\n/**\n * 내부 업데이트 플래그\n * - true: 컴포넌트 내부에서 테마를 변경 중 (MutationObserver가 무시해야 함)\n * - false: 외부에서 테마가 변경됨 (MutationObserver가 동기화해야 함)\n */\nlet isInternalUpdate = false\n\n/**\n * MutationObserver 인스턴스\n */\nlet themeObserver: MutationObserver | null = null\nlet styleObserver: MutationObserver | null = null\n\n/**\n * Storybook 환경인지 확인하는 함수\n * \n * @returns Storybook 환경이면 true, 아니면 false\n */\nconst isStorybook = (): boolean => {\n if (typeof window === 'undefined') return false\n return !!(\n window.location?.href?.includes('storybook') ||\n (window as any).__STORYBOOK_GLOBALS__\n )\n}\n\n/**\n * 테마 목록을 동적으로 감지하고 업데이트합니다.\n */\nconst updateThemeOptions = () => {\n const detectedThemes = detectThemeClasses()\n const ensuredThemes = ensureDefaultTheme(detectedThemes)\n \n // availableThemes prop이 지정되면 필터링\n if (props.availableThemes && props.availableThemes.length > 0) {\n const filtered = ensuredThemes.filter(theme => props.availableThemes!.includes(theme))\n // default는 항상 포함\n themeOptions.value = ensureDefaultTheme(filtered)\n } else {\n themeOptions.value = ensuredThemes\n }\n}\n\n/**\n * localStorage에서 저장된 다크모드 테마를 읽어오는 함수\n * \n * 우선순위:\n * 1. localStorage에 저장된 'theme' 값 (사용자가 이전에 선택한 테마)\n * 2. 시스템 설정 (OS의 다크모드 설정)\n * 3. 기본값 'light' (라이트 모드)\n * \n * @returns 'light' | 'dark' - 적용할 테마\n */\nconst getStoredDarkModeTheme = (): 'light' | 'dark' => {\n if (typeof window === 'undefined') return 'light'\n \n const stored = localStorage.getItem('theme')\n if (stored === 'dark' || stored === 'light') {\n return stored\n }\n \n if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n return 'dark'\n }\n \n return 'light'\n}\n\n/**\n * 다크모드 테마를 HTML 문서에 적용하는 함수\n * \n * @param theme - 적용할 테마 ('light' 또는 'dark')\n */\nconst applyDarkModeTheme = (theme: 'light' | 'dark') => {\n isInternalUpdate = true\n \n const root = document.documentElement\n \n if (theme === 'dark') {\n root.classList.add('dark')\n isDarkMode.value = true\n } else {\n root.classList.remove('dark')\n isDarkMode.value = false\n }\n \n localStorage.setItem('theme', theme)\n \n isInternalUpdate = false\n}\n\n/**\n * 테마 토글 버튼 클릭 시 호출되는 핸들러 함수\n */\nconst handleThemeToggle = () => {\n const newTheme = isDarkMode.value ? 'light' : 'dark'\n applyDarkModeTheme(newTheme)\n}\n\n/**\n * 저장된 tweakcn 테마를 가져와서 적용합니다.\n */\nconst getStoredTweakcnTheme = (): ThemeName => {\n // defaultTheme prop이 지정되면 우선 사용\n if (props.defaultTheme) {\n const validated = validateThemeExists(props.defaultTheme)\n return validated\n }\n \n const stored = getStoredTheme('tweakcn-theme')\n if (!stored) {\n return getDefaultTheme()\n }\n \n // 저장된 테마가 유효한지 검증\n const validated = validateThemeExists(stored)\n return validated\n}\n\n/**\n * tweakcn 테마를 HTML 문서에 적용하는 함수\n */\nconst applyTweakcnTheme = (theme: ThemeName): boolean => {\n isInternalUpdate = true\n \n const validatedTheme = validateThemeExists(theme)\n const success = applyTheme(validatedTheme)\n \n if (success) {\n currentTheme.value = validatedTheme\n setStoredTheme(validatedTheme, 'tweakcn-theme')\n \n // 스토리북 환경에서만 전역 상태 동기화\n if (isStorybook()) {\n try {\n const storybookGlobals = (window as any).__STORYBOOK_GLOBALS__\n if (storybookGlobals) {\n storybookGlobals.theme = validatedTheme\n }\n } catch (e) {\n // 무시\n }\n }\n } else {\n // 실패 시 디폴트 테마로 폴백\n const defaultTheme = getDefaultTheme()\n applyTheme(defaultTheme)\n currentTheme.value = defaultTheme\n setStoredTheme(defaultTheme, 'tweakcn-theme')\n }\n \n isInternalUpdate = false\n return success\n}\n\n/**\n * 테마 변경 핸들러\n */\nconst handleThemeChange = (theme: ThemeName | string | number) => {\n applyTweakcnTheme(String(theme))\n}\n\n/**\n * 현재 테마에 따라 표시할 아이콘을 계산하는 computed 속성\n */\nconst themeIcon = computed(() => {\n return isDarkMode.value ? 'sun' : 'moon'\n})\n\n/**\n * 초기화: 저장된 테마 적용 및 테마 목록 감지\n */\nonMounted(() => {\n // 테마 목록 초기 감지\n updateThemeOptions()\n \n // 저장된 다크모드 테마 적용\n const darkModeTheme = getStoredDarkModeTheme()\n applyDarkModeTheme(darkModeTheme)\n \n // 저장된 tweakcn 테마 적용\n const storedTheme = getStoredTweakcnTheme()\n applyTweakcnTheme(storedTheme)\n \n // HTML의 dark 클래스 변경을 감지하여 헤더 컴포넌트 상태를 동기화\n const root = document.documentElement\n \n // 현재 dark 클래스 상태로 초기화\n isDarkMode.value = root.classList.contains('dark')\n \n // MutationObserver로 동적 테마 클래스 감지\n themeObserver = new MutationObserver(() => {\n if (isInternalUpdate) return\n \n // 다크모드 클래스 변경 감지\n const hasDarkClass = root.classList.contains('dark')\n if (hasDarkClass !== isDarkMode.value) {\n isDarkMode.value = hasDarkClass\n localStorage.setItem('theme', hasDarkClass ? 'dark' : 'light')\n }\n \n // 테마 목록 업데이트\n updateThemeOptions()\n \n // 현재 적용된 tweakcn 테마 확인\n const currentThemeClass = Array.from(root.classList).find(cls => cls.startsWith('theme-'))\n if (currentThemeClass) {\n const themeName = currentThemeClass.replace('theme-', '')\n if (themeOptions.value.includes(themeName)) {\n currentTheme.value = themeName\n }\n }\n })\n \n // document.documentElement의 클래스 변경 감지\n themeObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['class'],\n })\n \n // 스타일시트 변경 감지 (동적으로 추가된 테마 감지)\n styleObserver = new MutationObserver(() => {\n updateThemeOptions()\n })\n \n styleObserver.observe(document.head, {\n childList: true,\n subtree: true,\n })\n \n // 스토리북 환경에서만 다크모드 및 테마 변경 감지 (추가 보완)\n let intervalId: ReturnType<typeof setInterval> | null = null\n \n if (isStorybook()) {\n const checkStorybookGlobals = () => {\n try {\n const storybookGlobals = (window as any).__STORYBOOK_GLOBALS__\n if (storybookGlobals) {\n // 다크모드 동기화\n if (typeof storybookGlobals.darkMode !== 'undefined') {\n const storybookDarkMode = storybookGlobals.darkMode\n if (storybookDarkMode !== isDarkMode.value) {\n applyDarkModeTheme(storybookDarkMode ? 'dark' : 'light')\n }\n }\n \n // tweakcn 테마 동기화\n if (storybookGlobals.theme && storybookGlobals.theme !== currentTheme.value) {\n const storybookTheme = String(storybookGlobals.theme)\n if (themeOptions.value.includes(storybookTheme)) {\n applyTweakcnTheme(storybookTheme)\n }\n }\n }\n } catch (e) {\n // 무시\n }\n }\n \n // 초기 확인\n checkStorybookGlobals()\n \n // 주기적으로 확인 (스토리북이 직접 이벤트를 제공하지 않는 경우)\n intervalId = setInterval(checkStorybookGlobals, 500)\n }\n \n // 컴포넌트 언마운트 시 정리\n onUnmounted(() => {\n if (themeObserver) {\n themeObserver.disconnect()\n themeObserver = null\n }\n if (styleObserver) {\n styleObserver.disconnect()\n styleObserver = null\n }\n if (intervalId) {\n clearInterval(intervalId)\n }\n })\n})\n</script>\n\n<template>\n <header :class=\"cn('flex items-center justify-between w-full', preset.containerClass)\">\n <!-- 왼쪽: 햄버거 메뉴 + 로고 + 네비게이션 -->\n <div class=\"flex items-center gap-6 flex-1\">\n <!-- 햄버거 메뉴 버튼 (사이드바 토글) -->\n <JButton\n v-if=\"showSidebarToggle\"\n variant=\"ghost\"\n size=\"icon\"\n class=\"flex-shrink-0\"\n aria-label=\"사이드바 토글\"\n @click=\"handleSidebarToggle\"\n >\n <JIcon name=\"menu\" size=\"md\" />\n </JButton>\n\n <!-- 로고 -->\n <div\n v-if=\"displayLogoImage || logoText\"\n class=\"flex items-center cursor-pointer\"\n @click=\"handleLogoClick\"\n >\n <!-- 로고 이미지 (logo prop이 있고 에러가 2 미만일 때) -->\n <img\n v-if=\"displayLogoImage && logoImageError < 2\"\n :src=\"displayLogoImage\"\n alt=\"로고\"\n class=\"h-8 w-auto\"\n @error=\"handleLogoError\"\n />\n <!-- 로고 텍스트 (기본 또는 모든 이미지 실패 시) -->\n <span\n v-else-if=\"logoText\"\n class=\"text-lg font-bold text-foreground\"\n >\n {{ logoText }}\n </span>\n </div>\n\n <!-- 네비게이션 -->\n <nav\n v-if=\"navItems && navItems.length > 0\"\n class=\"flex items-center gap-1\"\n >\n <JButton\n v-for=\"(item, index) in navItems\"\n :key=\"index\"\n variant=\"ghost\"\n :class=\"cn(\n preset.navItemClass,\n item.active && preset.navItemActiveClass\n )\"\n @click=\"handleNavClick(item, index)\"\n >\n <JIcon\n v-if=\"item.icon\"\n :name=\"item.icon\"\n size=\"sm\"\n class=\"mr-1.5\"\n />\n {{ item.label }}\n </JButton>\n </nav>\n </div>\n\n <!-- 오른쪽: 테마 선택기 + 알림 + 다크모드 토글 + 사용자 메뉴 -->\n <div class=\"flex items-center gap-3\">\n <!-- tweakcn 테마 선택기 -->\n <JPopover\n v-if=\"showThemeSelector\"\n position=\"bottom\"\n align=\"end\"\n styletype=\"default-sm\"\n >\n <template #trigger>\n <JButton\n variant=\"ghost\"\n size=\"icon\"\n aria-label=\"테마 선택\"\n >\n <JIcon name=\"palette\" size=\"md\" />\n </JButton>\n </template>\n <div class=\"p-2 min-w-[200px]\">\n <div class=\"text-xs font-medium text-muted-foreground px-2 py-1.5 mb-1\">\n 테마 선택\n </div>\n <div class=\"space-y-1\">\n <button\n v-for=\"theme in themeOptions\"\n :key=\"theme\"\n :class=\"cn(\n 'w-full flex items-center gap-2 px-2 py-1.5 text-sm rounded-md transition-colors',\n 'hover:bg-accent hover:text-accent-foreground',\n 'border-0 outline-none text-left',\n currentTheme === theme && 'bg-accent text-accent-foreground font-medium'\n )\"\n @click=\"handleThemeChange(theme)\"\n >\n <JIcon\n v-if=\"currentTheme === theme\"\n name=\"check\"\n size=\"sm\"\n class=\"flex-shrink-0\"\n />\n <span v-else class=\"w-4\" />\n <span class=\"flex-1 capitalize\">{{ theme }}</span>\n </button>\n </div>\n </div>\n </JPopover>\n\n <!-- 알림 -->\n <JPopover\n v-if=\"showNotifications\"\n position=\"bottom\"\n align=\"end\"\n styletype=\"default-sm\"\n >\n <template #trigger>\n <JButton\n variant=\"ghost\"\n size=\"icon\"\n class=\"relative\"\n aria-label=\"알림\"\n >\n <JIcon name=\"circleAlert\" size=\"md\" />\n <span\n v-if=\"unreadCount > 0\"\n class=\"absolute top-0 right-0 h-4 w-4 rounded-full bg-destructive text-destructive-foreground text-xs flex items-center justify-center\"\n >\n {{ unreadCount > 9 ? '9+' : unreadCount }}\n </span>\n </JButton>\n </template>\n <div class=\"p-2\">\n <div\n v-if=\"notifications && notifications.length > 0\"\n class=\"max-h-96 overflow-y-auto space-y-1\"\n >\n <div\n v-for=\"notification in notifications\"\n :key=\"notification.id\"\n :class=\"cn(\n 'p-3 rounded-md cursor-pointer transition-colors',\n !notification.read ? 'bg-accent' : 'hover:bg-accent/50'\n )\"\n @click=\"handleNotificationClick(notification)\"\n >\n <div class=\"flex items-start gap-2\">\n <JIcon\n v-if=\"notification.icon\"\n :name=\"notification.icon\"\n size=\"sm\"\n class=\"mt-0.5 flex-shrink-0\"\n />\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-sm font-medium text-foreground\">\n {{ notification.title }}\n </p>\n <p\n v-if=\"notification.message\"\n class=\"text-xs text-muted-foreground mt-1\"\n >\n {{ notification.message }}\n </p>\n <p\n v-if=\"notification.time\"\n class=\"text-xs text-muted-foreground/60 mt-1\"\n >\n {{ notification.time }}\n </p>\n </div>\n </div>\n </div>\n </div>\n <div\n v-else\n class=\"p-4 text-center text-sm text-muted-foreground\"\n >\n 알림이 없습니다.\n </div>\n </div>\n </JPopover>\n\n <!-- 다크모드 토글 버튼 -->\n <JButton\n variant=\"ghost\"\n size=\"icon\"\n :aria-label=\"isDarkMode ? '라이트 모드로 전환' : '다크 모드로 전환'\"\n @click=\"handleThemeToggle\"\n >\n <JIcon :name=\"themeIcon\" size=\"md\" />\n </JButton>\n\n <!-- 사용자 메뉴 (userName이 있으면 표시) -->\n <JPopover\n v-if=\"userName\"\n position=\"bottom\"\n align=\"end\"\n styletype=\"default-sm\"\n >\n <template #trigger>\n <div class=\"flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity\">\n <JAvatar\n :src=\"userAvatar\"\n :fallback=\"userName ? userName[0] : 'U'\"\n size=\"sm\"\n />\n <span\n class=\"text-sm text-foreground hidden sm:inline\"\n >\n {{ userName }}\n </span>\n <JIcon name=\"chevronDown\" size=\"sm\" class=\"text-muted-foreground hidden sm:inline\" />\n </div>\n </template>\n <div class=\"w-full rounded-md overflow-hidden\">\n <!-- 프로필 정보 영역 -->\n <div class=\"px-3 py-2 border-b border-border\">\n <p class=\"text-sm font-medium text-foreground\">{{ userName }}</p>\n <p\n v-if=\"userEmail\"\n class=\"text-xs text-muted-foreground mt-0.5\"\n >\n {{ userEmail }}\n </p>\n </div>\n \n <!-- 메뉴 아이템 -->\n <template v-for=\"(group, groupIndex) in userMenuGroups\" :key=\"groupIndex\">\n <template v-for=\"(item, itemIndex) in group.items\" :key=\"item.id\">\n <button\n v-if=\"!item.separator\"\n :disabled=\"item.disabled\"\n :class=\"cn(\n 'w-full flex items-center gap-2 px-3 py-1.5 text-sm transition-colors',\n item.id === 'logout' \n ? 'text-destructive hover:bg-destructive/10 hover:text-destructive' \n : 'hover:bg-accent hover:text-accent-foreground',\n 'disabled:opacity-50 disabled:pointer-events-none',\n 'border-0 outline-none',\n group.items[itemIndex + 1]?.separator && 'border-b-0'\n )\"\n @click=\"handleUserMenuSelect(item.id)\"\n >\n <JIcon\n v-if=\"item.icon\"\n :name=\"item.icon\"\n size=\"sm\"\n :class=\"cn(\n 'flex-shrink-0',\n item.id === 'logout' && 'text-destructive'\n )\"\n />\n <span class=\"flex-1 text-left truncate\">{{ item.label }}</span>\n </button>\n <div\n v-else-if=\"item.separator && itemIndex > 0\"\n class=\"h-px bg-muted my-1\"\n />\n </template>\n <div\n v-if=\"groupIndex < userMenuGroups.length - 1\"\n class=\"h-px bg-muted my-1\"\n />\n </template>\n </div>\n </JPopover>\n\n <!-- 로그인 버튼 (userName이 없으면 표시) -->\n <JButton\n v-else\n variant=\"ghost\"\n size=\"sm\"\n aria-label=\"로그인\"\n @click=\"handleLogin\"\n >\n <JIcon name=\"logIn\" size=\"sm\" class=\"sm:mr-1.5\" />\n <span class=\"hidden sm:inline\">로그인</span>\n </JButton>\n\n <!-- 커스텀 슬롯 -->\n <slot name=\"actions\" />\n </div>\n </header>\n</template>\n"],"names":["props","__props","emit","__emit","STYLE_PRESETS","preset","computed","logoImageError","ref","logoImageSrc","displayLogoImage","logoFallback","handleLogoError","unreadCount","n","fixedUserMenuItems","userMenuGroups","handleLogoClick","handleNavClick","item","index","handleNotificationClick","handleUserMenuSelect","itemId","handleLogin","handleSidebarToggle","isDarkMode","currentTheme","getDefaultTheme","themeOptions","isInternalUpdate","themeObserver","styleObserver","isStorybook","updateThemeOptions","detectedThemes","detectThemeClasses","ensuredThemes","ensureDefaultTheme","filtered","theme","getStoredDarkModeTheme","stored","applyDarkModeTheme","root","handleThemeToggle","newTheme","getStoredTweakcnTheme","validateThemeExists","getStoredTheme","applyTweakcnTheme","validatedTheme","success","applyTheme","setStoredTheme","storybookGlobals","defaultTheme","handleThemeChange","themeIcon","onMounted","darkModeTheme","storedTheme","hasDarkClass","currentThemeClass","cls","themeName","intervalId","checkStorybookGlobals","storybookDarkMode","storybookTheme","onUnmounted","_createElementBlock","_normalizeClass","_unref","cn","_createElementVNode","_hoisted_1","_createBlock","JButton","_createVNode","JIcon","_hoisted_3","_toDisplayString","_openBlock","_hoisted_4","_Fragment","_renderList","$event","_hoisted_5","JPopover","_hoisted_6","_cache","_hoisted_7","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","notification","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21","JAvatar","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","group","groupIndex","itemIndex","_hoisted_29","_hoisted_28","_hoisted_30","_renderSlot","_ctx"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"JHeader.vue.cjs","sources":["../../../../src/components/organisms/JHeader.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, onMounted, onUnmounted } from 'vue'\nimport JIcon from '@/components/atoms/JIcon.vue'\nimport JAvatar from '@/components/atoms/JAvatar.vue'\nimport JButton from '@/components/atoms/JButton.vue'\nimport JPopover from '@/components/atoms/JPopover.vue'\nimport { cn } from '@/lib/utils'\nimport type { ContextMenuItem, ContextMenuGroup } from '@/types/context-menu.types'\nimport {\n detectThemeClasses,\n ensureDefaultTheme,\n validateThemeExists,\n getDefaultTheme,\n getStoredTheme,\n setStoredTheme,\n applyTheme,\n} from '@/lib/theme-utils'\n\n// Fallback 로고 이미지 import (logo prop이 없거나 로드 실패 시 사용)\nimport logoFallback from '@/assets/images/logo-fallback.png'\n\n/**\n * JHeader - 상단 헤더 컴포넌트 (organisms)\n * Header Component\n * \n * @description\n * 애플리케이션의 상단 헤더 영역을 담당하는 컴포넌트입니다.\n * 로고, 네비게이션, 검색, 알림, 사용자 메뉴 등을 포함할 수 있습니다.\n * \n * @example\n * ```vue\n * <JHeader\n * logo=\"/logo.png\"\n * :user-menu-items=\"userMenuItems\"\n * @logo-click=\"handleLogoClick\"\n * />\n * ```\n */\n\nexport type HeaderNavItem = {\n /** 라벨 */\n label: string\n /** 링크 URL */\n href?: string\n /** 아이콘 */\n icon?: string\n /** 활성 상태 */\n active?: boolean\n /** 클릭 핸들러 */\n onClick?: () => void\n}\n\nexport type NotificationItem = {\n /** 알림 ID */\n id: string\n /** 알림 제목 */\n title: string\n /** 알림 내용 */\n message?: string\n /** 알림 시간 */\n time?: string\n /** 읽음 상태 */\n read?: boolean\n /** 아이콘 */\n icon?: string\n /** 클릭 핸들러 */\n onClick?: () => void\n}\n\ntype StyleType =\n | 'default' // 기본 스타일\n | 'minimal' // 최소 스타일\n\nconst props = withDefaults(\n defineProps<{\n /** 로고 이미지 URL */\n logo?: string\n /** 로고 텍스트 (기본값, 이미지가 없을 때) */\n logoText?: string\n /** 네비게이션 아이템 목록 */\n navItems?: HeaderNavItem[]\n /** 알림 표시 여부 */\n showNotifications?: boolean\n /** 알림 목록 */\n notifications?: NotificationItem[]\n /** 사용자 아바타 이미지 */\n userAvatar?: string\n /** 사용자 이름 */\n userName?: string\n /** 로그인 상태 */\n isLoggedIn?: boolean\n /** 사용자 이메일 */\n userEmail?: string\n /** 사용자 ID */\n userId?: string\n /** 스타일 타입 */\n styletype?: StyleType\n /** 사이드바 토글 버튼 표시 여부 */\n showSidebarToggle?: boolean\n /** 사이드바 열림 상태 */\n isSidebarOpen?: boolean\n /** 테마 선택기 표시 여부 */\n showThemeSelector?: boolean\n /** 초기 테마 (기본값: 'default' 또는 저장된 테마) */\n defaultTheme?: string\n /** 선택 가능한 테마 목록 (지정하지 않으면 모든 감지된 테마 사용) */\n availableThemes?: string[]\n }>(),\n {\n logoText: 'JWMS Portal', // 기본값: 텍스트 로고\n showNotifications: false,\n notifications: () => [],\n styletype: 'default',\n showSidebarToggle: true,\n isSidebarOpen: true,\n showThemeSelector: true,\n defaultTheme: undefined,\n availableThemes: undefined,\n }\n)\n\nconst emit = defineEmits<{\n /** 로고 클릭 이벤트 */\n logoClick: []\n /** 네비게이션 아이템 클릭 이벤트 */\n navClick: [item: HeaderNavItem, index: number]\n /** 알림 클릭 이벤트 */\n notificationClick: [item: NotificationItem]\n /** 사용자 메뉴 아이템 선택 이벤트 */\n userMenuSelect: [itemId: string]\n /** 사이드바 토글 이벤트 */\n sidebarToggle: []\n /** 로그인 버튼 클릭 이벤트 */\n login: []\n}>()\n\n/**\n * 스타일 프리셋\n */\nconst STYLE_PRESETS: Record<StyleType, {\n containerClass: string\n navItemClass: string\n navItemActiveClass: string\n}> = {\n default: {\n containerClass: 'h-10 px-4 border-b border-border bg-background',\n navItemClass: 'text-xs text-muted-foreground hover:text-foreground transition-colors px-3 py-1.5 rounded-md hover:bg-accent',\n navItemActiveClass: 'text-foreground font-medium bg-accent',\n },\n minimal: {\n containerClass: 'h-8 px-3 border-b border-border bg-background',\n navItemClass: 'text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded-md hover:bg-accent',\n navItemActiveClass: 'text-foreground font-medium bg-accent',\n },\n}\n\nconst preset = computed(() => {\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\n})\n\n/**\n * 로고 이미지 로드 에러 상태\n * 0: 에러 없음, 1: 첫 번째 이미지(logo) 에러, 2: fallback 이미지도 에러\n */\nconst logoImageError = ref(0)\n\n/**\n * 로고 이미지 경로 계산\n */\nconst logoImageSrc = computed(() => {\n // props.logo가 있으면 사용\n if (props.logo) {\n return props.logo\n }\n return undefined\n})\n\n/**\n * 실제 표시할 로고 이미지 경로\n * 1. props.logo가 있으면 사용\n * 2. logo 에러 시 fallback 이미지\n * 3. fallback도 에러면 undefined (텍스트로 대체)\n */\nconst displayLogoImage = computed(() => {\n // logo prop이 없으면 이미지 표시 안함 (텍스트 사용)\n if (!props.logo) {\n return undefined\n }\n \n // logo 에러가 발생했으면 fallback 사용\n if (logoImageError.value >= 1 && logoFallback) {\n return logoFallback\n }\n \n // 정상적으로 logo 사용\n return logoImageSrc.value\n})\n\n/**\n * 로고 이미지 로드 에러 핸들러\n */\nconst handleLogoError = () => {\n // 첫 번째 이미지(logo) 에러\n if (logoImageError.value === 0) {\n logoImageError.value = 1\n }\n // fallback 이미지도 에러면 2로 설정 (텍스트로 대체)\n else if (logoImageError.value === 1) {\n logoImageError.value = 2\n }\n}\n\n/**\n * 읽지 않은 알림 개수\n */\nconst unreadCount = computed(() => {\n return props.notifications?.filter(n => !n.read).length || 0\n})\n\n/**\n * 고정 사용자 메뉴 아이템\n */\nconst fixedUserMenuItems: ContextMenuItem[] = [\n { id: 'profile', label: '프로필', icon: 'user' },\n { id: 'settings', label: '설정', icon: 'settings' },\n { id: 'separator', label: '', separator: true },\n { id: 'logout', label: '로그아웃', icon: 'logOut' },\n]\n\n/**\n * 사용자 메뉴 아이템을 ContextMenuGroup 형식으로 변환\n */\nconst userMenuGroups = computed<ContextMenuGroup[]>(() => {\n return [{\n items: fixedUserMenuItems\n }]\n})\n\n/**\n * 로고 클릭 핸들러\n */\nconst handleLogoClick = () => {\n emit('logoClick')\n}\n\n/**\n * 네비게이션 아이템 클릭 핸들러\n */\nconst handleNavClick = (item: HeaderNavItem, index: number) => {\n item.onClick?.()\n emit('navClick', item, index)\n}\n\n/**\n * 알림 클릭 핸들러\n */\nconst handleNotificationClick = (item: NotificationItem) => {\n item.onClick?.()\n emit('notificationClick', item)\n}\n\n/**\n * 사용자 메뉴 선택 핸들러\n */\nconst handleUserMenuSelect = (itemId: string) => {\n emit('userMenuSelect', itemId)\n}\n\n/**\n * 로그인 버튼 클릭 핸들러\n */\nconst handleLogin = () => {\n emit('login')\n}\n\n/**\n * 사이드바 토글 핸들러\n */\nconst handleSidebarToggle = () => {\n emit('sidebarToggle')\n}\n\n/**\n * tweakcn 테마 타입 정의 (동적 감지 지원)\n */\ntype ThemeName = string\n\n/**\n * 다크모드 상태를 추적하는 반응형 변수\n * - false: 라이트 모드\n * - true: 다크 모드\n * 초기값은 false(라이트 모드)이며, 컴포넌트 마운트 시 실제 테마로 업데이트됩니다.\n */\nconst isDarkMode = ref(false)\n\n/**\n * 현재 선택된 tweakcn 테마 (디폴트 테마로 초기화)\n */\nconst currentTheme = ref<ThemeName>(getDefaultTheme())\n\n/**\n * 테마 옵션 목록 (동적으로 감지)\n */\nconst themeOptions = ref<ThemeName[]>([])\n\n/**\n * 내부 업데이트 플래그\n * - true: 컴포넌트 내부에서 테마를 변경 중 (MutationObserver가 무시해야 함)\n * - false: 외부에서 테마가 변경됨 (MutationObserver가 동기화해야 함)\n */\nlet isInternalUpdate = false\n\n/**\n * MutationObserver 인스턴스\n */\nlet themeObserver: MutationObserver | null = null\nlet styleObserver: MutationObserver | null = null\n\n/**\n * Storybook 환경인지 확인하는 함수\n * \n * @returns Storybook 환경이면 true, 아니면 false\n */\nconst isStorybook = (): boolean => {\n if (typeof window === 'undefined') return false\n return !!(\n window.location?.href?.includes('storybook') ||\n (window as any).__STORYBOOK_GLOBALS__\n )\n}\n\n/**\n * 테마 목록을 동적으로 감지하고 업데이트합니다.\n */\nconst updateThemeOptions = () => {\n const detectedThemes = detectThemeClasses()\n const ensuredThemes = ensureDefaultTheme(detectedThemes)\n \n // availableThemes prop이 지정되면 필터링\n if (props.availableThemes && props.availableThemes.length > 0) {\n const filtered = ensuredThemes.filter(theme => props.availableThemes!.includes(theme))\n // default는 항상 포함\n themeOptions.value = ensureDefaultTheme(filtered)\n } else {\n themeOptions.value = ensuredThemes\n }\n}\n\n/**\n * localStorage에서 저장된 다크모드 테마를 읽어오는 함수\n * \n * 우선순위:\n * 1. localStorage에 저장된 'theme' 값 (사용자가 이전에 선택한 테마)\n * 2. 시스템 설정 (OS의 다크모드 설정)\n * 3. 기본값 'light' (라이트 모드)\n * \n * @returns 'light' | 'dark' - 적용할 테마\n */\nconst getStoredDarkModeTheme = (): 'light' | 'dark' => {\n if (typeof window === 'undefined') return 'light'\n \n const stored = localStorage.getItem('theme')\n if (stored === 'dark' || stored === 'light') {\n return stored\n }\n \n if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {\n return 'dark'\n }\n \n return 'light'\n}\n\n/**\n * 다크모드 테마를 HTML 문서에 적용하는 함수\n * \n * @param theme - 적용할 테마 ('light' 또는 'dark')\n */\nconst applyDarkModeTheme = (theme: 'light' | 'dark') => {\n isInternalUpdate = true\n \n const root = document.documentElement\n \n if (theme === 'dark') {\n root.classList.add('dark')\n isDarkMode.value = true\n } else {\n root.classList.remove('dark')\n isDarkMode.value = false\n }\n \n localStorage.setItem('theme', theme)\n \n isInternalUpdate = false\n}\n\n/**\n * 테마 토글 버튼 클릭 시 호출되는 핸들러 함수\n */\nconst handleThemeToggle = () => {\n const newTheme = isDarkMode.value ? 'light' : 'dark'\n applyDarkModeTheme(newTheme)\n}\n\n/**\n * 저장된 tweakcn 테마를 가져와서 적용합니다.\n */\nconst getStoredTweakcnTheme = (): ThemeName => {\n // defaultTheme prop이 지정되면 우선 사용\n if (props.defaultTheme) {\n const validated = validateThemeExists(props.defaultTheme)\n return validated\n }\n \n const stored = getStoredTheme('tweakcn-theme')\n if (!stored) {\n return getDefaultTheme()\n }\n \n // 저장된 테마가 유효한지 검증\n const validated = validateThemeExists(stored)\n return validated\n}\n\n/**\n * tweakcn 테마를 HTML 문서에 적용하는 함수\n */\nconst applyTweakcnTheme = (theme: ThemeName): boolean => {\n isInternalUpdate = true\n \n const validatedTheme = validateThemeExists(theme)\n const success = applyTheme(validatedTheme)\n \n if (success) {\n currentTheme.value = validatedTheme\n setStoredTheme(validatedTheme, 'tweakcn-theme')\n \n // 스토리북 환경에서만 전역 상태 동기화\n if (isStorybook()) {\n try {\n const storybookGlobals = (window as any).__STORYBOOK_GLOBALS__\n if (storybookGlobals) {\n storybookGlobals.theme = validatedTheme\n }\n } catch (e) {\n // 무시\n }\n }\n } else {\n // 실패 시 디폴트 테마로 폴백\n const defaultTheme = getDefaultTheme()\n applyTheme(defaultTheme)\n currentTheme.value = defaultTheme\n setStoredTheme(defaultTheme, 'tweakcn-theme')\n }\n \n isInternalUpdate = false\n return success\n}\n\n/**\n * 테마 변경 핸들러\n */\nconst handleThemeChange = (theme: ThemeName | string | number) => {\n applyTweakcnTheme(String(theme))\n}\n\n/**\n * 현재 테마에 따라 표시할 아이콘을 계산하는 computed 속성\n */\nconst themeIcon = computed(() => {\n return isDarkMode.value ? 'sun' : 'moon'\n})\n\n/**\n * 초기화: 저장된 테마 적용 및 테마 목록 감지\n */\nonMounted(() => {\n // 테마 목록 초기 감지\n updateThemeOptions()\n \n // 저장된 다크모드 테마 적용\n const darkModeTheme = getStoredDarkModeTheme()\n applyDarkModeTheme(darkModeTheme)\n \n // 저장된 tweakcn 테마 적용\n const storedTheme = getStoredTweakcnTheme()\n applyTweakcnTheme(storedTheme)\n \n // HTML의 dark 클래스 변경을 감지하여 헤더 컴포넌트 상태를 동기화\n const root = document.documentElement\n \n // 현재 dark 클래스 상태로 초기화\n isDarkMode.value = root.classList.contains('dark')\n \n // MutationObserver로 동적 테마 클래스 감지\n themeObserver = new MutationObserver(() => {\n if (isInternalUpdate) return\n \n // 다크모드 클래스 변경 감지\n const hasDarkClass = root.classList.contains('dark')\n if (hasDarkClass !== isDarkMode.value) {\n isDarkMode.value = hasDarkClass\n localStorage.setItem('theme', hasDarkClass ? 'dark' : 'light')\n }\n \n // 테마 목록 업데이트\n updateThemeOptions()\n \n // 현재 적용된 tweakcn 테마 확인\n const currentThemeClass = Array.from(root.classList).find(cls => cls.startsWith('theme-'))\n if (currentThemeClass) {\n const themeName = currentThemeClass.replace('theme-', '')\n if (themeOptions.value.includes(themeName)) {\n currentTheme.value = themeName\n }\n }\n })\n \n // document.documentElement의 클래스 변경 감지\n themeObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['class'],\n })\n \n // 스타일시트 변경 감지 (동적으로 추가된 테마 감지)\n styleObserver = new MutationObserver(() => {\n updateThemeOptions()\n })\n \n styleObserver.observe(document.head, {\n childList: true,\n subtree: true,\n })\n \n // 스토리북 환경에서만 다크모드 및 테마 변경 감지 (추가 보완)\n let intervalId: ReturnType<typeof setInterval> | null = null\n \n if (isStorybook()) {\n const checkStorybookGlobals = () => {\n try {\n const storybookGlobals = (window as any).__STORYBOOK_GLOBALS__\n if (storybookGlobals) {\n // 다크모드 동기화\n if (typeof storybookGlobals.darkMode !== 'undefined') {\n const storybookDarkMode = storybookGlobals.darkMode\n if (storybookDarkMode !== isDarkMode.value) {\n applyDarkModeTheme(storybookDarkMode ? 'dark' : 'light')\n }\n }\n \n // tweakcn 테마 동기화\n if (storybookGlobals.theme && storybookGlobals.theme !== currentTheme.value) {\n const storybookTheme = String(storybookGlobals.theme)\n if (themeOptions.value.includes(storybookTheme)) {\n applyTweakcnTheme(storybookTheme)\n }\n }\n }\n } catch (e) {\n // 무시\n }\n }\n \n // 초기 확인\n checkStorybookGlobals()\n \n // 주기적으로 확인 (스토리북이 직접 이벤트를 제공하지 않는 경우)\n intervalId = setInterval(checkStorybookGlobals, 500)\n }\n \n // 컴포넌트 언마운트 시 정리\n onUnmounted(() => {\n if (themeObserver) {\n themeObserver.disconnect()\n themeObserver = null\n }\n if (styleObserver) {\n styleObserver.disconnect()\n styleObserver = null\n }\n if (intervalId) {\n clearInterval(intervalId)\n }\n })\n})\n</script>\n\n<template>\n <header :class=\"cn('flex items-center justify-between w-full', preset.containerClass)\">\n <!-- 왼쪽: 햄버거 메뉴 + 로고 + 네비게이션 -->\n <div class=\"flex items-center gap-6 flex-1\">\n <!-- 햄버거 메뉴 버튼 (사이드바 토글) -->\n <JButton\n v-if=\"showSidebarToggle\"\n variant=\"ghost\"\n size=\"sm\"\n class=\"flex-shrink-0 w-8 h-8 p-0\"\n aria-label=\"사이드바 토글\"\n @click=\"handleSidebarToggle\"\n >\n <JIcon name=\"menu\" size=\"sm\" />\n </JButton>\n\n <!-- 로고 -->\n <div\n v-if=\"displayLogoImage || logoText\"\n class=\"flex items-center cursor-pointer\"\n @click=\"handleLogoClick\"\n >\n <!-- 로고 이미지 (logo prop이 있고 에러가 2 미만일 때) -->\n <img\n v-if=\"displayLogoImage && logoImageError < 2\"\n :src=\"displayLogoImage\"\n alt=\"로고\"\n class=\"h-6 w-auto\"\n @error=\"handleLogoError\"\n />\n <!-- 로고 텍스트 (기본 또는 모든 이미지 실패 시) -->\n <span\n v-else-if=\"logoText\"\n class=\"text-sm font-bold text-foreground\"\n >\n {{ logoText }}\n </span>\n </div>\n\n <!-- 네비게이션 -->\n <nav\n v-if=\"navItems && navItems.length > 0\"\n class=\"flex items-center gap-1\"\n >\n <JButton\n v-for=\"(item, index) in navItems\"\n :key=\"index\"\n variant=\"ghost\"\n :class=\"cn(\n preset.navItemClass,\n item.active && preset.navItemActiveClass\n )\"\n @click=\"handleNavClick(item, index)\"\n >\n <JIcon\n v-if=\"item.icon\"\n :name=\"item.icon\"\n size=\"sm\"\n class=\"mr-1.5\"\n />\n {{ item.label }}\n </JButton>\n </nav>\n </div>\n\n <!-- 오른쪽: 테마 선택기 + 알림 + 다크모드 토글 + 사용자 메뉴 -->\n <div class=\"flex items-center gap-3\">\n <!-- tweakcn 테마 선택기 -->\n <JPopover\n v-if=\"showThemeSelector\"\n position=\"bottom\"\n align=\"end\"\n styletype=\"default-sm\"\n >\n <template #trigger>\n <JButton\n variant=\"ghost\"\n size=\"sm\"\n class=\"w-8 h-8 p-0\"\n aria-label=\"테마 선택\"\n >\n <JIcon name=\"palette\" size=\"sm\" />\n </JButton>\n </template>\n <div class=\"p-2 min-w-[200px]\">\n <div class=\"text-xs font-medium text-muted-foreground px-2 py-1.5 mb-1\">\n 테마 선택\n </div>\n <div class=\"space-y-1\">\n <button\n v-for=\"theme in themeOptions\"\n :key=\"theme\"\n :class=\"cn(\n 'w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded-md transition-colors',\n 'hover:bg-accent hover:text-accent-foreground',\n 'border-0 outline-none text-left',\n currentTheme === theme && 'bg-accent text-accent-foreground font-medium'\n )\"\n @click=\"handleThemeChange(theme)\"\n >\n <JIcon\n v-if=\"currentTheme === theme\"\n name=\"check\"\n size=\"sm\"\n class=\"flex-shrink-0\"\n />\n <span v-else class=\"w-4\" />\n <span class=\"flex-1 capitalize\">{{ theme }}</span>\n </button>\n </div>\n </div>\n </JPopover>\n\n <!-- 알림 -->\n <JPopover\n v-if=\"showNotifications\"\n position=\"bottom\"\n align=\"end\"\n styletype=\"default-sm\"\n >\n <template #trigger>\n <JButton\n variant=\"ghost\"\n size=\"sm\"\n class=\"relative w-8 h-8 p-0\"\n aria-label=\"알림\"\n >\n <JIcon name=\"circleAlert\" size=\"sm\" />\n <span\n v-if=\"unreadCount > 0\"\n class=\"absolute top-0 right-0 h-4 w-4 rounded-full bg-destructive text-destructive-foreground text-xs flex items-center justify-center\"\n >\n {{ unreadCount > 9 ? '9+' : unreadCount }}\n </span>\n </JButton>\n </template>\n <div class=\"p-2\">\n <div\n v-if=\"notifications && notifications.length > 0\"\n class=\"max-h-96 overflow-y-auto space-y-1\"\n >\n <div\n v-for=\"notification in notifications\"\n :key=\"notification.id\"\n :class=\"cn(\n 'p-3 rounded-md cursor-pointer transition-colors',\n !notification.read ? 'bg-accent' : 'hover:bg-accent/50'\n )\"\n @click=\"handleNotificationClick(notification)\"\n >\n <div class=\"flex items-start gap-2\">\n <JIcon\n v-if=\"notification.icon\"\n :name=\"notification.icon\"\n size=\"sm\"\n class=\"mt-0.5 flex-shrink-0\"\n />\n <div class=\"flex-1 min-w-0\">\n <p class=\"text-xs font-medium text-foreground\">\n {{ notification.title }}\n </p>\n <p\n v-if=\"notification.message\"\n class=\"text-xs text-muted-foreground mt-1\"\n >\n {{ notification.message }}\n </p>\n <p\n v-if=\"notification.time\"\n class=\"text-xs text-muted-foreground/60 mt-1\"\n >\n {{ notification.time }}\n </p>\n </div>\n </div>\n </div>\n </div>\n <div\n v-else\n class=\"p-4 text-center text-xs text-muted-foreground\"\n >\n 알림이 없습니다.\n </div>\n </div>\n </JPopover>\n\n <!-- 다크모드 토글 버튼 -->\n <JButton\n variant=\"ghost\"\n size=\"sm\"\n class=\"w-8 h-8 p-0\"\n :aria-label=\"isDarkMode ? '라이트 모드로 전환' : '다크 모드로 전환'\"\n @click=\"handleThemeToggle\"\n >\n <JIcon :name=\"themeIcon\" size=\"sm\" />\n </JButton>\n\n <!-- 커스텀 툴바 슬롯 (다크모드 토글 버튼 우측) -->\n <slot name=\"toolbar\" />\n\n <!-- 사용자 메뉴 (userName이 있으면 표시) -->\n <JPopover\n v-if=\"userName\"\n position=\"bottom\"\n align=\"end\"\n styletype=\"default-sm\"\n >\n <template #trigger>\n <div class=\"flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity\">\n <JAvatar\n :src=\"userAvatar\"\n :fallback=\"userName ? userName[0] : 'U'\"\n size=\"xs\"\n />\n <span\n class=\"text-xs text-foreground hidden sm:inline\"\n >\n {{ userName }}\n </span>\n <JIcon name=\"chevronDown\" size=\"sm\" class=\"text-muted-foreground hidden sm:inline\" />\n </div>\n </template>\n <div class=\"w-full rounded-md overflow-hidden\">\n <!-- 프로필 정보 영역 -->\n <div class=\"px-3 py-1.5 border-b border-border\">\n <p class=\"text-xs font-medium text-foreground\">{{ userName }}</p>\n <p\n v-if=\"userEmail\"\n class=\"text-xs text-muted-foreground mt-0.5\"\n >\n {{ userEmail }}\n </p>\n </div>\n \n <!-- 메뉴 아이템 -->\n <template v-for=\"(group, groupIndex) in userMenuGroups\" :key=\"groupIndex\">\n <template v-for=\"(item, itemIndex) in group.items\" :key=\"item.id\">\n <button\n v-if=\"!item.separator\"\n :disabled=\"item.disabled\"\n :class=\"cn(\n 'w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors',\n item.id === 'logout' \n ? 'text-destructive hover:bg-destructive/10 hover:text-destructive' \n : 'hover:bg-accent hover:text-accent-foreground',\n 'disabled:opacity-50 disabled:pointer-events-none',\n 'border-0 outline-none',\n group.items[itemIndex + 1]?.separator && 'border-b-0'\n )\"\n @click=\"handleUserMenuSelect(item.id)\"\n >\n <JIcon\n v-if=\"item.icon\"\n :name=\"item.icon\"\n size=\"sm\"\n :class=\"cn(\n 'flex-shrink-0',\n item.id === 'logout' && 'text-destructive'\n )\"\n />\n <span class=\"flex-1 text-left truncate\">{{ item.label }}</span>\n </button>\n <div\n v-else-if=\"item.separator && itemIndex > 0\"\n class=\"h-px bg-muted my-1\"\n />\n </template>\n <div\n v-if=\"groupIndex < userMenuGroups.length - 1\"\n class=\"h-px bg-muted my-1\"\n />\n </template>\n </div>\n </JPopover>\n\n <!-- 로그인 버튼 (userName이 없으면 표시) -->\n <JButton\n v-else\n variant=\"ghost\"\n size=\"sm\"\n aria-label=\"로그인\"\n @click=\"handleLogin\"\n >\n <JIcon name=\"logIn\" size=\"sm\" class=\"sm:mr-1.5\" />\n <span class=\"hidden sm:inline\">로그인</span>\n </JButton>\n </div>\n </header>\n</template>\n"],"names":["props","__props","emit","__emit","STYLE_PRESETS","preset","computed","logoImageError","ref","logoImageSrc","displayLogoImage","logoFallback","handleLogoError","unreadCount","n","fixedUserMenuItems","userMenuGroups","handleLogoClick","handleNavClick","item","index","handleNotificationClick","handleUserMenuSelect","itemId","handleLogin","handleSidebarToggle","isDarkMode","currentTheme","getDefaultTheme","themeOptions","isInternalUpdate","themeObserver","styleObserver","isStorybook","updateThemeOptions","detectedThemes","detectThemeClasses","ensuredThemes","ensureDefaultTheme","filtered","theme","getStoredDarkModeTheme","stored","applyDarkModeTheme","root","handleThemeToggle","newTheme","getStoredTweakcnTheme","validateThemeExists","getStoredTheme","applyTweakcnTheme","validatedTheme","success","applyTheme","setStoredTheme","storybookGlobals","defaultTheme","handleThemeChange","themeIcon","onMounted","darkModeTheme","storedTheme","hasDarkClass","currentThemeClass","cls","themeName","intervalId","checkStorybookGlobals","storybookDarkMode","storybookTheme","onUnmounted","_createElementBlock","_normalizeClass","_unref","cn","_createElementVNode","_hoisted_1","_createBlock","JButton","_createVNode","JIcon","_hoisted_3","_toDisplayString","_openBlock","_hoisted_4","_Fragment","_renderList","$event","_hoisted_5","JPopover","_hoisted_6","_cache","_hoisted_7","_hoisted_9","_hoisted_10","_hoisted_11","_hoisted_12","_hoisted_13","notification","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_renderSlot","_ctx","_hoisted_21","JAvatar","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","group","groupIndex","itemIndex","_hoisted_29","_hoisted_28","_hoisted_30"],"mappings":"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"}
|
|
@@ -8,7 +8,7 @@ import { getDefaultTheme as V, detectThemeClasses as ke, ensureDefaultTheme as J
|
|
|
8
8
|
import H from "../../assets/images/logo-fallback.png.js";
|
|
9
9
|
const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
10
10
|
key: 1,
|
|
11
|
-
class: "text-
|
|
11
|
+
class: "text-sm font-bold text-foreground"
|
|
12
12
|
}, _e = {
|
|
13
13
|
key: 2,
|
|
14
14
|
class: "flex items-center gap-1"
|
|
@@ -21,7 +21,7 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
21
21
|
}, ze = { class: "p-2" }, Me = {
|
|
22
22
|
key: 0,
|
|
23
23
|
class: "max-h-96 overflow-y-auto space-y-1"
|
|
24
|
-
}, Ee = ["onClick"], Ne = { class: "flex items-start gap-2" }, Be = { class: "flex-1 min-w-0" }, Ae = { class: "text-
|
|
24
|
+
}, Ee = ["onClick"], Ne = { class: "flex items-start gap-2" }, Be = { class: "flex-1 min-w-0" }, Ae = { class: "text-xs font-medium text-foreground" }, De = {
|
|
25
25
|
key: 0,
|
|
26
26
|
class: "text-xs text-muted-foreground mt-1"
|
|
27
27
|
}, $e = {
|
|
@@ -29,8 +29,8 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
29
29
|
class: "text-xs text-muted-foreground/60 mt-1"
|
|
30
30
|
}, Ge = {
|
|
31
31
|
key: 1,
|
|
32
|
-
class: "p-4 text-center text-
|
|
33
|
-
}, Ue = { class: "flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity" }, Re = { class: "text-
|
|
32
|
+
class: "p-4 text-center text-xs text-muted-foreground"
|
|
33
|
+
}, Ue = { class: "flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity" }, Re = { class: "text-xs text-foreground hidden sm:inline" }, Ve = { class: "w-full rounded-md overflow-hidden" }, Ye = { class: "px-3 py-1.5 border-b border-border" }, Fe = { class: "text-xs font-medium text-foreground" }, Ke = {
|
|
34
34
|
key: 0,
|
|
35
35
|
class: "text-xs text-muted-foreground mt-0.5"
|
|
36
36
|
}, je = ["disabled", "onClick"], Je = { class: "flex-1 text-left truncate" }, Pe = {
|
|
@@ -63,12 +63,12 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
63
63
|
setup(n, { emit: q }) {
|
|
64
64
|
const h = n, y = q, F = {
|
|
65
65
|
default: {
|
|
66
|
-
containerClass: "h-
|
|
67
|
-
navItemClass: "text-
|
|
66
|
+
containerClass: "h-10 px-4 border-b border-border bg-background",
|
|
67
|
+
navItemClass: "text-xs text-muted-foreground hover:text-foreground transition-colors px-3 py-1.5 rounded-md hover:bg-accent",
|
|
68
68
|
navItemActiveClass: "text-foreground font-medium bg-accent"
|
|
69
69
|
},
|
|
70
70
|
minimal: {
|
|
71
|
-
containerClass: "h-
|
|
71
|
+
containerClass: "h-8 px-3 border-b border-border bg-background",
|
|
72
72
|
navItemClass: "text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-1 rounded-md hover:bg-accent",
|
|
73
73
|
navItemActiveClass: "text-foreground font-medium bg-accent"
|
|
74
74
|
}
|
|
@@ -197,15 +197,15 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
197
197
|
n.showSidebarToggle ? (t(), v(S, {
|
|
198
198
|
key: 0,
|
|
199
199
|
variant: "ghost",
|
|
200
|
-
size: "
|
|
201
|
-
class: "flex-shrink-0",
|
|
200
|
+
size: "sm",
|
|
201
|
+
class: "flex-shrink-0 w-8 h-8 p-0",
|
|
202
202
|
"aria-label": "사이드바 토글",
|
|
203
203
|
onClick: ae
|
|
204
204
|
}, {
|
|
205
205
|
default: u(() => [
|
|
206
206
|
g(k, {
|
|
207
207
|
name: "menu",
|
|
208
|
-
size: "
|
|
208
|
+
size: "sm"
|
|
209
209
|
})
|
|
210
210
|
]),
|
|
211
211
|
_: 1
|
|
@@ -219,7 +219,7 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
219
219
|
key: 0,
|
|
220
220
|
src: A.value,
|
|
221
221
|
alt: "로고",
|
|
222
|
-
class: "h-
|
|
222
|
+
class: "h-6 w-auto",
|
|
223
223
|
onError: X
|
|
224
224
|
}, null, 40, ye)) : n.logoText ? (t(), o("span", pe, f(n.logoText), 1)) : c("", !0)
|
|
225
225
|
])) : c("", !0),
|
|
@@ -256,13 +256,14 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
256
256
|
trigger: u(() => [
|
|
257
257
|
g(S, {
|
|
258
258
|
variant: "ghost",
|
|
259
|
-
size: "
|
|
259
|
+
size: "sm",
|
|
260
|
+
class: "w-8 h-8 p-0",
|
|
260
261
|
"aria-label": "테마 선택"
|
|
261
262
|
}, {
|
|
262
263
|
default: u(() => [
|
|
263
264
|
g(k, {
|
|
264
265
|
name: "palette",
|
|
265
|
-
size: "
|
|
266
|
+
size: "sm"
|
|
266
267
|
})
|
|
267
268
|
]),
|
|
268
269
|
_: 1
|
|
@@ -275,7 +276,7 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
275
276
|
(t(!0), o(x, null, E(O.value, (e) => (t(), o("button", {
|
|
276
277
|
key: e,
|
|
277
278
|
class: w(C(I)(
|
|
278
|
-
"w-full flex items-center gap-2 px-2 py-1.5 text-
|
|
279
|
+
"w-full flex items-center gap-2 px-2 py-1.5 text-xs rounded-md transition-colors",
|
|
279
280
|
"hover:bg-accent hover:text-accent-foreground",
|
|
280
281
|
"border-0 outline-none text-left",
|
|
281
282
|
_.value === e && "bg-accent text-accent-foreground font-medium"
|
|
@@ -304,14 +305,14 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
304
305
|
trigger: u(() => [
|
|
305
306
|
g(S, {
|
|
306
307
|
variant: "ghost",
|
|
307
|
-
size: "
|
|
308
|
-
class: "relative",
|
|
308
|
+
size: "sm",
|
|
309
|
+
class: "relative w-8 h-8 p-0",
|
|
309
310
|
"aria-label": "알림"
|
|
310
311
|
}, {
|
|
311
312
|
default: u(() => [
|
|
312
313
|
g(k, {
|
|
313
314
|
name: "circleAlert",
|
|
314
|
-
size: "
|
|
315
|
+
size: "sm"
|
|
315
316
|
}),
|
|
316
317
|
D.value > 0 ? (t(), o("span", Le, f(D.value > 9 ? "9+" : D.value), 1)) : c("", !0)
|
|
317
318
|
]),
|
|
@@ -350,18 +351,20 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
350
351
|
})) : c("", !0),
|
|
351
352
|
g(S, {
|
|
352
353
|
variant: "ghost",
|
|
353
|
-
size: "
|
|
354
|
+
size: "sm",
|
|
355
|
+
class: "w-8 h-8 p-0",
|
|
354
356
|
"aria-label": b.value ? "라이트 모드로 전환" : "다크 모드로 전환",
|
|
355
357
|
onClick: re
|
|
356
358
|
}, {
|
|
357
359
|
default: u(() => [
|
|
358
360
|
g(k, {
|
|
359
361
|
name: de.value,
|
|
360
|
-
size: "
|
|
362
|
+
size: "sm"
|
|
361
363
|
}, null, 8, ["name"])
|
|
362
364
|
]),
|
|
363
365
|
_: 1
|
|
364
366
|
}, 8, ["aria-label"]),
|
|
367
|
+
ve(s.$slots, "toolbar"),
|
|
365
368
|
n.userName ? (t(), v(R, {
|
|
366
369
|
key: 2,
|
|
367
370
|
position: "bottom",
|
|
@@ -373,7 +376,7 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
373
376
|
g(ge, {
|
|
374
377
|
src: n.userAvatar,
|
|
375
378
|
fallback: n.userName ? n.userName[0] : "U",
|
|
376
|
-
size: "
|
|
379
|
+
size: "xs"
|
|
377
380
|
}, null, 8, ["src", "fallback"]),
|
|
378
381
|
i("span", Re, f(n.userName), 1),
|
|
379
382
|
g(k, {
|
|
@@ -397,7 +400,7 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
397
400
|
key: 0,
|
|
398
401
|
disabled: r.disabled,
|
|
399
402
|
class: w(C(I)(
|
|
400
|
-
"w-full flex items-center gap-2 px-3 py-1.5 text-
|
|
403
|
+
"w-full flex items-center gap-2 px-3 py-1.5 text-xs transition-colors",
|
|
401
404
|
r.id === "logout" ? "text-destructive hover:bg-destructive/10 hover:text-destructive" : "hover:bg-accent hover:text-accent-foreground",
|
|
402
405
|
"disabled:opacity-50 disabled:pointer-events-none",
|
|
403
406
|
"border-0 outline-none",
|
|
@@ -438,8 +441,7 @@ const xe = { class: "flex items-center gap-6 flex-1" }, ye = ["src"], pe = {
|
|
|
438
441
|
l[1] || (l[1] = i("span", { class: "hidden sm:inline" }, "로그인", -1))
|
|
439
442
|
]),
|
|
440
443
|
_: 1
|
|
441
|
-
}))
|
|
442
|
-
ve(s.$slots, "actions")
|
|
444
|
+
}))
|
|
443
445
|
])
|
|
444
446
|
], 2));
|
|
445
447
|
}
|