@j-solution/components 1.9.2 → 1.9.4

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.
Files changed (66) hide show
  1. package/README.md +5 -6
  2. package/assets/jwms-portal-frontend-Bd12BmyZ.css +1 -0
  3. package/assets/styles/j-components.css +1 -1
  4. package/assets/styles/main.css +15 -5
  5. package/assets/styles/themes.css +46 -21
  6. package/components/atoms/JButton.vue.cjs +1 -1
  7. package/components/atoms/JButton.vue.js +1 -1
  8. package/components/atoms/JButton.vue2.cjs.map +1 -1
  9. package/components/atoms/JButton.vue2.js.map +1 -1
  10. package/components/atoms/JGrid.vue.cjs +1 -1
  11. package/components/atoms/JGrid.vue.js +2 -2
  12. package/components/atoms/JGrid.vue2.cjs +1 -1
  13. package/components/atoms/JGrid.vue2.cjs.map +1 -1
  14. package/components/atoms/JGrid.vue2.js +138 -119
  15. package/components/atoms/JGrid.vue2.js.map +1 -1
  16. package/components/atoms/JSplitter.vue.cjs +1 -1
  17. package/components/atoms/JSplitter.vue.js +2 -2
  18. package/components/atoms/JSplitter.vue2.cjs +1 -1
  19. package/components/atoms/JSplitter.vue2.cjs.map +1 -1
  20. package/components/atoms/JSplitter.vue2.js +17 -17
  21. package/components/atoms/JSplitter.vue2.js.map +1 -1
  22. package/components/molecules/JFormField.vue.cjs +1 -1
  23. package/components/molecules/JFormField.vue.js +2 -2
  24. package/components/molecules/JFormField.vue2.cjs +1 -1
  25. package/components/molecules/JFormField.vue2.cjs.map +1 -1
  26. package/components/molecules/JFormField.vue2.js +28 -28
  27. package/components/molecules/JFormField.vue2.js.map +1 -1
  28. package/components/molecules/JTabs.vue.cjs +1 -1
  29. package/components/molecules/JTabs.vue.js +2 -2
  30. package/components/molecules/JTabs.vue2.cjs +1 -1
  31. package/components/molecules/JTabs.vue2.cjs.map +1 -1
  32. package/components/molecules/JTabs.vue2.js +9 -9
  33. package/components/molecules/JTabs.vue2.js.map +1 -1
  34. package/components/organisms/JDynamicForm.vue.cjs +1 -1
  35. package/components/organisms/JDynamicForm.vue.js +2 -2
  36. package/components/organisms/JDynamicForm.vue2.cjs +1 -1
  37. package/components/organisms/JDynamicForm.vue2.cjs.map +1 -1
  38. package/components/organisms/JDynamicForm.vue2.js +5 -5
  39. package/components/organisms/JDynamicForm.vue2.js.map +1 -1
  40. package/components/organisms/JFilterBar.vue.cjs +1 -1
  41. package/components/organisms/JFilterBar.vue.js +2 -2
  42. package/components/organisms/JFilterBar.vue2.cjs +1 -1
  43. package/components/organisms/JFilterBar.vue2.cjs.map +1 -1
  44. package/components/organisms/JFilterBar.vue2.js +2 -2
  45. package/components/organisms/JFilterBar.vue2.js.map +1 -1
  46. package/components/organisms/JPageContainer.vue.cjs +1 -1
  47. package/components/organisms/JPageContainer.vue.cjs.map +1 -1
  48. package/components/organisms/JPageContainer.vue.js +1 -1
  49. package/components/organisms/JPageContainer.vue.js.map +1 -1
  50. package/components/organisms/JSearchPanel.vue.cjs +1 -1
  51. package/components/organisms/JSearchPanel.vue.js +7 -7
  52. package/components/organisms/JSearchPanel.vue2.cjs +1 -1
  53. package/components/organisms/JSearchPanel.vue2.cjs.map +1 -1
  54. package/components/organisms/JSearchPanel.vue2.js +4 -4
  55. package/components/organisms/JSearchPanel.vue2.js.map +1 -1
  56. package/components/shadcn/index.cjs +1 -1
  57. package/components/shadcn/index.cjs.map +1 -1
  58. package/components/shadcn/index.js +6 -6
  59. package/components/shadcn/index.js.map +1 -1
  60. package/components/shadcn/resizable/ResizableHandle.vue.cjs +1 -1
  61. package/components/shadcn/resizable/ResizableHandle.vue.cjs.map +1 -1
  62. package/components/shadcn/resizable/ResizableHandle.vue.js +3 -3
  63. package/components/shadcn/resizable/ResizableHandle.vue.js.map +1 -1
  64. package/package.json +1 -1
  65. package/types/index.d.ts +41 -6
  66. package/assets/jwms-portal-frontend-DQAXe8y7.css +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"JSplitter.vue2.js","sources":["../../../../src/components/atoms/JSplitter.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport {\n ResizablePanelGroup,\n ResizablePanel,\n ResizableHandle,\n} from '@/components/shadcn/resizable'\n\ntype Orientation = 'horizontal' | 'vertical'\n\nconst props = withDefaults(\n defineProps<{\n /** 분할 방향 (horizontal: 좌우, vertical: 상하) */\n direction?: Orientation\n /** 첫 번째 패널의 기본 크기 (%) */\n defaultSize?: number\n /** 첫 번째 패널의 최소 크기 (%) */\n minSize?: number\n /** 첫 번째 패널의 최대 크기 (%) */\n maxSize?: number\n /** 두 번째 패널의 최소 크기 (%) */\n secondMinSize?: number\n /** 두 번째 패널의 최대 크기 (%) */\n secondMaxSize?: number\n /** ResizableHandle에 grip 아이콘 표시 여부 */\n withHandle?: boolean\n /** 패널 간 여백 (px) */\n gap?: number\n /** 추가 CSS 클래스 */\n class?: string\n }>(),\n {\n direction: 'horizontal',\n defaultSize: 40,\n minSize: 20,\n withHandle: true,\n gap: 10,\n },\n)\n\n// 두 번째 패널의 기본 크기 계산\nconst secondDefaultSize = computed(() => 100 - props.defaultSize)\n</script>\n\n<template>\n <ResizablePanelGroup :direction=\"direction\" :class=\"class\">\n <!-- 첫 번째 패널 (좌측/상단) -->\n <ResizablePanel \n :default-size=\"defaultSize\" \n :min-size=\"minSize\" \n :max-size=\"maxSize\"\n :class=\"gap > 0 ? (direction === 'horizontal' ? 'pr-[calc(var(--gap)/2)]' : 'pb-[calc(var(--gap)/2)]') : ''\"\n :style=\"gap > 0 ? { '--gap': `${gap}px` } : {}\"\n >\n <slot name=\"first\" />\n <!-- direction=\"horizontal\"일 때 left, vertical일 때 top으로도 사용 가능 -->\n <slot name=\"left\" />\n <slot name=\"top\" />\n </ResizablePanel>\n\n <!-- 크기 조정 핸들 -->\n <ResizableHandle :with-handle=\"withHandle\" />\n\n <!-- 두 번째 패널 (우측/하단) -->\n <ResizablePanel\n :default-size=\"secondDefaultSize\"\n :min-size=\"secondMinSize\"\n :max-size=\"secondMaxSize\"\n :class=\"gap > 0 ? (direction === 'horizontal' ? 'pl-[calc(var(--gap)/2)]' : 'pt-[calc(var(--gap)/2)]') : ''\"\n :style=\"gap > 0 ? { '--gap': `${gap}px` } : {}\"\n >\n <slot name=\"second\" />\n <!-- direction=\"horizontal\"일 때 right, vertical일 때 bottom으로도 사용 가능 -->\n <slot name=\"right\" />\n <slot name=\"bottom\" />\n </ResizablePanel>\n </ResizablePanelGroup>\n</template>\n\n<style scoped>\n/* ========================================\n 패턴 5: Splitter Handle 스타일\n ======================================== */\n\n:deep([data-panel-resize-handle-id]) {\n background: hsl(var(--border) / 0.8);\n transition: all 0.2s ease;\n position: relative;\n}\n\n:deep([data-panel-resize-handle-id]::before) {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: hsl(var(--muted-foreground) / 0.2);\n}\n\n:deep([data-panel-resize-handle-id]:hover) {\n background: hsl(var(--primary) / 0.6);\n}\n\n:deep([data-panel-resize-handle-id]:hover::before) {\n background: hsl(var(--primary) / 0.3);\n}\n\n:deep([data-panel-resize-handle-id]:active) {\n background: hsl(var(--primary));\n}\n\n:deep([data-panel-resize-handle-id]:active::before) {\n background: hsl(var(--primary) / 0.5);\n}\n\n:deep([data-panel-resize-handle-id][data-resize-handle-orientation=\"horizontal\"]) {\n cursor: col-resize;\n width: 6px;\n}\n\n:deep([data-panel-resize-handle-id][data-resize-handle-orientation=\"vertical\"]) {\n cursor: row-resize;\n height: 6px;\n}\n</style>\n"],"names":["props","__props","secondDefaultSize","computed","_createBlock","_unref","ResizablePanelGroup","_createVNode","ResizablePanel","_normalizeClass","_normalizeStyle","_renderSlot","_ctx","ResizableHandle"],"mappings":";;;;;;;;;;;;;;;;;;AAUA,UAAMA,IAAQC,GA+BRC,IAAoBC,EAAS,MAAM,MAAMH,EAAM,WAAW;2BAI9DI,EA+BsBC,EAAAC,CAAA,GAAA;AAAA,MA/BA,WAAWL,EAAA;AAAA,MAAY,SAAOA,EAAA,KAAK;AAAA,IAAA;iBAEvD,MAWiB;AAAA,QAXjBM,EAWiBF,EAAAG,CAAA,GAAA;AAAA,UAVd,gBAAcP,EAAA;AAAA,UACd,YAAUA,EAAA;AAAA,UACV,YAAUA,EAAA;AAAA,UACV,OAAKQ,EAAER,EAAA,MAAG,IAAQA,EAAA,cAAS,eAAA,4BAAA,4BAAA,EAAA;AAAA,UAC3B,OAAKS,EAAET,EAAA,MAAG,IAAA,EAAA,SAAA,GAAqBA,EAAA,GAAG,KAAA,IAAA,CAAA,CAAA;AAAA,QAAA;qBAEnC,MAAqB;AAAA,YAArBU,EAAqBC,EAAA,QAAA,SAAA,CAAA,GAAA,QAAA,EAAA;AAAA,YAErBD,EAAoBC,EAAA,QAAA,QAAA,CAAA,GAAA,QAAA,EAAA;AAAA,YACpBD,EAAmBC,EAAA,QAAA,OAAA,CAAA,GAAA,QAAA,EAAA;AAAA,UAAA;;;QAIrBL,EAA6CF,EAAAQ,CAAA,GAAA,EAA3B,eAAaZ,EAAA,WAAA,GAAU,MAAA,GAAA,CAAA,aAAA,CAAA;AAAA,QAGzCM,EAWiBF,EAAAG,CAAA,GAAA;AAAA,UAVd,gBAAcN,EAAA;AAAA,UACd,YAAUD,EAAA;AAAA,UACV,YAAUA,EAAA;AAAA,UACV,OAAKQ,EAAER,EAAA,MAAG,IAAQA,EAAA,cAAS,eAAA,4BAAA,4BAAA,EAAA;AAAA,UAC3B,OAAKS,EAAET,EAAA,MAAG,IAAA,EAAA,SAAA,GAAqBA,EAAA,GAAG,KAAA,IAAA,CAAA,CAAA;AAAA,QAAA;qBAEnC,MAAsB;AAAA,YAAtBU,EAAsBC,EAAA,QAAA,UAAA,CAAA,GAAA,QAAA,EAAA;AAAA,YAEtBD,EAAqBC,EAAA,QAAA,SAAA,CAAA,GAAA,QAAA,EAAA;AAAA,YACrBD,EAAsBC,EAAA,QAAA,UAAA,CAAA,GAAA,QAAA,EAAA;AAAA,UAAA;;;;;;;;"}
1
+ {"version":3,"file":"JSplitter.vue2.js","sources":["../../../../src/components/atoms/JSplitter.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport {\n ResizablePanelGroup,\n ResizablePanel,\n ResizableHandle,\n} from '@/components/shadcn/resizable'\n\ntype Orientation = 'horizontal' | 'vertical'\n\nconst props = withDefaults(\n defineProps<{\n /** 분할 방향 (horizontal: 좌우, vertical: 상하) */\n direction?: Orientation\n /** 첫 번째 패널의 기본 크기 (%) */\n defaultSize?: number\n /** 첫 번째 패널의 최소 크기 (%) */\n minSize?: number\n /** 첫 번째 패널의 최대 크기 (%) */\n maxSize?: number\n /** 두 번째 패널의 최소 크기 (%) */\n secondMinSize?: number\n /** 두 번째 패널의 최대 크기 (%) */\n secondMaxSize?: number\n /** ResizableHandle에 grip 아이콘 표시 여부 */\n withHandle?: boolean\n /** 패널 간 여백 (px) */\n gap?: number\n /** 추가 CSS 클래스 */\n class?: string\n }>(),\n {\n direction: 'horizontal',\n defaultSize: 40,\n minSize: 20,\n withHandle: false,\n gap: 4,\n },\n)\n\n// 두 번째 패널의 기본 크기 계산\nconst secondDefaultSize = computed(() => 100 - props.defaultSize)\n</script>\n\n<template>\n <ResizablePanelGroup :direction=\"direction\" :class=\"class\">\n <!-- 첫 번째 패널 (좌측/상단) -->\n <ResizablePanel \n :default-size=\"defaultSize\" \n :min-size=\"minSize\" \n :max-size=\"maxSize\"\n :class=\"gap > 0 ? (direction === 'horizontal' ? 'pr-[calc(var(--gap)/2)]' : 'pb-[calc(var(--gap)/2)]') : ''\"\n :style=\"gap > 0 ? { '--gap': `${gap}px` } : {}\"\n >\n <slot name=\"first\" />\n <!-- direction=\"horizontal\"일 때 left, vertical일 때 top으로도 사용 가능 -->\n <slot name=\"left\" />\n <slot name=\"top\" />\n </ResizablePanel>\n\n <!-- 크기 조정 핸들 -->\n <ResizableHandle :with-handle=\"withHandle\" />\n\n <!-- 두 번째 패널 (우측/하단) -->\n <ResizablePanel\n :default-size=\"secondDefaultSize\"\n :min-size=\"secondMinSize\"\n :max-size=\"secondMaxSize\"\n :class=\"gap > 0 ? (direction === 'horizontal' ? 'pl-[calc(var(--gap)/2)]' : 'pt-[calc(var(--gap)/2)]') : ''\"\n :style=\"gap > 0 ? { '--gap': `${gap}px` } : {}\"\n >\n <slot name=\"second\" />\n <!-- direction=\"horizontal\"일 때 right, vertical일 때 bottom으로도 사용 가능 -->\n <slot name=\"right\" />\n <slot name=\"bottom\" />\n </ResizablePanel>\n </ResizablePanelGroup>\n</template>\n\n<style scoped>\n/* ========================================\n 패턴 5: Splitter Handle 스타일\n ======================================== */\n\n:deep([data-panel-resize-handle-id]) {\n background: transparent;\n transition: background-color 0.15s ease;\n position: relative;\n user-select: none;\n touch-action: none;\n}\n\n:deep([data-panel-resize-handle-id]::before) {\n content: '';\n position: absolute;\n background: hsl(var(--border));\n border-radius: 9999px;\n}\n\n:deep([data-panel-resize-handle-id]:hover) {\n background: hsl(var(--primary) / 0.06);\n}\n\n:deep([data-panel-resize-handle-id]:hover::before) {\n background: hsl(var(--primary) / 0.55);\n}\n\n:deep([data-panel-resize-handle-id]:active) {\n background: hsl(var(--primary) / 0.1);\n}\n\n:deep([data-panel-resize-handle-id]:active::before) {\n background: hsl(var(--primary));\n}\n\n:deep([data-panel-resize-handle-id][data-resize-handle-orientation=\"horizontal\"]) {\n cursor: col-resize;\n width: 6px;\n}\n\n:deep([data-panel-resize-handle-id][data-resize-handle-orientation=\"horizontal\"]::before) {\n top: 6px;\n bottom: 6px;\n left: 50%;\n width: 1px;\n transform: translateX(-50%);\n}\n\n:deep([data-panel-resize-handle-id][data-resize-handle-orientation=\"vertical\"]) {\n cursor: row-resize;\n height: 6px;\n}\n\n:deep([data-panel-resize-handle-id][data-resize-handle-orientation=\"vertical\"]::before) {\n left: 6px;\n right: 6px;\n top: 50%;\n height: 1px;\n transform: translateY(-50%);\n}\n</style>\n"],"names":["props","__props","secondDefaultSize","computed","_createBlock","_unref","ResizablePanelGroup","_createVNode","ResizablePanel","_normalizeClass","_normalizeStyle","_renderSlot","_ctx","ResizableHandle"],"mappings":";;;;;;;;;;;;;;;;;;AAUA,UAAMA,IAAQC,GA+BRC,IAAoBC,EAAS,MAAM,MAAMH,EAAM,WAAW;2BAI9DI,EA+BsBC,EAAAC,CAAA,GAAA;AAAA,MA/BA,WAAWL,EAAA;AAAA,MAAY,SAAOA,EAAA,KAAK;AAAA,IAAA;iBAEvD,MAWiB;AAAA,QAXjBM,EAWiBF,EAAAG,CAAA,GAAA;AAAA,UAVd,gBAAcP,EAAA;AAAA,UACd,YAAUA,EAAA;AAAA,UACV,YAAUA,EAAA;AAAA,UACV,OAAKQ,EAAER,EAAA,MAAG,IAAQA,EAAA,cAAS,eAAA,4BAAA,4BAAA,EAAA;AAAA,UAC3B,OAAKS,EAAET,EAAA,MAAG,IAAA,EAAA,SAAA,GAAqBA,EAAA,GAAG,KAAA,IAAA,CAAA,CAAA;AAAA,QAAA;qBAEnC,MAAqB;AAAA,YAArBU,EAAqBC,EAAA,QAAA,SAAA,CAAA,GAAA,QAAA,EAAA;AAAA,YAErBD,EAAoBC,EAAA,QAAA,QAAA,CAAA,GAAA,QAAA,EAAA;AAAA,YACpBD,EAAmBC,EAAA,QAAA,OAAA,CAAA,GAAA,QAAA,EAAA;AAAA,UAAA;;;QAIrBL,EAA6CF,EAAAQ,CAAA,GAAA,EAA3B,eAAaZ,EAAA,WAAA,GAAU,MAAA,GAAA,CAAA,aAAA,CAAA;AAAA,QAGzCM,EAWiBF,EAAAG,CAAA,GAAA;AAAA,UAVd,gBAAcN,EAAA;AAAA,UACd,YAAUD,EAAA;AAAA,UACV,YAAUA,EAAA;AAAA,UACV,OAAKQ,EAAER,EAAA,MAAG,IAAQA,EAAA,cAAS,eAAA,4BAAA,4BAAA,EAAA;AAAA,UAC3B,OAAKS,EAAET,EAAA,MAAG,IAAA,EAAA,SAAA,GAAqBA,EAAA,GAAG,KAAA,IAAA,CAAA,CAAA;AAAA,QAAA;qBAEnC,MAAsB;AAAA,YAAtBU,EAAsBC,EAAA,QAAA,UAAA,CAAA,GAAA,QAAA,EAAA;AAAA,YAEtBD,EAAqBC,EAAA,QAAA,SAAA,CAAA,GAAA,QAAA,EAAA;AAAA,YACrBD,EAAsBC,EAAA,QAAA,UAAA,CAAA,GAAA,QAAA,EAAA;AAAA,UAAA;;;;;;;;"}
@@ -3,5 +3,5 @@
3
3
  for (const [t_key, t_val] of t_opts)
