[{"data":1,"prerenderedAt":4027},["ShallowReactive",2],{"page-\u002Fvue\u002F23-design-patterns":3},{"id":4,"title":5,"body":6,"description":4020,"extension":4021,"meta":4022,"navigation":69,"path":4023,"seo":4024,"stem":4025,"__hash__":4026},"content\u002Fvue\u002F23-design-patterns.md","Vue 3 Engineering Reference — Design Patterns",{"type":7,"value":8,"toc":4009},"minimark",[9,14,19,860,864,3506,3510,3604,3608,3721,3725,3728,3859,4005],[10,11,13],"h1",{"id":12},"_23-design-patterns","23 — Design Patterns",[15,16,18],"h2",{"id":17},"store-pattern-module-scoped-singleton","Store Pattern — Module-Scoped Singleton",[20,21,24],"code-wrapper",{"filename":22,"language":23},"patterns\u002Fstore.ts","typescript",[25,26,30],"pre",{"className":27,"code":28,"language":23,"meta":29,"style":29},"language-typescript shiki shiki-themes github-light github-dark","import { ref, readonly, computed, type Ref, type ComputedRef } from 'vue'\n\n\u002F\u002F ── Store pattern: module-scoped reactive state + controlled mutations ──\n\u002F\u002F Lightweight alternative to Pinia for simple global state.\n\u002F\u002F State is private (module scope); only the returned API is public.\n\ninterface Todo {\n  id: string\n  text: string\n  done: boolean\n  createdAt: number\n}\n\n\u002F\u002F ── Private state (module scope — not exported) ──────────\nconst _todos: Ref\u003CTodo[]> = ref([])\nconst _filter: Ref\u003C'all' | 'active' | 'done'> = ref('all')\n\n\u002F\u002F ── Private mutation function (not exported) ────────────\nfunction _addTodo(text: string): void {\n  _todos.value.push({\n    id: crypto.randomUUID(),\n    text,\n    done: false,\n    createdAt: Date.now(),\n  })\n}\n\n\u002F\u002F ── Public API (exported composable) ────────────────────\nexport function useTodoStore() {\n  return {\n    \u002F\u002F ── Read-only state: consumers can't mutate directly ──\n    todos: readonly(_todos),\n    filter: readonly(_filter),\n\n    \u002F\u002F ── Computed getters: derived from state ──\n    filtered: computed(() => {\n      switch (_filter.value) {\n        case 'active': return _todos.value.filter(t => !t.done)\n        case 'done': return _todos.value.filter(t => t.done)\n        default: return _todos.value\n      }\n    }) as ComputedRef\u003CTodo[]>,\n\n    remaining: computed(() => _todos.value.filter(t => !t.done).length),\n    total: computed(() => _todos.value.length),\n\n    \u002F\u002F ── Actions: the ONLY way to mutate state ────────────\n    add(text: string) { _addTodo(text) },\n    toggle(id: string) {\n      const todo = _todos.value.find(t => t.id === id)\n      if (todo) todo.done = !todo.done\n    },\n    remove(id: string) {\n      _todos.value = _todos.value.filter(t => t.id !== id)\n    },\n    setFilter(f: 'all' | 'active' | 'done') { _filter.value = f },\n    clearDone() {\n      _todos.value = _todos.value.filter(t => !t.done)\n    },\n  }\n}\n\n\u002F\u002F ── Why readonly: ──────────────────────────────────────\n\u002F\u002F Consumers can read but can't write: todos.value.push(...) throws in dev.\n\u002F\u002F Forces all mutations through the action API → traceable, debuggable.\n\u002F\u002F All state changes go through known functions (add, toggle, remove, etc.).\n","",[31,32,33,64,71,78,84,90,95,108,122,132,143,154,160,165,171,203,246,251,257,286,298,310,316,328,339,345,350,355,361,376,384,390,402,413,418,424,441,450,484,508,521,527,546,551,584,602,607,613,636,654,686,702,708,724,749,754,786,794,815,820,826,831,836,842,848,854],"code",{"__ignoreMap":29},[34,35,38,42,46,49,52,54,57,60],"span",{"class":36,"line":37},"line",1,[34,39,41],{"class":40},"svdQ7","import",[34,43,45],{"class":44},"ssxIu"," { ref, readonly, computed, ",[34,47,48],{"class":40},"type",[34,50,51],{"class":44}," Ref, ",[34,53,48],{"class":40},[34,55,56],{"class":44}," ComputedRef } ",[34,58,59],{"class":40},"from",[34,61,63],{"class":62},"sJ6F3"," 'vue'\n",[34,65,67],{"class":36,"line":66},2,[34,68,70],{"emptyLinePlaceholder":69},true,"\n",[34,72,74],{"class":36,"line":73},3,[34,75,77],{"class":76},"sdCPZ","\u002F\u002F ── Store pattern: module-scoped reactive state + controlled mutations ──\n",[34,79,81],{"class":36,"line":80},4,[34,82,83],{"class":76},"\u002F\u002F Lightweight alternative to Pinia for simple global state.\n",[34,85,87],{"class":36,"line":86},5,[34,88,89],{"class":76},"\u002F\u002F State is private (module scope); only the returned API is public.\n",[34,91,93],{"class":36,"line":92},6,[34,94,70],{"emptyLinePlaceholder":69},[34,96,98,101,105],{"class":36,"line":97},7,[34,99,100],{"class":40},"interface",[34,102,104],{"class":103},"sIsaT"," Todo",[34,106,107],{"class":44}," {\n",[34,109,111,115,118],{"class":36,"line":110},8,[34,112,114],{"class":113},"sCrzJ","  id",[34,116,117],{"class":40},":",[34,119,121],{"class":120},"snvgF"," string\n",[34,123,125,128,130],{"class":36,"line":124},9,[34,126,127],{"class":113},"  text",[34,129,117],{"class":40},[34,131,121],{"class":120},[34,133,135,138,140],{"class":36,"line":134},10,[34,136,137],{"class":113},"  done",[34,139,117],{"class":40},[34,141,142],{"class":120}," boolean\n",[34,144,146,149,151],{"class":36,"line":145},11,[34,147,148],{"class":113},"  createdAt",[34,150,117],{"class":40},[34,152,153],{"class":120}," number\n",[34,155,157],{"class":36,"line":156},12,[34,158,159],{"class":44},"}\n",[34,161,163],{"class":36,"line":162},13,[34,164,70],{"emptyLinePlaceholder":69},[34,166,168],{"class":36,"line":167},14,[34,169,170],{"class":76},"\u002F\u002F ── Private state (module scope — not exported) ──────────\n",[34,172,174,177,180,182,185,188,191,194,197,200],{"class":36,"line":173},15,[34,175,176],{"class":40},"const",[34,178,179],{"class":120}," _todos",[34,181,117],{"class":40},[34,183,184],{"class":103}," Ref",[34,186,187],{"class":44},"\u003C",[34,189,190],{"class":103},"Todo",[34,192,193],{"class":44},"[]> ",[34,195,196],{"class":40},"=",[34,198,199],{"class":103}," ref",[34,201,202],{"class":44},"([])\n",[34,204,206,208,211,213,215,217,220,223,226,228,231,234,236,238,241,243],{"class":36,"line":205},16,[34,207,176],{"class":40},[34,209,210],{"class":120}," _filter",[34,212,117],{"class":40},[34,214,184],{"class":103},[34,216,187],{"class":44},[34,218,219],{"class":62},"'all'",[34,221,222],{"class":40}," |",[34,224,225],{"class":62}," 'active'",[34,227,222],{"class":40},[34,229,230],{"class":62}," 'done'",[34,232,233],{"class":44},"> ",[34,235,196],{"class":40},[34,237,199],{"class":103},[34,239,240],{"class":44},"(",[34,242,219],{"class":62},[34,244,245],{"class":44},")\n",[34,247,249],{"class":36,"line":248},17,[34,250,70],{"emptyLinePlaceholder":69},[34,252,254],{"class":36,"line":253},18,[34,255,256],{"class":76},"\u002F\u002F ── Private mutation function (not exported) ────────────\n",[34,258,260,263,266,268,271,273,276,279,281,284],{"class":36,"line":259},19,[34,261,262],{"class":40},"function",[34,264,265],{"class":103}," _addTodo",[34,267,240],{"class":44},[34,269,270],{"class":113},"text",[34,272,117],{"class":40},[34,274,275],{"class":120}," string",[34,277,278],{"class":44},")",[34,280,117],{"class":40},[34,282,283],{"class":120}," void",[34,285,107],{"class":44},[34,287,289,292,295],{"class":36,"line":288},20,[34,290,291],{"class":44},"  _todos.value.",[34,293,294],{"class":103},"push",[34,296,297],{"class":44},"({\n",[34,299,301,304,307],{"class":36,"line":300},21,[34,302,303],{"class":44},"    id: crypto.",[34,305,306],{"class":103},"randomUUID",[34,308,309],{"class":44},"(),\n",[34,311,313],{"class":36,"line":312},22,[34,314,315],{"class":44},"    text,\n",[34,317,319,322,325],{"class":36,"line":318},23,[34,320,321],{"class":44},"    done: ",[34,323,324],{"class":120},"false",[34,326,327],{"class":44},",\n",[34,329,331,334,337],{"class":36,"line":330},24,[34,332,333],{"class":44},"    createdAt: Date.",[34,335,336],{"class":103},"now",[34,338,309],{"class":44},[34,340,342],{"class":36,"line":341},25,[34,343,344],{"class":44},"  })\n",[34,346,348],{"class":36,"line":347},26,[34,349,159],{"class":44},[34,351,353],{"class":36,"line":352},27,[34,354,70],{"emptyLinePlaceholder":69},[34,356,358],{"class":36,"line":357},28,[34,359,360],{"class":76},"\u002F\u002F ── Public API (exported composable) ────────────────────\n",[34,362,364,367,370,373],{"class":36,"line":363},29,[34,365,366],{"class":40},"export",[34,368,369],{"class":40}," function",[34,371,372],{"class":103}," useTodoStore",[34,374,375],{"class":44},"() {\n",[34,377,379,382],{"class":36,"line":378},30,[34,380,381],{"class":40},"  return",[34,383,107],{"class":44},[34,385,387],{"class":36,"line":386},31,[34,388,389],{"class":76},"    \u002F\u002F ── Read-only state: consumers can't mutate directly ──\n",[34,391,393,396,399],{"class":36,"line":392},32,[34,394,395],{"class":44},"    todos: ",[34,397,398],{"class":103},"readonly",[34,400,401],{"class":44},"(_todos),\n",[34,403,405,408,410],{"class":36,"line":404},33,[34,406,407],{"class":44},"    filter: ",[34,409,398],{"class":103},[34,411,412],{"class":44},"(_filter),\n",[34,414,416],{"class":36,"line":415},34,[34,417,70],{"emptyLinePlaceholder":69},[34,419,421],{"class":36,"line":420},35,[34,422,423],{"class":76},"    \u002F\u002F ── Computed getters: derived from state ──\n",[34,425,427,430,433,436,439],{"class":36,"line":426},36,[34,428,429],{"class":44},"    filtered: ",[34,431,432],{"class":103},"computed",[34,434,435],{"class":44},"(() ",[34,437,438],{"class":40},"=>",[34,440,107],{"class":44},[34,442,444,447],{"class":36,"line":443},37,[34,445,446],{"class":40},"      switch",[34,448,449],{"class":44}," (_filter.value) {\n",[34,451,453,456,458,461,464,467,470,472,475,478,481],{"class":36,"line":452},38,[34,454,455],{"class":40},"        case",[34,457,225],{"class":62},[34,459,460],{"class":44},": ",[34,462,463],{"class":40},"return",[34,465,466],{"class":44}," _todos.value.",[34,468,469],{"class":103},"filter",[34,471,240],{"class":44},[34,473,474],{"class":113},"t",[34,476,477],{"class":40}," =>",[34,479,480],{"class":40}," !",[34,482,483],{"class":44},"t.done)\n",[34,485,487,489,491,493,495,497,499,501,503,505],{"class":36,"line":486},39,[34,488,455],{"class":40},[34,490,230],{"class":62},[34,492,460],{"class":44},[34,494,463],{"class":40},[34,496,466],{"class":44},[34,498,469],{"class":103},[34,500,240],{"class":44},[34,502,474],{"class":113},[34,504,477],{"class":40},[34,506,507],{"class":44}," t.done)\n",[34,509,511,514,516,518],{"class":36,"line":510},40,[34,512,513],{"class":40},"        default",[34,515,460],{"class":44},[34,517,463],{"class":40},[34,519,520],{"class":44}," _todos.value\n",[34,522,524],{"class":36,"line":523},41,[34,525,526],{"class":44},"      }\n",[34,528,530,533,536,539,541,543],{"class":36,"line":529},42,[34,531,532],{"class":44},"    }) ",[34,534,535],{"class":40},"as",[34,537,538],{"class":103}," ComputedRef",[34,540,187],{"class":44},[34,542,190],{"class":103},[34,544,545],{"class":44},"[]>,\n",[34,547,549],{"class":36,"line":548},43,[34,550,70],{"emptyLinePlaceholder":69},[34,552,554,557,559,561,563,565,567,569,571,573,575,578,581],{"class":36,"line":553},44,[34,555,556],{"class":44},"    remaining: ",[34,558,432],{"class":103},[34,560,435],{"class":44},[34,562,438],{"class":40},[34,564,466],{"class":44},[34,566,469],{"class":103},[34,568,240],{"class":44},[34,570,474],{"class":113},[34,572,477],{"class":40},[34,574,480],{"class":40},[34,576,577],{"class":44},"t.done).",[34,579,580],{"class":120},"length",[34,582,583],{"class":44},"),\n",[34,585,587,590,592,594,596,598,600],{"class":36,"line":586},45,[34,588,589],{"class":44},"    total: ",[34,591,432],{"class":103},[34,593,435],{"class":44},[34,595,438],{"class":40},[34,597,466],{"class":44},[34,599,580],{"class":120},[34,601,583],{"class":44},[34,603,605],{"class":36,"line":604},46,[34,606,70],{"emptyLinePlaceholder":69},[34,608,610],{"class":36,"line":609},47,[34,611,612],{"class":76},"    \u002F\u002F ── Actions: the ONLY way to mutate state ────────────\n",[34,614,616,619,621,623,625,627,630,633],{"class":36,"line":615},48,[34,617,618],{"class":103},"    add",[34,620,240],{"class":44},[34,622,270],{"class":113},[34,624,117],{"class":40},[34,626,275],{"class":120},[34,628,629],{"class":44},") { ",[34,631,632],{"class":103},"_addTodo",[34,634,635],{"class":44},"(text) },\n",[34,637,639,642,644,647,649,651],{"class":36,"line":638},49,[34,640,641],{"class":103},"    toggle",[34,643,240],{"class":44},[34,645,646],{"class":113},"id",[34,648,117],{"class":40},[34,650,275],{"class":120},[34,652,653],{"class":44},") {\n",[34,655,657,660,663,666,668,671,673,675,677,680,683],{"class":36,"line":656},50,[34,658,659],{"class":40},"      const",[34,661,662],{"class":120}," todo",[34,664,665],{"class":40}," =",[34,667,466],{"class":44},[34,669,670],{"class":103},"find",[34,672,240],{"class":44},[34,674,474],{"class":113},[34,676,477],{"class":40},[34,678,679],{"class":44}," t.id ",[34,681,682],{"class":40},"===",[34,684,685],{"class":44}," id)\n",[34,687,689,692,695,697,699],{"class":36,"line":688},51,[34,690,691],{"class":40},"      if",[34,693,694],{"class":44}," (todo) todo.done ",[34,696,196],{"class":40},[34,698,480],{"class":40},[34,700,701],{"class":44},"todo.done\n",[34,703,705],{"class":36,"line":704},52,[34,706,707],{"class":44},"    },\n",[34,709,711,714,716,718,720,722],{"class":36,"line":710},53,[34,712,713],{"class":103},"    remove",[34,715,240],{"class":44},[34,717,646],{"class":113},[34,719,117],{"class":40},[34,721,275],{"class":120},[34,723,653],{"class":44},[34,725,727,730,732,734,736,738,740,742,744,747],{"class":36,"line":726},54,[34,728,729],{"class":44},"      _todos.value ",[34,731,196],{"class":40},[34,733,466],{"class":44},[34,735,469],{"class":103},[34,737,240],{"class":44},[34,739,474],{"class":113},[34,741,477],{"class":40},[34,743,679],{"class":44},[34,745,746],{"class":40},"!==",[34,748,685],{"class":44},[34,750,752],{"class":36,"line":751},55,[34,753,707],{"class":44},[34,755,757,760,762,765,767,770,772,774,776,778,781,783],{"class":36,"line":756},56,[34,758,759],{"class":103},"    setFilter",[34,761,240],{"class":44},[34,763,764],{"class":113},"f",[34,766,117],{"class":40},[34,768,769],{"class":62}," 'all'",[34,771,222],{"class":40},[34,773,225],{"class":62},[34,775,222],{"class":40},[34,777,230],{"class":62},[34,779,780],{"class":44},") { _filter.value ",[34,782,196],{"class":40},[34,784,785],{"class":44}," f },\n",[34,787,789,792],{"class":36,"line":788},57,[34,790,791],{"class":103},"    clearDone",[34,793,375],{"class":44},[34,795,797,799,801,803,805,807,809,811,813],{"class":36,"line":796},58,[34,798,729],{"class":44},[34,800,196],{"class":40},[34,802,466],{"class":44},[34,804,469],{"class":103},[34,806,240],{"class":44},[34,808,474],{"class":113},[34,810,477],{"class":40},[34,812,480],{"class":40},[34,814,483],{"class":44},[34,816,818],{"class":36,"line":817},59,[34,819,707],{"class":44},[34,821,823],{"class":36,"line":822},60,[34,824,825],{"class":44},"  }\n",[34,827,829],{"class":36,"line":828},61,[34,830,159],{"class":44},[34,832,834],{"class":36,"line":833},62,[34,835,70],{"emptyLinePlaceholder":69},[34,837,839],{"class":36,"line":838},63,[34,840,841],{"class":76},"\u002F\u002F ── Why readonly: ──────────────────────────────────────\n",[34,843,845],{"class":36,"line":844},64,[34,846,847],{"class":76},"\u002F\u002F Consumers can read but can't write: todos.value.push(...) throws in dev.\n",[34,849,851],{"class":36,"line":850},65,[34,852,853],{"class":76},"\u002F\u002F Forces all mutations through the action API → traceable, debuggable.\n",[34,855,857],{"class":36,"line":856},66,[34,858,859],{"class":76},"\u002F\u002F All state changes go through known functions (add, toggle, remove, etc.).\n",[15,861,863],{"id":862},"renderless-component-logic-via-scoped-slots","Renderless Component — Logic via Scoped Slots",[20,865,868,1075,1343,1347,1939,1943,2457,2461,3005,3009],{"filename":866,"language":867},"RenderlessToggle.vue","vue",[25,869,872],{"className":870,"code":871,"language":867,"meta":29,"style":29},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup>\nimport { ref, computed } from 'vue'\n\n\u002F\u002F ── Renderless component: provides state, renders nothing itself ──\n\u002F\u002F Parent has 100% control over the template via the scoped slot.\nconst isOpen = ref(false)\n\nfunction open() { isOpen.value = true }\nfunction close() { isOpen.value = false }\nfunction toggle() { isOpen.value = !isOpen.value }\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003C!-- ── Slot receives: state + actions ──────────────────── -->\n  \u003Cslot\n    :is-open=\"isOpen\"\n    :open=\"open\"\n    :close=\"close\"\n    :toggle=\"toggle\"\n  \u002F>\n\u003C\u002Ftemplate>\n",[31,873,874,888,899,903,908,913,930,934,952,968,984,993,997,1006,1011,1019,1029,1039,1049,1059,1067],{"__ignoreMap":29},[34,875,876,878,882,885],{"class":36,"line":37},[34,877,187],{"class":44},[34,879,881],{"class":880},"sk71V","script",[34,883,884],{"class":103}," setup",[34,886,887],{"class":44},">\n",[34,889,890,892,895,897],{"class":36,"line":66},[34,891,41],{"class":40},[34,893,894],{"class":44}," { ref, computed } ",[34,896,59],{"class":40},[34,898,63],{"class":62},[34,900,901],{"class":36,"line":73},[34,902,70],{"emptyLinePlaceholder":69},[34,904,905],{"class":36,"line":80},[34,906,907],{"class":76},"\u002F\u002F ── Renderless component: provides state, renders nothing itself ──\n",[34,909,910],{"class":36,"line":86},[34,911,912],{"class":76},"\u002F\u002F Parent has 100% control over the template via the scoped slot.\n",[34,914,915,917,920,922,924,926,928],{"class":36,"line":92},[34,916,176],{"class":40},[34,918,919],{"class":120}," isOpen",[34,921,665],{"class":40},[34,923,199],{"class":103},[34,925,240],{"class":44},[34,927,324],{"class":120},[34,929,245],{"class":44},[34,931,932],{"class":36,"line":97},[34,933,70],{"emptyLinePlaceholder":69},[34,935,936,938,941,944,946,949],{"class":36,"line":110},[34,937,262],{"class":40},[34,939,940],{"class":103}," open",[34,942,943],{"class":44},"() { isOpen.value ",[34,945,196],{"class":40},[34,947,948],{"class":120}," true",[34,950,951],{"class":44}," }\n",[34,953,954,956,959,961,963,966],{"class":36,"line":124},[34,955,262],{"class":40},[34,957,958],{"class":103}," close",[34,960,943],{"class":44},[34,962,196],{"class":40},[34,964,965],{"class":120}," false",[34,967,951],{"class":44},[34,969,970,972,975,977,979,981],{"class":36,"line":134},[34,971,262],{"class":40},[34,973,974],{"class":103}," toggle",[34,976,943],{"class":44},[34,978,196],{"class":40},[34,980,480],{"class":40},[34,982,983],{"class":44},"isOpen.value }\n",[34,985,986,989,991],{"class":36,"line":145},[34,987,988],{"class":44},"\u003C\u002F",[34,990,881],{"class":880},[34,992,887],{"class":44},[34,994,995],{"class":36,"line":156},[34,996,70],{"emptyLinePlaceholder":69},[34,998,999,1001,1004],{"class":36,"line":162},[34,1000,187],{"class":44},[34,1002,1003],{"class":880},"template",[34,1005,887],{"class":44},[34,1007,1008],{"class":36,"line":167},[34,1009,1010],{"class":76},"  \u003C!-- ── Slot receives: state + actions ──────────────────── -->\n",[34,1012,1013,1016],{"class":36,"line":173},[34,1014,1015],{"class":44},"  \u003C",[34,1017,1018],{"class":880},"slot\n",[34,1020,1021,1024,1026],{"class":36,"line":205},[34,1022,1023],{"class":103},"    :is-open",[34,1025,196],{"class":44},[34,1027,1028],{"class":62},"\"isOpen\"\n",[34,1030,1031,1034,1036],{"class":36,"line":248},[34,1032,1033],{"class":103},"    :open",[34,1035,196],{"class":44},[34,1037,1038],{"class":62},"\"open\"\n",[34,1040,1041,1044,1046],{"class":36,"line":253},[34,1042,1043],{"class":103},"    :close",[34,1045,196],{"class":44},[34,1047,1048],{"class":62},"\"close\"\n",[34,1050,1051,1054,1056],{"class":36,"line":259},[34,1052,1053],{"class":103},"    :toggle",[34,1055,196],{"class":44},[34,1057,1058],{"class":62},"\"toggle\"\n",[34,1060,1061,1065],{"class":36,"line":288},[34,1062,1064],{"class":1063},"sMKGj","  \u002F",[34,1066,887],{"class":44},[34,1068,1069,1071,1073],{"class":36,"line":300},[34,1070,988],{"class":44},[34,1072,1003],{"class":880},[34,1074,887],{"class":44},[20,1076,1078],{"filename":1077,"language":867},"RenderlessUsage.vue",[25,1079,1081],{"className":870,"code":1080,"language":867,"meta":29,"style":29},"\u003Ctemplate>\n  \u003C!-- ── Parent: full control over rendering ── -->\n  \u003CRenderlessToggle v-slot=\"{ isOpen, toggle, close }\">\n    \u003Cdiv>\n      \u003Cbutton @click=\"toggle\">{{ isOpen ? 'Close' : 'Open' }}\u003C\u002Fbutton>\n\n      \u003Cdiv v-if=\"isOpen\" class=\"panel\">\n        \u003Cp>Panel content\u003C\u002Fp>\n        \u003Cbutton @click=\"close\">Close X\u003C\u002Fbutton>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  \u003C\u002FRenderlessToggle>\n\n  \u003C!-- ── Same logic, different template: ── -->\n  \u003CRenderlessToggle v-slot=\"{ isOpen, toggle }\">\n    \u003Cdetails :open=\"isOpen\" @toggle=\"toggle\">\n      \u003Csummary>Click to {{ isOpen ? 'collapse' : 'expand' }}\u003C\u002Fsummary>\n      \u003Cp>Content\u003C\u002Fp>\n    \u003C\u002Fdetails>\n  \u003C\u002FRenderlessToggle>\n\u003C\u002Ftemplate>\n",[31,1082,1083,1091,1096,1118,1128,1151,1155,1179,1194,1214,1223,1232,1241,1245,1250,1269,1292,1306,1319,1327,1335],{"__ignoreMap":29},[34,1084,1085,1087,1089],{"class":36,"line":37},[34,1086,187],{"class":44},[34,1088,1003],{"class":880},[34,1090,887],{"class":44},[34,1092,1093],{"class":36,"line":66},[34,1094,1095],{"class":76},"  \u003C!-- ── Parent: full control over rendering ── -->\n",[34,1097,1098,1100,1103,1106,1108,1111,1114,1116],{"class":36,"line":73},[34,1099,1015],{"class":44},[34,1101,1102],{"class":880},"RenderlessToggle",[34,1104,1105],{"class":103}," v-slot",[34,1107,196],{"class":44},[34,1109,1110],{"class":62},"\"",[34,1112,1113],{"class":44},"{ isOpen, toggle, close }",[34,1115,1110],{"class":62},[34,1117,887],{"class":44},[34,1119,1120,1123,1126],{"class":36,"line":80},[34,1121,1122],{"class":44},"    \u003C",[34,1124,1125],{"class":880},"div",[34,1127,887],{"class":44},[34,1129,1130,1133,1136,1139,1141,1144,1147,1149],{"class":36,"line":86},[34,1131,1132],{"class":44},"      \u003C",[34,1134,1135],{"class":880},"button",[34,1137,1138],{"class":103}," @click",[34,1140,196],{"class":44},[34,1142,1143],{"class":62},"\"toggle\"",[34,1145,1146],{"class":44},">{{ isOpen ? 'Close' : 'Open' }}\u003C\u002F",[34,1148,1135],{"class":880},[34,1150,887],{"class":44},[34,1152,1153],{"class":36,"line":92},[34,1154,70],{"emptyLinePlaceholder":69},[34,1156,1157,1159,1161,1164,1166,1169,1172,1174,1177],{"class":36,"line":97},[34,1158,1132],{"class":44},[34,1160,1125],{"class":880},[34,1162,1163],{"class":103}," v-if",[34,1165,196],{"class":44},[34,1167,1168],{"class":62},"\"isOpen\"",[34,1170,1171],{"class":103}," class",[34,1173,196],{"class":44},[34,1175,1176],{"class":62},"\"panel\"",[34,1178,887],{"class":44},[34,1180,1181,1184,1187,1190,1192],{"class":36,"line":110},[34,1182,1183],{"class":44},"        \u003C",[34,1185,1186],{"class":880},"p",[34,1188,1189],{"class":44},">Panel content\u003C\u002F",[34,1191,1186],{"class":880},[34,1193,887],{"class":44},[34,1195,1196,1198,1200,1202,1204,1207,1210,1212],{"class":36,"line":124},[34,1197,1183],{"class":44},[34,1199,1135],{"class":880},[34,1201,1138],{"class":103},[34,1203,196],{"class":44},[34,1205,1206],{"class":62},"\"close\"",[34,1208,1209],{"class":44},">Close X\u003C\u002F",[34,1211,1135],{"class":880},[34,1213,887],{"class":44},[34,1215,1216,1219,1221],{"class":36,"line":134},[34,1217,1218],{"class":44},"      \u003C\u002F",[34,1220,1125],{"class":880},[34,1222,887],{"class":44},[34,1224,1225,1228,1230],{"class":36,"line":145},[34,1226,1227],{"class":44},"    \u003C\u002F",[34,1229,1125],{"class":880},[34,1231,887],{"class":44},[34,1233,1234,1237,1239],{"class":36,"line":156},[34,1235,1236],{"class":44},"  \u003C\u002F",[34,1238,1102],{"class":880},[34,1240,887],{"class":44},[34,1242,1243],{"class":36,"line":162},[34,1244,70],{"emptyLinePlaceholder":69},[34,1246,1247],{"class":36,"line":167},[34,1248,1249],{"class":76},"  \u003C!-- ── Same logic, different template: ── -->\n",[34,1251,1252,1254,1256,1258,1260,1262,1265,1267],{"class":36,"line":173},[34,1253,1015],{"class":44},[34,1255,1102],{"class":880},[34,1257,1105],{"class":103},[34,1259,196],{"class":44},[34,1261,1110],{"class":62},[34,1263,1264],{"class":44},"{ isOpen, toggle }",[34,1266,1110],{"class":62},[34,1268,887],{"class":44},[34,1270,1271,1273,1276,1279,1281,1283,1286,1288,1290],{"class":36,"line":205},[34,1272,1122],{"class":44},[34,1274,1275],{"class":880},"details",[34,1277,1278],{"class":103}," :open",[34,1280,196],{"class":44},[34,1282,1168],{"class":62},[34,1284,1285],{"class":103}," @toggle",[34,1287,196],{"class":44},[34,1289,1143],{"class":62},[34,1291,887],{"class":44},[34,1293,1294,1296,1299,1302,1304],{"class":36,"line":248},[34,1295,1132],{"class":44},[34,1297,1298],{"class":880},"summary",[34,1300,1301],{"class":44},">Click to {{ isOpen ? 'collapse' : 'expand' }}\u003C\u002F",[34,1303,1298],{"class":880},[34,1305,887],{"class":44},[34,1307,1308,1310,1312,1315,1317],{"class":36,"line":253},[34,1309,1132],{"class":44},[34,1311,1186],{"class":880},[34,1313,1314],{"class":44},">Content\u003C\u002F",[34,1316,1186],{"class":880},[34,1318,887],{"class":44},[34,1320,1321,1323,1325],{"class":36,"line":259},[34,1322,1227],{"class":44},[34,1324,1275],{"class":880},[34,1326,887],{"class":44},[34,1328,1329,1331,1333],{"class":36,"line":288},[34,1330,1236],{"class":44},[34,1332,1102],{"class":880},[34,1334,887],{"class":44},[34,1336,1337,1339,1341],{"class":36,"line":300},[34,1338,988],{"class":44},[34,1340,1003],{"class":880},[34,1342,887],{"class":44},[15,1344,1346],{"id":1345},"composable-composition-higher-order-composables","Composable Composition — Higher-Order Composables",[20,1348,1350],{"filename":1349,"language":23},"composable-composition.ts",[25,1351,1353],{"className":27,"code":1352,"language":23,"meta":29,"style":29},"import { ref, computed, type Ref, type MaybeRefOrGetter } from 'vue'\n\n\u002F\u002F ── Base composable: useFetch ───────────────────────────\nfunction useFetch\u003CT>(url: MaybeRefOrGetter\u003Cstring>) {\n  const data: Ref\u003CT | null> = ref(null) as Ref\u003CT | null>\n  const loading = ref(false)\n  const error = ref\u003CError | null>(null)\n  \u002F\u002F ... fetch logic ...\n  return { data, loading, error }\n}\n\n\u002F\u002F ── Higher-order composable: wraps useFetch with caching ──\nfunction useCachedFetch\u003CT>(url: MaybeRefOrGetter\u003Cstring>, ttl = 60_000) {\n  const { data, loading, error } = useFetch\u003CT>(url)\n  const lastFetch = ref(0)\n  const isStale = computed(() => Date.now() - lastFetch.value > ttl)\n\n  async function refresh() {\n    if (!isStale.value && data.value) return data.value\n    \u002F\u002F ... refetch ...\n    lastFetch.value = Date.now()\n  }\n\n  return { data, loading, error, isStale, refresh }\n}\n\n\u002F\u002F ── Composing multiple composables: useUserWithPosts ──\nfunction useUserWithPosts(userId: MaybeRefOrGetter\u003Cnumber>) {\n  \u002F\u002F ── Compose two fetches into one composable ──\n  const user = useCachedFetch(() => `\u002Fapi\u002Fusers\u002F${toValue(userId)}`)\n  const posts = useCachedFetch(() => `\u002Fapi\u002Fusers\u002F${toValue(userId)}\u002Fposts`)\n\n  const isLoading = computed(() => user.loading.value || posts.loading.value)\n  const hasError = computed(() => !!user.error.value || !!posts.error.value)\n\n  return {\n    user: user.data,\n    posts: posts.data,\n    isLoading,\n    hasError,\n    refreshUser: user.refresh,\n    refreshPosts: posts.refresh,\n  }\n}\n",[31,1354,1355,1375,1379,1384,1415,1464,1481,1507,1512,1519,1523,1527,1532,1568,1603,1621,1657,1661,1673,1698,1703,1717,1721,1725,1732,1736,1740,1745,1768,1773,1805,1835,1839,1863,1891,1895,1901,1906,1911,1916,1921,1926,1931,1935],{"__ignoreMap":29},[34,1356,1357,1359,1362,1364,1366,1368,1371,1373],{"class":36,"line":37},[34,1358,41],{"class":40},[34,1360,1361],{"class":44}," { ref, computed, ",[34,1363,48],{"class":40},[34,1365,51],{"class":44},[34,1367,48],{"class":40},[34,1369,1370],{"class":44}," MaybeRefOrGetter } ",[34,1372,59],{"class":40},[34,1374,63],{"class":62},[34,1376,1377],{"class":36,"line":66},[34,1378,70],{"emptyLinePlaceholder":69},[34,1380,1381],{"class":36,"line":73},[34,1382,1383],{"class":76},"\u002F\u002F ── Base composable: useFetch ───────────────────────────\n",[34,1385,1386,1388,1391,1393,1396,1399,1402,1404,1407,1409,1412],{"class":36,"line":80},[34,1387,262],{"class":40},[34,1389,1390],{"class":103}," useFetch",[34,1392,187],{"class":44},[34,1394,1395],{"class":103},"T",[34,1397,1398],{"class":44},">(",[34,1400,1401],{"class":113},"url",[34,1403,117],{"class":40},[34,1405,1406],{"class":103}," MaybeRefOrGetter",[34,1408,187],{"class":44},[34,1410,1411],{"class":120},"string",[34,1413,1414],{"class":44},">) {\n",[34,1416,1417,1420,1423,1425,1427,1429,1431,1433,1436,1438,1440,1442,1444,1447,1450,1452,1454,1456,1458,1460,1462],{"class":36,"line":86},[34,1418,1419],{"class":40},"  const",[34,1421,1422],{"class":120}," data",[34,1424,117],{"class":40},[34,1426,184],{"class":103},[34,1428,187],{"class":44},[34,1430,1395],{"class":103},[34,1432,222],{"class":40},[34,1434,1435],{"class":120}," null",[34,1437,233],{"class":44},[34,1439,196],{"class":40},[34,1441,199],{"class":103},[34,1443,240],{"class":44},[34,1445,1446],{"class":120},"null",[34,1448,1449],{"class":44},") ",[34,1451,535],{"class":40},[34,1453,184],{"class":103},[34,1455,187],{"class":44},[34,1457,1395],{"class":103},[34,1459,222],{"class":40},[34,1461,1435],{"class":120},[34,1463,887],{"class":44},[34,1465,1466,1468,1471,1473,1475,1477,1479],{"class":36,"line":92},[34,1467,1419],{"class":40},[34,1469,1470],{"class":120}," loading",[34,1472,665],{"class":40},[34,1474,199],{"class":103},[34,1476,240],{"class":44},[34,1478,324],{"class":120},[34,1480,245],{"class":44},[34,1482,1483,1485,1488,1490,1492,1494,1497,1499,1501,1503,1505],{"class":36,"line":97},[34,1484,1419],{"class":40},[34,1486,1487],{"class":120}," error",[34,1489,665],{"class":40},[34,1491,199],{"class":103},[34,1493,187],{"class":44},[34,1495,1496],{"class":103},"Error",[34,1498,222],{"class":40},[34,1500,1435],{"class":120},[34,1502,1398],{"class":44},[34,1504,1446],{"class":120},[34,1506,245],{"class":44},[34,1508,1509],{"class":36,"line":110},[34,1510,1511],{"class":76},"  \u002F\u002F ... fetch logic ...\n",[34,1513,1514,1516],{"class":36,"line":124},[34,1515,381],{"class":40},[34,1517,1518],{"class":44}," { data, loading, error }\n",[34,1520,1521],{"class":36,"line":134},[34,1522,159],{"class":44},[34,1524,1525],{"class":36,"line":145},[34,1526,70],{"emptyLinePlaceholder":69},[34,1528,1529],{"class":36,"line":156},[34,1530,1531],{"class":76},"\u002F\u002F ── Higher-order composable: wraps useFetch with caching ──\n",[34,1533,1534,1536,1539,1541,1543,1545,1547,1549,1551,1553,1555,1558,1561,1563,1566],{"class":36,"line":162},[34,1535,262],{"class":40},[34,1537,1538],{"class":103}," useCachedFetch",[34,1540,187],{"class":44},[34,1542,1395],{"class":103},[34,1544,1398],{"class":44},[34,1546,1401],{"class":113},[34,1548,117],{"class":40},[34,1550,1406],{"class":103},[34,1552,187],{"class":44},[34,1554,1411],{"class":120},[34,1556,1557],{"class":44},">, ",[34,1559,1560],{"class":113},"ttl",[34,1562,665],{"class":40},[34,1564,1565],{"class":120}," 60_000",[34,1567,653],{"class":44},[34,1569,1570,1572,1575,1578,1581,1584,1586,1589,1592,1594,1596,1598,1600],{"class":36,"line":167},[34,1571,1419],{"class":40},[34,1573,1574],{"class":44}," { ",[34,1576,1577],{"class":120},"data",[34,1579,1580],{"class":44},", ",[34,1582,1583],{"class":120},"loading",[34,1585,1580],{"class":44},[34,1587,1588],{"class":120},"error",[34,1590,1591],{"class":44}," } ",[34,1593,196],{"class":40},[34,1595,1390],{"class":103},[34,1597,187],{"class":44},[34,1599,1395],{"class":103},[34,1601,1602],{"class":44},">(url)\n",[34,1604,1605,1607,1610,1612,1614,1616,1619],{"class":36,"line":173},[34,1606,1419],{"class":40},[34,1608,1609],{"class":120}," lastFetch",[34,1611,665],{"class":40},[34,1613,199],{"class":103},[34,1615,240],{"class":44},[34,1617,1618],{"class":120},"0",[34,1620,245],{"class":44},[34,1622,1623,1625,1628,1630,1633,1635,1637,1640,1642,1645,1648,1651,1654],{"class":36,"line":205},[34,1624,1419],{"class":40},[34,1626,1627],{"class":120}," isStale",[34,1629,665],{"class":40},[34,1631,1632],{"class":103}," computed",[34,1634,435],{"class":44},[34,1636,438],{"class":40},[34,1638,1639],{"class":44}," Date.",[34,1641,336],{"class":103},[34,1643,1644],{"class":44},"() ",[34,1646,1647],{"class":40},"-",[34,1649,1650],{"class":44}," lastFetch.value ",[34,1652,1653],{"class":40},">",[34,1655,1656],{"class":44}," ttl)\n",[34,1658,1659],{"class":36,"line":248},[34,1660,70],{"emptyLinePlaceholder":69},[34,1662,1663,1666,1668,1671],{"class":36,"line":253},[34,1664,1665],{"class":40},"  async",[34,1667,369],{"class":40},[34,1669,1670],{"class":103}," refresh",[34,1672,375],{"class":44},[34,1674,1675,1678,1681,1684,1687,1690,1693,1695],{"class":36,"line":259},[34,1676,1677],{"class":40},"    if",[34,1679,1680],{"class":44}," (",[34,1682,1683],{"class":40},"!",[34,1685,1686],{"class":44},"isStale.value ",[34,1688,1689],{"class":40},"&&",[34,1691,1692],{"class":44}," data.value) ",[34,1694,463],{"class":40},[34,1696,1697],{"class":44}," data.value\n",[34,1699,1700],{"class":36,"line":288},[34,1701,1702],{"class":76},"    \u002F\u002F ... refetch ...\n",[34,1704,1705,1708,1710,1712,1714],{"class":36,"line":300},[34,1706,1707],{"class":44},"    lastFetch.value ",[34,1709,196],{"class":40},[34,1711,1639],{"class":44},[34,1713,336],{"class":103},[34,1715,1716],{"class":44},"()\n",[34,1718,1719],{"class":36,"line":312},[34,1720,825],{"class":44},[34,1722,1723],{"class":36,"line":318},[34,1724,70],{"emptyLinePlaceholder":69},[34,1726,1727,1729],{"class":36,"line":330},[34,1728,381],{"class":40},[34,1730,1731],{"class":44}," { data, loading, error, isStale, refresh }\n",[34,1733,1734],{"class":36,"line":341},[34,1735,159],{"class":44},[34,1737,1738],{"class":36,"line":347},[34,1739,70],{"emptyLinePlaceholder":69},[34,1741,1742],{"class":36,"line":352},[34,1743,1744],{"class":76},"\u002F\u002F ── Composing multiple composables: useUserWithPosts ──\n",[34,1746,1747,1749,1752,1754,1757,1759,1761,1763,1766],{"class":36,"line":357},[34,1748,262],{"class":40},[34,1750,1751],{"class":103}," useUserWithPosts",[34,1753,240],{"class":44},[34,1755,1756],{"class":113},"userId",[34,1758,117],{"class":40},[34,1760,1406],{"class":103},[34,1762,187],{"class":44},[34,1764,1765],{"class":120},"number",[34,1767,1414],{"class":44},[34,1769,1770],{"class":36,"line":363},[34,1771,1772],{"class":76},"  \u002F\u002F ── Compose two fetches into one composable ──\n",[34,1774,1775,1777,1780,1782,1784,1786,1788,1791,1794,1796,1798,1800,1803],{"class":36,"line":378},[34,1776,1419],{"class":40},[34,1778,1779],{"class":120}," user",[34,1781,665],{"class":40},[34,1783,1538],{"class":103},[34,1785,435],{"class":44},[34,1787,438],{"class":40},[34,1789,1790],{"class":62}," `\u002Fapi\u002Fusers\u002F${",[34,1792,1793],{"class":103},"toValue",[34,1795,240],{"class":62},[34,1797,1756],{"class":44},[34,1799,278],{"class":62},[34,1801,1802],{"class":62},"}`",[34,1804,245],{"class":44},[34,1806,1807,1809,1812,1814,1816,1818,1820,1822,1824,1826,1828,1830,1833],{"class":36,"line":386},[34,1808,1419],{"class":40},[34,1810,1811],{"class":120}," posts",[34,1813,665],{"class":40},[34,1815,1538],{"class":103},[34,1817,435],{"class":44},[34,1819,438],{"class":40},[34,1821,1790],{"class":62},[34,1823,1793],{"class":103},[34,1825,240],{"class":62},[34,1827,1756],{"class":44},[34,1829,278],{"class":62},[34,1831,1832],{"class":62},"}\u002Fposts`",[34,1834,245],{"class":44},[34,1836,1837],{"class":36,"line":392},[34,1838,70],{"emptyLinePlaceholder":69},[34,1840,1841,1843,1846,1848,1850,1852,1854,1857,1860],{"class":36,"line":404},[34,1842,1419],{"class":40},[34,1844,1845],{"class":120}," isLoading",[34,1847,665],{"class":40},[34,1849,1632],{"class":103},[34,1851,435],{"class":44},[34,1853,438],{"class":40},[34,1855,1856],{"class":44}," user.loading.value ",[34,1858,1859],{"class":40},"||",[34,1861,1862],{"class":44}," posts.loading.value)\n",[34,1864,1865,1867,1870,1872,1874,1876,1878,1881,1884,1886,1888],{"class":36,"line":415},[34,1866,1419],{"class":40},[34,1868,1869],{"class":120}," hasError",[34,1871,665],{"class":40},[34,1873,1632],{"class":103},[34,1875,435],{"class":44},[34,1877,438],{"class":40},[34,1879,1880],{"class":40}," !!",[34,1882,1883],{"class":44},"user.error.value ",[34,1885,1859],{"class":40},[34,1887,1880],{"class":40},[34,1889,1890],{"class":44},"posts.error.value)\n",[34,1892,1893],{"class":36,"line":420},[34,1894,70],{"emptyLinePlaceholder":69},[34,1896,1897,1899],{"class":36,"line":426},[34,1898,381],{"class":40},[34,1900,107],{"class":44},[34,1902,1903],{"class":36,"line":443},[34,1904,1905],{"class":44},"    user: user.data,\n",[34,1907,1908],{"class":36,"line":452},[34,1909,1910],{"class":44},"    posts: posts.data,\n",[34,1912,1913],{"class":36,"line":486},[34,1914,1915],{"class":44},"    isLoading,\n",[34,1917,1918],{"class":36,"line":510},[34,1919,1920],{"class":44},"    hasError,\n",[34,1922,1923],{"class":36,"line":523},[34,1924,1925],{"class":44},"    refreshUser: user.refresh,\n",[34,1927,1928],{"class":36,"line":529},[34,1929,1930],{"class":44},"    refreshPosts: posts.refresh,\n",[34,1932,1933],{"class":36,"line":548},[34,1934,825],{"class":44},[34,1936,1937],{"class":36,"line":553},[34,1938,159],{"class":44},[15,1940,1942],{"id":1941},"providerconsumer-typed-dependency-injection","Provider\u002FConsumer — Typed Dependency Injection",[20,1944,1946],{"filename":1945,"language":23},"provider-consumer.ts",[25,1947,1949],{"className":27,"code":1948,"language":23,"meta":29,"style":29},"import { provide, inject, ref, readonly, type Ref, type InjectionKey } from 'vue'\n\n\u002F\u002F ── InjectionKey: typed symbol for type-safe DI ──────────\nconst NotificationContextKey: InjectionKey\u003C{\n  notifications: Readonly\u003CRef\u003CNotification[]>>\n  notify: (message: string, type?: 'info' | 'error') => void\n  dismiss: (id: string) => void\n}> = Symbol('notifications')\n\n\u002F\u002F ── Provider: set up at app or component root ──────────\nexport function provideNotifications() {\n  const notifications = ref\u003CNotification[]>([])\n\n  function notify(message: string, type: 'info' | 'error' = 'info') {\n    const id = crypto.randomUUID()\n    notifications.value.push({ id, message, type, timestamp: Date.now() })\n    setTimeout(() => dismiss(id), 5000)  \u002F\u002F auto-dismiss after 5s\n  }\n\n  function dismiss(id: string) {\n    notifications.value = notifications.value.filter(n => n.id !== id)\n  }\n\n  const context = {\n    notifications: readonly(notifications),  \u002F\u002F read-only for consumers\n    notify,\n    dismiss,\n  }\n\n  provide(NotificationContextKey, context)\n  return context  \u002F\u002F provider also gets the mutable API\n}\n\n\u002F\u002F ── Consumer: use in any descendant ─────────────────────\nexport function useNotifications() {\n  const ctx = inject(NotificationContextKey)\n  if (!ctx) {\n    throw new Error('useNotifications() must be used within a component that calls provideNotifications()')\n  }\n  return ctx\n}\n\n\u002F\u002F ── Usage in root component: ────────────────────────────\n\u002F\u002F provideNotifications()\n\u002F\u002F ── Usage in any descendant: ────────────────────────────\n\u002F\u002F const { notify, dismiss, notifications } = useNotifications()\n\u002F\u002F notify('Saved!', 'info')  → adds a notification, auto-dismisses in 5s\n",[31,1950,1951,1971,1975,1980,1995,2018,2056,2077,2094,2098,2103,2114,2132,2136,2170,2187,2202,2226,2230,2234,2250,2276,2280,2284,2295,2308,2313,2318,2322,2326,2334,2344,2348,2352,2357,2368,2383,2395,2413,2417,2424,2428,2432,2437,2442,2447,2452],{"__ignoreMap":29},[34,1952,1953,1955,1958,1960,1962,1964,1967,1969],{"class":36,"line":37},[34,1954,41],{"class":40},[34,1956,1957],{"class":44}," { provide, inject, ref, readonly, ",[34,1959,48],{"class":40},[34,1961,51],{"class":44},[34,1963,48],{"class":40},[34,1965,1966],{"class":44}," InjectionKey } ",[34,1968,59],{"class":40},[34,1970,63],{"class":62},[34,1972,1973],{"class":36,"line":66},[34,1974,70],{"emptyLinePlaceholder":69},[34,1976,1977],{"class":36,"line":73},[34,1978,1979],{"class":76},"\u002F\u002F ── InjectionKey: typed symbol for type-safe DI ──────────\n",[34,1981,1982,1984,1987,1989,1992],{"class":36,"line":80},[34,1983,176],{"class":40},[34,1985,1986],{"class":120}," NotificationContextKey",[34,1988,117],{"class":40},[34,1990,1991],{"class":103}," InjectionKey",[34,1993,1994],{"class":44},"\u003C{\n",[34,1996,1997,2000,2002,2005,2007,2010,2012,2015],{"class":36,"line":86},[34,1998,1999],{"class":113},"  notifications",[34,2001,117],{"class":40},[34,2003,2004],{"class":103}," Readonly",[34,2006,187],{"class":44},[34,2008,2009],{"class":103},"Ref",[34,2011,187],{"class":44},[34,2013,2014],{"class":103},"Notification",[34,2016,2017],{"class":44},"[]>>\n",[34,2019,2020,2023,2025,2027,2030,2032,2034,2036,2038,2041,2044,2046,2049,2051,2053],{"class":36,"line":92},[34,2021,2022],{"class":103},"  notify",[34,2024,117],{"class":40},[34,2026,1680],{"class":44},[34,2028,2029],{"class":113},"message",[34,2031,117],{"class":40},[34,2033,275],{"class":120},[34,2035,1580],{"class":44},[34,2037,48],{"class":113},[34,2039,2040],{"class":40},"?:",[34,2042,2043],{"class":62}," 'info'",[34,2045,222],{"class":40},[34,2047,2048],{"class":62}," 'error'",[34,2050,1449],{"class":44},[34,2052,438],{"class":40},[34,2054,2055],{"class":120}," void\n",[34,2057,2058,2061,2063,2065,2067,2069,2071,2073,2075],{"class":36,"line":97},[34,2059,2060],{"class":103},"  dismiss",[34,2062,117],{"class":40},[34,2064,1680],{"class":44},[34,2066,646],{"class":113},[34,2068,117],{"class":40},[34,2070,275],{"class":120},[34,2072,1449],{"class":44},[34,2074,438],{"class":40},[34,2076,2055],{"class":120},[34,2078,2079,2082,2084,2087,2089,2092],{"class":36,"line":110},[34,2080,2081],{"class":44},"}> ",[34,2083,196],{"class":40},[34,2085,2086],{"class":103}," Symbol",[34,2088,240],{"class":44},[34,2090,2091],{"class":62},"'notifications'",[34,2093,245],{"class":44},[34,2095,2096],{"class":36,"line":124},[34,2097,70],{"emptyLinePlaceholder":69},[34,2099,2100],{"class":36,"line":134},[34,2101,2102],{"class":76},"\u002F\u002F ── Provider: set up at app or component root ──────────\n",[34,2104,2105,2107,2109,2112],{"class":36,"line":145},[34,2106,366],{"class":40},[34,2108,369],{"class":40},[34,2110,2111],{"class":103}," provideNotifications",[34,2113,375],{"class":44},[34,2115,2116,2118,2121,2123,2125,2127,2129],{"class":36,"line":156},[34,2117,1419],{"class":40},[34,2119,2120],{"class":120}," notifications",[34,2122,665],{"class":40},[34,2124,199],{"class":103},[34,2126,187],{"class":44},[34,2128,2014],{"class":103},[34,2130,2131],{"class":44},"[]>([])\n",[34,2133,2134],{"class":36,"line":162},[34,2135,70],{"emptyLinePlaceholder":69},[34,2137,2138,2141,2144,2146,2148,2150,2152,2154,2156,2158,2160,2162,2164,2166,2168],{"class":36,"line":167},[34,2139,2140],{"class":40},"  function",[34,2142,2143],{"class":103}," notify",[34,2145,240],{"class":44},[34,2147,2029],{"class":113},[34,2149,117],{"class":40},[34,2151,275],{"class":120},[34,2153,1580],{"class":44},[34,2155,48],{"class":113},[34,2157,117],{"class":40},[34,2159,2043],{"class":62},[34,2161,222],{"class":40},[34,2163,2048],{"class":62},[34,2165,665],{"class":40},[34,2167,2043],{"class":62},[34,2169,653],{"class":44},[34,2171,2172,2175,2178,2180,2183,2185],{"class":36,"line":173},[34,2173,2174],{"class":40},"    const",[34,2176,2177],{"class":120}," id",[34,2179,665],{"class":40},[34,2181,2182],{"class":44}," crypto.",[34,2184,306],{"class":103},[34,2186,1716],{"class":44},[34,2188,2189,2192,2194,2197,2199],{"class":36,"line":205},[34,2190,2191],{"class":44},"    notifications.value.",[34,2193,294],{"class":103},[34,2195,2196],{"class":44},"({ id, message, type, timestamp: Date.",[34,2198,336],{"class":103},[34,2200,2201],{"class":44},"() })\n",[34,2203,2204,2207,2209,2211,2214,2217,2220,2223],{"class":36,"line":248},[34,2205,2206],{"class":103},"    setTimeout",[34,2208,435],{"class":44},[34,2210,438],{"class":40},[34,2212,2213],{"class":103}," dismiss",[34,2215,2216],{"class":44},"(id), ",[34,2218,2219],{"class":120},"5000",[34,2221,2222],{"class":44},")  ",[34,2224,2225],{"class":76},"\u002F\u002F auto-dismiss after 5s\n",[34,2227,2228],{"class":36,"line":253},[34,2229,825],{"class":44},[34,2231,2232],{"class":36,"line":259},[34,2233,70],{"emptyLinePlaceholder":69},[34,2235,2236,2238,2240,2242,2244,2246,2248],{"class":36,"line":288},[34,2237,2140],{"class":40},[34,2239,2213],{"class":103},[34,2241,240],{"class":44},[34,2243,646],{"class":113},[34,2245,117],{"class":40},[34,2247,275],{"class":120},[34,2249,653],{"class":44},[34,2251,2252,2255,2257,2260,2262,2264,2267,2269,2272,2274],{"class":36,"line":300},[34,2253,2254],{"class":44},"    notifications.value ",[34,2256,196],{"class":40},[34,2258,2259],{"class":44}," notifications.value.",[34,2261,469],{"class":103},[34,2263,240],{"class":44},[34,2265,2266],{"class":113},"n",[34,2268,477],{"class":40},[34,2270,2271],{"class":44}," n.id ",[34,2273,746],{"class":40},[34,2275,685],{"class":44},[34,2277,2278],{"class":36,"line":312},[34,2279,825],{"class":44},[34,2281,2282],{"class":36,"line":318},[34,2283,70],{"emptyLinePlaceholder":69},[34,2285,2286,2288,2291,2293],{"class":36,"line":330},[34,2287,1419],{"class":40},[34,2289,2290],{"class":120}," context",[34,2292,665],{"class":40},[34,2294,107],{"class":44},[34,2296,2297,2300,2302,2305],{"class":36,"line":341},[34,2298,2299],{"class":44},"    notifications: ",[34,2301,398],{"class":103},[34,2303,2304],{"class":44},"(notifications),  ",[34,2306,2307],{"class":76},"\u002F\u002F read-only for consumers\n",[34,2309,2310],{"class":36,"line":347},[34,2311,2312],{"class":44},"    notify,\n",[34,2314,2315],{"class":36,"line":352},[34,2316,2317],{"class":44},"    dismiss,\n",[34,2319,2320],{"class":36,"line":357},[34,2321,825],{"class":44},[34,2323,2324],{"class":36,"line":363},[34,2325,70],{"emptyLinePlaceholder":69},[34,2327,2328,2331],{"class":36,"line":378},[34,2329,2330],{"class":103},"  provide",[34,2332,2333],{"class":44},"(NotificationContextKey, context)\n",[34,2335,2336,2338,2341],{"class":36,"line":386},[34,2337,381],{"class":40},[34,2339,2340],{"class":44}," context  ",[34,2342,2343],{"class":76},"\u002F\u002F provider also gets the mutable API\n",[34,2345,2346],{"class":36,"line":392},[34,2347,159],{"class":44},[34,2349,2350],{"class":36,"line":404},[34,2351,70],{"emptyLinePlaceholder":69},[34,2353,2354],{"class":36,"line":415},[34,2355,2356],{"class":76},"\u002F\u002F ── Consumer: use in any descendant ─────────────────────\n",[34,2358,2359,2361,2363,2366],{"class":36,"line":420},[34,2360,366],{"class":40},[34,2362,369],{"class":40},[34,2364,2365],{"class":103}," useNotifications",[34,2367,375],{"class":44},[34,2369,2370,2372,2375,2377,2380],{"class":36,"line":426},[34,2371,1419],{"class":40},[34,2373,2374],{"class":120}," ctx",[34,2376,665],{"class":40},[34,2378,2379],{"class":103}," inject",[34,2381,2382],{"class":44},"(NotificationContextKey)\n",[34,2384,2385,2388,2390,2392],{"class":36,"line":443},[34,2386,2387],{"class":40},"  if",[34,2389,1680],{"class":44},[34,2391,1683],{"class":40},[34,2393,2394],{"class":44},"ctx) {\n",[34,2396,2397,2400,2403,2406,2408,2411],{"class":36,"line":452},[34,2398,2399],{"class":40},"    throw",[34,2401,2402],{"class":40}," new",[34,2404,2405],{"class":103}," Error",[34,2407,240],{"class":44},[34,2409,2410],{"class":62},"'useNotifications() must be used within a component that calls provideNotifications()'",[34,2412,245],{"class":44},[34,2414,2415],{"class":36,"line":486},[34,2416,825],{"class":44},[34,2418,2419,2421],{"class":36,"line":510},[34,2420,381],{"class":40},[34,2422,2423],{"class":44}," ctx\n",[34,2425,2426],{"class":36,"line":523},[34,2427,159],{"class":44},[34,2429,2430],{"class":36,"line":529},[34,2431,70],{"emptyLinePlaceholder":69},[34,2433,2434],{"class":36,"line":548},[34,2435,2436],{"class":76},"\u002F\u002F ── Usage in root component: ────────────────────────────\n",[34,2438,2439],{"class":36,"line":553},[34,2440,2441],{"class":76},"\u002F\u002F provideNotifications()\n",[34,2443,2444],{"class":36,"line":586},[34,2445,2446],{"class":76},"\u002F\u002F ── Usage in any descendant: ────────────────────────────\n",[34,2448,2449],{"class":36,"line":604},[34,2450,2451],{"class":76},"\u002F\u002F const { notify, dismiss, notifications } = useNotifications()\n",[34,2453,2454],{"class":36,"line":609},[34,2455,2456],{"class":76},"\u002F\u002F notify('Saved!', 'info')  → adds a notification, auto-dismisses in 5s\n",[15,2458,2460],{"id":2459},"observer-pattern-reactive-event-bus","Observer Pattern — Reactive Event Bus",[20,2462,2464],{"filename":2463,"language":23},"event-bus.ts",[25,2465,2467],{"className":27,"code":2466,"language":23,"meta":29,"style":29},"import { ref, type Ref } from 'vue'\n\n\u002F\u002F ── Event bus: for cross-component communication without Pinia ──\n\u002F\u002F Use sparingly — prefer Pinia for state, provide\u002Finject for DI.\n\u002F\u002F Event bus is for ephemeral events (notifications, toasts), not state.\n\ntype EventHandler\u003CT = any> = (payload: T) => void\n\nclass EventBus {\n  private handlers = new Map\u003Cstring, Set\u003CEventHandler>>()\n\n  on\u003CT>(event: string, handler: EventHandler\u003CT>): () => void {\n    if (!this.handlers.has(event)) {\n      this.handlers.set(event, new Set())\n    }\n    this.handlers.get(event)!.add(handler as EventHandler)\n\n    \u002F\u002F ── Return unsubscribe function (composable cleanup pattern) ──\n    return () => this.handlers.get(event)?.delete(handler as EventHandler)\n  }\n\n  emit\u003CT>(event: string, payload: T): void {\n    this.handlers.get(event)?.forEach(handler => handler(payload))\n  }\n\n  off(event: string): void {\n    this.handlers.delete(event)\n  }\n}\n\nexport const bus = new EventBus()\n\n\u002F\u002F ── Usage in composable with cleanup: ───────────────────\nimport { onScopeDispose } from 'vue'\n\nexport function useEventBus(event: string, handler: EventHandler) {\n  const unsubscribe = bus.on(event, handler)\n  onScopeDispose(() => unsubscribe())  \u002F\u002F auto-cleanup on component unmount\n}\n\n\u002F\u002F ── Emitting: ──────────────────────────────────────────\n\u002F\u002F bus.emit('user:login', { id: 42, name: 'Ada' })\n\u002F\u002F ── Listening: ──────────────────────────────────────────\n\u002F\u002F useEventBus('user:login', (payload) => { console.log(payload) })\n",[31,2468,2469,2485,2489,2494,2499,2504,2508,2544,2548,2558,2590,2594,2639,2659,2681,2686,2716,2720,2725,2755,2759,2763,2796,2821,2825,2829,2850,2861,2865,2869,2873,2891,2895,2900,2911,2915,2942,2960,2977,2981,2985,2990,2995,3000],{"__ignoreMap":29},[34,2470,2471,2473,2476,2478,2481,2483],{"class":36,"line":37},[34,2472,41],{"class":40},[34,2474,2475],{"class":44}," { ref, ",[34,2477,48],{"class":40},[34,2479,2480],{"class":44}," Ref } ",[34,2482,59],{"class":40},[34,2484,63],{"class":62},[34,2486,2487],{"class":36,"line":66},[34,2488,70],{"emptyLinePlaceholder":69},[34,2490,2491],{"class":36,"line":73},[34,2492,2493],{"class":76},"\u002F\u002F ── Event bus: for cross-component communication without Pinia ──\n",[34,2495,2496],{"class":36,"line":80},[34,2497,2498],{"class":76},"\u002F\u002F Use sparingly — prefer Pinia for state, provide\u002Finject for DI.\n",[34,2500,2501],{"class":36,"line":86},[34,2502,2503],{"class":76},"\u002F\u002F Event bus is for ephemeral events (notifications, toasts), not state.\n",[34,2505,2506],{"class":36,"line":92},[34,2507,70],{"emptyLinePlaceholder":69},[34,2509,2510,2512,2515,2517,2519,2521,2524,2526,2528,2530,2533,2535,2538,2540,2542],{"class":36,"line":97},[34,2511,48],{"class":40},[34,2513,2514],{"class":103}," EventHandler",[34,2516,187],{"class":44},[34,2518,1395],{"class":103},[34,2520,665],{"class":40},[34,2522,2523],{"class":120}," any",[34,2525,233],{"class":44},[34,2527,196],{"class":40},[34,2529,1680],{"class":44},[34,2531,2532],{"class":113},"payload",[34,2534,117],{"class":40},[34,2536,2537],{"class":103}," T",[34,2539,1449],{"class":44},[34,2541,438],{"class":40},[34,2543,2055],{"class":120},[34,2545,2546],{"class":36,"line":110},[34,2547,70],{"emptyLinePlaceholder":69},[34,2549,2550,2553,2556],{"class":36,"line":124},[34,2551,2552],{"class":40},"class",[34,2554,2555],{"class":103}," EventBus",[34,2557,107],{"class":44},[34,2559,2560,2563,2566,2568,2570,2573,2575,2577,2579,2582,2584,2587],{"class":36,"line":134},[34,2561,2562],{"class":40},"  private",[34,2564,2565],{"class":113}," handlers",[34,2567,665],{"class":40},[34,2569,2402],{"class":40},[34,2571,2572],{"class":103}," Map",[34,2574,187],{"class":44},[34,2576,1411],{"class":120},[34,2578,1580],{"class":44},[34,2580,2581],{"class":103},"Set",[34,2583,187],{"class":44},[34,2585,2586],{"class":103},"EventHandler",[34,2588,2589],{"class":44},">>()\n",[34,2591,2592],{"class":36,"line":145},[34,2593,70],{"emptyLinePlaceholder":69},[34,2595,2596,2599,2601,2603,2605,2608,2610,2612,2614,2617,2619,2621,2623,2625,2628,2630,2633,2635,2637],{"class":36,"line":156},[34,2597,2598],{"class":103},"  on",[34,2600,187],{"class":44},[34,2602,1395],{"class":103},[34,2604,1398],{"class":44},[34,2606,2607],{"class":113},"event",[34,2609,117],{"class":40},[34,2611,275],{"class":120},[34,2613,1580],{"class":44},[34,2615,2616],{"class":113},"handler",[34,2618,117],{"class":40},[34,2620,2514],{"class":103},[34,2622,187],{"class":44},[34,2624,1395],{"class":103},[34,2626,2627],{"class":44},">)",[34,2629,117],{"class":40},[34,2631,2632],{"class":44}," () ",[34,2634,438],{"class":40},[34,2636,283],{"class":120},[34,2638,107],{"class":44},[34,2640,2641,2643,2645,2647,2650,2653,2656],{"class":36,"line":162},[34,2642,1677],{"class":40},[34,2644,1680],{"class":44},[34,2646,1683],{"class":40},[34,2648,2649],{"class":120},"this",[34,2651,2652],{"class":44},".handlers.",[34,2654,2655],{"class":103},"has",[34,2657,2658],{"class":44},"(event)) {\n",[34,2660,2661,2664,2666,2669,2672,2675,2678],{"class":36,"line":167},[34,2662,2663],{"class":120},"      this",[34,2665,2652],{"class":44},[34,2667,2668],{"class":103},"set",[34,2670,2671],{"class":44},"(event, ",[34,2673,2674],{"class":40},"new",[34,2676,2677],{"class":103}," Set",[34,2679,2680],{"class":44},"())\n",[34,2682,2683],{"class":36,"line":173},[34,2684,2685],{"class":44},"    }\n",[34,2687,2688,2691,2693,2696,2699,2701,2704,2707,2710,2712,2714],{"class":36,"line":205},[34,2689,2690],{"class":120},"    this",[34,2692,2652],{"class":44},[34,2694,2695],{"class":103},"get",[34,2697,2698],{"class":44},"(event)",[34,2700,1683],{"class":40},[34,2702,2703],{"class":44},".",[34,2705,2706],{"class":103},"add",[34,2708,2709],{"class":44},"(handler ",[34,2711,535],{"class":40},[34,2713,2514],{"class":103},[34,2715,245],{"class":44},[34,2717,2718],{"class":36,"line":248},[34,2719,70],{"emptyLinePlaceholder":69},[34,2721,2722],{"class":36,"line":253},[34,2723,2724],{"class":76},"    \u002F\u002F ── Return unsubscribe function (composable cleanup pattern) ──\n",[34,2726,2727,2730,2732,2734,2737,2739,2741,2744,2747,2749,2751,2753],{"class":36,"line":259},[34,2728,2729],{"class":40},"    return",[34,2731,2632],{"class":44},[34,2733,438],{"class":40},[34,2735,2736],{"class":120}," this",[34,2738,2652],{"class":44},[34,2740,2695],{"class":103},[34,2742,2743],{"class":44},"(event)?.",[34,2745,2746],{"class":103},"delete",[34,2748,2709],{"class":44},[34,2750,535],{"class":40},[34,2752,2514],{"class":103},[34,2754,245],{"class":44},[34,2756,2757],{"class":36,"line":288},[34,2758,825],{"class":44},[34,2760,2761],{"class":36,"line":300},[34,2762,70],{"emptyLinePlaceholder":69},[34,2764,2765,2768,2770,2772,2774,2776,2778,2780,2782,2784,2786,2788,2790,2792,2794],{"class":36,"line":312},[34,2766,2767],{"class":103},"  emit",[34,2769,187],{"class":44},[34,2771,1395],{"class":103},[34,2773,1398],{"class":44},[34,2775,2607],{"class":113},[34,2777,117],{"class":40},[34,2779,275],{"class":120},[34,2781,1580],{"class":44},[34,2783,2532],{"class":113},[34,2785,117],{"class":40},[34,2787,2537],{"class":103},[34,2789,278],{"class":44},[34,2791,117],{"class":40},[34,2793,283],{"class":120},[34,2795,107],{"class":44},[34,2797,2798,2800,2802,2804,2806,2809,2811,2813,2815,2818],{"class":36,"line":318},[34,2799,2690],{"class":120},[34,2801,2652],{"class":44},[34,2803,2695],{"class":103},[34,2805,2743],{"class":44},[34,2807,2808],{"class":103},"forEach",[34,2810,240],{"class":44},[34,2812,2616],{"class":113},[34,2814,477],{"class":40},[34,2816,2817],{"class":103}," handler",[34,2819,2820],{"class":44},"(payload))\n",[34,2822,2823],{"class":36,"line":330},[34,2824,825],{"class":44},[34,2826,2827],{"class":36,"line":341},[34,2828,70],{"emptyLinePlaceholder":69},[34,2830,2831,2834,2836,2838,2840,2842,2844,2846,2848],{"class":36,"line":347},[34,2832,2833],{"class":103},"  off",[34,2835,240],{"class":44},[34,2837,2607],{"class":113},[34,2839,117],{"class":40},[34,2841,275],{"class":120},[34,2843,278],{"class":44},[34,2845,117],{"class":40},[34,2847,283],{"class":120},[34,2849,107],{"class":44},[34,2851,2852,2854,2856,2858],{"class":36,"line":352},[34,2853,2690],{"class":120},[34,2855,2652],{"class":44},[34,2857,2746],{"class":103},[34,2859,2860],{"class":44},"(event)\n",[34,2862,2863],{"class":36,"line":357},[34,2864,825],{"class":44},[34,2866,2867],{"class":36,"line":363},[34,2868,159],{"class":44},[34,2870,2871],{"class":36,"line":378},[34,2872,70],{"emptyLinePlaceholder":69},[34,2874,2875,2877,2880,2883,2885,2887,2889],{"class":36,"line":386},[34,2876,366],{"class":40},[34,2878,2879],{"class":40}," const",[34,2881,2882],{"class":120}," bus",[34,2884,665],{"class":40},[34,2886,2402],{"class":40},[34,2888,2555],{"class":103},[34,2890,1716],{"class":44},[34,2892,2893],{"class":36,"line":392},[34,2894,70],{"emptyLinePlaceholder":69},[34,2896,2897],{"class":36,"line":404},[34,2898,2899],{"class":76},"\u002F\u002F ── Usage in composable with cleanup: ───────────────────\n",[34,2901,2902,2904,2907,2909],{"class":36,"line":415},[34,2903,41],{"class":40},[34,2905,2906],{"class":44}," { onScopeDispose } ",[34,2908,59],{"class":40},[34,2910,63],{"class":62},[34,2912,2913],{"class":36,"line":420},[34,2914,70],{"emptyLinePlaceholder":69},[34,2916,2917,2919,2921,2924,2926,2928,2930,2932,2934,2936,2938,2940],{"class":36,"line":426},[34,2918,366],{"class":40},[34,2920,369],{"class":40},[34,2922,2923],{"class":103}," useEventBus",[34,2925,240],{"class":44},[34,2927,2607],{"class":113},[34,2929,117],{"class":40},[34,2931,275],{"class":120},[34,2933,1580],{"class":44},[34,2935,2616],{"class":113},[34,2937,117],{"class":40},[34,2939,2514],{"class":103},[34,2941,653],{"class":44},[34,2943,2944,2946,2949,2951,2954,2957],{"class":36,"line":443},[34,2945,1419],{"class":40},[34,2947,2948],{"class":120}," unsubscribe",[34,2950,665],{"class":40},[34,2952,2953],{"class":44}," bus.",[34,2955,2956],{"class":103},"on",[34,2958,2959],{"class":44},"(event, handler)\n",[34,2961,2962,2965,2967,2969,2971,2974],{"class":36,"line":452},[34,2963,2964],{"class":103},"  onScopeDispose",[34,2966,435],{"class":44},[34,2968,438],{"class":40},[34,2970,2948],{"class":103},[34,2972,2973],{"class":44},"())  ",[34,2975,2976],{"class":76},"\u002F\u002F auto-cleanup on component unmount\n",[34,2978,2979],{"class":36,"line":486},[34,2980,159],{"class":44},[34,2982,2983],{"class":36,"line":510},[34,2984,70],{"emptyLinePlaceholder":69},[34,2986,2987],{"class":36,"line":523},[34,2988,2989],{"class":76},"\u002F\u002F ── Emitting: ──────────────────────────────────────────\n",[34,2991,2992],{"class":36,"line":529},[34,2993,2994],{"class":76},"\u002F\u002F bus.emit('user:login', { id: 42, name: 'Ada' })\n",[34,2996,2997],{"class":36,"line":548},[34,2998,2999],{"class":76},"\u002F\u002F ── Listening: ──────────────────────────────────────────\n",[34,3001,3002],{"class":36,"line":553},[34,3003,3004],{"class":76},"\u002F\u002F useEventBus('user:login', (payload) => { console.log(payload) })\n",[15,3006,3008],{"id":3007},"feature-based-module-organization","Feature-Based Module Organization",[20,3010,3013,3410],{"filename":3011,"language":3012},"feature-modules.sh","bash",[25,3014,3017],{"className":3015,"code":3016,"language":3012,"meta":29,"style":29},"language-bash shiki shiki-themes github-light github-dark","# ── Feature-based: group by business domain, not by file type ──\n# Each feature is self-contained: components, composables, stores, types.\n\nsrc\u002F\n├── features\u002F\n│   ├── auth\u002F\n│   │   ├── components\u002F\n│   │   │   ├── LoginForm.vue\n│   │   │   └── AuthGuard.vue\n│   │   ├── composables\u002F\n│   │   │   └── useAuth.ts\n│   │   ├── stores\u002F\n│   │   │   └── authStore.ts\n│   │   ├── types\u002F\n│   │   │   └── auth.ts\n│   │   └── index.ts          # public API: export only what other features need\n│   ├── products\u002F\n│   │   ├── components\u002F\n│   │   │   ├── ProductList.vue\n│   │   │   └── ProductDetail.vue\n│   │   ├── composables\u002F\n│   │   │   └── useProducts.ts\n│   │   ├── stores\u002F\n│   │   │   └── productStore.ts\n│   │   ├── types\u002F\n│   │   │   └── product.ts\n│   │   └── index.ts\n│   └── checkout\u002F\n│       └── ...\n├── shared\u002F                   # cross-feature utilities, types, components\n│   ├── components\u002F\n│   │   └── BaseButton.vue\n│   ├── composables\u002F\n│   │   └── useFetch.ts\n│   └── utils\u002F\n└── app\u002F                      # app-level config, routing, providers\n    ├── App.vue\n    └── router.ts\n",[31,3018,3019,3024,3029,3033,3038,3046,3057,3069,3082,3096,3107,3120,3131,3144,3155,3168,3182,3191,3201,3214,3227,3237,3250,3260,3273,3283,3296,3307,3316,3326,3336,3344,3355,3363,3374,3383,3394,3402],{"__ignoreMap":29},[34,3020,3021],{"class":36,"line":37},[34,3022,3023],{"class":76},"# ── Feature-based: group by business domain, not by file type ──\n",[34,3025,3026],{"class":36,"line":66},[34,3027,3028],{"class":76},"# Each feature is self-contained: components, composables, stores, types.\n",[34,3030,3031],{"class":36,"line":73},[34,3032,70],{"emptyLinePlaceholder":69},[34,3034,3035],{"class":36,"line":80},[34,3036,3037],{"class":103},"src\u002F\n",[34,3039,3040,3043],{"class":36,"line":86},[34,3041,3042],{"class":103},"├──",[34,3044,3045],{"class":62}," features\u002F\n",[34,3047,3048,3051,3054],{"class":36,"line":92},[34,3049,3050],{"class":103},"│",[34,3052,3053],{"class":62},"   ├──",[34,3055,3056],{"class":62}," auth\u002F\n",[34,3058,3059,3061,3064,3066],{"class":36,"line":97},[34,3060,3050],{"class":103},[34,3062,3063],{"class":62},"   │",[34,3065,3053],{"class":62},[34,3067,3068],{"class":62}," components\u002F\n",[34,3070,3071,3073,3075,3077,3079],{"class":36,"line":110},[34,3072,3050],{"class":103},[34,3074,3063],{"class":62},[34,3076,3063],{"class":62},[34,3078,3053],{"class":62},[34,3080,3081],{"class":62}," LoginForm.vue\n",[34,3083,3084,3086,3088,3090,3093],{"class":36,"line":124},[34,3085,3050],{"class":103},[34,3087,3063],{"class":62},[34,3089,3063],{"class":62},[34,3091,3092],{"class":62},"   └──",[34,3094,3095],{"class":62}," AuthGuard.vue\n",[34,3097,3098,3100,3102,3104],{"class":36,"line":134},[34,3099,3050],{"class":103},[34,3101,3063],{"class":62},[34,3103,3053],{"class":62},[34,3105,3106],{"class":62}," composables\u002F\n",[34,3108,3109,3111,3113,3115,3117],{"class":36,"line":145},[34,3110,3050],{"class":103},[34,3112,3063],{"class":62},[34,3114,3063],{"class":62},[34,3116,3092],{"class":62},[34,3118,3119],{"class":62}," useAuth.ts\n",[34,3121,3122,3124,3126,3128],{"class":36,"line":156},[34,3123,3050],{"class":103},[34,3125,3063],{"class":62},[34,3127,3053],{"class":62},[34,3129,3130],{"class":62}," stores\u002F\n",[34,3132,3133,3135,3137,3139,3141],{"class":36,"line":162},[34,3134,3050],{"class":103},[34,3136,3063],{"class":62},[34,3138,3063],{"class":62},[34,3140,3092],{"class":62},[34,3142,3143],{"class":62}," authStore.ts\n",[34,3145,3146,3148,3150,3152],{"class":36,"line":167},[34,3147,3050],{"class":103},[34,3149,3063],{"class":62},[34,3151,3053],{"class":62},[34,3153,3154],{"class":62}," types\u002F\n",[34,3156,3157,3159,3161,3163,3165],{"class":36,"line":173},[34,3158,3050],{"class":103},[34,3160,3063],{"class":62},[34,3162,3063],{"class":62},[34,3164,3092],{"class":62},[34,3166,3167],{"class":62}," auth.ts\n",[34,3169,3170,3172,3174,3176,3179],{"class":36,"line":205},[34,3171,3050],{"class":103},[34,3173,3063],{"class":62},[34,3175,3092],{"class":62},[34,3177,3178],{"class":62}," index.ts",[34,3180,3181],{"class":76},"          # public API: export only what other features need\n",[34,3183,3184,3186,3188],{"class":36,"line":248},[34,3185,3050],{"class":103},[34,3187,3053],{"class":62},[34,3189,3190],{"class":62}," products\u002F\n",[34,3192,3193,3195,3197,3199],{"class":36,"line":253},[34,3194,3050],{"class":103},[34,3196,3063],{"class":62},[34,3198,3053],{"class":62},[34,3200,3068],{"class":62},[34,3202,3203,3205,3207,3209,3211],{"class":36,"line":259},[34,3204,3050],{"class":103},[34,3206,3063],{"class":62},[34,3208,3063],{"class":62},[34,3210,3053],{"class":62},[34,3212,3213],{"class":62}," ProductList.vue\n",[34,3215,3216,3218,3220,3222,3224],{"class":36,"line":288},[34,3217,3050],{"class":103},[34,3219,3063],{"class":62},[34,3221,3063],{"class":62},[34,3223,3092],{"class":62},[34,3225,3226],{"class":62}," ProductDetail.vue\n",[34,3228,3229,3231,3233,3235],{"class":36,"line":300},[34,3230,3050],{"class":103},[34,3232,3063],{"class":62},[34,3234,3053],{"class":62},[34,3236,3106],{"class":62},[34,3238,3239,3241,3243,3245,3247],{"class":36,"line":312},[34,3240,3050],{"class":103},[34,3242,3063],{"class":62},[34,3244,3063],{"class":62},[34,3246,3092],{"class":62},[34,3248,3249],{"class":62}," useProducts.ts\n",[34,3251,3252,3254,3256,3258],{"class":36,"line":318},[34,3253,3050],{"class":103},[34,3255,3063],{"class":62},[34,3257,3053],{"class":62},[34,3259,3130],{"class":62},[34,3261,3262,3264,3266,3268,3270],{"class":36,"line":330},[34,3263,3050],{"class":103},[34,3265,3063],{"class":62},[34,3267,3063],{"class":62},[34,3269,3092],{"class":62},[34,3271,3272],{"class":62}," productStore.ts\n",[34,3274,3275,3277,3279,3281],{"class":36,"line":341},[34,3276,3050],{"class":103},[34,3278,3063],{"class":62},[34,3280,3053],{"class":62},[34,3282,3154],{"class":62},[34,3284,3285,3287,3289,3291,3293],{"class":36,"line":347},[34,3286,3050],{"class":103},[34,3288,3063],{"class":62},[34,3290,3063],{"class":62},[34,3292,3092],{"class":62},[34,3294,3295],{"class":62}," product.ts\n",[34,3297,3298,3300,3302,3304],{"class":36,"line":352},[34,3299,3050],{"class":103},[34,3301,3063],{"class":62},[34,3303,3092],{"class":62},[34,3305,3306],{"class":62}," index.ts\n",[34,3308,3309,3311,3313],{"class":36,"line":357},[34,3310,3050],{"class":103},[34,3312,3092],{"class":62},[34,3314,3315],{"class":62}," checkout\u002F\n",[34,3317,3318,3320,3323],{"class":36,"line":363},[34,3319,3050],{"class":103},[34,3321,3322],{"class":62},"       └──",[34,3324,3325],{"class":62}," ...\n",[34,3327,3328,3330,3333],{"class":36,"line":378},[34,3329,3042],{"class":103},[34,3331,3332],{"class":62}," shared\u002F",[34,3334,3335],{"class":76},"                   # cross-feature utilities, types, components\n",[34,3337,3338,3340,3342],{"class":36,"line":386},[34,3339,3050],{"class":103},[34,3341,3053],{"class":62},[34,3343,3068],{"class":62},[34,3345,3346,3348,3350,3352],{"class":36,"line":392},[34,3347,3050],{"class":103},[34,3349,3063],{"class":62},[34,3351,3092],{"class":62},[34,3353,3354],{"class":62}," BaseButton.vue\n",[34,3356,3357,3359,3361],{"class":36,"line":404},[34,3358,3050],{"class":103},[34,3360,3053],{"class":62},[34,3362,3106],{"class":62},[34,3364,3365,3367,3369,3371],{"class":36,"line":415},[34,3366,3050],{"class":103},[34,3368,3063],{"class":62},[34,3370,3092],{"class":62},[34,3372,3373],{"class":62}," useFetch.ts\n",[34,3375,3376,3378,3380],{"class":36,"line":420},[34,3377,3050],{"class":103},[34,3379,3092],{"class":62},[34,3381,3382],{"class":62}," utils\u002F\n",[34,3384,3385,3388,3391],{"class":36,"line":426},[34,3386,3387],{"class":103},"└──",[34,3389,3390],{"class":62}," app\u002F",[34,3392,3393],{"class":76},"                      # app-level config, routing, providers\n",[34,3395,3396,3399],{"class":36,"line":443},[34,3397,3398],{"class":103},"    ├──",[34,3400,3401],{"class":62}," App.vue\n",[34,3403,3404,3407],{"class":36,"line":452},[34,3405,3406],{"class":103},"    └──",[34,3408,3409],{"class":62}," router.ts\n",[20,3411,3413],{"filename":3412,"language":23},"features\u002Fauth\u002Findex.ts",[25,3414,3416],{"className":27,"code":3415,"language":23,"meta":29,"style":29},"\u002F\u002F ── Feature barrel: export only the public API ──────────\n\u002F\u002F Other features import from the index, not internal files.\nexport { useAuth } from '.\u002Fcomposables\u002FuseAuth'\nexport { useAuthStore } from '.\u002Fstores\u002FauthStore'\nexport type { User, Credentials } from '.\u002Ftypes\u002Fauth'\nexport { default as LoginForm } from '.\u002Fcomponents\u002FLoginForm.vue'\n\n\u002F\u002F ── Internal files are NOT exported — they're implementation details ──\n\u002F\u002F This creates a clear boundary: features communicate through their public API.\n\u002F\u002F Other features: import { useAuth } from '@\u002Ffeatures\u002Fauth' (not from auth\u002Fcomposables\u002FuseAuth)\n",[31,3417,3418,3423,3428,3440,3452,3467,3487,3491,3496,3501],{"__ignoreMap":29},[34,3419,3420],{"class":36,"line":37},[34,3421,3422],{"class":76},"\u002F\u002F ── Feature barrel: export only the public API ──────────\n",[34,3424,3425],{"class":36,"line":66},[34,3426,3427],{"class":76},"\u002F\u002F Other features import from the index, not internal files.\n",[34,3429,3430,3432,3435,3437],{"class":36,"line":73},[34,3431,366],{"class":40},[34,3433,3434],{"class":44}," { useAuth } ",[34,3436,59],{"class":40},[34,3438,3439],{"class":62}," '.\u002Fcomposables\u002FuseAuth'\n",[34,3441,3442,3444,3447,3449],{"class":36,"line":80},[34,3443,366],{"class":40},[34,3445,3446],{"class":44}," { useAuthStore } ",[34,3448,59],{"class":40},[34,3450,3451],{"class":62}," '.\u002Fstores\u002FauthStore'\n",[34,3453,3454,3456,3459,3462,3464],{"class":36,"line":86},[34,3455,366],{"class":40},[34,3457,3458],{"class":40}," type",[34,3460,3461],{"class":44}," { User, Credentials } ",[34,3463,59],{"class":40},[34,3465,3466],{"class":62}," '.\u002Ftypes\u002Fauth'\n",[34,3468,3469,3471,3473,3476,3479,3482,3484],{"class":36,"line":92},[34,3470,366],{"class":40},[34,3472,1574],{"class":44},[34,3474,3475],{"class":40},"default",[34,3477,3478],{"class":40}," as",[34,3480,3481],{"class":44}," LoginForm } ",[34,3483,59],{"class":40},[34,3485,3486],{"class":62}," '.\u002Fcomponents\u002FLoginForm.vue'\n",[34,3488,3489],{"class":36,"line":97},[34,3490,70],{"emptyLinePlaceholder":69},[34,3492,3493],{"class":36,"line":110},[34,3494,3495],{"class":76},"\u002F\u002F ── Internal files are NOT exported — they're implementation details ──\n",[34,3497,3498],{"class":36,"line":124},[34,3499,3500],{"class":76},"\u002F\u002F This creates a clear boundary: features communicate through their public API.\n",[34,3502,3503],{"class":36,"line":134},[34,3504,3505],{"class":76},"\u002F\u002F Other features: import { useAuth } from '@\u002Ffeatures\u002Fauth' (not from auth\u002Fcomposables\u002FuseAuth)\n",[15,3507,3509],{"id":3508},"tips-tricks","💡 Tips & Tricks",[20,3511,3513],{"filename":3512,"language":23},"tips.ts",[25,3514,3516],{"className":27,"code":3515,"language":23,"meta":29,"style":29},"\u002F\u002F ── 1. Store pattern vs Pinia: use Pinia for anything non-trivial ──\n\u002F\u002F Store pattern is for 1-2 pieces of simple state. Pinia for: DevTools,\n\u002F\u002F persistence, SSR, multiple stores, getters with parameters.\n\n\u002F\u002F ── 2. Renderless components vs composables: prefer composables ──\n\u002F\u002F Composables are lighter (no component instance). Renderless components\n\u002F\u002F shine when you need slot-based template injection (parent controls markup).\n\n\u002F\u002F ── 3. EventBus: use for ephemeral events, NOT for state ──\n\u002F\u002F State belongs in Pinia. EventBus is for: toasts, notifications, analytics.\n\u002F\u002F If you're storing data in the event bus, you've reinvented a bad Pinia.\n\n\u002F\u002F ── 4. Feature modules: index.ts as the public API boundary ──\n\u002F\u002F Only export what other features should use. Internal files are private.\n\u002F\u002F This prevents tight coupling between features' internal implementations.\n\n\u002F\u002F ── 5. readonly on injected\u002Fprovided state: prevents external mutation ──\n\u002F\u002F Forces consumers to use the action API → all mutations are traceable.\n",[31,3517,3518,3523,3528,3533,3537,3542,3547,3552,3556,3561,3566,3571,3575,3580,3585,3590,3594,3599],{"__ignoreMap":29},[34,3519,3520],{"class":36,"line":37},[34,3521,3522],{"class":76},"\u002F\u002F ── 1. Store pattern vs Pinia: use Pinia for anything non-trivial ──\n",[34,3524,3525],{"class":36,"line":66},[34,3526,3527],{"class":76},"\u002F\u002F Store pattern is for 1-2 pieces of simple state. Pinia for: DevTools,\n",[34,3529,3530],{"class":36,"line":73},[34,3531,3532],{"class":76},"\u002F\u002F persistence, SSR, multiple stores, getters with parameters.\n",[34,3534,3535],{"class":36,"line":80},[34,3536,70],{"emptyLinePlaceholder":69},[34,3538,3539],{"class":36,"line":86},[34,3540,3541],{"class":76},"\u002F\u002F ── 2. Renderless components vs composables: prefer composables ──\n",[34,3543,3544],{"class":36,"line":92},[34,3545,3546],{"class":76},"\u002F\u002F Composables are lighter (no component instance). Renderless components\n",[34,3548,3549],{"class":36,"line":97},[34,3550,3551],{"class":76},"\u002F\u002F shine when you need slot-based template injection (parent controls markup).\n",[34,3553,3554],{"class":36,"line":110},[34,3555,70],{"emptyLinePlaceholder":69},[34,3557,3558],{"class":36,"line":124},[34,3559,3560],{"class":76},"\u002F\u002F ── 3. EventBus: use for ephemeral events, NOT for state ──\n",[34,3562,3563],{"class":36,"line":134},[34,3564,3565],{"class":76},"\u002F\u002F State belongs in Pinia. EventBus is for: toasts, notifications, analytics.\n",[34,3567,3568],{"class":36,"line":145},[34,3569,3570],{"class":76},"\u002F\u002F If you're storing data in the event bus, you've reinvented a bad Pinia.\n",[34,3572,3573],{"class":36,"line":156},[34,3574,70],{"emptyLinePlaceholder":69},[34,3576,3577],{"class":36,"line":162},[34,3578,3579],{"class":76},"\u002F\u002F ── 4. Feature modules: index.ts as the public API boundary ──\n",[34,3581,3582],{"class":36,"line":167},[34,3583,3584],{"class":76},"\u002F\u002F Only export what other features should use. Internal files are private.\n",[34,3586,3587],{"class":36,"line":173},[34,3588,3589],{"class":76},"\u002F\u002F This prevents tight coupling between features' internal implementations.\n",[34,3591,3592],{"class":36,"line":205},[34,3593,70],{"emptyLinePlaceholder":69},[34,3595,3596],{"class":36,"line":248},[34,3597,3598],{"class":76},"\u002F\u002F ── 5. readonly on injected\u002Fprovided state: prevents external mutation ──\n",[34,3600,3601],{"class":36,"line":253},[34,3602,3603],{"class":76},"\u002F\u002F Forces consumers to use the action API → all mutations are traceable.\n",[15,3605,3607],{"id":3606},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[20,3609,3611],{"filename":3610,"language":23},"edge-cases.ts",[25,3612,3614],{"className":27,"code":3613,"language":23,"meta":29,"style":29},"\u002F\u002F ── 1. Module-scoped store leaks state on SSR ──\n\u002F\u002F Server: module scope persists across requests → data leak.\n\u002F\u002F Fix: use Pinia (request-scoped) or reset module state in server entry.\n\n\u002F\u002F ── 2. EventBus without cleanup = memory leak ───────────\n\u002F\u002F Listeners persist after component unmount if not unsubscribed.\n\u002F\u002F Always return an unsubscribe function and call it in onScopeDispose.\n\n\u002F\u002F ── 3. Renderless component: slot can only have ONE default slot ──\n\u002F\u002F Named slots are possible but complex. For multiple \"slots\", use multiple\n\u002F\u002F composables or pass multiple render functions.\n\n\u002F\u002F ── 4. Composable returning refs: must return refs, not values ──\n\u002F\u002F return { count: ref(0) } → reactive. return { count: 0 } → static snapshot.\n\n\u002F\u002F ── 5. Feature barrel: circular imports if two features import each other ──\n\u002F\u002F If auth imports from products and products imports from auth → circular.\n\u002F\u002F Fix: extract shared types to a common module, or use lazy imports.\n\n\u002F\u002F ── 6. readonly() is shallow — nested objects are still mutable ──\n\u002F\u002F readonly(state).nested.prop = x → allowed (readonly only protects top level).\n\u002F\u002F Use readonly(reactive(...)) for deep, or accept shallow readonly.\n",[31,3615,3616,3621,3626,3631,3635,3640,3645,3650,3654,3659,3664,3669,3673,3678,3683,3687,3692,3697,3702,3706,3711,3716],{"__ignoreMap":29},[34,3617,3618],{"class":36,"line":37},[34,3619,3620],{"class":76},"\u002F\u002F ── 1. Module-scoped store leaks state on SSR ──\n",[34,3622,3623],{"class":36,"line":66},[34,3624,3625],{"class":76},"\u002F\u002F Server: module scope persists across requests → data leak.\n",[34,3627,3628],{"class":36,"line":73},[34,3629,3630],{"class":76},"\u002F\u002F Fix: use Pinia (request-scoped) or reset module state in server entry.\n",[34,3632,3633],{"class":36,"line":80},[34,3634,70],{"emptyLinePlaceholder":69},[34,3636,3637],{"class":36,"line":86},[34,3638,3639],{"class":76},"\u002F\u002F ── 2. EventBus without cleanup = memory leak ───────────\n",[34,3641,3642],{"class":36,"line":92},[34,3643,3644],{"class":76},"\u002F\u002F Listeners persist after component unmount if not unsubscribed.\n",[34,3646,3647],{"class":36,"line":97},[34,3648,3649],{"class":76},"\u002F\u002F Always return an unsubscribe function and call it in onScopeDispose.\n",[34,3651,3652],{"class":36,"line":110},[34,3653,70],{"emptyLinePlaceholder":69},[34,3655,3656],{"class":36,"line":124},[34,3657,3658],{"class":76},"\u002F\u002F ── 3. Renderless component: slot can only have ONE default slot ──\n",[34,3660,3661],{"class":36,"line":134},[34,3662,3663],{"class":76},"\u002F\u002F Named slots are possible but complex. For multiple \"slots\", use multiple\n",[34,3665,3666],{"class":36,"line":145},[34,3667,3668],{"class":76},"\u002F\u002F composables or pass multiple render functions.\n",[34,3670,3671],{"class":36,"line":156},[34,3672,70],{"emptyLinePlaceholder":69},[34,3674,3675],{"class":36,"line":162},[34,3676,3677],{"class":76},"\u002F\u002F ── 4. Composable returning refs: must return refs, not values ──\n",[34,3679,3680],{"class":36,"line":167},[34,3681,3682],{"class":76},"\u002F\u002F return { count: ref(0) } → reactive. return { count: 0 } → static snapshot.\n",[34,3684,3685],{"class":36,"line":173},[34,3686,70],{"emptyLinePlaceholder":69},[34,3688,3689],{"class":36,"line":205},[34,3690,3691],{"class":76},"\u002F\u002F ── 5. Feature barrel: circular imports if two features import each other ──\n",[34,3693,3694],{"class":36,"line":248},[34,3695,3696],{"class":76},"\u002F\u002F If auth imports from products and products imports from auth → circular.\n",[34,3698,3699],{"class":36,"line":253},[34,3700,3701],{"class":76},"\u002F\u002F Fix: extract shared types to a common module, or use lazy imports.\n",[34,3703,3704],{"class":36,"line":259},[34,3705,70],{"emptyLinePlaceholder":69},[34,3707,3708],{"class":36,"line":288},[34,3709,3710],{"class":76},"\u002F\u002F ── 6. readonly() is shallow — nested objects are still mutable ──\n",[34,3712,3713],{"class":36,"line":300},[34,3714,3715],{"class":76},"\u002F\u002F readonly(state).nested.prop = x → allowed (readonly only protects top level).\n",[34,3717,3718],{"class":36,"line":312},[34,3719,3720],{"class":76},"\u002F\u002F Use readonly(reactive(...)) for deep, or accept shallow readonly.\n",[15,3722,3724],{"id":3723},"spot-the-bug","🧠 Spot the Bug",[1186,3726,3727],{},"An event bus listener keeps firing after the component unmounts, causing errors.",[20,3729,3731],{"filename":3730,"language":23},"EventBusBug.ts",[25,3732,3734],{"className":27,"code":3733,"language":23,"meta":29,"style":29},"import { onMounted } from 'vue'\nimport { bus } from '.\u002FeventBus'\n\nexport default {\n  setup() {\n    onMounted(() => {\n      \u002F\u002F ❌ Listener added but never removed — persists after unmount\n      bus.on('user:login', (user) => {\n        \u002F\u002F After unmount, this still fires → errors if it references unmounted state\n        console.log('User logged in:', user.name)\n      })\n    })\n  },\n}\n",[31,3735,3736,3747,3759,3763,3772,3779,3790,3795,3819,3824,3840,3845,3850,3855],{"__ignoreMap":29},[34,3737,3738,3740,3743,3745],{"class":36,"line":37},[34,3739,41],{"class":40},[34,3741,3742],{"class":44}," { onMounted } ",[34,3744,59],{"class":40},[34,3746,63],{"class":62},[34,3748,3749,3751,3754,3756],{"class":36,"line":66},[34,3750,41],{"class":40},[34,3752,3753],{"class":44}," { bus } ",[34,3755,59],{"class":40},[34,3757,3758],{"class":62}," '.\u002FeventBus'\n",[34,3760,3761],{"class":36,"line":73},[34,3762,70],{"emptyLinePlaceholder":69},[34,3764,3765,3767,3770],{"class":36,"line":80},[34,3766,366],{"class":40},[34,3768,3769],{"class":40}," default",[34,3771,107],{"class":44},[34,3773,3774,3777],{"class":36,"line":86},[34,3775,3776],{"class":103},"  setup",[34,3778,375],{"class":44},[34,3780,3781,3784,3786,3788],{"class":36,"line":92},[34,3782,3783],{"class":103},"    onMounted",[34,3785,435],{"class":44},[34,3787,438],{"class":40},[34,3789,107],{"class":44},[34,3791,3792],{"class":36,"line":97},[34,3793,3794],{"class":76},"      \u002F\u002F ❌ Listener added but never removed — persists after unmount\n",[34,3796,3797,3800,3802,3804,3807,3810,3813,3815,3817],{"class":36,"line":110},[34,3798,3799],{"class":44},"      bus.",[34,3801,2956],{"class":103},[34,3803,240],{"class":44},[34,3805,3806],{"class":62},"'user:login'",[34,3808,3809],{"class":44},", (",[34,3811,3812],{"class":113},"user",[34,3814,1449],{"class":44},[34,3816,438],{"class":40},[34,3818,107],{"class":44},[34,3820,3821],{"class":36,"line":124},[34,3822,3823],{"class":76},"        \u002F\u002F After unmount, this still fires → errors if it references unmounted state\n",[34,3825,3826,3829,3832,3834,3837],{"class":36,"line":134},[34,3827,3828],{"class":44},"        console.",[34,3830,3831],{"class":103},"log",[34,3833,240],{"class":44},[34,3835,3836],{"class":62},"'User logged in:'",[34,3838,3839],{"class":44},", user.name)\n",[34,3841,3842],{"class":36,"line":145},[34,3843,3844],{"class":44},"      })\n",[34,3846,3847],{"class":36,"line":156},[34,3848,3849],{"class":44},"    })\n",[34,3851,3852],{"class":36,"line":162},[34,3853,3854],{"class":44},"  },\n",[34,3856,3857],{"class":36,"line":167},[34,3858,159],{"class":44},[1275,3860,3861,3864,3871,3882,3996],{},[1298,3862,3863],{},"Answer",[1186,3865,3866,3867,3870],{},"The event listener is registered in ",[31,3868,3869],{},"onMounted"," but never unsubscribed. When the component unmounts, the listener remains in the bus's handler set, still firing on future events. If the handler references component state (refs, DOM elements), it errors or leaks memory.",[1186,3872,3873,3877,3878,3881],{},[3874,3875,3876],"strong",{},"Fix"," — use ",[31,3879,3880],{},"onScopeDispose"," for automatic cleanup:",[20,3883,3885],{"filename":3884,"language":23},"EventBusFixed.ts",[25,3886,3888],{"className":27,"code":3887,"language":23,"meta":29,"style":29},"import { onScopeDispose } from 'vue'\nimport { bus } from '.\u002FeventBus'\n\nexport function useUserEvents() {\n  const unsubscribe = bus.on('user:login', (user) => {\n    console.log('User logged in:', user.name)\n  })\n\n  \u002F\u002F ── onScopeDispose: runs when the hosting scope stops (component unmount) ──\n  onScopeDispose(() => unsubscribe())  \u002F\u002F removes the listener automatically\n}\n",[31,3889,3890,3900,3910,3914,3925,3951,3964,3968,3972,3977,3992],{"__ignoreMap":29},[34,3891,3892,3894,3896,3898],{"class":36,"line":37},[34,3893,41],{"class":40},[34,3895,2906],{"class":44},[34,3897,59],{"class":40},[34,3899,63],{"class":62},[34,3901,3902,3904,3906,3908],{"class":36,"line":66},[34,3903,41],{"class":40},[34,3905,3753],{"class":44},[34,3907,59],{"class":40},[34,3909,3758],{"class":62},[34,3911,3912],{"class":36,"line":73},[34,3913,70],{"emptyLinePlaceholder":69},[34,3915,3916,3918,3920,3923],{"class":36,"line":80},[34,3917,366],{"class":40},[34,3919,369],{"class":40},[34,3921,3922],{"class":103}," useUserEvents",[34,3924,375],{"class":44},[34,3926,3927,3929,3931,3933,3935,3937,3939,3941,3943,3945,3947,3949],{"class":36,"line":86},[34,3928,1419],{"class":40},[34,3930,2948],{"class":120},[34,3932,665],{"class":40},[34,3934,2953],{"class":44},[34,3936,2956],{"class":103},[34,3938,240],{"class":44},[34,3940,3806],{"class":62},[34,3942,3809],{"class":44},[34,3944,3812],{"class":113},[34,3946,1449],{"class":44},[34,3948,438],{"class":40},[34,3950,107],{"class":44},[34,3952,3953,3956,3958,3960,3962],{"class":36,"line":92},[34,3954,3955],{"class":44},"    console.",[34,3957,3831],{"class":103},[34,3959,240],{"class":44},[34,3961,3836],{"class":62},[34,3963,3839],{"class":44},[34,3965,3966],{"class":36,"line":97},[34,3967,344],{"class":44},[34,3969,3970],{"class":36,"line":110},[34,3971,70],{"emptyLinePlaceholder":69},[34,3973,3974],{"class":36,"line":124},[34,3975,3976],{"class":76},"  \u002F\u002F ── onScopeDispose: runs when the hosting scope stops (component unmount) ──\n",[34,3978,3979,3981,3983,3985,3987,3989],{"class":36,"line":134},[34,3980,2964],{"class":103},[34,3982,435],{"class":44},[34,3984,438],{"class":40},[34,3986,2948],{"class":103},[34,3988,2973],{"class":44},[34,3990,3991],{"class":76},"\u002F\u002F removes the listener automatically\n",[34,3993,3994],{"class":36,"line":145},[34,3995,159],{"class":44},[1186,3997,3998,4001,4002,4004],{},[3874,3999,4000],{},"The lesson",": every event bus subscription, observer, or external listener registered in a component or composable must be unsubscribed on cleanup. ",[31,4003,3880],{}," automates this — the returned unsubscribe function is called when the component unmounts.",[4006,4007,4008],"style",{},"html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sk71V, html code.shiki .sk71V{--shiki-default:#22863A;--shiki-github-dark:#85E89D}html pre.shiki code .sMKGj, html code.shiki .sMKGj{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-github-dark:#FDAEB7;--shiki-github-dark-font-style:italic}",{"title":29,"searchDepth":66,"depth":66,"links":4010},[4011,4012,4013,4014,4015,4016,4017,4018,4019],{"id":17,"depth":66,"text":18},{"id":862,"depth":66,"text":863},{"id":1345,"depth":66,"text":1346},{"id":1941,"depth":66,"text":1942},{"id":2459,"depth":66,"text":2460},{"id":3007,"depth":66,"text":3008},{"id":3508,"depth":66,"text":3509},{"id":3606,"depth":66,"text":3607},{"id":3723,"depth":66,"text":3724},"Store pattern, renderless components, composable composition, higher-order composables, provider\u002Fconsumer DI, observer pattern with reactive state, and feature-based module organization.","md",{},"\u002Fvue\u002F23-design-patterns",{"title":5,"description":4020},"vue\u002F23-design-patterns","uNpmEAk3NSzb-qMggCVuPHqZlf1VEqDRQGA-rSzG7kY",1789924655874]