4
4
  t_merged[t_key] = t_val;
5
5
  return t_merged;
6
- };,u=t(e.default,[["__scopeId","data-v-9053fa66"]]);exports.default=u;
6
+ };,u=t(e.default,[["__scopeId","data-v-f600f461"]]);exports.default=u;
7
7
  //# sourceMappingURL=JFormField.vue.cjs.map
@@ -6,8 +6,8 @@ const r = (r_comp, r_opts) => {
6
6
  r_merged[r_key] = r_val;
7
7
  return r_merged;
8
8
  };
9
- const f = /* @__PURE__ */ r(o, [["__scopeId", "data-v-9053fa66"]]);
9
+ const p = /* @__PURE__ */ r(o, [["__scopeId", "data-v-f600f461"]]);
10
10
  export {
11
- f as default
11
+ p as default
12
12
  };
13
13
  //# sourceMappingURL=JFormField.vue.js.map
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue");require("../shadcn/index.cjs");require("lucide-vue-next");;/* empty css */const h=require("../atoms/JInput.vue.cjs"),F=require("../atoms/JTextarea.vue.cjs"),N=require("../atoms/JCheckbox.vue.cjs"),z=require("../atoms/JCombo.vue.cjs"),T=require("../atoms/JSearchCombo.vue.cjs"),S=require("../atoms/JRadio.vue.cjs"),J=require("../atoms/JSwitch.vue.cjs"),M=require("../atoms/JDatepicker.vue.cjs");require("md-editor-v3");;/* empty css */;/* empty css */const P=require("../../lib/utils.cjs");require("../shadcn/badge-variants.cjs");require("@vueuse/core");require("reka-ui");;/* empty css */require("../shadcn/avatar-variants.cjs");require("dompurify");;/* empty css */require("ag-grid-vue3");require("ag-grid-community");require("ag-grid-enterprise");;/* empty css */;/* empty css */;/* empty css */require("vue-sonner");const i=require("../shadcn/FieldGroup.vue.cjs"),g=require("../shadcn/Field.vue.cjs"),k=require("../shadcn/FieldLabel.vue.cjs"),x=require("../shadcn/FieldContent.vue.cjs"),E=require("../shadcn/FieldDescription.vue.cjs"),L=require("../shadcn/FieldError.vue.cjs"),j={key:0,class:"text-destructive ml-1"},A=e.defineComponent({__name:"JFormField",props:{class:{},label:{},description:{},errorMsg:{},type:{default:"input"},inlineLabel:{},orientation:{default:"vertical"},labelAlign:{default:"left"},labelWidth:{default:"8rem"},id:{},modelValue:{},placeholder:{},disabled:{type:Boolean},readonly:{type:Boolean},required:{type:Boolean},name:{},styleType:{},inputType:{},options:{},multiple:{type:Boolean},radioDirection:{default:"horizontal"}},emits:["update:modelValue","change","focus","blur"],setup(r,{expose:q,emit:C}){const b=["class","label","description","errorMsg","type","inlineLabel","orientation","labelAlign","labelWidth","radioDirection"],t=r,u=C,o=e.ref(""),c=e.computed(()=>t.errorMsg||o.value),s=e.computed(()=>{const l={},a=t;for(const n in a)b.includes(n)||(l[n]=a[n]);if(t.inputType&&t.type==="input"&&(l.type=t.inputType,delete l.inputType,!t.placeholder)){const n={text:"텍스트를 입력하세요",email:"이메일을 입력하세요",password:"비밀번호를 입력하세요",tel:"전화번호를 입력하세요",url:"URL을 입력하세요",number:"숫자를 입력하세요",search:"검색어를 입력하세요",date:"날짜를 선택하세요",time:"시간을 선택하세요","datetime-local":"날짜와 시간을 선택하세요",month:"월을 선택하세요",week:"주를 선택하세요"};l.placeholder=n[t.inputType]||"입력하세요"}return t.radioDirection&&t.type==="radio"&&(l.styletype=t.radioDirection,delete l.radioDirection),l}),d=l=>{if(!t.required)return;o.value="";const a=l!==void 0?l:t.modelValue;t.type==="checkbox"||t.type==="switch"?a!=="Y"&&(o.value="필수 항목입니다."):(a==null||a==="")&&(o.value="필수 입력 항목입니다.")},B=e.computed(()=>!(t.modelValue!==null&&t.modelValue!==void 0&&t.modelValue!=="")),p=l=>{u("update:modelValue",l),d(l)},f=l=>{u("change",l),d(l)},_=l=>{u("focus",l)},m=l=>{B.value&&d(),u("blur",l)},V=e.computed(()=>{const l={left:"justify-start",middle:"justify-center",right:"justify-end"},a={left:"text-left",middle:"text-center",right:"text-right"};return t.orientation==="horizontal"?l[t.labelAlign]:a[t.labelAlign]}),w=e.computed(()=>{switch(t.styleType){case"md":return"form-density-md";case"lg":return"form-density-lg";default:return"form-density-sm"}}),v=e.computed(()=>{const l="h-[var(--ctl-h)] leading-[var(--ctl-h)]";return t.type==="datepicker"?`${l} max-w-xs`:t.type==="radio"&&t.radioDirection==="vertical"?"":l}),D={input:h.default,textarea:F.default,checkbox:N.default,switch:J.default,combo:z.default,radio:S.default,searchCombo:T.default,datepicker:M.default},y=e.computed(()=>D[t.type]||h.default);return q({clearError:()=>{o.value=""}}),(l,a)=>(e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(e.unref(P.cn)("space-y-2 flex-1 min-w-0",w.value,t.class))},[e.createVNode(e.unref(i.default),null,{default:e.withCtx(()=>[e.createVNode(e.unref(g.default),{class:e.normalizeClass([r.orientation==="horizontal"?"grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2":"space-y-1 gap-1"]),style:e.normalizeStyle(r.orientation==="horizontal"?`--label-w:${r.labelWidth};`:"")},{default:e.withCtx(()=>[e.createVNode(e.unref(k.default),{for:r.id,class:e.normalizeClass(["text-xs",r.orientation==="horizontal"?"flex items-center h-[var(--ctl-h)] w-full":"flex items-center",V.value])},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(r.label)+" ",1),r.required?(e.openBlock(),e.createElementBlock("span",j,"*")):e.createCommentVNode("",!0)]),_:1},8,["for","class"]),e.createVNode(e.unref(x.default),{class:e.normalizeClass([r.orientation==="horizontal"?"min-h-[var(--ctl-h)] flex flex-col justify-start gap-0.5 mt-0":"space-y-2 gap-0"])},{default:e.withCtx(()=>[r.type==="checkbox"||r.type==="switch"?(e.openBlock(),e.createBlock(e.unref(i.default),{key:0,"data-slot":"checkbox-group"},{default:e.withCtx(()=>[e.createVNode(e.unref(g.default),{orientation:"horizontal",class:"flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center"},{default:e.withCtx(()=>[(e.openBlock(),e.createBlock(e.resolveDynamicComponent(y.value),e.mergeProps(s.value,{"onUpdate:modelValue":p,onChange:f,onFocus:_,onBlur:m}),null,16)),r.inlineLabel?(e.openBlock(),e.createBlock(e.unref(k.default),{key:0,for:r.id,class:"text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(r.inlineLabel),1)]),_:1},8,["for"])):e.createCommentVNode("",!0)]),_:1})]),_:1})):r.type==="radio"?(e.openBlock(),e.createBlock(e.unref(i.default),{key:1},{default:e.withCtx(()=>[(e.openBlock(),e.createBlock(e.resolveDynamicComponent(y.value),e.mergeProps(s.value,{"onUpdate:modelValue":p,onChange:f,onFocus:_,onBlur:m,class:v.value}),null,16,["class"]))]),_:1})):(e.openBlock(),e.createBlock(e.unref(i.default),{key:2},{default:e.withCtx(()=>[(e.openBlock(),e.createBlock(e.resolveDynamicComponent(y.value),e.mergeProps(s.value,{"onUpdate:modelValue":p,onChange:f,onFocus:_,onBlur:m,class:v.value}),null,16,["class"]))]),_:1})),r.description||c.value?(e.openBlock(),e.createBlock(e.unref(x.default),{key:3},{default:e.withCtx(()=>[r.description?(e.openBlock(),e.createBlock(e.unref(E.default),{key:0},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(r.description),1)]),_:1})):e.createCommentVNode("",!0),c.value?(e.openBlock(),e.createBlock(e.unref(L.default),{key:1},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(c.value),1)]),_:1})):e.createCommentVNode("",!0)]),_:1})):e.createCommentVNode("",!0)]),_:1},8,["class"])]),_:1},8,["class","style"])]),_:1})],2))}});exports.default=A;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue");require("../shadcn/index.cjs");require("lucide-vue-next");;/* empty css */const h=require("../atoms/JInput.vue.cjs"),F=require("../atoms/JTextarea.vue.cjs"),N=require("../atoms/JCheckbox.vue.cjs"),z=require("../atoms/JCombo.vue.cjs"),T=require("../atoms/JSearchCombo.vue.cjs"),S=require("../atoms/JRadio.vue.cjs"),J=require("../atoms/JSwitch.vue.cjs"),M=require("../atoms/JDatepicker.vue.cjs");require("md-editor-v3");;/* empty css */;/* empty css */const P=require("../../lib/utils.cjs");require("../shadcn/badge-variants.cjs");require("@vueuse/core");require("reka-ui");;/* empty css */require("../shadcn/avatar-variants.cjs");require("dompurify");;/* empty css */require("ag-grid-vue3");require("ag-grid-community");require("ag-grid-enterprise");;/* empty css */;/* empty css */;/* empty css */require("vue-sonner");const i=require("../shadcn/FieldGroup.vue.cjs"),g=require("../shadcn/Field.vue.cjs"),k=require("../shadcn/FieldLabel.vue.cjs"),x=require("../shadcn/FieldContent.vue.cjs"),E=require("../shadcn/FieldDescription.vue.cjs"),L=require("../shadcn/FieldError.vue.cjs"),j={key:0,class:"text-destructive ml-1"},A=e.defineComponent({__name:"JFormField",props:{class:{},label:{},description:{},errorMsg:{},type:{default:"input"},inlineLabel:{},orientation:{default:"vertical"},labelAlign:{default:"left"},labelWidth:{default:void 0},id:{},modelValue:{},placeholder:{},disabled:{type:Boolean},readonly:{type:Boolean},required:{type:Boolean},name:{},styleType:{},inputType:{},options:{},multiple:{type:Boolean},radioDirection:{default:"horizontal"}},emits:["update:modelValue","change","focus","blur"],setup(r,{expose:q,emit:C}){const b=["class","label","description","errorMsg","type","inlineLabel","orientation","labelAlign","labelWidth","radioDirection"],t=r,u=C,o=e.ref(""),c=e.computed(()=>t.errorMsg||o.value),s=e.computed(()=>{const l={},a=t;for(const n in a)b.includes(n)||(l[n]=a[n]);if(t.inputType&&t.type==="input"&&(l.type=t.inputType,delete l.inputType,!t.placeholder)){const n={text:"텍스트를 입력하세요",email:"이메일을 입력하세요",password:"비밀번호를 입력하세요",tel:"전화번호를 입력하세요",url:"URL을 입력하세요",number:"숫자를 입력하세요",search:"검색어를 입력하세요",date:"날짜를 선택하세요",time:"시간을 선택하세요","datetime-local":"날짜와 시간을 선택하세요",month:"월을 선택하세요",week:"주를 선택하세요"};l.placeholder=n[t.inputType]||"입력하세요"}return t.radioDirection&&t.type==="radio"&&(l.styletype=t.radioDirection,delete l.radioDirection),l}),d=l=>{if(!t.required)return;o.value="";const a=l!==void 0?l:t.modelValue;t.type==="checkbox"||t.type==="switch"?a!=="Y"&&(o.value="필수 항목입니다."):(a==null||a==="")&&(o.value="필수 입력 항목입니다.")},B=e.computed(()=>!(t.modelValue!==null&&t.modelValue!==void 0&&t.modelValue!=="")),p=l=>{u("update:modelValue",l),d(l)},f=l=>{u("change",l),d(l)},_=l=>{u("focus",l)},m=l=>{B.value&&d(),u("blur",l)},V=e.computed(()=>{const l={left:"justify-start",middle:"justify-center",right:"justify-end"},a={left:"text-left",middle:"text-center",right:"text-right"};return t.orientation==="horizontal"?l[t.labelAlign]:a[t.labelAlign]}),w=e.computed(()=>{switch(t.styleType){case"md":return"form-density-md";case"lg":return"form-density-lg";default:return"form-density-sm"}}),v=e.computed(()=>{const l="h-[var(--ctl-h)] leading-[var(--ctl-h)]";return t.type==="datepicker"?`${l} max-w-xs`:t.type==="radio"&&t.radioDirection==="vertical"?"":l}),D={input:h.default,textarea:F.default,checkbox:N.default,switch:J.default,combo:z.default,radio:S.default,searchCombo:T.default,datepicker:M.default},y=e.computed(()=>D[t.type]||h.default);return q({clearError:()=>{o.value=""}}),(l,a)=>(e.openBlock(),e.createElementBlock("div",{class:e.normalizeClass(e.unref(P.cn)("space-y-2 flex-1 min-w-0",w.value,t.class))},[e.createVNode(e.unref(i.default),null,{default:e.withCtx(()=>[e.createVNode(e.unref(g.default),{class:e.normalizeClass([r.orientation==="horizontal"?"grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2":"space-y-1 gap-1"]),style:e.normalizeStyle(r.orientation==="horizontal"&&r.labelWidth?`--label-w:${r.labelWidth};`:"")},{default:e.withCtx(()=>[e.createVNode(e.unref(k.default),{for:r.id,class:e.normalizeClass(["text-xs",r.orientation==="horizontal"?"flex items-center h-[var(--ctl-h)] w-full":"flex items-center",V.value])},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(r.label)+" ",1),r.required?(e.openBlock(),e.createElementBlock("span",j,"*")):e.createCommentVNode("",!0)]),_:1},8,["for","class"]),e.createVNode(e.unref(x.default),{class:e.normalizeClass([r.orientation==="horizontal"?"min-h-[var(--ctl-h)] flex flex-col justify-start gap-0.5 mt-0":"space-y-2 gap-0"])},{default:e.withCtx(()=>[r.type==="checkbox"||r.type==="switch"?(e.openBlock(),e.createBlock(e.unref(i.default),{key:0,"data-slot":"checkbox-group"},{default:e.withCtx(()=>[e.createVNode(e.unref(g.default),{orientation:"horizontal",class:"flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center"},{default:e.withCtx(()=>[(e.openBlock(),e.createBlock(e.resolveDynamicComponent(y.value),e.mergeProps(s.value,{"onUpdate:modelValue":p,onChange:f,onFocus:_,onBlur:m}),null,16)),r.inlineLabel?(e.openBlock(),e.createBlock(e.unref(k.default),{key:0,for:r.id,class:"text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]"},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(r.inlineLabel),1)]),_:1},8,["for"])):e.createCommentVNode("",!0)]),_:1})]),_:1})):r.type==="radio"?(e.openBlock(),e.createBlock(e.unref(i.default),{key:1},{default:e.withCtx(()=>[(e.openBlock(),e.createBlock(e.resolveDynamicComponent(y.value),e.mergeProps(s.value,{"onUpdate:modelValue":p,onChange:f,onFocus:_,onBlur:m,class:v.value}),null,16,["class"]))]),_:1})):(e.openBlock(),e.createBlock(e.unref(i.default),{key:2},{default:e.withCtx(()=>[(e.openBlock(),e.createBlock(e.resolveDynamicComponent(y.value),e.mergeProps(s.value,{"onUpdate:modelValue":p,onChange:f,onFocus:_,onBlur:m,class:v.value}),null,16,["class"]))]),_:1})),r.description||c.value?(e.openBlock(),e.createBlock(e.unref(x.default),{key:3},{default:e.withCtx(()=>[r.description?(e.openBlock(),e.createBlock(e.unref(E.default),{key:0},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(r.description),1)]),_:1})):e.createCommentVNode("",!0),c.value?(e.openBlock(),e.createBlock(e.unref(L.default),{key:1},{default:e.withCtx(()=>[e.createTextVNode(e.toDisplayString(c.value),1)]),_:1})):e.createCommentVNode("",!0)]),_:1})):e.createCommentVNode("",!0)]),_:1},8,["class"])]),_:1},8,["class","style"])]),_:1})],2))}});exports.default=A;
2
2
  //# sourceMappingURL=JFormField.vue2.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"JFormField.vue2.cjs","sources":["../../../../src/components/molecules/JFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Field, FieldContent, FieldLabel, FieldDescription, FieldError, FieldGroup } from '@/components/shadcn'\nimport { JInput, JTextarea, JCheckbox, JSwitch, JCombo, JRadio, JSearchCombo, JDatepicker } from '@/components/atoms'\nimport { cn } from '@/lib/utils'\n\n// 컴포넌트 타입 정의\ntype ComponentType = 'input' | 'textarea' | 'checkbox' | 'switch' | 'combo' | 'radio' | 'searchCombo' | 'datepicker'\n\n// FormField 자체의 props (레이아웃 관련)\nconst FORM_FIELD_PROPS = [\n 'class',\n 'label',\n 'description',\n 'errorMsg',\n 'type',\n 'inlineLabel',\n 'orientation',\n 'labelAlign',\n 'labelWidth',\n 'radioDirection',\n] as const\n\nconst props = withDefaults(\n defineProps<{\n // ============ FormField 자체 props (레이아웃만) ============\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n /** 필드 레이블 */\n label?: string\n /** 필드 설명 (레이블 아래 표시) */\n description?: string\n /** 에러 메시지 */\n errorMsg?: string\n /** 컴포넌트 타입 (렌더링할 컴포넌트 지정) */\n type?: ComponentType\n /** 체크박스/스위치 타입일 때만 사용하는 옆 라벨 */\n inlineLabel?: string\n /** 레이블과 컨트롤의 배치 방향 */\n orientation?: 'vertical' | 'horizontal' | 'responsive'\n /** 레이블 텍스트 정렬 */\n labelAlign?: 'left' | 'middle' | 'right'\n /** 레이블 영역 너비 (horizontal orientation일 때만 적용) */\n labelWidth?: string\n\n // ============ 내부 컴포넌트로 전달할 공통 props ============\n /** Input 요소의 id (label for와 연결) */\n id?: string\n /** v-model로 양방향 데이터 바인딩 */\n modelValue?: any\n /** 입력 전 표시되는 안내문 (Input/Textarea/Select/Combobox) */\n placeholder?: string\n /** 비활성화 상태 (전체) */\n disabled?: boolean\n /** 읽기 전용 상태 (Input/Textarea) */\n readonly?: boolean\n /** 필수 입력/선택 여부 (전체) */\n required?: boolean\n /** form 데이터 전송 시 키 이름 (전체) */\n name?: string\n /** 스타일 테마 지정 (J-prefixed 컴포넌트의 styleType) */\n styleType?: string\n\n // ============ Input 전용 props ============\n /** Input 타입 (text, email, password 등) */\n inputType?: string\n\n\n // ============ Select/Combobox/Radio 전용 props ============\n /** 선택 가능한 항목 배열 */\n options?: Array<{ label: string; value: string | number; disabled?: boolean }>\n \n // ============ Select/Combobox 전용 props ============\n /** 다중 선택 허용 여부 */\n multiple?: boolean\n\n\n // ============ Radio 전용 props ============\n /** Radio 옵션 나열 방향 */\n radioDirection?: 'horizontal' | 'vertical'\n }>(),\n {\n type: 'input',\n orientation: 'vertical',\n labelAlign: 'left',\n labelWidth: '8rem',\n radioDirection: 'horizontal',\n }\n)\n\n// 이벤트 정의\nconst emit = defineEmits<{\n 'update:modelValue': [value: any]\n 'change': [value: any]\n 'focus': [event: FocusEvent]\n 'blur': [event: FocusEvent]\n}>()\n\n// 내부 에러 상태 (props.error가 없을 때만 사용)\nconst internalError = ref<string>('')\n\n// 최종 에러 메시지 (외부 errorMsg가 우선)\nconst finalError = computed(() => props.errorMsg || internalError.value)\n\n// FormField 자체 props와 내부 컴포넌트 props 분리\nconst controlProps = computed(() => {\n const result: Record<string, any> = {}\n const propsObj = props as Record<string, any>\n \n for (const key in propsObj) {\n // FormField 자체 props는 제외\n if (!FORM_FIELD_PROPS.includes(key as any)) {\n result[key] = propsObj[key]\n }\n }\n \n // inputType을 type으로 변환 (JInput 컴포넌트에서 사용)\n if (props.inputType && props.type === 'input') {\n result.type = props.inputType\n delete result.inputType // inputType은 제거\n \n // placeholder가 없으면 inputType에 따라 기본값 설정\n if (!props.placeholder) {\n const defaultPlaceholders: Record<string, string> = {\n 'text': '텍스트를 입력하세요',\n 'email': '이메일을 입력하세요',\n 'password': '비밀번호를 입력하세요',\n 'tel': '전화번호를 입력하세요',\n 'url': 'URL을 입력하세요',\n 'number': '숫자를 입력하세요',\n 'search': '검색어를 입력하세요',\n 'date': '날짜를 선택하세요',\n 'time': '시간을 선택하세요',\n 'datetime-local': '날짜와 시간을 선택하세요',\n 'month': '월을 선택하세요',\n 'week': '주를 선택하세요',\n }\n \n result.placeholder = defaultPlaceholders[props.inputType] || '입력하세요'\n }\n }\n \n // radioDirection을 styletype으로 변환 (JRadio 컴포넌트에서 사용)\n if (props.radioDirection && props.type === 'radio') {\n result.styletype = props.radioDirection\n delete result.radioDirection // radioDirection은 제거\n }\n \n return result\n})\n\n // Built-in validation\n const validateField = (currentValue?: any) => {\n if (!props.required) return\n\n internalError.value = ''\n\n // 현재 값 또는 props.modelValue 사용\n const value = currentValue !== undefined ? currentValue : props.modelValue\n\n // Required 체크\n if (props.type === 'checkbox' || props.type === 'switch') {\n if (value !== 'Y') {\n internalError.value = '필수 항목입니다.'\n }\n } else {\n if (value === null || value === undefined || value === '') {\n internalError.value = '필수 입력 항목입니다.'\n }\n }\n }\n\n // 초기 로드 시 validation 실행 (blur 이벤트에서만)\n const shouldValidateOnMount = computed(() => {\n // Storybook에서 초기값이 있는 경우 validation 스킵\n if (props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '') {\n return false\n }\n return true\n })\n\n// 이벤트 핸들러\nconst handleUpdateModelValue = (value: any) => {\n emit('update:modelValue', value)\n \n // 값이 변경되면 즉시 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleChange = (value: any) => {\n emit('change', value)\n \n // change 이벤트에서도 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleFocus = (event: FocusEvent) => {\n emit('focus', event)\n}\n\n const handleBlur = (event: FocusEvent) => {\n // blur 시에만 validation 실행 (초기 로드 시에는 실행하지 않음)\n if (shouldValidateOnMount.value) {\n validateField()\n }\n emit('blur', event)\n }\n\n// 레이블 정렬 클래스 (레이블 영역 내에서 레이블의 위치 제어)\nconst labelAlignClass = computed(() => {\n // horizontal일 때는 레이블 영역 내에서 레이블의 위치를 제어\n const mapHorizontal = { \n left: 'justify-start', // 레이블 영역의 왼쪽에 레이블 위치\n middle: 'justify-center', // 레이블 영역의 가운데에 레이블 위치\n right: 'justify-end' // 레이블 영역의 오른쪽에 레이블 위치 (input과 가까이)\n }\n // vertical일 때는 텍스트 정렬만 제어\n const mapVertical = { left: 'text-left', middle: 'text-center', right: 'text-right' }\n return props.orientation === 'horizontal' ? mapHorizontal[props.labelAlign] : mapVertical[props.labelAlign]\n})\n\n/** 행높이 토큰 클래스 매핑 */\nconst densityClass = computed(() => {\n switch (props.styleType) {\n case 'md': return 'form-density-md'\n case 'lg': return 'form-density-lg'\n default: return 'form-density-sm' // 기본값 변경: md → sm (컴팩트 디자인)\n }\n})\n\n/** 컨트롤 클래스 (datepicker는 최대 너비 제한, radio vertical은 높이 제한 없음) */\nconst controlClass = computed(() => {\n const baseClass = 'h-[var(--ctl-h)] leading-[var(--ctl-h)]'\n \n if (props.type === 'datepicker') {\n return `${baseClass} max-w-xs`\n }\n \n // Radio vertical일 때는 높이 제한 없음\n if (props.type === 'radio' && props.radioDirection === 'vertical') {\n return ''\n }\n \n return baseClass\n})\n\n// 컴포넌트 매핑\nconst componentMap: Record<ComponentType, any> = {\n input: JInput,\n textarea: JTextarea,\n checkbox: JCheckbox,\n switch: JSwitch,\n combo: JCombo,\n radio: JRadio,\n searchCombo: JSearchCombo,\n datepicker: JDatepicker,\n}\n\n// type에 따라 렌더링할 컴포넌트 결정\nconst resolvedComponent = computed(() => {\n return componentMap[props.type!] || JInput\n})\n\n// 외부에서 수동으로 에러 클리어 가능하도록 expose\ndefineExpose({\n clearError: () => { internalError.value = '' }\n})\n</script>\n\n<template>\n <div :class=\"cn('space-y-2 flex-1 min-w-0', densityClass, props.class)\">\n <FieldGroup >\n <Field :class=\"[\n orientation === 'horizontal'\n ? 'grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2'\n : 'space-y-1 gap-1'\n ]\"\n :style=\"orientation === 'horizontal' ? `--label-w:${labelWidth};` : ''\"\n >\n <!-- 메인 라벨 (필수표기 포함) -->\n <FieldLabel \n :for=\"id\" \n :class=\"[\n 'text-xs', // 컴팩트 디자인을 위해 폰트 크기 축소\n orientation === 'horizontal'\n ? 'flex items-center h-[var(--ctl-h)] w-full'\n : 'flex items-center',\n labelAlignClass\n ]\"\n >\n {{ label }}\n <span v-if=\"required\" class=\"text-destructive ml-1\">*</span>\n </FieldLabel>\n\n <FieldContent \n :class=\"[\n orientation === 'horizontal'\n ? 'min-h-[var(--ctl-h)] flex flex-col justify-start gap-0.5 mt-0'\n : 'space-y-2 gap-0'\n ]\"\n >\n <!-- 체크박스/스위치: 항상 가로 정렬로 컨트롤 + 인라인 라벨 묶기 -->\n <FieldGroup v-if=\"type === 'checkbox' || type === 'switch'\" data-slot=\"checkbox-group\">\n <!-- 부모 orientation과 무관하게 수평 정렬 고정 -->\n <Field orientation=\"horizontal\" class=\"flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n />\n <FieldLabel\n v-if=\"inlineLabel\"\n :for=\"id\"\n class=\"text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]\"\n >\n {{ inlineLabel }}\n </FieldLabel>\n </Field>\n </FieldGroup>\n\n <!-- Radio 버튼: radioDirection에 따라 분기 처리 -->\n <FieldGroup v-else-if=\"type === 'radio'\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n\n <!-- 그 외 컨트롤: orientation 규칙 그대로 따름 -->\n <FieldGroup v-else>\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n \n <!-- 설명/에러: 항상 컨트롤 '바로 아래'에 표시 -->\n <FieldContent v-if=\"description || finalError\">\n <FieldDescription v-if=\"description\">\n {{ description }}\n </FieldDescription>\n <FieldError v-if=\"finalError\">\n {{ finalError }}\n </FieldError>\n </FieldContent>\n </FieldContent>\n </Field>\n </FieldGroup>\n </div>\n</template>\n\n\n\n<style scoped>\n/* 높이 토큰(밀도) — CSS 변수는 scoped에서도 자식 요소에 상속됨 */\n.form-density-sm { --ctl-h: 2rem; /* 32px */ }\n.form-density-md { --ctl-h: 2.25rem; /* 36px (shadcn 기본에 근접) */ }\n.form-density-lg { --ctl-h: 2.5rem; /* 40px */ }\n</style>"],"names":["FORM_FIELD_PROPS","props","__props","emit","__emit","internalError","ref","finalError","computed","controlProps","result","propsObj","key","defaultPlaceholders","validateField","currentValue","value","shouldValidateOnMount","handleUpdateModelValue","handleChange","handleFocus","event","handleBlur","labelAlignClass","mapHorizontal","mapVertical","densityClass","controlClass","baseClass","componentMap","JInput","JTextarea","JCheckbox","JSwitch","JCombo","JRadio","JSearchCombo","JDatepicker","resolvedComponent","__expose","_createElementBlock","_unref","cn","_createVNode","FieldGroup","Field","_normalizeClass","_normalizeStyle","FieldLabel","_createTextVNode","_toDisplayString","_hoisted_1","FieldContent","_createBlock","_openBlock","_resolveDynamicComponent","_mergeProps","FieldDescription","FieldError"],"mappings":"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"}
1
+ {"version":3,"file":"JFormField.vue2.cjs","sources":["../../../../src/components/molecules/JFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Field, FieldContent, FieldLabel, FieldDescription, FieldError, FieldGroup } from '@/components/shadcn'\nimport { JInput, JTextarea, JCheckbox, JSwitch, JCombo, JRadio, JSearchCombo, JDatepicker } from '@/components/atoms'\nimport { cn } from '@/lib/utils'\n\n// 컴포넌트 타입 정의\ntype ComponentType = 'input' | 'textarea' | 'checkbox' | 'switch' | 'combo' | 'radio' | 'searchCombo' | 'datepicker'\n\n// FormField 자체의 props (레이아웃 관련)\nconst FORM_FIELD_PROPS = [\n 'class',\n 'label',\n 'description',\n 'errorMsg',\n 'type',\n 'inlineLabel',\n 'orientation',\n 'labelAlign',\n 'labelWidth',\n 'radioDirection',\n] as const\n\nconst props = withDefaults(\n defineProps<{\n // ============ FormField 자체 props (레이아웃만) ============\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n /** 필드 레이블 */\n label?: string\n /** 필드 설명 (레이블 아래 표시) */\n description?: string\n /** 에러 메시지 */\n errorMsg?: string\n /** 컴포넌트 타입 (렌더링할 컴포넌트 지정) */\n type?: ComponentType\n /** 체크박스/스위치 타입일 때만 사용하는 옆 라벨 */\n inlineLabel?: string\n /** 레이블과 컨트롤의 배치 방향 */\n orientation?: 'vertical' | 'horizontal' | 'responsive'\n /** 레이블 텍스트 정렬 */\n labelAlign?: 'left' | 'middle' | 'right'\n /** 레이블 영역 너비 (horizontal orientation일 때만 적용) */\n labelWidth?: string\n\n // ============ 내부 컴포넌트로 전달할 공통 props ============\n /** Input 요소의 id (label for와 연결) */\n id?: string\n /** v-model로 양방향 데이터 바인딩 */\n modelValue?: any\n /** 입력 전 표시되는 안내문 (Input/Textarea/Select/Combobox) */\n placeholder?: string\n /** 비활성화 상태 (전체) */\n disabled?: boolean\n /** 읽기 전용 상태 (Input/Textarea) */\n readonly?: boolean\n /** 필수 입력/선택 여부 (전체) */\n required?: boolean\n /** form 데이터 전송 시 키 이름 (전체) */\n name?: string\n /** 스타일 테마 지정 (J-prefixed 컴포넌트의 styleType) */\n styleType?: string\n\n // ============ Input 전용 props ============\n /** Input 타입 (text, email, password 등) */\n inputType?: string\n\n\n // ============ Select/Combobox/Radio 전용 props ============\n /** 선택 가능한 항목 배열 */\n options?: Array<{ label: string; value: string | number; disabled?: boolean }>\n \n // ============ Select/Combobox 전용 props ============\n /** 다중 선택 허용 여부 */\n multiple?: boolean\n\n\n // ============ Radio 전용 props ============\n /** Radio 옵션 나열 방향 */\n radioDirection?: 'horizontal' | 'vertical'\n }>(),\n {\n type: 'input',\n orientation: 'vertical',\n labelAlign: 'left',\n labelWidth: undefined,\n radioDirection: 'horizontal',\n }\n)\n\n// 이벤트 정의\nconst emit = defineEmits<{\n 'update:modelValue': [value: any]\n 'change': [value: any]\n 'focus': [event: FocusEvent]\n 'blur': [event: FocusEvent]\n}>()\n\n// 내부 에러 상태 (props.error가 없을 때만 사용)\nconst internalError = ref<string>('')\n\n// 최종 에러 메시지 (외부 errorMsg가 우선)\nconst finalError = computed(() => props.errorMsg || internalError.value)\n\n// FormField 자체 props와 내부 컴포넌트 props 분리\nconst controlProps = computed(() => {\n const result: Record<string, any> = {}\n const propsObj = props as Record<string, any>\n \n for (const key in propsObj) {\n // FormField 자체 props는 제외\n if (!FORM_FIELD_PROPS.includes(key as any)) {\n result[key] = propsObj[key]\n }\n }\n \n // inputType을 type으로 변환 (JInput 컴포넌트에서 사용)\n if (props.inputType && props.type === 'input') {\n result.type = props.inputType\n delete result.inputType // inputType은 제거\n \n // placeholder가 없으면 inputType에 따라 기본값 설정\n if (!props.placeholder) {\n const defaultPlaceholders: Record<string, string> = {\n 'text': '텍스트를 입력하세요',\n 'email': '이메일을 입력하세요',\n 'password': '비밀번호를 입력하세요',\n 'tel': '전화번호를 입력하세요',\n 'url': 'URL을 입력하세요',\n 'number': '숫자를 입력하세요',\n 'search': '검색어를 입력하세요',\n 'date': '날짜를 선택하세요',\n 'time': '시간을 선택하세요',\n 'datetime-local': '날짜와 시간을 선택하세요',\n 'month': '월을 선택하세요',\n 'week': '주를 선택하세요',\n }\n \n result.placeholder = defaultPlaceholders[props.inputType] || '입력하세요'\n }\n }\n \n // radioDirection을 styletype으로 변환 (JRadio 컴포넌트에서 사용)\n if (props.radioDirection && props.type === 'radio') {\n result.styletype = props.radioDirection\n delete result.radioDirection // radioDirection은 제거\n }\n \n return result\n})\n\n // Built-in validation\n const validateField = (currentValue?: any) => {\n if (!props.required) return\n\n internalError.value = ''\n\n // 현재 값 또는 props.modelValue 사용\n const value = currentValue !== undefined ? currentValue : props.modelValue\n\n // Required 체크\n if (props.type === 'checkbox' || props.type === 'switch') {\n if (value !== 'Y') {\n internalError.value = '필수 항목입니다.'\n }\n } else {\n if (value === null || value === undefined || value === '') {\n internalError.value = '필수 입력 항목입니다.'\n }\n }\n }\n\n // 초기 로드 시 validation 실행 (blur 이벤트에서만)\n const shouldValidateOnMount = computed(() => {\n // Storybook에서 초기값이 있는 경우 validation 스킵\n if (props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '') {\n return false\n }\n return true\n })\n\n// 이벤트 핸들러\nconst handleUpdateModelValue = (value: any) => {\n emit('update:modelValue', value)\n \n // 값이 변경되면 즉시 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleChange = (value: any) => {\n emit('change', value)\n \n // change 이벤트에서도 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleFocus = (event: FocusEvent) => {\n emit('focus', event)\n}\n\n const handleBlur = (event: FocusEvent) => {\n // blur 시에만 validation 실행 (초기 로드 시에는 실행하지 않음)\n if (shouldValidateOnMount.value) {\n validateField()\n }\n emit('blur', event)\n }\n\n// 레이블 정렬 클래스 (레이블 영역 내에서 레이블의 위치 제어)\nconst labelAlignClass = computed(() => {\n // horizontal일 때는 레이블 영역 내에서 레이블의 위치를 제어\n const mapHorizontal = { \n left: 'justify-start', // 레이블 영역의 왼쪽에 레이블 위치\n middle: 'justify-center', // 레이블 영역의 가운데에 레이블 위치\n right: 'justify-end' // 레이블 영역의 오른쪽에 레이블 위치 (input과 가까이)\n }\n // vertical일 때는 텍스트 정렬만 제어\n const mapVertical = { left: 'text-left', middle: 'text-center', right: 'text-right' }\n return props.orientation === 'horizontal' ? mapHorizontal[props.labelAlign] : mapVertical[props.labelAlign]\n})\n\n/** 행높이 토큰 클래스 매핑 */\nconst densityClass = computed(() => {\n switch (props.styleType) {\n case 'md': return 'form-density-md'\n case 'lg': return 'form-density-lg'\n default: return 'form-density-sm' // 기본값 변경: md → sm (컴팩트 디자인)\n }\n})\n\n/** 컨트롤 클래스 (datepicker는 최대 너비 제한, radio vertical은 높이 제한 없음) */\nconst controlClass = computed(() => {\n const baseClass = 'h-[var(--ctl-h)] leading-[var(--ctl-h)]'\n \n if (props.type === 'datepicker') {\n return `${baseClass} max-w-xs`\n }\n \n // Radio vertical일 때는 높이 제한 없음\n if (props.type === 'radio' && props.radioDirection === 'vertical') {\n return ''\n }\n \n return baseClass\n})\n\n// 컴포넌트 매핑\nconst componentMap: Record<ComponentType, any> = {\n input: JInput,\n textarea: JTextarea,\n checkbox: JCheckbox,\n switch: JSwitch,\n combo: JCombo,\n radio: JRadio,\n searchCombo: JSearchCombo,\n datepicker: JDatepicker,\n}\n\n// type에 따라 렌더링할 컴포넌트 결정\nconst resolvedComponent = computed(() => {\n return componentMap[props.type!] || JInput\n})\n\n// 외부에서 수동으로 에러 클리어 가능하도록 expose\ndefineExpose({\n clearError: () => { internalError.value = '' }\n})\n</script>\n\n<template>\n <div :class=\"cn('space-y-2 flex-1 min-w-0', densityClass, props.class)\">\n <FieldGroup >\n <Field :class=\"[\n orientation === 'horizontal'\n ? 'grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2'\n : 'space-y-1 gap-1'\n ]\"\n :style=\"orientation === 'horizontal' && labelWidth ? `--label-w:${labelWidth};` : ''\"\n >\n <!-- 메인 라벨 (필수표기 포함) -->\n <FieldLabel \n :for=\"id\" \n :class=\"[\n 'text-xs', // 컴팩트 디자인을 위해 폰트 크기 축소\n orientation === 'horizontal'\n ? 'flex items-center h-[var(--ctl-h)] w-full'\n : 'flex items-center',\n labelAlignClass\n ]\"\n >\n {{ label }}\n <span v-if=\"required\" class=\"text-destructive ml-1\">*</span>\n </FieldLabel>\n\n <FieldContent \n :class=\"[\n orientation === 'horizontal'\n ? 'min-h-[var(--ctl-h)] flex flex-col justify-start gap-0.5 mt-0'\n : 'space-y-2 gap-0'\n ]\"\n >\n <!-- 체크박스/스위치: 항상 가로 정렬로 컨트롤 + 인라인 라벨 묶기 -->\n <FieldGroup v-if=\"type === 'checkbox' || type === 'switch'\" data-slot=\"checkbox-group\">\n <!-- 부모 orientation과 무관하게 수평 정렬 고정 -->\n <Field orientation=\"horizontal\" class=\"flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n />\n <FieldLabel\n v-if=\"inlineLabel\"\n :for=\"id\"\n class=\"text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]\"\n >\n {{ inlineLabel }}\n </FieldLabel>\n </Field>\n </FieldGroup>\n\n <!-- Radio 버튼: radioDirection에 따라 분기 처리 -->\n <FieldGroup v-else-if=\"type === 'radio'\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n\n <!-- 그 외 컨트롤: orientation 규칙 그대로 따름 -->\n <FieldGroup v-else>\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n \n <!-- 설명/에러: 항상 컨트롤 '바로 아래'에 표시 -->\n <FieldContent v-if=\"description || finalError\">\n <FieldDescription v-if=\"description\">\n {{ description }}\n </FieldDescription>\n <FieldError v-if=\"finalError\">\n {{ finalError }}\n </FieldError>\n </FieldContent>\n </FieldContent>\n </Field>\n </FieldGroup>\n </div>\n</template>\n\n\n\n<style scoped>\n/* 높이 토큰(밀도) — CSS 변수는 scoped에서도 자식 요소에 상속됨 */\n.form-density-sm { --ctl-h: 2rem; /* 32px */ }\n.form-density-md { --ctl-h: 2.25rem; /* 36px (shadcn 기본에 근접) */ }\n.form-density-lg { --ctl-h: 2.5rem; /* 40px */ }\n</style>"],"names":["FORM_FIELD_PROPS","props","__props","emit","__emit","internalError","ref","finalError","computed","controlProps","result","propsObj","key","defaultPlaceholders","validateField","currentValue","value","shouldValidateOnMount","handleUpdateModelValue","handleChange","handleFocus","event","handleBlur","labelAlignClass","mapHorizontal","mapVertical","densityClass","controlClass","baseClass","componentMap","JInput","JTextarea","JCheckbox","JSwitch","JCombo","JRadio","JSearchCombo","JDatepicker","resolvedComponent","__expose","_createElementBlock","_unref","cn","_createVNode","FieldGroup","Field","_normalizeClass","_normalizeStyle","FieldLabel","_createTextVNode","_toDisplayString","_hoisted_1","FieldContent","_createBlock","_openBlock","_resolveDynamicComponent","_mergeProps","FieldDescription","FieldError"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
- import { defineComponent as N, ref as R, computed as s, createElementBlock as D, openBlock as a, normalizeClass as p, unref as o, createVNode as d, withCtx as i, normalizeStyle as S, createTextVNode as y, createCommentVNode as u, toDisplayString as h, createBlock as n, resolveDynamicComponent as w, mergeProps as B } from "vue";
1
+ import { defineComponent as q, ref as N, computed as s, createElementBlock as D, openBlock as a, normalizeClass as p, unref as o, createVNode as d, withCtx as i, normalizeStyle as R, createTextVNode as h, createCommentVNode as u, toDisplayString as y, createBlock as n, resolveDynamicComponent as w, mergeProps as B } from "vue";
2
2
  import "../shadcn/index.js";
3
3
  import "lucide-vue-next";
4
4
  /* empty css */
5
5
  import F from "../atoms/JInput.vue.js";
6
- import W from "../atoms/JTextarea.vue.js";
6
+ import S from "../atoms/JTextarea.vue.js";
7
7
  import H from "../atoms/JCheckbox.vue.js";
8
8
  import I from "../atoms/JCombo.vue.js";
9
9
  import J from "../atoms/JSearchCombo.vue.js";
@@ -37,7 +37,7 @@ import Z from "../shadcn/FieldError.vue.js";
37
37
  const ee = {
38
38
  key: 0,
39
39
  class: "text-destructive ml-1"
40
- }, Pe = /* @__PURE__ */ N({
40
+ }, Pe = /* @__PURE__ */ q({
41
41
  __name: "JFormField",
42
42
  props: {
43
43
  class: {},
@@ -48,7 +48,7 @@ const ee = {
48
48
  inlineLabel: {},
49
49
  orientation: { default: "vertical" },
50
50
  labelAlign: { default: "left" },
51
- labelWidth: { default: "8rem" },
51
+ labelWidth: { default: void 0 },
52
52
  id: {},
53
53
  modelValue: {},
54
54
  placeholder: {},
@@ -75,7 +75,7 @@ const ee = {
75
75
  "labelAlign",
76
76
  "labelWidth",
77
77
  "radioDirection"
78
- ], e = l, f = A, c = R(""), g = s(() => e.errorMsg || c.value), b = s(() => {
78
+ ], e = l, f = A, c = N(""), g = s(() => e.errorMsg || c.value), b = s(() => {
79
79
  const t = {}, r = e;
80
80
  for (const m in r)
81
81
  E.includes(m) || (t[m] = r[m]);
@@ -104,11 +104,11 @@ const ee = {
104
104
  e.type === "checkbox" || e.type === "switch" ? r !== "Y" && (c.value = "필수 항목입니다.") : (r == null || r === "") && (c.value = "필수 입력 항목입니다.");
105
105
  }, P = s(() => !(e.modelValue !== null && e.modelValue !== void 0 && e.modelValue !== "")), k = (t) => {
106
106
  f("update:modelValue", t), x(t);
107
- }, _ = (t) => {
108
- f("change", t), x(t);
109
107
  }, $ = (t) => {
110
- f("focus", t);
108
+ f("change", t), x(t);
111
109
  }, C = (t) => {
110
+ f("focus", t);
111
+ }, V = (t) => {
112
112
  P.value && x(), f("blur", t);
113
113
  }, U = s(() => {
114
114
  const t = {
@@ -132,16 +132,16 @@ const ee = {
132
132
  }), z = s(() => {
133
133
  const t = "h-[var(--ctl-h)] leading-[var(--ctl-h)]";
134
134
  return e.type === "datepicker" ? `${t} max-w-xs` : e.type === "radio" && e.radioDirection === "vertical" ? "" : t;
135
- }), q = {
135
+ }), W = {
136
136
  input: F,
137
- textarea: W,
137
+ textarea: S,
138
138
  checkbox: H,
139
139
  switch: G,
140
140
  combo: I,
141
141
  radio: Y,
142
142
  searchCombo: J,
143
143
  datepicker: K
144
- }, V = s(() => q[e.type] || F);
144
+ }, _ = s(() => W[e.type] || F);
145
145
  return j({
146
146
  clearError: () => {
147
147
  c.value = "";
@@ -155,7 +155,7 @@ const ee = {
155
155
  class: p([
156
156
  l.orientation === "horizontal" ? "grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2" : "space-y-1 gap-1"
157
157
  ]),
158
- style: S(l.orientation === "horizontal" ? `--label-w:${l.labelWidth};` : "")
158
+ style: R(l.orientation === "horizontal" && l.labelWidth ? `--label-w:${l.labelWidth};` : "")
159
159
  }, {
160
160
  default: i(() => [
161
161
  d(o(M), {
@@ -168,7 +168,7 @@ const ee = {
168
168
  ])
169
169
  }, {
170
170
  default: i(() => [
171
- y(h(l.label) + " ", 1),
171
+ h(y(l.label) + " ", 1),
172
172
  l.required ? (a(), D("span", ee, "*")) : u("", !0)
173
173
  ]),
174
174
  _: 1
@@ -189,11 +189,11 @@ const ee = {
189
189
  class: "flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center"
190
190
  }, {
191
191
  default: i(() => [
192
- (a(), n(w(V.value), B(b.value, {
192
+ (a(), n(w(_.value), B(b.value, {
193
193
  "onUpdate:modelValue": k,
194
- onChange: _,
195
- onFocus: $,
196
- onBlur: C
194
+ onChange: $,
195
+ onFocus: C,
196
+ onBlur: V
197
197
  }), null, 16)),
198
198
  l.inlineLabel ? (a(), n(o(M), {
199
199
  key: 0,
@@ -201,7 +201,7 @@ const ee = {
201
201
  class: "text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]"
202
202
  }, {
203
203
  default: i(() => [
204
- y(h(l.inlineLabel), 1)
204
+ h(y(l.inlineLabel), 1)
205
205
  ]),
206
206
  _: 1
207
207
  }, 8, ["for"])) : u("", !0)
@@ -212,22 +212,22 @@ const ee = {
212
212
  _: 1
213
213
  })) : l.type === "radio" ? (a(), n(o(v), { key: 1 }, {
214
214
  default: i(() => [
215
- (a(), n(w(V.value), B(b.value, {
215
+ (a(), n(w(_.value), B(b.value, {
216
216
  "onUpdate:modelValue": k,
217
- onChange: _,
218
- onFocus: $,
219
- onBlur: C,
217
+ onChange: $,
218
+ onFocus: C,
219
+ onBlur: V,
220
220
  class: z.value
221
221
  }), null, 16, ["class"]))
222
222
  ]),
223
223
  _: 1
224
224
  })) : (a(), n(o(v), { key: 2 }, {
225
225
  default: i(() => [
226
- (a(), n(w(V.value), B(b.value, {
226
+ (a(), n(w(_.value), B(b.value, {
227
227
  "onUpdate:modelValue": k,
228
- onChange: _,
229
- onFocus: $,
230
- onBlur: C,
228
+ onChange: $,
229
+ onFocus: C,
230
+ onBlur: V,
231
231
  class: z.value
232
232
  }), null, 16, ["class"]))
233
233
  ]),
@@ -237,13 +237,13 @@ const ee = {
237
237
  default: i(() => [
238
238
  l.description ? (a(), n(o(X), { key: 0 }, {
239
239
  default: i(() => [
240
- y(h(l.description), 1)
240
+ h(y(l.description), 1)
241
241
  ]),
242
242
  _: 1
243
243
  })) : u("", !0),
244
244
  g.value ? (a(), n(o(Z), { key: 1 }, {
245
245
  default: i(() => [
246
- y(h(g.value), 1)
246
+ h(y(g.value), 1)
247
247
  ]),
248
248
  _: 1
249
249
  })) : u("", !0)
@@ -1 +1 @@
1
- {"version":3,"file":"JFormField.vue2.js","sources":["../../../../src/components/molecules/JFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Field, FieldContent, FieldLabel, FieldDescription, FieldError, FieldGroup } from '@/components/shadcn'\nimport { JInput, JTextarea, JCheckbox, JSwitch, JCombo, JRadio, JSearchCombo, JDatepicker } from '@/components/atoms'\nimport { cn } from '@/lib/utils'\n\n// 컴포넌트 타입 정의\ntype ComponentType = 'input' | 'textarea' | 'checkbox' | 'switch' | 'combo' | 'radio' | 'searchCombo' | 'datepicker'\n\n// FormField 자체의 props (레이아웃 관련)\nconst FORM_FIELD_PROPS = [\n 'class',\n 'label',\n 'description',\n 'errorMsg',\n 'type',\n 'inlineLabel',\n 'orientation',\n 'labelAlign',\n 'labelWidth',\n 'radioDirection',\n] as const\n\nconst props = withDefaults(\n defineProps<{\n // ============ FormField 자체 props (레이아웃만) ============\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n /** 필드 레이블 */\n label?: string\n /** 필드 설명 (레이블 아래 표시) */\n description?: string\n /** 에러 메시지 */\n errorMsg?: string\n /** 컴포넌트 타입 (렌더링할 컴포넌트 지정) */\n type?: ComponentType\n /** 체크박스/스위치 타입일 때만 사용하는 옆 라벨 */\n inlineLabel?: string\n /** 레이블과 컨트롤의 배치 방향 */\n orientation?: 'vertical' | 'horizontal' | 'responsive'\n /** 레이블 텍스트 정렬 */\n labelAlign?: 'left' | 'middle' | 'right'\n /** 레이블 영역 너비 (horizontal orientation일 때만 적용) */\n labelWidth?: string\n\n // ============ 내부 컴포넌트로 전달할 공통 props ============\n /** Input 요소의 id (label for와 연결) */\n id?: string\n /** v-model로 양방향 데이터 바인딩 */\n modelValue?: any\n /** 입력 전 표시되는 안내문 (Input/Textarea/Select/Combobox) */\n placeholder?: string\n /** 비활성화 상태 (전체) */\n disabled?: boolean\n /** 읽기 전용 상태 (Input/Textarea) */\n readonly?: boolean\n /** 필수 입력/선택 여부 (전체) */\n required?: boolean\n /** form 데이터 전송 시 키 이름 (전체) */\n name?: string\n /** 스타일 테마 지정 (J-prefixed 컴포넌트의 styleType) */\n styleType?: string\n\n // ============ Input 전용 props ============\n /** Input 타입 (text, email, password 등) */\n inputType?: string\n\n\n // ============ Select/Combobox/Radio 전용 props ============\n /** 선택 가능한 항목 배열 */\n options?: Array<{ label: string; value: string | number; disabled?: boolean }>\n \n // ============ Select/Combobox 전용 props ============\n /** 다중 선택 허용 여부 */\n multiple?: boolean\n\n\n // ============ Radio 전용 props ============\n /** Radio 옵션 나열 방향 */\n radioDirection?: 'horizontal' | 'vertical'\n }>(),\n {\n type: 'input',\n orientation: 'vertical',\n labelAlign: 'left',\n labelWidth: '8rem',\n radioDirection: 'horizontal',\n }\n)\n\n// 이벤트 정의\nconst emit = defineEmits<{\n 'update:modelValue': [value: any]\n 'change': [value: any]\n 'focus': [event: FocusEvent]\n 'blur': [event: FocusEvent]\n}>()\n\n// 내부 에러 상태 (props.error가 없을 때만 사용)\nconst internalError = ref<string>('')\n\n// 최종 에러 메시지 (외부 errorMsg가 우선)\nconst finalError = computed(() => props.errorMsg || internalError.value)\n\n// FormField 자체 props와 내부 컴포넌트 props 분리\nconst controlProps = computed(() => {\n const result: Record<string, any> = {}\n const propsObj = props as Record<string, any>\n \n for (const key in propsObj) {\n // FormField 자체 props는 제외\n if (!FORM_FIELD_PROPS.includes(key as any)) {\n result[key] = propsObj[key]\n }\n }\n \n // inputType을 type으로 변환 (JInput 컴포넌트에서 사용)\n if (props.inputType && props.type === 'input') {\n result.type = props.inputType\n delete result.inputType // inputType은 제거\n \n // placeholder가 없으면 inputType에 따라 기본값 설정\n if (!props.placeholder) {\n const defaultPlaceholders: Record<string, string> = {\n 'text': '텍스트를 입력하세요',\n 'email': '이메일을 입력하세요',\n 'password': '비밀번호를 입력하세요',\n 'tel': '전화번호를 입력하세요',\n 'url': 'URL을 입력하세요',\n 'number': '숫자를 입력하세요',\n 'search': '검색어를 입력하세요',\n 'date': '날짜를 선택하세요',\n 'time': '시간을 선택하세요',\n 'datetime-local': '날짜와 시간을 선택하세요',\n 'month': '월을 선택하세요',\n 'week': '주를 선택하세요',\n }\n \n result.placeholder = defaultPlaceholders[props.inputType] || '입력하세요'\n }\n }\n \n // radioDirection을 styletype으로 변환 (JRadio 컴포넌트에서 사용)\n if (props.radioDirection && props.type === 'radio') {\n result.styletype = props.radioDirection\n delete result.radioDirection // radioDirection은 제거\n }\n \n return result\n})\n\n // Built-in validation\n const validateField = (currentValue?: any) => {\n if (!props.required) return\n\n internalError.value = ''\n\n // 현재 값 또는 props.modelValue 사용\n const value = currentValue !== undefined ? currentValue : props.modelValue\n\n // Required 체크\n if (props.type === 'checkbox' || props.type === 'switch') {\n if (value !== 'Y') {\n internalError.value = '필수 항목입니다.'\n }\n } else {\n if (value === null || value === undefined || value === '') {\n internalError.value = '필수 입력 항목입니다.'\n }\n }\n }\n\n // 초기 로드 시 validation 실행 (blur 이벤트에서만)\n const shouldValidateOnMount = computed(() => {\n // Storybook에서 초기값이 있는 경우 validation 스킵\n if (props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '') {\n return false\n }\n return true\n })\n\n// 이벤트 핸들러\nconst handleUpdateModelValue = (value: any) => {\n emit('update:modelValue', value)\n \n // 값이 변경되면 즉시 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleChange = (value: any) => {\n emit('change', value)\n \n // change 이벤트에서도 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleFocus = (event: FocusEvent) => {\n emit('focus', event)\n}\n\n const handleBlur = (event: FocusEvent) => {\n // blur 시에만 validation 실행 (초기 로드 시에는 실행하지 않음)\n if (shouldValidateOnMount.value) {\n validateField()\n }\n emit('blur', event)\n }\n\n// 레이블 정렬 클래스 (레이블 영역 내에서 레이블의 위치 제어)\nconst labelAlignClass = computed(() => {\n // horizontal일 때는 레이블 영역 내에서 레이블의 위치를 제어\n const mapHorizontal = { \n left: 'justify-start', // 레이블 영역의 왼쪽에 레이블 위치\n middle: 'justify-center', // 레이블 영역의 가운데에 레이블 위치\n right: 'justify-end' // 레이블 영역의 오른쪽에 레이블 위치 (input과 가까이)\n }\n // vertical일 때는 텍스트 정렬만 제어\n const mapVertical = { left: 'text-left', middle: 'text-center', right: 'text-right' }\n return props.orientation === 'horizontal' ? mapHorizontal[props.labelAlign] : mapVertical[props.labelAlign]\n})\n\n/** 행높이 토큰 클래스 매핑 */\nconst densityClass = computed(() => {\n switch (props.styleType) {\n case 'md': return 'form-density-md'\n case 'lg': return 'form-density-lg'\n default: return 'form-density-sm' // 기본값 변경: md → sm (컴팩트 디자인)\n }\n})\n\n/** 컨트롤 클래스 (datepicker는 최대 너비 제한, radio vertical은 높이 제한 없음) */\nconst controlClass = computed(() => {\n const baseClass = 'h-[var(--ctl-h)] leading-[var(--ctl-h)]'\n \n if (props.type === 'datepicker') {\n return `${baseClass} max-w-xs`\n }\n \n // Radio vertical일 때는 높이 제한 없음\n if (props.type === 'radio' && props.radioDirection === 'vertical') {\n return ''\n }\n \n return baseClass\n})\n\n// 컴포넌트 매핑\nconst componentMap: Record<ComponentType, any> = {\n input: JInput,\n textarea: JTextarea,\n checkbox: JCheckbox,\n switch: JSwitch,\n combo: JCombo,\n radio: JRadio,\n searchCombo: JSearchCombo,\n datepicker: JDatepicker,\n}\n\n// type에 따라 렌더링할 컴포넌트 결정\nconst resolvedComponent = computed(() => {\n return componentMap[props.type!] || JInput\n})\n\n// 외부에서 수동으로 에러 클리어 가능하도록 expose\ndefineExpose({\n clearError: () => { internalError.value = '' }\n})\n</script>\n\n<template>\n <div :class=\"cn('space-y-2 flex-1 min-w-0', densityClass, props.class)\">\n <FieldGroup >\n <Field :class=\"[\n orientation === 'horizontal'\n ? 'grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2'\n : 'space-y-1 gap-1'\n ]\"\n :style=\"orientation === 'horizontal' ? `--label-w:${labelWidth};` : ''\"\n >\n <!-- 메인 라벨 (필수표기 포함) -->\n <FieldLabel \n :for=\"id\" \n :class=\"[\n 'text-xs', // 컴팩트 디자인을 위해 폰트 크기 축소\n orientation === 'horizontal'\n ? 'flex items-center h-[var(--ctl-h)] w-full'\n : 'flex items-center',\n labelAlignClass\n ]\"\n >\n {{ label }}\n <span v-if=\"required\" class=\"text-destructive ml-1\">*</span>\n </FieldLabel>\n\n <FieldContent \n :class=\"[\n orientation === 'horizontal'\n ? 'min-h-[var(--ctl-h)] flex flex-col justify-start gap-0.5 mt-0'\n : 'space-y-2 gap-0'\n ]\"\n >\n <!-- 체크박스/스위치: 항상 가로 정렬로 컨트롤 + 인라인 라벨 묶기 -->\n <FieldGroup v-if=\"type === 'checkbox' || type === 'switch'\" data-slot=\"checkbox-group\">\n <!-- 부모 orientation과 무관하게 수평 정렬 고정 -->\n <Field orientation=\"horizontal\" class=\"flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n />\n <FieldLabel\n v-if=\"inlineLabel\"\n :for=\"id\"\n class=\"text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]\"\n >\n {{ inlineLabel }}\n </FieldLabel>\n </Field>\n </FieldGroup>\n\n <!-- Radio 버튼: radioDirection에 따라 분기 처리 -->\n <FieldGroup v-else-if=\"type === 'radio'\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n\n <!-- 그 외 컨트롤: orientation 규칙 그대로 따름 -->\n <FieldGroup v-else>\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n \n <!-- 설명/에러: 항상 컨트롤 '바로 아래'에 표시 -->\n <FieldContent v-if=\"description || finalError\">\n <FieldDescription v-if=\"description\">\n {{ description }}\n </FieldDescription>\n <FieldError v-if=\"finalError\">\n {{ finalError }}\n </FieldError>\n </FieldContent>\n </FieldContent>\n </Field>\n </FieldGroup>\n </div>\n</template>\n\n\n\n<style scoped>\n/* 높이 토큰(밀도) — CSS 변수는 scoped에서도 자식 요소에 상속됨 */\n.form-density-sm { --ctl-h: 2rem; /* 32px */ }\n.form-density-md { --ctl-h: 2.25rem; /* 36px (shadcn 기본에 근접) */ }\n.form-density-lg { --ctl-h: 2.5rem; /* 40px */ }\n</style>"],"names":["FORM_FIELD_PROPS","props","__props","emit","__emit","internalError","ref","finalError","computed","controlProps","result","propsObj","key","defaultPlaceholders","validateField","currentValue","value","shouldValidateOnMount","handleUpdateModelValue","handleChange","handleFocus","event","handleBlur","labelAlignClass","mapHorizontal","mapVertical","densityClass","controlClass","baseClass","componentMap","JInput","JTextarea","JCheckbox","JSwitch","JCombo","JRadio","JSearchCombo","JDatepicker","resolvedComponent","__expose","_createElementBlock","_unref","cn","_createVNode","FieldGroup","Field","_normalizeClass","_normalizeStyle","FieldLabel","_createTextVNode","_toDisplayString","_hoisted_1","FieldContent","_createBlock","_openBlock","_resolveDynamicComponent","_mergeProps","FieldDescription","FieldError"],"mappings":"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"}
1
+ {"version":3,"file":"JFormField.vue2.js","sources":["../../../../src/components/molecules/JFormField.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Field, FieldContent, FieldLabel, FieldDescription, FieldError, FieldGroup } from '@/components/shadcn'\nimport { JInput, JTextarea, JCheckbox, JSwitch, JCombo, JRadio, JSearchCombo, JDatepicker } from '@/components/atoms'\nimport { cn } from '@/lib/utils'\n\n// 컴포넌트 타입 정의\ntype ComponentType = 'input' | 'textarea' | 'checkbox' | 'switch' | 'combo' | 'radio' | 'searchCombo' | 'datepicker'\n\n// FormField 자체의 props (레이아웃 관련)\nconst FORM_FIELD_PROPS = [\n 'class',\n 'label',\n 'description',\n 'errorMsg',\n 'type',\n 'inlineLabel',\n 'orientation',\n 'labelAlign',\n 'labelWidth',\n 'radioDirection',\n] as const\n\nconst props = withDefaults(\n defineProps<{\n // ============ FormField 자체 props (레이아웃만) ============\n /** 추가 클래스 (외부 커스터마이징용) */\n class?: string\n /** 필드 레이블 */\n label?: string\n /** 필드 설명 (레이블 아래 표시) */\n description?: string\n /** 에러 메시지 */\n errorMsg?: string\n /** 컴포넌트 타입 (렌더링할 컴포넌트 지정) */\n type?: ComponentType\n /** 체크박스/스위치 타입일 때만 사용하는 옆 라벨 */\n inlineLabel?: string\n /** 레이블과 컨트롤의 배치 방향 */\n orientation?: 'vertical' | 'horizontal' | 'responsive'\n /** 레이블 텍스트 정렬 */\n labelAlign?: 'left' | 'middle' | 'right'\n /** 레이블 영역 너비 (horizontal orientation일 때만 적용) */\n labelWidth?: string\n\n // ============ 내부 컴포넌트로 전달할 공통 props ============\n /** Input 요소의 id (label for와 연결) */\n id?: string\n /** v-model로 양방향 데이터 바인딩 */\n modelValue?: any\n /** 입력 전 표시되는 안내문 (Input/Textarea/Select/Combobox) */\n placeholder?: string\n /** 비활성화 상태 (전체) */\n disabled?: boolean\n /** 읽기 전용 상태 (Input/Textarea) */\n readonly?: boolean\n /** 필수 입력/선택 여부 (전체) */\n required?: boolean\n /** form 데이터 전송 시 키 이름 (전체) */\n name?: string\n /** 스타일 테마 지정 (J-prefixed 컴포넌트의 styleType) */\n styleType?: string\n\n // ============ Input 전용 props ============\n /** Input 타입 (text, email, password 등) */\n inputType?: string\n\n\n // ============ Select/Combobox/Radio 전용 props ============\n /** 선택 가능한 항목 배열 */\n options?: Array<{ label: string; value: string | number; disabled?: boolean }>\n \n // ============ Select/Combobox 전용 props ============\n /** 다중 선택 허용 여부 */\n multiple?: boolean\n\n\n // ============ Radio 전용 props ============\n /** Radio 옵션 나열 방향 */\n radioDirection?: 'horizontal' | 'vertical'\n }>(),\n {\n type: 'input',\n orientation: 'vertical',\n labelAlign: 'left',\n labelWidth: undefined,\n radioDirection: 'horizontal',\n }\n)\n\n// 이벤트 정의\nconst emit = defineEmits<{\n 'update:modelValue': [value: any]\n 'change': [value: any]\n 'focus': [event: FocusEvent]\n 'blur': [event: FocusEvent]\n}>()\n\n// 내부 에러 상태 (props.error가 없을 때만 사용)\nconst internalError = ref<string>('')\n\n// 최종 에러 메시지 (외부 errorMsg가 우선)\nconst finalError = computed(() => props.errorMsg || internalError.value)\n\n// FormField 자체 props와 내부 컴포넌트 props 분리\nconst controlProps = computed(() => {\n const result: Record<string, any> = {}\n const propsObj = props as Record<string, any>\n \n for (const key in propsObj) {\n // FormField 자체 props는 제외\n if (!FORM_FIELD_PROPS.includes(key as any)) {\n result[key] = propsObj[key]\n }\n }\n \n // inputType을 type으로 변환 (JInput 컴포넌트에서 사용)\n if (props.inputType && props.type === 'input') {\n result.type = props.inputType\n delete result.inputType // inputType은 제거\n \n // placeholder가 없으면 inputType에 따라 기본값 설정\n if (!props.placeholder) {\n const defaultPlaceholders: Record<string, string> = {\n 'text': '텍스트를 입력하세요',\n 'email': '이메일을 입력하세요',\n 'password': '비밀번호를 입력하세요',\n 'tel': '전화번호를 입력하세요',\n 'url': 'URL을 입력하세요',\n 'number': '숫자를 입력하세요',\n 'search': '검색어를 입력하세요',\n 'date': '날짜를 선택하세요',\n 'time': '시간을 선택하세요',\n 'datetime-local': '날짜와 시간을 선택하세요',\n 'month': '월을 선택하세요',\n 'week': '주를 선택하세요',\n }\n \n result.placeholder = defaultPlaceholders[props.inputType] || '입력하세요'\n }\n }\n \n // radioDirection을 styletype으로 변환 (JRadio 컴포넌트에서 사용)\n if (props.radioDirection && props.type === 'radio') {\n result.styletype = props.radioDirection\n delete result.radioDirection // radioDirection은 제거\n }\n \n return result\n})\n\n // Built-in validation\n const validateField = (currentValue?: any) => {\n if (!props.required) return\n\n internalError.value = ''\n\n // 현재 값 또는 props.modelValue 사용\n const value = currentValue !== undefined ? currentValue : props.modelValue\n\n // Required 체크\n if (props.type === 'checkbox' || props.type === 'switch') {\n if (value !== 'Y') {\n internalError.value = '필수 항목입니다.'\n }\n } else {\n if (value === null || value === undefined || value === '') {\n internalError.value = '필수 입력 항목입니다.'\n }\n }\n }\n\n // 초기 로드 시 validation 실행 (blur 이벤트에서만)\n const shouldValidateOnMount = computed(() => {\n // Storybook에서 초기값이 있는 경우 validation 스킵\n if (props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '') {\n return false\n }\n return true\n })\n\n// 이벤트 핸들러\nconst handleUpdateModelValue = (value: any) => {\n emit('update:modelValue', value)\n \n // 값이 변경되면 즉시 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleChange = (value: any) => {\n emit('change', value)\n \n // change 이벤트에서도 validation 실행 (현재 값 전달)\n validateField(value)\n}\n\nconst handleFocus = (event: FocusEvent) => {\n emit('focus', event)\n}\n\n const handleBlur = (event: FocusEvent) => {\n // blur 시에만 validation 실행 (초기 로드 시에는 실행하지 않음)\n if (shouldValidateOnMount.value) {\n validateField()\n }\n emit('blur', event)\n }\n\n// 레이블 정렬 클래스 (레이블 영역 내에서 레이블의 위치 제어)\nconst labelAlignClass = computed(() => {\n // horizontal일 때는 레이블 영역 내에서 레이블의 위치를 제어\n const mapHorizontal = { \n left: 'justify-start', // 레이블 영역의 왼쪽에 레이블 위치\n middle: 'justify-center', // 레이블 영역의 가운데에 레이블 위치\n right: 'justify-end' // 레이블 영역의 오른쪽에 레이블 위치 (input과 가까이)\n }\n // vertical일 때는 텍스트 정렬만 제어\n const mapVertical = { left: 'text-left', middle: 'text-center', right: 'text-right' }\n return props.orientation === 'horizontal' ? mapHorizontal[props.labelAlign] : mapVertical[props.labelAlign]\n})\n\n/** 행높이 토큰 클래스 매핑 */\nconst densityClass = computed(() => {\n switch (props.styleType) {\n case 'md': return 'form-density-md'\n case 'lg': return 'form-density-lg'\n default: return 'form-density-sm' // 기본값 변경: md → sm (컴팩트 디자인)\n }\n})\n\n/** 컨트롤 클래스 (datepicker는 최대 너비 제한, radio vertical은 높이 제한 없음) */\nconst controlClass = computed(() => {\n const baseClass = 'h-[var(--ctl-h)] leading-[var(--ctl-h)]'\n \n if (props.type === 'datepicker') {\n return `${baseClass} max-w-xs`\n }\n \n // Radio vertical일 때는 높이 제한 없음\n if (props.type === 'radio' && props.radioDirection === 'vertical') {\n return ''\n }\n \n return baseClass\n})\n\n// 컴포넌트 매핑\nconst componentMap: Record<ComponentType, any> = {\n input: JInput,\n textarea: JTextarea,\n checkbox: JCheckbox,\n switch: JSwitch,\n combo: JCombo,\n radio: JRadio,\n searchCombo: JSearchCombo,\n datepicker: JDatepicker,\n}\n\n// type에 따라 렌더링할 컴포넌트 결정\nconst resolvedComponent = computed(() => {\n return componentMap[props.type!] || JInput\n})\n\n// 외부에서 수동으로 에러 클리어 가능하도록 expose\ndefineExpose({\n clearError: () => { internalError.value = '' }\n})\n</script>\n\n<template>\n <div :class=\"cn('space-y-2 flex-1 min-w-0', densityClass, props.class)\">\n <FieldGroup >\n <Field :class=\"[\n orientation === 'horizontal'\n ? 'grid grid-cols-[var(--label-w,8rem)_1fr] items-start space-y-0 gap-2'\n : 'space-y-1 gap-1'\n ]\"\n :style=\"orientation === 'horizontal' && labelWidth ? `--label-w:${labelWidth};` : ''\"\n >\n <!-- 메인 라벨 (필수표기 포함) -->\n <FieldLabel \n :for=\"id\" \n :class=\"[\n 'text-xs', // 컴팩트 디자인을 위해 폰트 크기 축소\n orientation === 'horizontal'\n ? 'flex items-center h-[var(--ctl-h)] w-full'\n : 'flex items-center',\n labelAlignClass\n ]\"\n >\n {{ label }}\n <span v-if=\"required\" class=\"text-destructive ml-1\">*</span>\n </FieldLabel>\n\n <FieldContent \n :class=\"[\n orientation === 'horizontal'\n ? 'min-h-[var(--ctl-h)] flex flex-col justify-start gap-0.5 mt-0'\n : 'space-y-2 gap-0'\n ]\"\n >\n <!-- 체크박스/스위치: 항상 가로 정렬로 컨트롤 + 인라인 라벨 묶기 -->\n <FieldGroup v-if=\"type === 'checkbox' || type === 'switch'\" data-slot=\"checkbox-group\">\n <!-- 부모 orientation과 무관하게 수평 정렬 고정 -->\n <Field orientation=\"horizontal\" class=\"flex gap-2 space-y-0 h-[var(--ctl-h)] leading-[var(--ctl-h)] items-center\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n />\n <FieldLabel\n v-if=\"inlineLabel\"\n :for=\"id\"\n class=\"text-xs font-normal m-0 h-[var(--ctl-h)] leading-[var(--ctl-h)]\"\n >\n {{ inlineLabel }}\n </FieldLabel>\n </Field>\n </FieldGroup>\n\n <!-- Radio 버튼: radioDirection에 따라 분기 처리 -->\n <FieldGroup v-else-if=\"type === 'radio'\">\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n\n <!-- 그 외 컨트롤: orientation 규칙 그대로 따름 -->\n <FieldGroup v-else>\n <component\n :is=\"resolvedComponent\"\n v-bind=\"controlProps\"\n @update:modelValue=\"handleUpdateModelValue\"\n @change=\"handleChange\"\n @focus=\"handleFocus\"\n @blur=\"handleBlur\"\n :class=\"controlClass\"\n />\n </FieldGroup>\n \n <!-- 설명/에러: 항상 컨트롤 '바로 아래'에 표시 -->\n <FieldContent v-if=\"description || finalError\">\n <FieldDescription v-if=\"description\">\n {{ description }}\n </FieldDescription>\n <FieldError v-if=\"finalError\">\n {{ finalError }}\n </FieldError>\n </FieldContent>\n </FieldContent>\n </Field>\n </FieldGroup>\n </div>\n</template>\n\n\n\n<style scoped>\n/* 높이 토큰(밀도) — CSS 변수는 scoped에서도 자식 요소에 상속됨 */\n.form-density-sm { --ctl-h: 2rem; /* 32px */ }\n.form-density-md { --ctl-h: 2.25rem; /* 36px (shadcn 기본에 근접) */ }\n.form-density-lg { --ctl-h: 2.5rem; /* 40px */ }\n</style>"],"names":["FORM_FIELD_PROPS","props","__props","emit","__emit","internalError","ref","finalError","computed","controlProps","result","propsObj","key","defaultPlaceholders","validateField","currentValue","value","shouldValidateOnMount","handleUpdateModelValue","handleChange","handleFocus","event","handleBlur","labelAlignClass","mapHorizontal","mapVertical","densityClass","controlClass","baseClass","componentMap","JInput","JTextarea","JCheckbox","JSwitch","JCombo","JRadio","JSearchCombo","JDatepicker","resolvedComponent","__expose","_createElementBlock","_unref","cn","_createVNode","FieldGroup","Field","_normalizeClass","_normalizeStyle","FieldLabel","_createTextVNode","_toDisplayString","_hoisted_1","FieldContent","_createBlock","_openBlock","_resolveDynamicComponent","_mergeProps","FieldDescription","FieldError"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUA,UAAMA,IAAmB;AAAA,MACvB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA,GAGIC,IAAQC,GAoERC,IAAOC,GAQPC,IAAgBC,EAAY,EAAE,GAG9BC,IAAaC,EAAS,MAAMP,EAAM,YAAYI,EAAc,KAAK,GAGjEI,IAAeD,EAAS,MAAM;AAClC,YAAME,IAA8B,CAAA,GAC9BC,IAAWV;AAEjB,iBAAWW,KAAOD;AAEhB,QAAKX,EAAiB,SAASY,CAAU,MACvCF,EAAOE,CAAG,IAAID,EAASC,CAAG;AAK9B,UAAIX,EAAM,aAAaA,EAAM,SAAS,YACpCS,EAAO,OAAOT,EAAM,WACpB,OAAOS,EAAO,WAGV,CAACT,EAAM,cAAa;AACtB,cAAMY,IAA8C;AAAA,UAClD,MAAQ;AAAA,UACR,OAAS;AAAA,UACT,UAAY;AAAA,UACZ,KAAO;AAAA,UACP,KAAO;AAAA,UACP,QAAU;AAAA,UACV,QAAU;AAAA,UACV,MAAQ;AAAA,UACR,MAAQ;AAAA,UACR,kBAAkB;AAAA,UAClB,OAAS;AAAA,UACT,MAAQ;AAAA,QAAA;AAGV,QAAAH,EAAO,cAAcG,EAAoBZ,EAAM,SAAS,KAAK;AAAA,MAC/D;AAIF,aAAIA,EAAM,kBAAkBA,EAAM,SAAS,YACzCS,EAAO,YAAYT,EAAM,gBACzB,OAAOS,EAAO,iBAGTA;AAAA,IACT,CAAC,GAGOI,IAAgB,CAACC,MAAuB;AAC5C,UAAI,CAACd,EAAM,SAAU;AAErB,MAAAI,EAAc,QAAQ;AAGtB,YAAMW,IAAQD,MAAiB,SAAYA,IAAed,EAAM;AAGhE,MAAIA,EAAM,SAAS,cAAcA,EAAM,SAAS,WAC1Ce,MAAU,QACZX,EAAc,QAAQ,gBAGpBW,KAAU,QAA+BA,MAAU,QACrDX,EAAc,QAAQ;AAAA,IAG5B,GAGMY,IAAwBT,EAAS,MAEjC,EAAAP,EAAM,eAAe,QAAQA,EAAM,eAAe,UAAaA,EAAM,eAAe,GAIzF,GAGGiB,IAAyB,CAACF,MAAe;AAC7C,MAAAb,EAAK,qBAAqBa,CAAK,GAG/BF,EAAcE,CAAK;AAAA,IACrB,GAEMG,IAAe,CAACH,MAAe;AACnC,MAAAb,EAAK,UAAUa,CAAK,GAGpBF,EAAcE,CAAK;AAAA,IACrB,GAEMI,IAAc,CAACC,MAAsB;AACzC,MAAAlB,EAAK,SAASkB,CAAK;AAAA,IACrB,GAEQC,IAAa,CAACD,MAAsB;AAExC,MAAIJ,EAAsB,SACxBH,EAAA,GAEFX,EAAK,QAAQkB,CAAK;AAAA,IACpB,GAGIE,IAAkBf,EAAS,MAAM;AAErC,YAAMgB,IAAgB;AAAA,QACpB,MAAM;AAAA;AAAA,QACN,QAAQ;AAAA;AAAA,QACR,OAAO;AAAA;AAAA,MAAA,GAGHC,IAAc,EAAE,MAAM,aAAa,QAAQ,eAAe,OAAO,aAAA;AACvE,aAAOxB,EAAM,gBAAgB,eAAeuB,EAAcvB,EAAM,UAAU,IAAIwB,EAAYxB,EAAM,UAAU;AAAA,IAC5G,CAAC,GAGKyB,IAAelB,EAAS,MAAM;AAClC,cAAQP,EAAM,WAAA;AAAA,QACZ,KAAK;AAAM,iBAAO;AAAA,QAClB,KAAK;AAAM,iBAAO;AAAA,QAClB;AAAW,iBAAO;AAAA,MAAA;AAAA,IAEtB,CAAC,GAGK0B,IAAenB,EAAS,MAAM;AAClC,YAAMoB,IAAY;AAElB,aAAI3B,EAAM,SAAS,eACV,GAAG2B,CAAS,cAIjB3B,EAAM,SAAS,WAAWA,EAAM,mBAAmB,aAC9C,KAGF2B;AAAA,IACT,CAAC,GAGKC,IAA2C;AAAA,MAC/C,OAAOC;AAAAA,MACP,UAAUC;AAAAA,MACV,UAAUC;AAAAA,MACV,QAAQC;AAAAA,MACR,OAAOC;AAAAA,MACP,OAAOC;AAAAA,MACP,aAAaC;AAAAA,MACb,YAAYC;AAAAA,IAAA,GAIRC,IAAoB9B,EAAS,MAC1BqB,EAAa5B,EAAM,IAAK,KAAK6B,CACrC;AAGD,WAAAS,EAAa;AAAA,MACX,YAAY,MAAM;AAAE,QAAAlC,EAAc,QAAQ;AAAA,MAAG;AAAA,IAAA,CAC9C,mBAICmC,EA2FM,OAAA;AAAA,MA3FA,SAAOC,EAAAC,CAAA,EAAE,4BAA6BhB,SAAczB,EAAM,KAAK,CAAA;AAAA,IAAA;MACnE0C,EAyFaF,EAAAG,CAAA,GAAA,MAAA;AAAA,mBAxFX,MAuFQ;AAAA,UAvFRD,EAuFQF,EAAAI,CAAA,GAAA;AAAA,YAvFA,OAAKC,EAAA;AAAA,cAAc5C,EAAA,gBAAW;;YAKnC,OAAK6C,EAAE7C,EAAA,gBAAW,gBAAqBA,EAAA,0BAA0BA,EAAA,UAAU,MAAA,EAAA;AAAA,UAAA;uBAG5E,MAYa;AAAA,cAZbyC,EAYaF,EAAAO,CAAA,GAAA;AAAA,gBAXV,KAAK9C,EAAA;AAAA,gBACL,OAAK4C,EAAA;AAAA;;kBAAgE5C,EAAA,gBAAW;kBAA+HqB,EAAA;AAAA,gBAAA;;2BAQhN,MAAW;AAAA,kBAAR0B,EAAAC,EAAAhD,EAAA,KAAK,IAAG,KACX,CAAA;AAAA,kBAAYA,EAAA,iBAAZsC,EAA4D,QAA5DW,IAAoD,GAAC;;;;cAGvDR,EAgEeF,EAAAW,CAAA,GAAA;AAAA,gBA/DZ,OAAKN,EAAA;AAAA,kBAAgB5C,EAAA,gBAAW;;;2BAOjC,MAmBa;AAAA,kBAnBKA,EAAA,uBAAuBA,EAAA,SAAI,iBAA7CmD,EAmBaZ,EAAAG,CAAA,GAAA;AAAA;oBAnB+C,aAAU;AAAA,kBAAA;+BAEpE,MAgBQ;AAAA,sBAhBRD,EAgBQF,EAAAI,CAAA,GAAA;AAAA,wBAhBD,aAAY;AAAA,wBAAa,OAAM;AAAA,sBAAA;mCACpC,MAOE;AAAA,2BAPFS,EAAA,GAAAD,EAOEE,EANKjB,EAAA,KAAiB,GADxBkB,EAEU/C,EAKR,OALoB;AAAA,4BACnB,uBAAmBS;AAAA,4BACnB,UAAQC;AAAA,4BACR,SAAOC;AAAA,4BACP,QAAME;AAAA,0BAAA;0BAGDpB,EAAA,oBADRmD,EAMaZ,EAAAO,CAAA,GAAA;AAAA;4BAJV,KAAK9C,EAAA;AAAA,4BACN,OAAM;AAAA,0BAAA;uCAEN,MAAiB;AAAA,kCAAdA,EAAA,WAAW,GAAA,CAAA;AAAA,4BAAA;;;;;;;;wBAMGA,EAAA,SAAI,gBAA3BmD,EAUaZ,EAAAG,CAAA,GAAA,EAAA,KAAA,KAAA;AAAA,+BATX,MAQE;AAAA,uBARFU,EAAA,GAAAD,EAQEE,EAPKjB,EAAA,KAAiB,GADxBkB,EAEU/C,EAMR,OANoB;AAAA,wBACnB,uBAAmBS;AAAA,wBACnB,UAAQC;AAAA,wBACR,SAAOC;AAAA,wBACP,QAAME;AAAA,wBACN,OAAOK,EAAA;AAAA,sBAAA;;;8BAKZ0B,EAUaZ,EAAAG,CAAA,GAAA,EAAA,KAAA,KAAA;AAAA,+BATX,MAQE;AAAA,uBARFU,EAAA,GAAAD,EAQEE,EAPKjB,EAAA,KAAiB,GADxBkB,EAEU/C,EAMR,OANoB;AAAA,wBACnB,uBAAmBS;AAAA,wBACnB,UAAQC;AAAA,wBACR,SAAOC;AAAA,wBACP,QAAME;AAAA,wBACN,OAAOK,EAAA;AAAA,sBAAA;;;;kBAKQzB,EAAA,eAAeK,EAAA,cAAnC8C,EAOeZ,EAAAW,CAAA,GAAA,EAAA,KAAA,KAAA;AAAA,+BANb,MAEmB;AAAA,sBAFKlD,EAAA,oBAAxBmD,EAEmBZ,EAAAgB,CAAA,GAAA,EAAA,KAAA,KAAA;AAAA,mCADjB,MAAiB;AAAA,8BAAdvD,EAAA,WAAW,GAAA,CAAA;AAAA,wBAAA;;;sBAEEK,EAAA,cAAlB8C,EAEaZ,EAAAiB,CAAA,GAAA,EAAA,KAAA,KAAA;AAAA,mCADX,MAAgB;AAAA,8BAAbnD,EAAA,KAAU,GAAA,CAAA;AAAA,wBAAA;;;;;;;;;;;;;;;;;;"}
@@ -3,5 +3,5 @@
3
3
  for (const [t_key, t_val] of t_opts)
4
4
  t_merged[t_key] = t_val;
5
5
  return t_merged;
6
- };,u=t(e.default,[["__scopeId","data-v-e15773b9"]]);exports.default=u;
6
+ };,u=t(e.default,[["__scopeId","data-v-1e163e81"]]);exports.default=u;
7
7
  //# sourceMappingURL=JTabs.vue.cjs.map
@@ -6,8 +6,8 @@ const t = (t_comp, t_opts) => {
6
6
  t_merged[t_key] = t_val;
7
7
  return t_merged;
8
8
  };
9
- const p = /* @__PURE__ */ t(o, [["__scopeId", "data-v-e15773b9"]]);
9
+ const m = /* @__PURE__ */ t(o, [["__scopeId", "data-v-1e163e81"]]);
10
10
  export {
11
- p as default
11
+ m as default
12
12
  };
13
13
  //# sourceMappingURL=JTabs.vue.js.map
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue");require("../shadcn/index.cjs");const h=require("lucide-vue-next"),i=require("../../lib/utils.cjs"),x=require("../atoms/JIcon.vue.cjs"),y=require("../shadcn/Tabs.vue.cjs"),T=require("../shadcn/TabsList.vue.cjs"),b=require("../shadcn/TabsTrigger.vue.cjs"),B=require("../shadcn/TabsContent.vue.cjs"),S={class:"flex-1 truncate"},E=["aria-label","onClick"],P={class:"flex-1 w-full overflow-auto"},q={key:1,class:"p-4"},V={class:"text-muted-foreground"},z=e.defineComponent({__name:"JTabs",props:{tabs:{},activeTabId:{},class:{},listClass:{},styletype:{default:"default"}},emits:["tabChange","tabClose","update:activeTabId"],setup(f,{emit:v}){const n=f,r=v,u=e.computed(()=>Array.isArray(n.tabs)?n.tabs:[]),a=e.ref(n.activeTabId||(u.value.length>0?u.value[0]?.id:"")||"");let o=!1;e.watch(()=>n.activeTabId,t=>{t!==void 0&&t!==a.value&&(a.value=t)},{immediate:!0}),e.watch(u,t=>{!n.activeTabId&&t.length>0&&!t.find(s=>s.id===a.value)&&t[0]&&(a.value=t[0].id)});const _=t=>{if(o)return;const s=String(t);s!==a.value&&(o=!0,a.value=s,r("update:activeTabId",s),r("tabChange",s),e.nextTick(()=>{o=!1}))},m=t=>{o||t===a.value||(o=!0,a.value=t,r("update:activeTabId",t),r("tabChange",t),e.nextTick(()=>{o=!1}))},C=(t,s)=>{t.stopPropagation(),r("tabClose",s)},g=e.computed(()=>i.cn("flex flex-col w-full h-full",n.class)),d={default:{tabPaddingClass:"px-2.5 py-1",tabTextSizeClass:"text-xs",listPaddingClass:"p-0.5"},minimal:{tabPaddingClass:"px-2 py-0.5",tabTextSizeClass:"text-xs",listPaddingClass:"p-0.5"}},c=e.computed(()=>d[n.styletype]??d.default),k=e.computed(()=>i.cn("w-full justify-start",c.value.listPaddingClass,n.listClass));return(t,s)=>(e.openBlock(),e.createBlock(e.unref(y.default),{"model-value":a.value,"onUpdate:modelValue":_,orientation:"horizontal",class:e.normalizeClass(g.value)},{default:e.withCtx(()=>[e.createVNode(e.unref(T.default),{class:e.normalizeClass(k.value)},{default:e.withCtx(()=>[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(u.value,l=>(e.openBlock(),e.createBlock(e.unref(b.default),{key:l.id,value:l.id,onClick:p=>m(l.id),class:e.normalizeClass(e.unref(i.cn)("!flex !items-center !gap-2",c.value.tabPaddingClass,c.value.tabTextSizeClass))},{default:e.withCtx(()=>[l.icon?(e.openBlock(),e.createBlock(x.default,{key:0,name:l.icon,size:"sm",class:"flex-shrink-0"},null,8,["name"])):e.createCommentVNode("",!0),e.createElementVNode("span",S,e.toDisplayString(l.label),1),l.closable?(e.openBlock(),e.createElementBlock("button",{key:1,type:"button",class:"flex-shrink-0 h-3.5 w-3.5 rounded-sm hover:bg-destructive/10 hover:text-destructive transition-colors focus:outline-none focus:ring-2 focus:ring-ring flex items-center justify-center","aria-label":`${l.label} 탭 닫기`,onClick:p=>C(p,l.id)},[e.createVNode(e.unref(h.X),{class:"h-2.5 w-2.5"})],8,E)):e.createCommentVNode("",!0)]),_:2},1032,["value","onClick","class"]))),128))]),_:1},8,["class"]),e.createElementVNode("div",P,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(u.value,l=>(e.openBlock(),e.createBlock(e.unref(B.default),{key:`content-${l.id}`,value:l.id,class:"h-full mt-0 data-[state=active]:flex data-[state=active]:flex-col"},{default:e.withCtx(()=>[e.renderSlot(t.$slots,`content-${l.id}`,{tab:l},()=>[l.component?(e.openBlock(),e.createBlock(e.resolveDynamicComponent(l.component),e.mergeProps({key:0,ref_for:!0},l.props||{}),null,16)):(e.openBlock(),e.createElementBlock("div",q,[e.createElementVNode("p",V,e.toDisplayString(l.label)+" 콘텐츠",1)]))],!0)]),_:2},1032,["value"]))),128))])]),_:3},8,["model-value","class"]))}});exports.default=z;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("vue");require("../shadcn/index.cjs");const h=require("lucide-vue-next"),i=require("../../lib/utils.cjs"),x=require("../atoms/JIcon.vue.cjs"),y=require("../shadcn/Tabs.vue.cjs"),T=require("../shadcn/TabsList.vue.cjs"),b=require("../shadcn/TabsTrigger.vue.cjs"),B=require("../shadcn/TabsContent.vue.cjs"),S={class:"flex-1 truncate"},E=["aria-label","onClick"],P={class:"flex-1 w-full overflow-auto"},q={key:1,class:"p-4"},V={class:"text-muted-foreground"},z=e.defineComponent({__name:"JTabs",props:{tabs:{},activeTabId:{},class:{},listClass:{},styletype:{default:"default"}},emits:["tabChange","tabClose","update:activeTabId"],setup(f,{emit:v}){const n=f,r=v,u=e.computed(()=>Array.isArray(n.tabs)?n.tabs:[]),a=e.ref(n.activeTabId||(u.value.length>0?u.value[0]?.id:"")||"");let o=!1;e.watch(()=>n.activeTabId,t=>{t!==void 0&&t!==a.value&&(a.value=t)},{immediate:!0}),e.watch(u,t=>{!n.activeTabId&&t.length>0&&!t.find(s=>s.id===a.value)&&t[0]&&(a.value=t[0].id)});const _=t=>{if(o)return;const s=String(t);s!==a.value&&(o=!0,a.value=s,r("update:activeTabId",s),r("tabChange",s),e.nextTick(()=>{o=!1}))},m=t=>{o||t===a.value||(o=!0,a.value=t,r("update:activeTabId",t),r("tabChange",t),e.nextTick(()=>{o=!1}))},C=(t,s)=>{t.stopPropagation(),r("tabClose",s)},g=e.computed(()=>i.cn("flex flex-col w-full h-full",n.class)),d={default:{tabPaddingClass:"px-2 py-0.5",tabTextSizeClass:"text-xs",listPaddingClass:"px-1.5 py-0.5"},minimal:{tabPaddingClass:"px-1.5 py-0.5",tabTextSizeClass:"text-xs",listPaddingClass:"px-1.5 py-0.5"}},c=e.computed(()=>d[n.styletype]??d.default),k=e.computed(()=>i.cn("w-full justify-start",c.value.listPaddingClass,n.listClass));return(t,s)=>(e.openBlock(),e.createBlock(e.unref(y.default),{"model-value":a.value,"onUpdate:modelValue":_,orientation:"horizontal",class:e.normalizeClass(g.value)},{default:e.withCtx(()=>[e.createVNode(e.unref(T.default),{class:e.normalizeClass(k.value)},{default:e.withCtx(()=>[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(u.value,l=>(e.openBlock(),e.createBlock(e.unref(b.default),{key:l.id,value:l.id,onClick:p=>m(l.id),class:e.normalizeClass(e.unref(i.cn)("!flex !items-center !gap-2",c.value.tabPaddingClass,c.value.tabTextSizeClass))},{default:e.withCtx(()=>[l.icon?(e.openBlock(),e.createBlock(x.default,{key:0,name:l.icon,size:"sm",class:"flex-shrink-0"},null,8,["name"])):e.createCommentVNode("",!0),e.createElementVNode("span",S,e.toDisplayString(l.label),1),l.closable?(e.openBlock(),e.createElementBlock("button",{key:1,type:"button",class:"flex-shrink-0 h-3.5 w-3.5 rounded-sm hover:bg-destructive/10 hover:text-destructive transition-colors focus:outline-none focus:ring-2 focus:ring-ring flex items-center justify-center","aria-label":`${l.label} 탭 닫기`,onClick:p=>C(p,l.id)},[e.createVNode(e.unref(h.X),{class:"h-2.5 w-2.5"})],8,E)):e.createCommentVNode("",!0)]),_:2},1032,["value","onClick","class"]))),128))]),_:1},8,["class"]),e.createElementVNode("div",P,[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(u.value,l=>(e.openBlock(),e.createBlock(e.unref(B.default),{key:`content-${l.id}`,value:l.id,class:"h-full mt-0 data-[state=active]:flex data-[state=active]:flex-col"},{default:e.withCtx(()=>[e.renderSlot(t.$slots,`content-${l.id}`,{tab:l},()=>[l.component?(e.openBlock(),e.createBlock(e.resolveDynamicComponent(l.component),e.mergeProps({key:0,ref_for:!0},l.props||{}),null,16)):(e.openBlock(),e.createElementBlock("div",q,[e.createElementVNode("p",V,e.toDisplayString(l.label)+" 콘텐츠",1)]))],!0)]),_:2},1032,["value"]))),128))])]),_:3},8,["model-value","class"]))}});exports.default=z;
2
2
  //# sourceMappingURL=JTabs.vue2.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"JTabs.vue2.cjs","sources":["../../../../src/components/molecules/JTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, watch, nextTick } from 'vue'\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/shadcn'\nimport type { JTabsProps, JTabsEmits } from '@/types/dynamic-tabs.types'\nimport { X } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport JIcon from '@/components/atoms/JIcon.vue'\n\n/**\n * JTabs - 기본 탭 UI 컴포넌트 (molecules)\n * Basic Tabs UI Component\n * \n * @description\n * 정적인 탭 목록을 렌더링하는 기본 탭 컴포넌트입니다.\n * 닫기 버튼, 아이콘 등을 지원합니다.\n * \n * @example\n * ```vue\n * <JTabs \n * :tabs=\"tabs\"\n * :active-tab-id=\"activeId\"\n * @tab-change=\"handleChange\"\n * @tab-close=\"handleClose\"\n * />\n * ```\n */\n\ntype StyleType = 'default' | 'minimal'\n\nconst props = withDefaults(defineProps<JTabsProps>(), {\n styletype: 'default',\n})\n\nconst emit = defineEmits<JTabsEmits>()\n\n/**\n * 안전한 tabs 배열 (undefined/null 체크)\n * Safe tabs array (undefined/null check)\n */\nconst safeTabs = computed(() => {\n return Array.isArray(props.tabs) ? props.tabs : []\n})\n\n/**\n * 현재 활성화된 탭 ID (내부 상태)\n * Current active tab ID (internal state)\n */\nconst internalActiveId = ref<string>(\n props.activeTabId || (safeTabs.value.length > 0 ? safeTabs.value[0]?.id : '') || ''\n)\n\n/**\n * 이벤트 처리 중 플래그 (중복 이벤트 방지)\n * Flag to prevent duplicate events\n */\nlet isHandlingEvent = false\n\n/**\n * props.activeTabId가 변경되면 내부 상태 동기화\n * Sync internal state when props.activeTabId changes\n */\nwatch(() => props.activeTabId, (newValue) => {\n if (newValue !== undefined && newValue !== internalActiveId.value) {\n internalActiveId.value = newValue\n }\n}, { immediate: true })\n\n/**\n * props.tabs가 변경되고 activeTabId가 없으면 첫 번째 탭 활성화\n * Activate first tab when tabs change and no activeTabId\n */\nwatch(safeTabs, (newTabs) => {\n if (!props.activeTabId && newTabs.length > 0 && !newTabs.find(t => t.id === internalActiveId.value) && newTabs[0]) {\n internalActiveId.value = newTabs[0].id\n }\n})\n\n/**\n * 탭 값 변경 핸들러 (reka-ui TabsRoot에서 직접 호출됨)\n * Tab value change handler (called directly from reka-ui TabsRoot)\n */\nconst handleTabValueChange = (value: string | number) => {\n if (isHandlingEvent) return\n \n const stringValue = String(value)\n if (stringValue !== internalActiveId.value) {\n isHandlingEvent = true\n internalActiveId.value = stringValue\n emit('update:activeTabId', stringValue)\n emit('tabChange', stringValue)\n // 다음 tick에서 플래그 리셋\n nextTick(() => {\n isHandlingEvent = false\n })\n }\n}\n\n/**\n * 탭 클릭 핸들러 (백업 방안 - reka-ui 이벤트가 작동하지 않을 경우)\n * Tab click handler (backup - in case reka-ui events don't work)\n */\nconst handleTabClick = (tabId: string) => {\n // reka-ui 이벤트가 작동하지 않을 경우 직접 처리\n // handleTabValueChange가 이미 처리했으면 중복 방지\n if (isHandlingEvent || tabId === internalActiveId.value) return\n \n isHandlingEvent = true\n internalActiveId.value = tabId\n emit('update:activeTabId', tabId)\n emit('tabChange', tabId)\n // 다음 tick에서 플래그 리셋\n nextTick(() => {\n isHandlingEvent = false\n })\n}\n\n/**\n * 탭 닫기 핸들러\n * Tab close handler\n */\nconst handleCloseTab = (e: Event, tabId: string) => {\n e.stopPropagation() // 탭 클릭 이벤트 전파 방지\n emit('tabClose', tabId)\n}\n\n/**\n * 루트 클래스\n * Root classes\n */\nconst rootClasses = computed(() => {\n return cn('flex flex-col w-full h-full', props.class)\n})\n\n/**\n * 스타일 프리셋\n */\nconst STYLE_PRESETS: Record<StyleType, {\n tabPaddingClass: string\n tabTextSizeClass: string\n listPaddingClass: string\n}> = {\n default: {\n tabPaddingClass: 'px-2.5 py-1',\n tabTextSizeClass: 'text-xs',\n listPaddingClass: 'p-0.5',\n },\n minimal: {\n tabPaddingClass: 'px-2 py-0.5',\n tabTextSizeClass: 'text-xs',\n listPaddingClass: 'p-0.5',\n },\n}\n\nconst preset = computed(() => {\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\n})\n\n/**\n * 탭 리스트 클래스\n * Tabs list classes\n */\nconst listClasses = computed(() => {\n return cn('w-full justify-start', preset.value.listPaddingClass, props.listClass)\n})\n</script>\n\n<template>\n <Tabs\n :model-value=\"internalActiveId\"\n @update:model-value=\"handleTabValueChange\"\n orientation=\"horizontal\"\n :class=\"rootClasses\"\n >\n <!-- 탭 헤더 영역 / Tab Headers -->\n <TabsList :class=\"listClasses\">\n <TabsTrigger\n v-for=\"tab in safeTabs\"\n :key=\"tab.id\"\n :value=\"tab.id\"\n @click=\"handleTabClick(tab.id)\"\n :class=\"cn('!flex !items-center !gap-2', preset.tabPaddingClass, preset.tabTextSizeClass)\"\n >\n <!-- 탭 아이콘 (있을 경우) / Tab Icon -->\n <JIcon \n v-if=\"tab.icon\" \n :name=\"tab.icon\" \n size=\"sm\"\n class=\"flex-shrink-0\"\n />\n \n <!-- 탭 레이블 / Tab Label -->\n <span class=\"flex-1 truncate\">{{ tab.label }}</span>\n \n <!-- 닫기 버튼 / Close Button (항상 표시) -->\n <button\n v-if=\"tab.closable\"\n type=\"button\"\n class=\"flex-shrink-0 h-3.5 w-3.5 rounded-sm hover:bg-destructive/10 hover:text-destructive transition-colors focus:outline-none focus:ring-2 focus:ring-ring flex items-center justify-center\"\n :aria-label=\"`${tab.label} 탭 닫기`\"\n @click=\"(e) => handleCloseTab(e, tab.id)\"\n >\n <X class=\"h-2.5 w-2.5\" />\n </button>\n </TabsTrigger>\n </TabsList>\n\n <!-- 탭 콘텐츠 영역 / Tab Contents -->\n <div class=\"flex-1 w-full overflow-auto\">\n <TabsContent\n v-for=\"tab in safeTabs\"\n :key=\"`content-${tab.id}`\"\n :value=\"tab.id\"\n class=\"h-full mt-0 data-[state=active]:flex data-[state=active]:flex-col\"\n >\n <!-- 슬롯 우선 / Slot First -->\n <slot :name=\"`content-${tab.id}`\" :tab=\"tab\">\n <!-- 동적 컴포넌트 렌더링 / Dynamic Component Rendering -->\n <component\n v-if=\"tab.component\"\n :is=\"tab.component\"\n v-bind=\"tab.props || {}\"\n />\n \n <!-- 기본 콘텐츠 / Default Content -->\n <div v-else class=\"p-4\">\n <p class=\"text-muted-foreground\">{{ tab.label }} 콘텐츠</p>\n </div>\n </slot>\n </TabsContent>\n </div>\n </Tabs>\n</template>\n\n<style scoped>\n/**\n * 탭 리스트 스타일 - 하단 보더 제거\n * Tab list styles without bottom border\n */\n:deep([role=\"tablist\"]:not(.ag-side-buttons)) {\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: thin;\n scrollbar-color: rgba(0, 0, 0, 0.2) transparent;\n background: hsl(var(--background));\n padding: 0 0.5rem;\n padding-top: 0.25rem;\n gap: 0.25rem;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar) {\n height: 6px;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-track) {\n background: transparent;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\n background-color: rgba(0, 0, 0, 0.2);\n border-radius: 3px;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\n background-color: rgba(0, 0, 0, 0.3);\n}\n\n/**\n * 다크모드에서 스크롤바 스타일\n * Scrollbar styles in dark mode\n */\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\n background-color: rgba(255, 255, 255, 0.2);\n}\n\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\n background-color: rgba(255, 255, 255, 0.3);\n}\n\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)) {\n scrollbar-color: rgba(255, 255, 255, 0.2) transparent;\n}\n\n/**\n * 탭 버튼 스타일 - 명확한 구분\n * Tab button styles - clear distinction\n */\n:deep([role=\"tab\"]) {\n position: relative;\n padding: 0.375rem 0.75rem;\n border-radius: 0.375rem 0.375rem 0 0;\n transition: all 0.2s ease;\n border: 1px solid transparent;\n border-bottom: none;\n}\n\n/**\n * Minimal 스타일 탭 버튼 - JSidebarAdvanced와 높이 맞춤\n */\n:deep([role=\"tablist\"][class*=\"p-0.5\"] [role=\"tab\"]) {\n padding: 0.25rem 0.5rem;\n}\n\n/**\n * 비활성 탭 - 명확하게 구분\n * Inactive tabs - clear distinction\n */\n:deep([role=\"tab\"][data-state=\"inactive\"]) {\n background: hsl(var(--muted) / 0.2);\n color: hsl(var(--muted-foreground));\n border-top: 1px solid hsl(var(--border) / 0.4);\n border-left: 1px solid hsl(var(--border) / 0.4);\n border-right: 1px solid hsl(var(--border) / 0.4);\n}\n\n:deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\n background: hsl(var(--muted) / 0.4);\n color: hsl(var(--foreground));\n}\n\n/**\n * 다크모드에서 비활성 탭 - 다크 배경색 사용\n * Inactive tabs in dark mode - use dark background\n */\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]) {\n background: hsl(var(--secondary));\n color: hsl(var(--secondary-foreground));\n border-top: 1px solid hsl(var(--border) / 0.5);\n border-left: 1px solid hsl(var(--border) / 0.5);\n border-right: 1px solid hsl(var(--border) / 0.5);\n}\n\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\n background: hsl(var(--muted));\n color: hsl(var(--muted-foreground));\n}\n\n/**\n * 활성 탭 - 강조된 스타일\n * Active tab - emphasized style\n */\n:deep([role=\"tab\"][data-state=\"active\"]) {\n background: hsl(var(--background));\n color: hsl(var(--foreground));\n font-weight: 500;\n border-top: 2px solid hsl(var(--primary));\n border-left: 1px solid hsl(var(--border));\n border-right: 1px solid hsl(var(--border));\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n z-index: 1;\n}\n\n/**\n * 다크모드에서 활성 탭 - 배경색 명확히 구분\n * Active tab in dark mode - clear background distinction\n */\n.dark :deep([role=\"tab\"][data-state=\"active\"]) {\n background: hsl(var(--card));\n color: hsl(var(--card-foreground));\n border-top: 2px solid hsl(var(--primary));\n border-left: 1px solid hsl(var(--border));\n border-right: 1px solid hsl(var(--border));\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n</style>\n\n"],"names":["props","__props","emit","__emit","safeTabs","computed","internalActiveId","ref","isHandlingEvent","watch","newValue","newTabs","t","handleTabValueChange","value","stringValue","nextTick","handleTabClick","tabId","handleCloseTab","e","rootClasses","cn","STYLE_PRESETS","preset","listClasses","_createBlock","_unref","Tabs","_createVNode","TabsList","_createElementBlock","_Fragment","_renderList","tab","TabsTrigger","$event","_normalizeClass","JIcon","_createElementVNode","_hoisted_1","_toDisplayString","X","_hoisted_3","TabsContent","_renderSlot","_ctx","_openBlock","_resolveDynamicComponent","_mergeProps","_hoisted_4","_hoisted_5"],"mappings":"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"}
1
+ {"version":3,"file":"JTabs.vue2.cjs","sources":["../../../../src/components/molecules/JTabs.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, ref, watch, nextTick } from 'vue'\nimport { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/shadcn'\nimport type { JTabsProps, JTabsEmits } from '@/types/dynamic-tabs.types'\nimport { X } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport JIcon from '@/components/atoms/JIcon.vue'\n\n/**\n * JTabs - 기본 탭 UI 컴포넌트 (molecules)\n * Basic Tabs UI Component\n * \n * @description\n * 정적인 탭 목록을 렌더링하는 기본 탭 컴포넌트입니다.\n * 닫기 버튼, 아이콘 등을 지원합니다.\n * \n * @example\n * ```vue\n * <JTabs \n * :tabs=\"tabs\"\n * :active-tab-id=\"activeId\"\n * @tab-change=\"handleChange\"\n * @tab-close=\"handleClose\"\n * />\n * ```\n */\n\ntype StyleType = 'default' | 'minimal'\n\nconst props = withDefaults(defineProps<JTabsProps>(), {\n styletype: 'default',\n})\n\nconst emit = defineEmits<JTabsEmits>()\n\n/**\n * 안전한 tabs 배열 (undefined/null 체크)\n * Safe tabs array (undefined/null check)\n */\nconst safeTabs = computed(() => {\n return Array.isArray(props.tabs) ? props.tabs : []\n})\n\n/**\n * 현재 활성화된 탭 ID (내부 상태)\n * Current active tab ID (internal state)\n */\nconst internalActiveId = ref<string>(\n props.activeTabId || (safeTabs.value.length > 0 ? safeTabs.value[0]?.id : '') || ''\n)\n\n/**\n * 이벤트 처리 중 플래그 (중복 이벤트 방지)\n * Flag to prevent duplicate events\n */\nlet isHandlingEvent = false\n\n/**\n * props.activeTabId가 변경되면 내부 상태 동기화\n * Sync internal state when props.activeTabId changes\n */\nwatch(() => props.activeTabId, (newValue) => {\n if (newValue !== undefined && newValue !== internalActiveId.value) {\n internalActiveId.value = newValue\n }\n}, { immediate: true })\n\n/**\n * props.tabs가 변경되고 activeTabId가 없으면 첫 번째 탭 활성화\n * Activate first tab when tabs change and no activeTabId\n */\nwatch(safeTabs, (newTabs) => {\n if (!props.activeTabId && newTabs.length > 0 && !newTabs.find(t => t.id === internalActiveId.value) && newTabs[0]) {\n internalActiveId.value = newTabs[0].id\n }\n})\n\n/**\n * 탭 값 변경 핸들러 (reka-ui TabsRoot에서 직접 호출됨)\n * Tab value change handler (called directly from reka-ui TabsRoot)\n */\nconst handleTabValueChange = (value: string | number) => {\n if (isHandlingEvent) return\n \n const stringValue = String(value)\n if (stringValue !== internalActiveId.value) {\n isHandlingEvent = true\n internalActiveId.value = stringValue\n emit('update:activeTabId', stringValue)\n emit('tabChange', stringValue)\n // 다음 tick에서 플래그 리셋\n nextTick(() => {\n isHandlingEvent = false\n })\n }\n}\n\n/**\n * 탭 클릭 핸들러 (백업 방안 - reka-ui 이벤트가 작동하지 않을 경우)\n * Tab click handler (backup - in case reka-ui events don't work)\n */\nconst handleTabClick = (tabId: string) => {\n // reka-ui 이벤트가 작동하지 않을 경우 직접 처리\n // handleTabValueChange가 이미 처리했으면 중복 방지\n if (isHandlingEvent || tabId === internalActiveId.value) return\n \n isHandlingEvent = true\n internalActiveId.value = tabId\n emit('update:activeTabId', tabId)\n emit('tabChange', tabId)\n // 다음 tick에서 플래그 리셋\n nextTick(() => {\n isHandlingEvent = false\n })\n}\n\n/**\n * 탭 닫기 핸들러\n * Tab close handler\n */\nconst handleCloseTab = (e: Event, tabId: string) => {\n e.stopPropagation() // 탭 클릭 이벤트 전파 방지\n emit('tabClose', tabId)\n}\n\n/**\n * 루트 클래스\n * Root classes\n */\nconst rootClasses = computed(() => {\n return cn('flex flex-col w-full h-full', props.class)\n})\n\n/**\n * 스타일 프리셋\n */\nconst STYLE_PRESETS: Record<StyleType, {\n tabPaddingClass: string\n tabTextSizeClass: string\n listPaddingClass: string\n}> = {\n default: {\n tabPaddingClass: 'px-2 py-0.5',\n tabTextSizeClass: 'text-xs',\n listPaddingClass: 'px-1.5 py-0.5',\n },\n minimal: {\n tabPaddingClass: 'px-1.5 py-0.5',\n tabTextSizeClass: 'text-xs',\n listPaddingClass: 'px-1.5 py-0.5',\n },\n}\n\nconst preset = computed(() => {\n return STYLE_PRESETS[props.styletype] ?? STYLE_PRESETS.default\n})\n\n/**\n * 탭 리스트 클래스\n * Tabs list classes\n */\nconst listClasses = computed(() => {\n return cn('w-full justify-start', preset.value.listPaddingClass, props.listClass)\n})\n</script>\n\n<template>\n <Tabs\n :model-value=\"internalActiveId\"\n @update:model-value=\"handleTabValueChange\"\n orientation=\"horizontal\"\n :class=\"rootClasses\"\n >\n <!-- 탭 헤더 영역 / Tab Headers -->\n <TabsList :class=\"listClasses\">\n <TabsTrigger\n v-for=\"tab in safeTabs\"\n :key=\"tab.id\"\n :value=\"tab.id\"\n @click=\"handleTabClick(tab.id)\"\n :class=\"cn('!flex !items-center !gap-2', preset.tabPaddingClass, preset.tabTextSizeClass)\"\n >\n <!-- 탭 아이콘 (있을 경우) / Tab Icon -->\n <JIcon \n v-if=\"tab.icon\" \n :name=\"tab.icon\" \n size=\"sm\"\n class=\"flex-shrink-0\"\n />\n \n <!-- 탭 레이블 / Tab Label -->\n <span class=\"flex-1 truncate\">{{ tab.label }}</span>\n \n <!-- 닫기 버튼 / Close Button (항상 표시) -->\n <button\n v-if=\"tab.closable\"\n type=\"button\"\n class=\"flex-shrink-0 h-3.5 w-3.5 rounded-sm hover:bg-destructive/10 hover:text-destructive transition-colors focus:outline-none focus:ring-2 focus:ring-ring flex items-center justify-center\"\n :aria-label=\"`${tab.label} 탭 닫기`\"\n @click=\"(e) => handleCloseTab(e, tab.id)\"\n >\n <X class=\"h-2.5 w-2.5\" />\n </button>\n </TabsTrigger>\n </TabsList>\n\n <!-- 탭 콘텐츠 영역 / Tab Contents -->\n <div class=\"flex-1 w-full overflow-auto\">\n <TabsContent\n v-for=\"tab in safeTabs\"\n :key=\"`content-${tab.id}`\"\n :value=\"tab.id\"\n class=\"h-full mt-0 data-[state=active]:flex data-[state=active]:flex-col\"\n >\n <!-- 슬롯 우선 / Slot First -->\n <slot :name=\"`content-${tab.id}`\" :tab=\"tab\">\n <!-- 동적 컴포넌트 렌더링 / Dynamic Component Rendering -->\n <component\n v-if=\"tab.component\"\n :is=\"tab.component\"\n v-bind=\"tab.props || {}\"\n />\n \n <!-- 기본 콘텐츠 / Default Content -->\n <div v-else class=\"p-4\">\n <p class=\"text-muted-foreground\">{{ tab.label }} 콘텐츠</p>\n </div>\n </slot>\n </TabsContent>\n </div>\n </Tabs>\n</template>\n\n<style scoped>\n/**\n * 탭 리스트 스타일 - 하단 보더 제거\n * Tab list styles without bottom border\n */\n:deep([role=\"tablist\"]:not(.ag-side-buttons)) {\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: thin;\n scrollbar-color: rgba(0, 0, 0, 0.2) transparent;\n background: hsl(var(--background));\n padding: 0.125rem 0.375rem 0;\n gap: 0.125rem;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar) {\n height: 6px;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-track) {\n background: transparent;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\n background-color: rgba(0, 0, 0, 0.2);\n border-radius: 3px;\n}\n\n:deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\n background-color: rgba(0, 0, 0, 0.3);\n}\n\n/**\n * 다크모드에서 스크롤바 스타일\n * Scrollbar styles in dark mode\n */\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb) {\n background-color: rgba(255, 255, 255, 0.2);\n}\n\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)::-webkit-scrollbar-thumb:hover) {\n background-color: rgba(255, 255, 255, 0.3);\n}\n\n.dark :deep([role=\"tablist\"]:not(.ag-side-buttons)) {\n scrollbar-color: rgba(255, 255, 255, 0.2) transparent;\n}\n\n/**\n * 탭 버튼 스타일 - 명확한 구분\n * Tab button styles - clear distinction\n */\n:deep([role=\"tab\"]) {\n position: relative;\n padding: 0.25rem 0.625rem;\n min-height: 1.75rem;\n border-radius: 0.375rem 0.375rem 0 0;\n transition: all 0.2s ease;\n border: 1px solid transparent;\n border-bottom: none;\n}\n\n/**\n * Minimal 스타일 탭 버튼 - JSidebarAdvanced와 높이 맞춤\n */\n:deep([role=\"tablist\"][class*=\"p-0.5\"] [role=\"tab\"]) {\n padding: 0.25rem 0.5rem;\n}\n\n/**\n * 비활성 탭 - 명확하게 구분\n * Inactive tabs - clear distinction\n */\n:deep([role=\"tab\"][data-state=\"inactive\"]) {\n background: hsl(var(--muted) / 0.2);\n color: hsl(var(--muted-foreground));\n border-top: 1px solid hsl(var(--border) / 0.4);\n border-left: 1px solid hsl(var(--border) / 0.4);\n border-right: 1px solid hsl(var(--border) / 0.4);\n}\n\n:deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\n background: hsl(var(--muted) / 0.4);\n color: hsl(var(--foreground));\n}\n\n/**\n * 다크모드에서 비활성 탭 - 다크 배경색 사용\n * Inactive tabs in dark mode - use dark background\n */\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]) {\n background: hsl(var(--secondary));\n color: hsl(var(--secondary-foreground));\n border-top: 1px solid hsl(var(--border) / 0.5);\n border-left: 1px solid hsl(var(--border) / 0.5);\n border-right: 1px solid hsl(var(--border) / 0.5);\n}\n\n.dark :deep([role=\"tab\"][data-state=\"inactive\"]:hover) {\n background: hsl(var(--muted));\n color: hsl(var(--muted-foreground));\n}\n\n/**\n * 활성 탭 - 강조된 스타일\n * Active tab - emphasized style\n */\n:deep([role=\"tab\"][data-state=\"active\"]) {\n background: hsl(var(--background));\n color: hsl(var(--foreground));\n font-weight: 500;\n border-top: 2px solid hsl(var(--primary));\n border-left: 1px solid hsl(var(--border));\n border-right: 1px solid hsl(var(--border));\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n z-index: 1;\n}\n\n/**\n * 다크모드에서 활성 탭 - 배경색 명확히 구분\n * Active tab in dark mode - clear background distinction\n */\n.dark :deep([role=\"tab\"][data-state=\"active\"]) {\n background: hsl(var(--card));\n color: hsl(var(--card-foreground));\n border-top: 2px solid hsl(var(--primary));\n border-left: 1px solid hsl(var(--border));\n border-right: 1px solid hsl(var(--border));\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n</style>\n"],"names":["props","__props","emit","__emit","safeTabs","computed","internalActiveId","ref","isHandlingEvent","watch","newValue","newTabs","t","handleTabValueChange","value","stringValue","nextTick","handleTabClick","tabId","handleCloseTab","e","rootClasses","cn","STYLE_PRESETS","preset","listClasses","_createBlock","_unref","Tabs","_createVNode","TabsList","_createElementBlock","_Fragment","_renderList","tab","TabsTrigger","$event","_normalizeClass","JIcon","_createElementVNode","_hoisted_1","_toDisplayString","X","_hoisted_3","TabsContent","_renderSlot","_ctx","_openBlock","_resolveDynamicComponent","_mergeProps","_hoisted_4","_hoisted_5"],"mappings":"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"}